Spaces:
Paused
Paused
google-labs-jules[bot]
commited on
Commit
·
93d826e
0
Parent(s):
Final deployment for HF with landing page
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .dockerignore +5 -0
- .gitattributes +1 -0
- .github/workflows/docker-publish.yml +113 -0
- .gitignore +27 -0
- LICENSE +21 -0
- README.md +234 -0
- app/.dockerignore +2 -0
- app/.gitignore +1 -0
- app/clear_local.sh +25 -0
- app/cli/api/clients.go +161 -0
- app/cli/api/errors.go +61 -0
- app/cli/api/methods.go +2454 -0
- app/cli/api/stream.go +85 -0
- app/cli/auth/account.go +367 -0
- app/cli/auth/api.go +48 -0
- app/cli/auth/auth.go +136 -0
- app/cli/auth/org.go +137 -0
- app/cli/auth/state.go +107 -0
- app/cli/auth/trial.go +87 -0
- app/cli/cmd/apply.go +74 -0
- app/cli/cmd/archive.go +99 -0
- app/cli/cmd/billing.go +29 -0
- app/cli/cmd/branches.go +92 -0
- app/cli/cmd/browser.go +259 -0
- app/cli/cmd/build.go +98 -0
- app/cli/cmd/cd.go +119 -0
- app/cli/cmd/chat.go +64 -0
- app/cli/cmd/checkout.go +172 -0
- app/cli/cmd/claude_max.go +75 -0
- app/cli/cmd/clear.go +62 -0
- app/cli/cmd/config.go +74 -0
- app/cli/cmd/connect.go +67 -0
- app/cli/cmd/context_show.go +69 -0
- app/cli/cmd/continue.go +83 -0
- app/cli/cmd/convo.go +193 -0
- app/cli/cmd/current.go +58 -0
- app/cli/cmd/debug.go +269 -0
- app/cli/cmd/delete_branch.go +129 -0
- app/cli/cmd/delete_plan.go +182 -0
- app/cli/cmd/diffs.go +288 -0
- app/cli/cmd/invite.go +102 -0
- app/cli/cmd/load.go +63 -0
- app/cli/cmd/log.go +88 -0
- app/cli/cmd/ls.go +115 -0
- app/cli/cmd/model_helpers.go +88 -0
- app/cli/cmd/model_packs.go +267 -0
- app/cli/cmd/model_providers.go +163 -0
- app/cli/cmd/models.go +612 -0
- app/cli/cmd/new.go +134 -0
- app/cli/cmd/plan_exec_helpers.go +287 -0
.dockerignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
dist-server/
|
| 2 |
+
cli/
|
| 3 |
+
node_modules/
|
| 4 |
+
plandex-server
|
| 5 |
+
plandex-cloud
|
.gitattributes
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
*.sh text eol=lf
|
.github/workflows/docker-publish.yml
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Build and publish Docker Image
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
release:
|
| 5 |
+
types: [created]
|
| 6 |
+
workflow_dispatch: # enable manual triggering
|
| 7 |
+
|
| 8 |
+
jobs:
|
| 9 |
+
check_release:
|
| 10 |
+
runs-on: ubuntu-latest
|
| 11 |
+
outputs:
|
| 12 |
+
should_build: ${{ steps.check_release.outputs.should_build }}
|
| 13 |
+
tag: ${{ steps.check_release.outputs.tag }}
|
| 14 |
+
|
| 15 |
+
steps:
|
| 16 |
+
- name: Check release tag and find latest server tag
|
| 17 |
+
id: check_release
|
| 18 |
+
run: |
|
| 19 |
+
if [ "${{ github.event_name }}" == "release" ]; then
|
| 20 |
+
# This is a release event - check if the tag starts with 'server'
|
| 21 |
+
if [[ "${{ github.ref_name }}" == server* ]]; then
|
| 22 |
+
echo "This is a server release: ${{ github.ref_name }}"
|
| 23 |
+
echo "should_build=true" >> $GITHUB_OUTPUT
|
| 24 |
+
echo "tag=${{ github.ref_name }}" >> $GITHUB_OUTPUT
|
| 25 |
+
else
|
| 26 |
+
echo "This is not a server release. Skipping build."
|
| 27 |
+
echo "should_build=false" >> $GITHUB_OUTPUT
|
| 28 |
+
fi
|
| 29 |
+
else
|
| 30 |
+
# This is a manual workflow_dispatch event
|
| 31 |
+
echo "This is a manual workflow trigger. Proceeding to find latest server tag."
|
| 32 |
+
echo "should_build=true" >> $GITHUB_OUTPUT
|
| 33 |
+
echo "tag=latest_server" >> $GITHUB_OUTPUT
|
| 34 |
+
fi
|
| 35 |
+
|
| 36 |
+
build_and_push:
|
| 37 |
+
needs: check_release
|
| 38 |
+
if: needs.check_release.outputs.should_build == 'true'
|
| 39 |
+
runs-on: ubuntu-latest
|
| 40 |
+
|
| 41 |
+
steps:
|
| 42 |
+
- name: Check out the repo
|
| 43 |
+
uses: actions/checkout@v2
|
| 44 |
+
with:
|
| 45 |
+
fetch-depth: 0 # Fetch all history and tags
|
| 46 |
+
|
| 47 |
+
- name: Find latest server tag
|
| 48 |
+
id: find_tag
|
| 49 |
+
if: needs.check_release.outputs.tag == 'latest_server'
|
| 50 |
+
run: |
|
| 51 |
+
# Find the latest tag that starts with 'server'
|
| 52 |
+
LATEST_SERVER_TAG=$(git tag -l "server*" --sort=-creatordate | head -n 1)
|
| 53 |
+
|
| 54 |
+
if [ -z "$LATEST_SERVER_TAG" ]; then
|
| 55 |
+
echo "No tags starting with 'server' found."
|
| 56 |
+
echo "skip=true" >> $GITHUB_OUTPUT
|
| 57 |
+
else
|
| 58 |
+
echo "Found latest server tag: $LATEST_SERVER_TAG"
|
| 59 |
+
echo "skip=false" >> $GITHUB_OUTPUT
|
| 60 |
+
echo "tag=$LATEST_SERVER_TAG" >> $GITHUB_OUTPUT
|
| 61 |
+
fi
|
| 62 |
+
shell: bash
|
| 63 |
+
|
| 64 |
+
- name: Set release tag
|
| 65 |
+
id: set_tag
|
| 66 |
+
if: needs.check_release.outputs.tag != 'latest_server'
|
| 67 |
+
run: |
|
| 68 |
+
echo "skip=false" >> $GITHUB_OUTPUT
|
| 69 |
+
echo "tag=${{ needs.check_release.outputs.tag }}" >> $GITHUB_OUTPUT
|
| 70 |
+
|
| 71 |
+
- name: Skip build if no server tag found
|
| 72 |
+
if: (steps.find_tag.outputs.skip == 'true' && needs.check_release.outputs.tag == 'latest_server')
|
| 73 |
+
run: |
|
| 74 |
+
echo "Skipping build because no tag starting with 'server' was found."
|
| 75 |
+
exit 1
|
| 76 |
+
|
| 77 |
+
- name: Set up QEMU
|
| 78 |
+
uses: docker/setup-qemu-action@v2
|
| 79 |
+
|
| 80 |
+
- name: Set up Docker Buildx
|
| 81 |
+
uses: docker/setup-buildx-action@v1
|
| 82 |
+
|
| 83 |
+
- name: Log in to Docker Hub
|
| 84 |
+
uses: docker/login-action@v1
|
| 85 |
+
with:
|
| 86 |
+
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
| 87 |
+
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
| 88 |
+
|
| 89 |
+
- name: Determine tag to use
|
| 90 |
+
id: determine_tag
|
| 91 |
+
run: |
|
| 92 |
+
if [ "${{ needs.check_release.outputs.tag }}" == "latest_server" ]; then
|
| 93 |
+
TAG="${{ steps.find_tag.outputs.tag }}"
|
| 94 |
+
else
|
| 95 |
+
TAG="${{ needs.check_release.outputs.tag }}"
|
| 96 |
+
fi
|
| 97 |
+
echo "Using tag: $TAG"
|
| 98 |
+
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
| 99 |
+
|
| 100 |
+
- name: Sanitize tag name
|
| 101 |
+
id: sanitize
|
| 102 |
+
run: echo "SANITIZED_TAG_NAME=$(echo ${{ steps.determine_tag.outputs.tag }} | tr '/' '-' | tr '+' '-')" >> $GITHUB_OUTPUT
|
| 103 |
+
|
| 104 |
+
- name: Build and push
|
| 105 |
+
uses: docker/build-push-action@v2
|
| 106 |
+
with:
|
| 107 |
+
context: ./app/
|
| 108 |
+
file: ./app/server/Dockerfile
|
| 109 |
+
push: true
|
| 110 |
+
platforms: linux/amd64,linux/arm64
|
| 111 |
+
tags: |
|
| 112 |
+
plandexai/plandex-server:${{ steps.sanitize.outputs.SANITIZED_TAG_NAME }}
|
| 113 |
+
plandexai/plandex-server:latest
|
.gitignore
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.plandex/
|
| 2 |
+
.plandex-dev/
|
| 3 |
+
.plandex-v2/
|
| 4 |
+
.plandex-dev-v2/
|
| 5 |
+
.envkey
|
| 6 |
+
.env
|
| 7 |
+
.env.*
|
| 8 |
+
plandex
|
| 9 |
+
plandex-dev
|
| 10 |
+
plandex-server
|
| 11 |
+
*.exe
|
| 12 |
+
node_modules/
|
| 13 |
+
/tools/
|
| 14 |
+
/static/
|
| 15 |
+
/infra/
|
| 16 |
+
/payments-dashboard/
|
| 17 |
+
.DS_Store
|
| 18 |
+
.goreleaser.yml
|
| 19 |
+
dist/
|
| 20 |
+
__pycache__/
|
| 21 |
+
|
| 22 |
+
.aider.*
|
| 23 |
+
*.code-workspace
|
| 24 |
+
|
| 25 |
+
__pycache__/
|
| 26 |
+
|
| 27 |
+
.repo_ignore
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 PlandexAI Inc.
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<h1 align="center">
|
| 2 |
+
<a href="https://plandex.ai">
|
| 3 |
+
<picture>
|
| 4 |
+
<source media="(prefers-color-scheme: dark)" srcset="images/plandex-logo-dark-v2.png"/>
|
| 5 |
+
<source media="(prefers-color-scheme: light)" srcset="images/plandex-logo-light-v2.png"/>
|
| 6 |
+
<img width="400" src="images/plandex-logo-dark-bg-v2.png"/>
|
| 7 |
+
</a>
|
| 8 |
+
<br />
|
| 9 |
+
</h1>
|
| 10 |
+
<br />
|
| 11 |
+
|
| 12 |
+
<div align="center">
|
| 13 |
+
|
| 14 |
+
<p align="center">
|
| 15 |
+
<!-- Call to Action Links -->
|
| 16 |
+
<a href="#install">
|
| 17 |
+
<b>30-Second Install</b>
|
| 18 |
+
</a>
|
| 19 |
+
·
|
| 20 |
+
<a href="https://plandex.ai">
|
| 21 |
+
<b>Website</b>
|
| 22 |
+
</a>
|
| 23 |
+
·
|
| 24 |
+
<a href="https://docs.plandex.ai/">
|
| 25 |
+
<b>Docs</b>
|
| 26 |
+
</a>
|
| 27 |
+
·
|
| 28 |
+
<a href="#examples-">
|
| 29 |
+
<b>Examples</b>
|
| 30 |
+
</a>
|
| 31 |
+
·
|
| 32 |
+
<a href="https://docs.plandex.ai/hosting/self-hosting/local-mode-quickstart">
|
| 33 |
+
<b>Local Self-Hosted Mode</b>
|
| 34 |
+
</a>
|
| 35 |
+
</p>
|
| 36 |
+
|
| 37 |
+
<br>
|
| 38 |
+
|
| 39 |
+
[](https://discord.gg/plandex-ai)
|
| 40 |
+
[](https://github.com/plandex-ai/plandex)
|
| 41 |
+
[](https://twitter.com/PlandexAI)
|
| 42 |
+
|
| 43 |
+
</div>
|
| 44 |
+
|
| 45 |
+
<p align="center">
|
| 46 |
+
<!-- Badges -->
|
| 47 |
+
<a href="https://github.com/plandex-ai/plandex/pulls"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome" /></a> <a href="https://github.com/plandex-ai/plandex/releases?q=cli"><img src="https://img.shields.io/github/v/release/plandex-ai/plandex?filter=cli*" alt="Release" /></a>
|
| 48 |
+
<a href="https://github.com/plandex-ai/plandex/releases?q=server"><img src="https://img.shields.io/github/v/release/plandex-ai/plandex?filter=server*" alt="Release" /></a>
|
| 49 |
+
|
| 50 |
+
<!-- <a href="https://github.com/your_username/your_project/issues">
|
| 51 |
+
<img src="https://img.shields.io/github/issues-closed/your_username/your_project.svg" alt="Issues Closed" />
|
| 52 |
+
</a> -->
|
| 53 |
+
|
| 54 |
+
</p>
|
| 55 |
+
|
| 56 |
+
<br />
|
| 57 |
+
|
| 58 |
+
<div align="center">
|
| 59 |
+
<a href="https://trendshift.io/repositories/8994" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8994" alt="plandex-ai%2Fplandex | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
| 60 |
+
</div>
|
| 61 |
+
|
| 62 |
+
<br>
|
| 63 |
+
|
| 64 |
+
<h1 align="center" >
|
| 65 |
+
An AI coding agent designed for large tasks and real world projects.<br/><br/>
|
| 66 |
+
</h1>
|
| 67 |
+
|
| 68 |
+
<!-- <h2 align="center">
|
| 69 |
+
Designed for large tasks and real world projects.<br/><br/>
|
| 70 |
+
</h2> -->
|
| 71 |
+
<br/>
|
| 72 |
+
|
| 73 |
+
<div align="center">
|
| 74 |
+
<a href="https://www.youtube.com/watch?v=SFSu2vNmlLk">
|
| 75 |
+
<img src="images/plandex-v2-yt.png" alt="Plandex v2 Demo Video" width="800">
|
| 76 |
+
</a>
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
<br/>
|
| 80 |
+
|
| 81 |
+
💻 Plandex is a terminal-based AI development tool that can **plan and execute** large coding tasks that span many steps and touch dozens of files. It can handle up to 2M tokens of context directly (~100k per file), and can index directories with 20M tokens or more using tree-sitter project maps.
|
| 82 |
+
|
| 83 |
+
🔬 **A cumulative diff review sandbox** keeps AI-generated changes separate from your project files until they are ready to go. Command execution is controlled so you can easily roll back and debug. Plandex helps you get the most out of AI without leaving behind a mess in your project.
|
| 84 |
+
|
| 85 |
+
🧠 **Combine the best models** from Anthropic, OpenAI, Google, and open source providers to build entire features and apps with a robust terminal-based workflow.
|
| 86 |
+
|
| 87 |
+
🚀 Plandex is capable of <strong>full autonomy</strong>—it can load relevant files, plan and implement changes, execute commands, and automatically debug—but it's also highly flexible and configurable, giving developers fine-grained control and a step-by-step review process when needed.
|
| 88 |
+
|
| 89 |
+
💪 Plandex is designed to be resilient to <strong>large projects and files</strong>. If you've found that others tools struggle once your project gets past a certain size or the changes are too complex, give Plandex a shot.
|
| 90 |
+
|
| 91 |
+
## Smart context management that works in big projects
|
| 92 |
+
|
| 93 |
+
- 🐘 **2M token effective context window** with default model pack. Plandex loads only what's needed for each step.
|
| 94 |
+
|
| 95 |
+
- 🗄️ **Reliable in large projects and files.** Easily generate, review, revise, and apply changes spanning dozens of files.
|
| 96 |
+
|
| 97 |
+
- 🗺️ **Fast project map generation** and syntax validation with tree-sitter. Supports 30+ languages.
|
| 98 |
+
|
| 99 |
+
- 💰 **Context caching** is used across the board for OpenAI, Anthropic, and Google models, reducing costs and latency.
|
| 100 |
+
|
| 101 |
+
## Tight control or full autonomy—it's up to you
|
| 102 |
+
|
| 103 |
+
- 🚦 **Configurable autonomy:** go from full auto mode to fine-grained control depending on the task.
|
| 104 |
+
|
| 105 |
+
- 🐞 **Automated debugging** of terminal commands (like builds, linters, tests, deployments, and scripts). If you have Chrome installed, you can also automatically debug browser applications.
|
| 106 |
+
|
| 107 |
+
## Tools that help you get production-ready results
|
| 108 |
+
|
| 109 |
+
- 💬 **A project-aware chat mode** that helps you flesh out ideas before moving to implementation. Also great for asking questions and learning about a codebase.
|
| 110 |
+
|
| 111 |
+
- 🧠 **Easily try + combine models** from multiple providers. Curated model packs offer different tradeoffs of capability, cost, and speed, as well as open source and provider-specific packs.
|
| 112 |
+
|
| 113 |
+
- 🛡️ **Reliable file edits** that prioritize correctness. While most edits are quick and cheap, Plandex validates both syntax and logic as needed, with multiple fallback layers when there are problems.
|
| 114 |
+
|
| 115 |
+
- 🔀 **Full-fledged version control** for every update to the plan, including branches for exploring multiple paths or comparing different models.
|
| 116 |
+
|
| 117 |
+
- 📂 **Git integration** with commit message generation and optional automatic commits.
|
| 118 |
+
|
| 119 |
+
## Dev-friendly, easy to install
|
| 120 |
+
|
| 121 |
+
- 🧑💻 **REPL mode** with fuzzy auto-complete for commands and file loading. Just run `plandex` in any project to get started.
|
| 122 |
+
|
| 123 |
+
- 🛠️ **CLI interface** for scripting or piping data into context.
|
| 124 |
+
|
| 125 |
+
- 📦 **One-line, zero dependency CLI install**. Dockerized local mode for easily self-hosting the server. Cloud-hosting options for extra reliability and convenience.
|
| 126 |
+
|
| 127 |
+
## Workflow 🔄
|
| 128 |
+
|
| 129 |
+
<img src="images/plandex-workflow.png" alt="Plandex workflow" width="100%"/>
|
| 130 |
+
|
| 131 |
+
## Examples 🎥
|
| 132 |
+
|
| 133 |
+
<br/>
|
| 134 |
+
|
| 135 |
+
<div align="center">
|
| 136 |
+
<a href="https://www.youtube.com/watch?v=g-_76U_nK0Y">
|
| 137 |
+
<img src="images/plandex-browser-debug-yt.png" alt="Plandex Browser Debugging Example" width="800">
|
| 138 |
+
</a>
|
| 139 |
+
</div>
|
| 140 |
+
|
| 141 |
+
<br/>
|
| 142 |
+
|
| 143 |
+
## Install 📥
|
| 144 |
+
|
| 145 |
+
```bash
|
| 146 |
+
curl -sL https://plandex.ai/install.sh | bash
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
**Note:** Windows is supported via [WSL](https://learn.microsoft.com/en-us/windows/wsl/install). Plandex only works correctly on Windows in the WSL shell. It doesn't work in the Windows CMD prompt or PowerShell.
|
| 150 |
+
|
| 151 |
+
[More installation options.](https://docs.plandex.ai/install)
|
| 152 |
+
|
| 153 |
+
## Hosting ⚖️
|
| 154 |
+
|
| 155 |
+
| Option | Description |
|
| 156 |
+
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 157 |
+
| **Plandex Cloud** | Winding down as of 10/3/2025 and no longer accepting new users. <a href="https://plandex.ai/blog/winding-down">Learn more.</a> |
|
| 158 |
+
| **Self-hosted/Local Mode** | • Run Plandex locally with Docker or host on your own server.<br/>• Use your own [OpenRouter.ai](https://openrouter.ai) key (or [other model provider](https://docs.plandex.ai/models/model-providers) accounts and API keys).<br/>• Follow the [local-mode quickstart](https://docs.plandex.ai/hosting/self-hosting/local-mode-quickstart) to get started. |
|
| 159 |
+
|
| 160 |
+
## Provider keys 🔑
|
| 161 |
+
|
| 162 |
+
<!-- If you're going with a 'BYO API Key' option above (whether cloud or self-hosted), you'll need to set API keys for the [model providers](https://docs.plandex.ai/models/model-providers) you're using: -->
|
| 163 |
+
|
| 164 |
+
```bash
|
| 165 |
+
export OPENROUTER_API_KEY=... # if using OpenRouter.ai
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
<br/>
|
| 169 |
+
|
| 170 |
+
## Claude Pro/Max subscription 🖇️
|
| 171 |
+
|
| 172 |
+
If you have a Claude Pro or Max subscription, Plandex can use it when calling Anthropic models. You'll be asked if you want to connect a subscription the first time you run Plandex.
|
| 173 |
+
|
| 174 |
+
<br/>
|
| 175 |
+
|
| 176 |
+
## Get started 🚀
|
| 177 |
+
|
| 178 |
+
First, `cd` into a **project directory** where you want to get something done or chat about the project. Make a new directory first with `mkdir your-project-dir` if you're starting on a new project.
|
| 179 |
+
|
| 180 |
+
```bash
|
| 181 |
+
cd your-project-dir
|
| 182 |
+
```
|
| 183 |
+
|
| 184 |
+
For a new project, you might also want to initialize a git repo. Plandex doesn't require that your project is in a git repo, but it does integrate well with git if you use it.
|
| 185 |
+
|
| 186 |
+
```bash
|
| 187 |
+
git init
|
| 188 |
+
```
|
| 189 |
+
|
| 190 |
+
Now start the Plandex REPL in your project:
|
| 191 |
+
|
| 192 |
+
```bash
|
| 193 |
+
plandex
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
or for short:
|
| 197 |
+
|
| 198 |
+
```bash
|
| 199 |
+
pdx
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
<!-- ☁️ _If you're using Plandex Cloud, you'll be prompted at this point to start a trial._
|
| 203 |
+
|
| 204 |
+
Then just give the REPL help text a quick read, and you're ready go. The REPL starts in _chat mode_ by default, which is good for fleshing out ideas before moving to implementation. Once the task is clear, Plandex will prompt you to switch to _tell mode_ to make a detailed plan and start writing code. -->
|
| 205 |
+
|
| 206 |
+
<br/>
|
| 207 |
+
|
| 208 |
+
## Docs 🛠���
|
| 209 |
+
|
| 210 |
+
### [👉 Full documentation.](https://docs.plandex.ai/)
|
| 211 |
+
|
| 212 |
+
<br/>
|
| 213 |
+
|
| 214 |
+
## Discussion and discord 💬
|
| 215 |
+
|
| 216 |
+
Please feel free to give your feedback, ask questions, report a bug, or just hang out:
|
| 217 |
+
|
| 218 |
+
- [Discord](https://discord.gg/plandex-ai)
|
| 219 |
+
- [Discussions](https://github.com/plandex-ai/plandex/discussions)
|
| 220 |
+
- [Issues](https://github.com/plandex-ai/plandex/issues)
|
| 221 |
+
|
| 222 |
+
## Follow and subscribe
|
| 223 |
+
|
| 224 |
+
- [Follow @PlandexAI](https://x.com/PlandexAI)
|
| 225 |
+
- [Follow @Danenania](https://x.com/Danenania) (Plandex's creator)
|
| 226 |
+
- [Subscribe on YouTube](https://x.com/PlandexAI)
|
| 227 |
+
|
| 228 |
+
<br/>
|
| 229 |
+
|
| 230 |
+
## Contributors 👥
|
| 231 |
+
|
| 232 |
+
⭐️ Please star, fork, explore, and contribute to Plandex. There's a lot of work to do and so much that can be improved.
|
| 233 |
+
|
| 234 |
+
[Here's an overview on setting up a development environment.](https://docs.plandex.ai/development)
|
app/.dockerignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
cli/
|
| 2 |
+
plandex-server
|
app/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
.env
|
app/clear_local.sh
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
|
| 3 |
+
# Get the absolute path to the script's directory, regardless of where it's run from
|
| 4 |
+
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
| 5 |
+
|
| 6 |
+
# Change to the app directory if we're not already there
|
| 7 |
+
cd "$SCRIPT_DIR"
|
| 8 |
+
|
| 9 |
+
echo "WARNING: This will delete all Plandex server data and reset the database."
|
| 10 |
+
echo "This action cannot be undone."
|
| 11 |
+
read -p "Are you sure you want to continue? (y/N) " -n 1 -r
|
| 12 |
+
echo
|
| 13 |
+
if [[ ! $REPLY =~ ^[Yy]$ ]]
|
| 14 |
+
then
|
| 15 |
+
echo "Reset cancelled."
|
| 16 |
+
exit 1
|
| 17 |
+
fi
|
| 18 |
+
|
| 19 |
+
echo "Resetting local mode..."
|
| 20 |
+
echo "Stopping containers and removing volumes..."
|
| 21 |
+
|
| 22 |
+
# Stop containers and remove volumes
|
| 23 |
+
docker compose down -v
|
| 24 |
+
|
| 25 |
+
echo "Database and data directories cleared. Server stopped."
|
app/cli/api/clients.go
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package api
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"math"
|
| 5 |
+
"math/rand"
|
| 6 |
+
"net"
|
| 7 |
+
"net/http"
|
| 8 |
+
"os"
|
| 9 |
+
"plandex-cli/auth"
|
| 10 |
+
"plandex-cli/types"
|
| 11 |
+
"time"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
const dialTimeout = 10 * time.Second
|
| 15 |
+
const fastReqTimeout = 30 * time.Second
|
| 16 |
+
const slowReqTimeout = 5 * time.Minute
|
| 17 |
+
|
| 18 |
+
type Api struct{}
|
| 19 |
+
|
| 20 |
+
var CloudApiHost string
|
| 21 |
+
var Client types.ApiClient = (*Api)(nil)
|
| 22 |
+
|
| 23 |
+
func init() {
|
| 24 |
+
if os.Getenv("PLANDEX_ENV") == "development" {
|
| 25 |
+
CloudApiHost = os.Getenv("PLANDEX_API_HOST")
|
| 26 |
+
if CloudApiHost == "" {
|
| 27 |
+
CloudApiHost = "http://localhost:8099"
|
| 28 |
+
}
|
| 29 |
+
} else {
|
| 30 |
+
CloudApiHost = "https://api-v2.plandex.ai"
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
func GetApiHost() string {
|
| 35 |
+
if auth.Current == nil {
|
| 36 |
+
return CloudApiHost
|
| 37 |
+
} else if auth.Current.IsCloud {
|
| 38 |
+
return CloudApiHost
|
| 39 |
+
} else {
|
| 40 |
+
return auth.Current.Host
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
type authenticatedTransport struct {
|
| 45 |
+
underlyingTransport http.RoundTripper
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
func (t *authenticatedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
| 49 |
+
err := auth.SetAuthHeader(req)
|
| 50 |
+
if err != nil {
|
| 51 |
+
return nil, err
|
| 52 |
+
}
|
| 53 |
+
auth.SetVersionHeader(req)
|
| 54 |
+
return t.underlyingTransport.RoundTrip(req)
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
type unauthenticatedTransport struct {
|
| 58 |
+
underlyingTransport http.RoundTripper
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
func (t *unauthenticatedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
| 62 |
+
auth.SetVersionHeader(req)
|
| 63 |
+
return t.underlyingTransport.RoundTrip(req)
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
type retryTransport struct {
|
| 67 |
+
Base http.RoundTripper
|
| 68 |
+
MaxRetries int
|
| 69 |
+
BaseDelay time.Duration
|
| 70 |
+
MaxDelay time.Duration
|
| 71 |
+
Jitter time.Duration
|
| 72 |
+
RetryStatuses map[int]bool
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
func (t *retryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
| 76 |
+
if t.Base == nil {
|
| 77 |
+
t.Base = http.DefaultTransport
|
| 78 |
+
}
|
| 79 |
+
var resp *http.Response
|
| 80 |
+
var err error
|
| 81 |
+
|
| 82 |
+
for attempt := 0; attempt <= t.MaxRetries; attempt++ {
|
| 83 |
+
resp, err = t.Base.RoundTrip(req)
|
| 84 |
+
|
| 85 |
+
// If there's a low-level error (e.g. network), retry unless it's a timeout, as these are often transient.
|
| 86 |
+
if err != nil {
|
| 87 |
+
if netErr, ok := err.(net.Error); ok {
|
| 88 |
+
if netErr.Timeout() {
|
| 89 |
+
return resp, err
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
// continue to next attempt
|
| 93 |
+
} else {
|
| 94 |
+
// If status code not in our RetryStatuses, return immediately.
|
| 95 |
+
if !t.RetryStatuses[resp.StatusCode] {
|
| 96 |
+
return resp, nil
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// Close the body before retrying.
|
| 100 |
+
_ = resp.Body.Close()
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// If we reached the max, break out of loop (will return last resp).
|
| 104 |
+
if attempt == t.MaxRetries {
|
| 105 |
+
break
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// Exponential backoff + jitter
|
| 109 |
+
backoff := float64(t.BaseDelay) * math.Pow(2, float64(attempt))
|
| 110 |
+
if backoff > float64(t.MaxDelay) {
|
| 111 |
+
backoff = float64(t.MaxDelay)
|
| 112 |
+
}
|
| 113 |
+
sleepDuration := time.Duration(backoff) + time.Duration(rand.Int63n(int64(t.Jitter)))
|
| 114 |
+
time.Sleep(sleepDuration)
|
| 115 |
+
}
|
| 116 |
+
return resp, err
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
var netDialer = &net.Dialer{
|
| 120 |
+
Timeout: dialTimeout,
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
var baseTransport = &http.Transport{
|
| 124 |
+
Dial: netDialer.Dial,
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
var sharedRetryTransport = &retryTransport{
|
| 128 |
+
Base: baseTransport,
|
| 129 |
+
MaxRetries: 3,
|
| 130 |
+
BaseDelay: 500 * time.Millisecond,
|
| 131 |
+
MaxDelay: 5 * time.Second,
|
| 132 |
+
Jitter: 300 * time.Millisecond,
|
| 133 |
+
RetryStatuses: map[int]bool{502: true, 503: true, 504: true},
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
var unauthenticatedClient = &http.Client{
|
| 137 |
+
Transport: &unauthenticatedTransport{
|
| 138 |
+
underlyingTransport: sharedRetryTransport,
|
| 139 |
+
},
|
| 140 |
+
Timeout: fastReqTimeout,
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
var authenticatedFastClient = &http.Client{
|
| 144 |
+
Transport: &authenticatedTransport{
|
| 145 |
+
underlyingTransport: sharedRetryTransport,
|
| 146 |
+
},
|
| 147 |
+
Timeout: fastReqTimeout,
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
var authenticatedSlowClient = &http.Client{
|
| 151 |
+
Transport: &authenticatedTransport{
|
| 152 |
+
underlyingTransport: sharedRetryTransport,
|
| 153 |
+
},
|
| 154 |
+
Timeout: slowReqTimeout,
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
var authenticatedStreamingClient = &http.Client{
|
| 158 |
+
Transport: &authenticatedTransport{
|
| 159 |
+
underlyingTransport: sharedRetryTransport,
|
| 160 |
+
},
|
| 161 |
+
}
|
app/cli/api/errors.go
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package api
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"log"
|
| 6 |
+
"net/http"
|
| 7 |
+
"plandex-cli/auth"
|
| 8 |
+
"plandex-cli/term"
|
| 9 |
+
"strings"
|
| 10 |
+
|
| 11 |
+
shared "plandex-shared"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
func HandleApiError(r *http.Response, errBody []byte) *shared.ApiError {
|
| 15 |
+
// Check if the response is JSON
|
| 16 |
+
if r.Header.Get("Content-Type") != "application/json" {
|
| 17 |
+
return &shared.ApiError{
|
| 18 |
+
Type: shared.ApiErrorTypeOther,
|
| 19 |
+
Status: r.StatusCode,
|
| 20 |
+
Msg: strings.TrimSpace(string(errBody)),
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
var apiError shared.ApiError
|
| 25 |
+
if err := json.Unmarshal(errBody, &apiError); err != nil {
|
| 26 |
+
log.Printf("Error unmarshalling JSON: %v\n", err)
|
| 27 |
+
return &shared.ApiError{
|
| 28 |
+
Type: shared.ApiErrorTypeOther,
|
| 29 |
+
Status: r.StatusCode,
|
| 30 |
+
Msg: strings.TrimSpace(string(errBody)),
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// return error if token/auth refresh is needed
|
| 35 |
+
if apiError.Type == shared.ApiErrorTypeInvalidToken || apiError.Type == shared.ApiErrorTypeAuthOutdated {
|
| 36 |
+
return &apiError
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
term.HandleApiError(&apiError)
|
| 40 |
+
|
| 41 |
+
return &apiError
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
func refreshAuthIfNeeded(apiErr *shared.ApiError) (bool, *shared.ApiError) {
|
| 45 |
+
if apiErr.Type == shared.ApiErrorTypeInvalidToken {
|
| 46 |
+
err := auth.RefreshInvalidToken()
|
| 47 |
+
if err != nil {
|
| 48 |
+
return false, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: "error refreshing invalid token"}
|
| 49 |
+
}
|
| 50 |
+
return true, nil
|
| 51 |
+
} else if apiErr.Type == shared.ApiErrorTypeAuthOutdated {
|
| 52 |
+
err := auth.RefreshAuth()
|
| 53 |
+
if err != nil {
|
| 54 |
+
return false, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: "error refreshing auth"}
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
return true, nil
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
return false, apiErr
|
| 61 |
+
}
|
app/cli/api/methods.go
ADDED
|
@@ -0,0 +1,2454 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package api
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"context"
|
| 6 |
+
"encoding/json"
|
| 7 |
+
"fmt"
|
| 8 |
+
"io"
|
| 9 |
+
"log"
|
| 10 |
+
"net/http"
|
| 11 |
+
"plandex-cli/types"
|
| 12 |
+
"strings"
|
| 13 |
+
|
| 14 |
+
shared "plandex-shared"
|
| 15 |
+
|
| 16 |
+
"github.com/shopspring/decimal"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
func (a *Api) CreateCliTrialSession() (string, *shared.ApiError) {
|
| 20 |
+
serverUrl := CloudApiHost + "/accounts/cli_trial_session"
|
| 21 |
+
|
| 22 |
+
resp, err := unauthenticatedClient.Post(serverUrl, "application/json", nil)
|
| 23 |
+
|
| 24 |
+
if err != nil {
|
| 25 |
+
return "", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
defer resp.Body.Close()
|
| 29 |
+
|
| 30 |
+
if resp.StatusCode >= 400 {
|
| 31 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 32 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 33 |
+
return "", apiErr
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
bytes, err := io.ReadAll(resp.Body)
|
| 37 |
+
|
| 38 |
+
if err != nil {
|
| 39 |
+
return "", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error reading response: %v", err)}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
return string(bytes), nil
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
func (a *Api) GetCliTrialSession(token string) (*shared.SessionResponse, *shared.ApiError) {
|
| 46 |
+
serverUrl := fmt.Sprintf("%s/accounts/cli_trial_session/%s", CloudApiHost, token)
|
| 47 |
+
|
| 48 |
+
resp, err := unauthenticatedClient.Get(serverUrl)
|
| 49 |
+
|
| 50 |
+
if err != nil {
|
| 51 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
if resp.StatusCode == 404 {
|
| 55 |
+
return nil, nil
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
defer resp.Body.Close()
|
| 59 |
+
|
| 60 |
+
if resp.StatusCode >= 400 {
|
| 61 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 62 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 63 |
+
return nil, apiErr
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
var session shared.SessionResponse
|
| 67 |
+
err = json.NewDecoder(resp.Body).Decode(&session)
|
| 68 |
+
if err != nil {
|
| 69 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
return &session, nil
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
func (a *Api) CreateProject(req shared.CreateProjectRequest) (*shared.CreateProjectResponse, *shared.ApiError) {
|
| 76 |
+
serverUrl := GetApiHost() + "/projects"
|
| 77 |
+
|
| 78 |
+
reqBytes, err := json.Marshal(req)
|
| 79 |
+
if err != nil {
|
| 80 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
resp, err := authenticatedFastClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
|
| 84 |
+
if err != nil {
|
| 85 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 86 |
+
}
|
| 87 |
+
defer resp.Body.Close()
|
| 88 |
+
|
| 89 |
+
if resp.StatusCode >= 400 {
|
| 90 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 91 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 92 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 93 |
+
if authRefreshed {
|
| 94 |
+
return a.CreateProject(req)
|
| 95 |
+
}
|
| 96 |
+
return nil, apiErr
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
var respBody shared.CreateProjectResponse
|
| 100 |
+
err = json.NewDecoder(resp.Body).Decode(&respBody)
|
| 101 |
+
if err != nil {
|
| 102 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
return &respBody, nil
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
func (a *Api) ListProjects() ([]*shared.Project, *shared.ApiError) {
|
| 109 |
+
serverUrl := GetApiHost() + "/projects"
|
| 110 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 111 |
+
if err != nil {
|
| 112 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 113 |
+
}
|
| 114 |
+
defer resp.Body.Close()
|
| 115 |
+
|
| 116 |
+
if resp.StatusCode >= 400 {
|
| 117 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 118 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 119 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 120 |
+
if authRefreshed {
|
| 121 |
+
return a.ListProjects()
|
| 122 |
+
}
|
| 123 |
+
return nil, apiErr
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
var projects []*shared.Project
|
| 127 |
+
err = json.NewDecoder(resp.Body).Decode(&projects)
|
| 128 |
+
if err != nil {
|
| 129 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
return projects, nil
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
func (a *Api) SetProjectPlan(projectId string, req shared.SetProjectPlanRequest) *shared.ApiError {
|
| 136 |
+
serverUrl := fmt.Sprintf("%s/projects/%s/set_plan", GetApiHost(), projectId)
|
| 137 |
+
reqBytes, err := json.Marshal(req)
|
| 138 |
+
if err != nil {
|
| 139 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
request, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))
|
| 143 |
+
if err != nil {
|
| 144 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 145 |
+
}
|
| 146 |
+
request.Header.Set("Content-Type", "application/json")
|
| 147 |
+
|
| 148 |
+
resp, err := authenticatedFastClient.Do(request)
|
| 149 |
+
if err != nil {
|
| 150 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 151 |
+
}
|
| 152 |
+
defer resp.Body.Close()
|
| 153 |
+
|
| 154 |
+
if resp.StatusCode >= 400 {
|
| 155 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 156 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 157 |
+
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
|
| 158 |
+
if didRefresh {
|
| 159 |
+
return a.SetProjectPlan(projectId, req)
|
| 160 |
+
}
|
| 161 |
+
return apiErr
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
return nil
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
func (a *Api) RenameProject(projectId string, req shared.RenameProjectRequest) *shared.ApiError {
|
| 168 |
+
serverUrl := fmt.Sprintf("%s/projects/%s/rename", GetApiHost(), projectId)
|
| 169 |
+
reqBytes, err := json.Marshal(req)
|
| 170 |
+
if err != nil {
|
| 171 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
request, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))
|
| 175 |
+
if err != nil {
|
| 176 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 177 |
+
}
|
| 178 |
+
request.Header.Set("Content-Type", "application/json")
|
| 179 |
+
|
| 180 |
+
resp, err := authenticatedFastClient.Do(request)
|
| 181 |
+
if err != nil {
|
| 182 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 183 |
+
}
|
| 184 |
+
defer resp.Body.Close()
|
| 185 |
+
|
| 186 |
+
if resp.StatusCode >= 400 {
|
| 187 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 188 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 189 |
+
|
| 190 |
+
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
|
| 191 |
+
if didRefresh {
|
| 192 |
+
return a.RenameProject(projectId, req)
|
| 193 |
+
}
|
| 194 |
+
return apiErr
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
return nil
|
| 198 |
+
}
|
| 199 |
+
func (a *Api) ListPlans(projectIds []string) ([]*shared.Plan, *shared.ApiError) {
|
| 200 |
+
serverUrl := fmt.Sprintf("%s/plans?", GetApiHost())
|
| 201 |
+
parts := []string{}
|
| 202 |
+
for _, projectId := range projectIds {
|
| 203 |
+
parts = append(parts, fmt.Sprintf("projectId=%s", projectId))
|
| 204 |
+
}
|
| 205 |
+
serverUrl += strings.Join(parts, "&")
|
| 206 |
+
|
| 207 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 208 |
+
if err != nil {
|
| 209 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 210 |
+
}
|
| 211 |
+
defer resp.Body.Close()
|
| 212 |
+
|
| 213 |
+
if resp.StatusCode >= 400 {
|
| 214 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 215 |
+
|
| 216 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 217 |
+
|
| 218 |
+
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
|
| 219 |
+
if didRefresh {
|
| 220 |
+
return a.ListPlans(projectIds)
|
| 221 |
+
}
|
| 222 |
+
return nil, apiErr
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
var plans []*shared.Plan
|
| 226 |
+
err = json.NewDecoder(resp.Body).Decode(&plans)
|
| 227 |
+
if err != nil {
|
| 228 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
return plans, nil
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
func (a *Api) ListArchivedPlans(projectIds []string) ([]*shared.Plan, *shared.ApiError) {
|
| 235 |
+
serverUrl := fmt.Sprintf("%s/plans/archive?", GetApiHost())
|
| 236 |
+
parts := []string{}
|
| 237 |
+
for _, projectId := range projectIds {
|
| 238 |
+
parts = append(parts, fmt.Sprintf("projectId=%s", projectId))
|
| 239 |
+
}
|
| 240 |
+
serverUrl += strings.Join(parts, "&")
|
| 241 |
+
|
| 242 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 243 |
+
if err != nil {
|
| 244 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 245 |
+
}
|
| 246 |
+
defer resp.Body.Close()
|
| 247 |
+
|
| 248 |
+
if resp.StatusCode >= 400 {
|
| 249 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 250 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 251 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 252 |
+
if authRefreshed {
|
| 253 |
+
return a.ListArchivedPlans(projectIds)
|
| 254 |
+
}
|
| 255 |
+
return nil, apiErr
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
var plans []*shared.Plan
|
| 259 |
+
err = json.NewDecoder(resp.Body).Decode(&plans)
|
| 260 |
+
if err != nil {
|
| 261 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
return plans, nil
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
func (a *Api) ListPlansRunning(projectIds []string, includeRecent bool) (*shared.ListPlansRunningResponse, *shared.ApiError) {
|
| 268 |
+
serverUrl := fmt.Sprintf("%s/plans/ps?", GetApiHost())
|
| 269 |
+
parts := []string{}
|
| 270 |
+
for _, projectId := range projectIds {
|
| 271 |
+
parts = append(parts, fmt.Sprintf("projectId=%s", projectId))
|
| 272 |
+
}
|
| 273 |
+
serverUrl += strings.Join(parts, "&")
|
| 274 |
+
if includeRecent {
|
| 275 |
+
serverUrl += "&recent=true"
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 279 |
+
if err != nil {
|
| 280 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 281 |
+
}
|
| 282 |
+
defer resp.Body.Close()
|
| 283 |
+
|
| 284 |
+
if resp.StatusCode >= 400 {
|
| 285 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 286 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 287 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 288 |
+
if authRefreshed {
|
| 289 |
+
return a.ListPlansRunning(projectIds, includeRecent)
|
| 290 |
+
}
|
| 291 |
+
return nil, apiErr
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
var respBody *shared.ListPlansRunningResponse
|
| 295 |
+
err = json.NewDecoder(resp.Body).Decode(&respBody)
|
| 296 |
+
if err != nil {
|
| 297 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
return respBody, nil
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
func (a *Api) GetCurrentBranchByPlanId(projectId string, req shared.GetCurrentBranchByPlanIdRequest) (map[string]*shared.Branch, *shared.ApiError) {
|
| 304 |
+
serverUrl := fmt.Sprintf("%s/projects/%s/plans/current_branches", GetApiHost(), projectId)
|
| 305 |
+
|
| 306 |
+
reqBytes, err := json.Marshal(req)
|
| 307 |
+
if err != nil {
|
| 308 |
+
return nil, &shared.ApiError{Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
request, err := http.NewRequest(http.MethodPost, serverUrl, bytes.NewBuffer(reqBytes))
|
| 312 |
+
|
| 313 |
+
if err != nil {
|
| 314 |
+
return nil, &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
request.Header.Set("Content-Type", "application/json")
|
| 318 |
+
|
| 319 |
+
resp, err := authenticatedFastClient.Do(request)
|
| 320 |
+
|
| 321 |
+
if err != nil {
|
| 322 |
+
return nil, &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 323 |
+
}
|
| 324 |
+
defer resp.Body.Close()
|
| 325 |
+
|
| 326 |
+
if resp.StatusCode >= 400 {
|
| 327 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 328 |
+
|
| 329 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 330 |
+
|
| 331 |
+
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
|
| 332 |
+
if didRefresh {
|
| 333 |
+
return a.GetCurrentBranchByPlanId(projectId, req)
|
| 334 |
+
}
|
| 335 |
+
return nil, apiErr
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
var respBody map[string]*shared.Branch
|
| 339 |
+
err = json.NewDecoder(resp.Body).Decode(&respBody)
|
| 340 |
+
if err != nil {
|
| 341 |
+
return nil, &shared.ApiError{Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
return respBody, nil
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
func (a *Api) CreatePlan(projectId string, req shared.CreatePlanRequest) (*shared.CreatePlanResponse, *shared.ApiError) {
|
| 348 |
+
serverUrl := fmt.Sprintf("%s/projects/%s/plans", GetApiHost(), projectId)
|
| 349 |
+
reqBytes, err := json.Marshal(req)
|
| 350 |
+
if err != nil {
|
| 351 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
resp, err := authenticatedFastClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
|
| 355 |
+
if err != nil {
|
| 356 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 357 |
+
}
|
| 358 |
+
defer resp.Body.Close()
|
| 359 |
+
|
| 360 |
+
if resp.StatusCode >= 400 {
|
| 361 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 362 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 363 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 364 |
+
if authRefreshed {
|
| 365 |
+
return a.CreatePlan(projectId, req)
|
| 366 |
+
}
|
| 367 |
+
return nil, apiErr
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
var respBody shared.CreatePlanResponse
|
| 371 |
+
err = json.NewDecoder(resp.Body).Decode(&respBody)
|
| 372 |
+
if err != nil {
|
| 373 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
return &respBody, nil
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
func (a *Api) GetPlan(planId string) (*shared.Plan, *shared.ApiError) {
|
| 380 |
+
serverUrl := fmt.Sprintf("%s/plans/%s", GetApiHost(), planId)
|
| 381 |
+
|
| 382 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 383 |
+
if err != nil {
|
| 384 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
defer resp.Body.Close()
|
| 388 |
+
|
| 389 |
+
if resp.StatusCode >= 400 {
|
| 390 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 391 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 392 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 393 |
+
if authRefreshed {
|
| 394 |
+
return a.GetPlan(planId)
|
| 395 |
+
}
|
| 396 |
+
return nil, apiErr
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
var plan shared.Plan
|
| 400 |
+
err = json.NewDecoder(resp.Body).Decode(&plan)
|
| 401 |
+
if err != nil {
|
| 402 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
return &plan, nil
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
func (a *Api) DeletePlan(planId string) *shared.ApiError {
|
| 409 |
+
serverUrl := fmt.Sprintf("%s/plans/%s", GetApiHost(), planId)
|
| 410 |
+
|
| 411 |
+
req, err := http.NewRequest(http.MethodDelete, serverUrl, nil)
|
| 412 |
+
if err != nil {
|
| 413 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
resp, err := authenticatedFastClient.Do(req)
|
| 417 |
+
if err != nil {
|
| 418 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 419 |
+
}
|
| 420 |
+
defer resp.Body.Close()
|
| 421 |
+
|
| 422 |
+
if resp.StatusCode >= 400 {
|
| 423 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 424 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 425 |
+
|
| 426 |
+
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
|
| 427 |
+
if didRefresh {
|
| 428 |
+
return a.DeletePlan(planId)
|
| 429 |
+
}
|
| 430 |
+
return apiErr
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
return nil
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
func (a *Api) DeleteAllPlans(projectId string) *shared.ApiError {
|
| 437 |
+
serverUrl := fmt.Sprintf("%s/projects/%s/plans", GetApiHost(), projectId)
|
| 438 |
+
|
| 439 |
+
req, err := http.NewRequest(http.MethodDelete, serverUrl, nil)
|
| 440 |
+
if err != nil {
|
| 441 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
resp, err := authenticatedFastClient.Do(req)
|
| 445 |
+
if err != nil {
|
| 446 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 447 |
+
}
|
| 448 |
+
defer resp.Body.Close()
|
| 449 |
+
|
| 450 |
+
if resp.StatusCode >= 400 {
|
| 451 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 452 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 453 |
+
|
| 454 |
+
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
|
| 455 |
+
|
| 456 |
+
if didRefresh {
|
| 457 |
+
return a.DeleteAllPlans(projectId)
|
| 458 |
+
}
|
| 459 |
+
return apiErr
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
return nil
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
func (a *Api) TellPlan(planId, branch string, req shared.TellPlanRequest, onStream types.OnStreamPlan) *shared.ApiError {
|
| 466 |
+
|
| 467 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/tell", GetApiHost(), planId, branch)
|
| 468 |
+
reqBytes, err := json.Marshal(req)
|
| 469 |
+
if err != nil {
|
| 470 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
request, err := http.NewRequest(http.MethodPost, serverUrl, bytes.NewBuffer(reqBytes))
|
| 474 |
+
if err != nil {
|
| 475 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 476 |
+
}
|
| 477 |
+
request.Header.Set("Content-Type", "application/json")
|
| 478 |
+
|
| 479 |
+
var client *http.Client
|
| 480 |
+
if req.ConnectStream {
|
| 481 |
+
client = authenticatedStreamingClient
|
| 482 |
+
} else {
|
| 483 |
+
client = authenticatedFastClient
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
resp, err := client.Do(request)
|
| 487 |
+
if err != nil {
|
| 488 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
if resp.StatusCode >= 400 {
|
| 492 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 493 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 494 |
+
|
| 495 |
+
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
|
| 496 |
+
|
| 497 |
+
if didRefresh {
|
| 498 |
+
return a.TellPlan(planId, branch, req, onStream)
|
| 499 |
+
}
|
| 500 |
+
return apiErr
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
if req.ConnectStream {
|
| 504 |
+
log.Println("Connecting stream")
|
| 505 |
+
connectPlanRespStream(resp.Body, onStream)
|
| 506 |
+
} else {
|
| 507 |
+
// log.Println("Background exec - not connecting stream")
|
| 508 |
+
resp.Body.Close()
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
return nil
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
func (a *Api) BuildPlan(planId, branch string, req shared.BuildPlanRequest, onStream types.OnStreamPlan) *shared.ApiError {
|
| 515 |
+
|
| 516 |
+
log.Println("Calling BuildPlan")
|
| 517 |
+
|
| 518 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/build", GetApiHost(), planId, branch)
|
| 519 |
+
reqBytes, err := json.Marshal(req)
|
| 520 |
+
if err != nil {
|
| 521 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
request, err := http.NewRequest(http.MethodPatch, serverUrl, bytes.NewBuffer(reqBytes))
|
| 525 |
+
if err != nil {
|
| 526 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 527 |
+
}
|
| 528 |
+
request.Header.Set("Content-Type", "application/json")
|
| 529 |
+
|
| 530 |
+
var client *http.Client
|
| 531 |
+
if req.ConnectStream {
|
| 532 |
+
client = authenticatedStreamingClient
|
| 533 |
+
} else {
|
| 534 |
+
client = authenticatedFastClient
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
resp, err := client.Do(request)
|
| 538 |
+
if err != nil {
|
| 539 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
if resp.StatusCode >= 400 {
|
| 543 |
+
log.Println("Error response from build plan", resp.StatusCode)
|
| 544 |
+
|
| 545 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 546 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 547 |
+
|
| 548 |
+
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
|
| 549 |
+
|
| 550 |
+
if didRefresh {
|
| 551 |
+
return a.BuildPlan(planId, branch, req, onStream)
|
| 552 |
+
}
|
| 553 |
+
return apiErr
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
if req.ConnectStream {
|
| 557 |
+
log.Println("Connecting stream")
|
| 558 |
+
connectPlanRespStream(resp.Body, onStream)
|
| 559 |
+
} else {
|
| 560 |
+
// log.Println("Background exec - not connecting stream")
|
| 561 |
+
resp.Body.Close()
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
return nil
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
func (a *Api) RespondMissingFile(planId, branch string, req shared.RespondMissingFileRequest) *shared.ApiError {
|
| 568 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/respond_missing_file", GetApiHost(), planId, branch)
|
| 569 |
+
|
| 570 |
+
reqBytes, err := json.Marshal(req)
|
| 571 |
+
if err != nil {
|
| 572 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
request, err := http.NewRequest(http.MethodPost, serverUrl, bytes.NewBuffer(reqBytes))
|
| 576 |
+
if err != nil {
|
| 577 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 578 |
+
}
|
| 579 |
+
request.Header.Set("Content-Type", "application/json")
|
| 580 |
+
|
| 581 |
+
resp, err := authenticatedFastClient.Do(request)
|
| 582 |
+
if err != nil {
|
| 583 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
if resp.StatusCode >= 400 {
|
| 587 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 588 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 589 |
+
|
| 590 |
+
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
|
| 591 |
+
|
| 592 |
+
if didRefresh {
|
| 593 |
+
return a.RespondMissingFile(planId, branch, req)
|
| 594 |
+
}
|
| 595 |
+
return apiErr
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
return nil
|
| 599 |
+
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
func (a *Api) ConnectPlan(planId, branch string, onStream types.OnStreamPlan) *shared.ApiError {
|
| 603 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/connect", GetApiHost(), planId, branch)
|
| 604 |
+
|
| 605 |
+
req, err := http.NewRequest(http.MethodPatch, serverUrl, nil)
|
| 606 |
+
if err != nil {
|
| 607 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
resp, err := authenticatedStreamingClient.Do(req)
|
| 611 |
+
if err != nil {
|
| 612 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
if resp.StatusCode >= 400 {
|
| 616 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 617 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 618 |
+
|
| 619 |
+
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
|
| 620 |
+
|
| 621 |
+
if didRefresh {
|
| 622 |
+
return a.ConnectPlan(planId, branch, onStream)
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
return apiErr
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
connectPlanRespStream(resp.Body, onStream)
|
| 629 |
+
|
| 630 |
+
return nil
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
func (a *Api) StopPlan(ctx context.Context, planId, branch string) *shared.ApiError {
|
| 634 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/stop", GetApiHost(), planId, branch)
|
| 635 |
+
|
| 636 |
+
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, serverUrl, nil)
|
| 637 |
+
if err != nil {
|
| 638 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
resp, err := authenticatedFastClient.Do(req)
|
| 642 |
+
if err != nil {
|
| 643 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 644 |
+
}
|
| 645 |
+
defer resp.Body.Close()
|
| 646 |
+
|
| 647 |
+
if resp.StatusCode >= 400 {
|
| 648 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 649 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 650 |
+
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
|
| 651 |
+
if didRefresh {
|
| 652 |
+
return a.StopPlan(ctx, planId, branch)
|
| 653 |
+
}
|
| 654 |
+
return apiErr
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
return nil
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
func (a *Api) GetCurrentPlanState(planId, branch string) (*shared.CurrentPlanState, *shared.ApiError) {
|
| 661 |
+
return a.getCurrentPlanState(planId, branch, "")
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
func (a *Api) GetCurrentPlanStateAtSha(planId, sha string) (*shared.CurrentPlanState, *shared.ApiError) {
|
| 665 |
+
return a.getCurrentPlanState(planId, "", sha)
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
func (a *Api) getCurrentPlanState(planId, branch, sha string) (*shared.CurrentPlanState, *shared.ApiError) {
|
| 669 |
+
var serverUrl string
|
| 670 |
+
if sha != "" {
|
| 671 |
+
serverUrl = fmt.Sprintf("%s/plans/%s/current_plan/%s", GetApiHost(), planId, sha)
|
| 672 |
+
} else {
|
| 673 |
+
serverUrl = fmt.Sprintf("%s/plans/%s/%s/current_plan", GetApiHost(), planId, branch)
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 677 |
+
if err != nil {
|
| 678 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 679 |
+
}
|
| 680 |
+
defer resp.Body.Close()
|
| 681 |
+
|
| 682 |
+
if resp.StatusCode >= 400 {
|
| 683 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 684 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 685 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 686 |
+
if authRefreshed {
|
| 687 |
+
return a.getCurrentPlanState(planId, branch, sha)
|
| 688 |
+
}
|
| 689 |
+
return nil, apiErr
|
| 690 |
+
}
|
| 691 |
+
|
| 692 |
+
var state shared.CurrentPlanState
|
| 693 |
+
err = json.NewDecoder(resp.Body).Decode(&state)
|
| 694 |
+
if err != nil {
|
| 695 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 696 |
+
}
|
| 697 |
+
|
| 698 |
+
return &state, nil
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
func (a *Api) ApplyPlan(planId, branch string, req shared.ApplyPlanRequest) (string, *shared.ApiError) {
|
| 702 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/apply", GetApiHost(), planId, branch)
|
| 703 |
+
|
| 704 |
+
reqBytes, err := json.Marshal(req)
|
| 705 |
+
if err != nil {
|
| 706 |
+
return "", &shared.ApiError{Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
request, err := http.NewRequest(http.MethodPatch, serverUrl, bytes.NewBuffer(reqBytes))
|
| 710 |
+
if err != nil {
|
| 711 |
+
return "", &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 712 |
+
}
|
| 713 |
+
request.Header.Set("Content-Type", "application/json")
|
| 714 |
+
|
| 715 |
+
resp, err := authenticatedFastClient.Do(request)
|
| 716 |
+
if err != nil {
|
| 717 |
+
return "", &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 718 |
+
}
|
| 719 |
+
defer resp.Body.Close()
|
| 720 |
+
|
| 721 |
+
if resp.StatusCode >= 400 {
|
| 722 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 723 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 724 |
+
|
| 725 |
+
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
|
| 726 |
+
if didRefresh {
|
| 727 |
+
return a.ApplyPlan(planId, branch, req)
|
| 728 |
+
}
|
| 729 |
+
return "", apiErr
|
| 730 |
+
}
|
| 731 |
+
|
| 732 |
+
// Reading the body on success
|
| 733 |
+
responseData, err := io.ReadAll(resp.Body)
|
| 734 |
+
if err != nil {
|
| 735 |
+
return "", &shared.ApiError{Msg: fmt.Sprintf("error reading response body: %v", err)}
|
| 736 |
+
}
|
| 737 |
+
|
| 738 |
+
return string(responseData), nil
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
func (a *Api) ArchivePlan(planId string) *shared.ApiError {
|
| 742 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/archive", GetApiHost(), planId)
|
| 743 |
+
|
| 744 |
+
req, err := http.NewRequest(http.MethodPatch, serverUrl, nil)
|
| 745 |
+
if err != nil {
|
| 746 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
resp, err := authenticatedFastClient.Do(req)
|
| 750 |
+
if err != nil {
|
| 751 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 752 |
+
}
|
| 753 |
+
defer resp.Body.Close()
|
| 754 |
+
|
| 755 |
+
if resp.StatusCode >= 400 {
|
| 756 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 757 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 758 |
+
|
| 759 |
+
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
|
| 760 |
+
if didRefresh {
|
| 761 |
+
return a.ArchivePlan(planId)
|
| 762 |
+
}
|
| 763 |
+
return apiErr
|
| 764 |
+
}
|
| 765 |
+
|
| 766 |
+
return nil
|
| 767 |
+
}
|
| 768 |
+
|
| 769 |
+
func (a *Api) UnarchivePlan(planId string) *shared.ApiError {
|
| 770 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/unarchive", GetApiHost(), planId)
|
| 771 |
+
|
| 772 |
+
req, err := http.NewRequest(http.MethodPatch, serverUrl, nil)
|
| 773 |
+
if err != nil {
|
| 774 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 775 |
+
}
|
| 776 |
+
|
| 777 |
+
resp, err := authenticatedFastClient.Do(req)
|
| 778 |
+
if err != nil {
|
| 779 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 780 |
+
}
|
| 781 |
+
defer resp.Body.Close()
|
| 782 |
+
|
| 783 |
+
if resp.StatusCode >= 400 {
|
| 784 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 785 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 786 |
+
|
| 787 |
+
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
|
| 788 |
+
if didRefresh {
|
| 789 |
+
return a.ArchivePlan(planId)
|
| 790 |
+
}
|
| 791 |
+
return apiErr
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
return nil
|
| 795 |
+
}
|
| 796 |
+
|
| 797 |
+
func (a *Api) RenamePlan(planId string, name string) *shared.ApiError {
|
| 798 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/rename", GetApiHost(), planId)
|
| 799 |
+
|
| 800 |
+
reqBytes, err := json.Marshal(shared.RenamePlanRequest{Name: name})
|
| 801 |
+
if err != nil {
|
| 802 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 803 |
+
}
|
| 804 |
+
|
| 805 |
+
request, err := http.NewRequest(http.MethodPatch, serverUrl, bytes.NewBuffer(reqBytes))
|
| 806 |
+
|
| 807 |
+
if err != nil {
|
| 808 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 809 |
+
}
|
| 810 |
+
|
| 811 |
+
request.Header.Set("Content-Type", "application/json")
|
| 812 |
+
|
| 813 |
+
resp, err := authenticatedFastClient.Do(request)
|
| 814 |
+
|
| 815 |
+
if err != nil {
|
| 816 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 817 |
+
}
|
| 818 |
+
|
| 819 |
+
defer resp.Body.Close()
|
| 820 |
+
|
| 821 |
+
if resp.StatusCode >= 400 {
|
| 822 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 823 |
+
|
| 824 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 825 |
+
|
| 826 |
+
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
|
| 827 |
+
if didRefresh {
|
| 828 |
+
return a.RenamePlan(planId, name)
|
| 829 |
+
}
|
| 830 |
+
return apiErr
|
| 831 |
+
}
|
| 832 |
+
|
| 833 |
+
return nil
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
func (a *Api) RejectAllChanges(planId, branch string) *shared.ApiError {
|
| 837 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/reject_all", GetApiHost(), planId, branch)
|
| 838 |
+
|
| 839 |
+
req, err := http.NewRequest(http.MethodPatch, serverUrl, nil)
|
| 840 |
+
if err != nil {
|
| 841 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
+
resp, err := authenticatedFastClient.Do(req)
|
| 845 |
+
if err != nil {
|
| 846 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 847 |
+
}
|
| 848 |
+
defer resp.Body.Close()
|
| 849 |
+
|
| 850 |
+
if resp.StatusCode >= 400 {
|
| 851 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 852 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 853 |
+
|
| 854 |
+
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
|
| 855 |
+
if didRefresh {
|
| 856 |
+
return a.RejectAllChanges(planId, branch)
|
| 857 |
+
}
|
| 858 |
+
return apiErr
|
| 859 |
+
}
|
| 860 |
+
|
| 861 |
+
return nil
|
| 862 |
+
}
|
| 863 |
+
|
| 864 |
+
func (a *Api) RejectFile(planId, branch, filePath string) *shared.ApiError {
|
| 865 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/reject_file", GetApiHost(), planId, branch)
|
| 866 |
+
|
| 867 |
+
reqBytes, err := json.Marshal(shared.RejectFileRequest{FilePath: filePath})
|
| 868 |
+
|
| 869 |
+
if err != nil {
|
| 870 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
+
req, err := http.NewRequest(http.MethodPatch, serverUrl, bytes.NewBuffer(reqBytes))
|
| 874 |
+
if err != nil {
|
| 875 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 876 |
+
}
|
| 877 |
+
req.Header.Set("Content-Type", "application/json")
|
| 878 |
+
|
| 879 |
+
resp, err := authenticatedFastClient.Do(req)
|
| 880 |
+
if err != nil {
|
| 881 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 882 |
+
}
|
| 883 |
+
defer resp.Body.Close()
|
| 884 |
+
|
| 885 |
+
if resp.StatusCode >= 400 {
|
| 886 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 887 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 888 |
+
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
|
| 889 |
+
if didRefresh {
|
| 890 |
+
a.RejectFile(planId, branch, filePath)
|
| 891 |
+
}
|
| 892 |
+
return apiErr
|
| 893 |
+
}
|
| 894 |
+
|
| 895 |
+
return nil
|
| 896 |
+
}
|
| 897 |
+
|
| 898 |
+
func (a *Api) RejectFiles(planId, branch string, paths []string) *shared.ApiError {
|
| 899 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/reject_files", GetApiHost(), planId, branch)
|
| 900 |
+
|
| 901 |
+
reqBytes, err := json.Marshal(shared.RejectFilesRequest{Paths: paths})
|
| 902 |
+
|
| 903 |
+
if err != nil {
|
| 904 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 905 |
+
}
|
| 906 |
+
|
| 907 |
+
req, err := http.NewRequest(http.MethodPatch, serverUrl, bytes.NewBuffer(reqBytes))
|
| 908 |
+
if err != nil {
|
| 909 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 910 |
+
}
|
| 911 |
+
req.Header.Set("Content-Type", "application/json")
|
| 912 |
+
|
| 913 |
+
resp, err := authenticatedFastClient.Do(req)
|
| 914 |
+
if err != nil {
|
| 915 |
+
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 916 |
+
}
|
| 917 |
+
defer resp.Body.Close()
|
| 918 |
+
|
| 919 |
+
if resp.StatusCode >= 400 {
|
| 920 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 921 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 922 |
+
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
|
| 923 |
+
if didRefresh {
|
| 924 |
+
a.RejectFiles(planId, branch, paths)
|
| 925 |
+
}
|
| 926 |
+
return apiErr
|
| 927 |
+
}
|
| 928 |
+
|
| 929 |
+
return nil
|
| 930 |
+
}
|
| 931 |
+
|
| 932 |
+
func (a *Api) LoadContext(planId, branch string, req shared.LoadContextRequest) (*shared.LoadContextResponse, *shared.ApiError) {
|
| 933 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/context", GetApiHost(), planId, branch)
|
| 934 |
+
reqBytes, err := json.Marshal(req)
|
| 935 |
+
if err != nil {
|
| 936 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 937 |
+
}
|
| 938 |
+
|
| 939 |
+
// use the slow client since we may be uploading relatively large files
|
| 940 |
+
resp, err := authenticatedSlowClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
|
| 941 |
+
if err != nil {
|
| 942 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 943 |
+
}
|
| 944 |
+
defer resp.Body.Close()
|
| 945 |
+
|
| 946 |
+
if resp.StatusCode >= 400 {
|
| 947 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 948 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 949 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 950 |
+
if authRefreshed {
|
| 951 |
+
return a.LoadContext(planId, branch, req)
|
| 952 |
+
}
|
| 953 |
+
return nil, apiErr
|
| 954 |
+
}
|
| 955 |
+
|
| 956 |
+
var loadContextResponse shared.LoadContextResponse
|
| 957 |
+
err = json.NewDecoder(resp.Body).Decode(&loadContextResponse)
|
| 958 |
+
if err != nil {
|
| 959 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 960 |
+
}
|
| 961 |
+
|
| 962 |
+
return &loadContextResponse, nil
|
| 963 |
+
}
|
| 964 |
+
|
| 965 |
+
func (a *Api) UpdateContext(planId, branch string, req shared.UpdateContextRequest) (*shared.UpdateContextResponse, *shared.ApiError) {
|
| 966 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/context", GetApiHost(), planId, branch)
|
| 967 |
+
|
| 968 |
+
reqBytes, err := json.Marshal(req)
|
| 969 |
+
if err != nil {
|
| 970 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 971 |
+
}
|
| 972 |
+
|
| 973 |
+
request, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))
|
| 974 |
+
|
| 975 |
+
if err != nil {
|
| 976 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 977 |
+
}
|
| 978 |
+
|
| 979 |
+
request.Header.Set("Content-Type", "application/json")
|
| 980 |
+
|
| 981 |
+
// use the slow client since we may be uploading relatively large files
|
| 982 |
+
resp, err := authenticatedSlowClient.Do(request)
|
| 983 |
+
if err != nil {
|
| 984 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 985 |
+
}
|
| 986 |
+
defer resp.Body.Close()
|
| 987 |
+
|
| 988 |
+
if resp.StatusCode >= 400 {
|
| 989 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 990 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 991 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 992 |
+
if authRefreshed {
|
| 993 |
+
return a.UpdateContext(planId, branch, req)
|
| 994 |
+
}
|
| 995 |
+
return nil, apiErr
|
| 996 |
+
}
|
| 997 |
+
|
| 998 |
+
var updateContextResponse shared.UpdateContextResponse
|
| 999 |
+
err = json.NewDecoder(resp.Body).Decode(&updateContextResponse)
|
| 1000 |
+
if err != nil {
|
| 1001 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 1002 |
+
}
|
| 1003 |
+
|
| 1004 |
+
return &updateContextResponse, nil
|
| 1005 |
+
}
|
| 1006 |
+
|
| 1007 |
+
func (a *Api) DeleteContext(planId, branch string, req shared.DeleteContextRequest) (*shared.DeleteContextResponse, *shared.ApiError) {
|
| 1008 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/context", GetApiHost(), planId, branch)
|
| 1009 |
+
reqBytes, err := json.Marshal(req)
|
| 1010 |
+
if err != nil {
|
| 1011 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 1012 |
+
}
|
| 1013 |
+
|
| 1014 |
+
request, err := http.NewRequest(http.MethodDelete, serverUrl, bytes.NewBuffer(reqBytes))
|
| 1015 |
+
if err != nil {
|
| 1016 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 1017 |
+
}
|
| 1018 |
+
request.Header.Set("Content-Type", "application/json")
|
| 1019 |
+
|
| 1020 |
+
resp, err := authenticatedFastClient.Do(request)
|
| 1021 |
+
if err != nil {
|
| 1022 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1023 |
+
}
|
| 1024 |
+
defer resp.Body.Close()
|
| 1025 |
+
|
| 1026 |
+
if resp.StatusCode >= 400 {
|
| 1027 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1028 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1029 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1030 |
+
if authRefreshed {
|
| 1031 |
+
return a.DeleteContext(planId, branch, req)
|
| 1032 |
+
}
|
| 1033 |
+
return nil, apiErr
|
| 1034 |
+
}
|
| 1035 |
+
|
| 1036 |
+
var deleteContextResponse shared.DeleteContextResponse
|
| 1037 |
+
err = json.NewDecoder(resp.Body).Decode(&deleteContextResponse)
|
| 1038 |
+
if err != nil {
|
| 1039 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 1040 |
+
}
|
| 1041 |
+
|
| 1042 |
+
return &deleteContextResponse, nil
|
| 1043 |
+
}
|
| 1044 |
+
|
| 1045 |
+
func (a *Api) ListContext(planId, branch string) ([]*shared.Context, *shared.ApiError) {
|
| 1046 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/context", GetApiHost(), planId, branch)
|
| 1047 |
+
|
| 1048 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 1049 |
+
if err != nil {
|
| 1050 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1051 |
+
}
|
| 1052 |
+
defer resp.Body.Close()
|
| 1053 |
+
|
| 1054 |
+
if resp.StatusCode >= 400 {
|
| 1055 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1056 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1057 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1058 |
+
if authRefreshed {
|
| 1059 |
+
return a.ListContext(planId, branch)
|
| 1060 |
+
}
|
| 1061 |
+
return nil, apiErr
|
| 1062 |
+
}
|
| 1063 |
+
|
| 1064 |
+
var contexts []*shared.Context
|
| 1065 |
+
err = json.NewDecoder(resp.Body).Decode(&contexts)
|
| 1066 |
+
if err != nil {
|
| 1067 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 1068 |
+
}
|
| 1069 |
+
|
| 1070 |
+
return contexts, nil
|
| 1071 |
+
}
|
| 1072 |
+
|
| 1073 |
+
func (a *Api) LoadCachedFileMap(planId, branch string, req shared.LoadCachedFileMapRequest) (*shared.LoadCachedFileMapResponse, *shared.ApiError) {
|
| 1074 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/load_cached_file_map", GetApiHost(), planId, branch)
|
| 1075 |
+
reqBytes, err := json.Marshal(req)
|
| 1076 |
+
if err != nil {
|
| 1077 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 1078 |
+
}
|
| 1079 |
+
|
| 1080 |
+
resp, err := authenticatedFastClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
|
| 1081 |
+
if err != nil {
|
| 1082 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1083 |
+
}
|
| 1084 |
+
defer resp.Body.Close()
|
| 1085 |
+
|
| 1086 |
+
if resp.StatusCode >= 400 {
|
| 1087 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1088 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1089 |
+
return nil, apiErr
|
| 1090 |
+
}
|
| 1091 |
+
|
| 1092 |
+
var loadResp shared.LoadCachedFileMapResponse
|
| 1093 |
+
err = json.NewDecoder(resp.Body).Decode(&loadResp)
|
| 1094 |
+
if err != nil {
|
| 1095 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 1096 |
+
}
|
| 1097 |
+
|
| 1098 |
+
return &loadResp, nil
|
| 1099 |
+
}
|
| 1100 |
+
|
| 1101 |
+
func (a *Api) ListConvo(planId, branch string) ([]*shared.ConvoMessage, *shared.ApiError) {
|
| 1102 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/convo", GetApiHost(), planId, branch)
|
| 1103 |
+
|
| 1104 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 1105 |
+
if err != nil {
|
| 1106 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1107 |
+
}
|
| 1108 |
+
defer resp.Body.Close()
|
| 1109 |
+
|
| 1110 |
+
if resp.StatusCode >= 400 {
|
| 1111 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1112 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1113 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1114 |
+
if authRefreshed {
|
| 1115 |
+
return a.ListConvo(planId, branch)
|
| 1116 |
+
}
|
| 1117 |
+
return nil, apiErr
|
| 1118 |
+
}
|
| 1119 |
+
|
| 1120 |
+
var convos []*shared.ConvoMessage
|
| 1121 |
+
err = json.NewDecoder(resp.Body).Decode(&convos)
|
| 1122 |
+
if err != nil {
|
| 1123 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 1124 |
+
}
|
| 1125 |
+
|
| 1126 |
+
return convos, nil
|
| 1127 |
+
}
|
| 1128 |
+
|
| 1129 |
+
func (a *Api) GetPlanStatus(planId, branch string) (string, *shared.ApiError) {
|
| 1130 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/status", GetApiHost(), planId, branch)
|
| 1131 |
+
|
| 1132 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 1133 |
+
if err != nil {
|
| 1134 |
+
return "", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1135 |
+
}
|
| 1136 |
+
defer resp.Body.Close()
|
| 1137 |
+
|
| 1138 |
+
if resp.StatusCode >= 400 {
|
| 1139 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1140 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1141 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1142 |
+
if authRefreshed {
|
| 1143 |
+
return a.GetPlanStatus(planId, branch)
|
| 1144 |
+
}
|
| 1145 |
+
return "", apiErr
|
| 1146 |
+
}
|
| 1147 |
+
|
| 1148 |
+
body, err := io.ReadAll(resp.Body)
|
| 1149 |
+
|
| 1150 |
+
if err != nil {
|
| 1151 |
+
return "", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error reading response body: %v", err)}
|
| 1152 |
+
}
|
| 1153 |
+
|
| 1154 |
+
return string(body), nil
|
| 1155 |
+
}
|
| 1156 |
+
|
| 1157 |
+
func (a *Api) GetPlanDiffs(planId, branch string, plain bool) (string, *shared.ApiError) {
|
| 1158 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/diffs", GetApiHost(), planId, branch)
|
| 1159 |
+
|
| 1160 |
+
if plain {
|
| 1161 |
+
serverUrl += "?plain=true"
|
| 1162 |
+
}
|
| 1163 |
+
|
| 1164 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 1165 |
+
if err != nil {
|
| 1166 |
+
return "", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1167 |
+
}
|
| 1168 |
+
defer resp.Body.Close()
|
| 1169 |
+
|
| 1170 |
+
if resp.StatusCode >= 400 {
|
| 1171 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1172 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1173 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1174 |
+
if authRefreshed {
|
| 1175 |
+
return a.GetPlanDiffs(planId, branch, plain)
|
| 1176 |
+
}
|
| 1177 |
+
return "", apiErr
|
| 1178 |
+
}
|
| 1179 |
+
|
| 1180 |
+
body, err := io.ReadAll(resp.Body)
|
| 1181 |
+
|
| 1182 |
+
if err != nil {
|
| 1183 |
+
return "", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error reading response body: %v", err)}
|
| 1184 |
+
}
|
| 1185 |
+
|
| 1186 |
+
return string(body), nil
|
| 1187 |
+
}
|
| 1188 |
+
|
| 1189 |
+
func (a *Api) ListLogs(planId, branch string) (*shared.LogResponse, *shared.ApiError) {
|
| 1190 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/logs", GetApiHost(), planId, branch)
|
| 1191 |
+
|
| 1192 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 1193 |
+
if err != nil {
|
| 1194 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1195 |
+
}
|
| 1196 |
+
|
| 1197 |
+
defer resp.Body.Close()
|
| 1198 |
+
|
| 1199 |
+
if resp.StatusCode >= 400 {
|
| 1200 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1201 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1202 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1203 |
+
if authRefreshed {
|
| 1204 |
+
return a.ListLogs(planId, branch)
|
| 1205 |
+
}
|
| 1206 |
+
return nil, apiErr
|
| 1207 |
+
}
|
| 1208 |
+
|
| 1209 |
+
var logs shared.LogResponse
|
| 1210 |
+
err = json.NewDecoder(resp.Body).Decode(&logs)
|
| 1211 |
+
if err != nil {
|
| 1212 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 1213 |
+
}
|
| 1214 |
+
|
| 1215 |
+
return &logs, nil
|
| 1216 |
+
}
|
| 1217 |
+
|
| 1218 |
+
func (a *Api) RewindPlan(planId, branch string, req shared.RewindPlanRequest) (*shared.RewindPlanResponse, *shared.ApiError) {
|
| 1219 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/rewind", GetApiHost(), planId, branch)
|
| 1220 |
+
reqBytes, err := json.Marshal(req)
|
| 1221 |
+
if err != nil {
|
| 1222 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 1223 |
+
}
|
| 1224 |
+
|
| 1225 |
+
request, err := http.NewRequest(http.MethodPatch, serverUrl, bytes.NewBuffer(reqBytes))
|
| 1226 |
+
if err != nil {
|
| 1227 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 1228 |
+
}
|
| 1229 |
+
request.Header.Set("Content-Type", "application/json")
|
| 1230 |
+
|
| 1231 |
+
resp, err := authenticatedFastClient.Do(request)
|
| 1232 |
+
if err != nil {
|
| 1233 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1234 |
+
}
|
| 1235 |
+
|
| 1236 |
+
defer resp.Body.Close()
|
| 1237 |
+
|
| 1238 |
+
if resp.StatusCode >= 400 {
|
| 1239 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1240 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1241 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1242 |
+
if authRefreshed {
|
| 1243 |
+
return a.RewindPlan(planId, branch, req)
|
| 1244 |
+
}
|
| 1245 |
+
return nil, apiErr
|
| 1246 |
+
}
|
| 1247 |
+
|
| 1248 |
+
var rewindPlanResponse shared.RewindPlanResponse
|
| 1249 |
+
err = json.NewDecoder(resp.Body).Decode(&rewindPlanResponse)
|
| 1250 |
+
if err != nil {
|
| 1251 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 1252 |
+
}
|
| 1253 |
+
|
| 1254 |
+
return &rewindPlanResponse, nil
|
| 1255 |
+
}
|
| 1256 |
+
|
| 1257 |
+
func (a *Api) SignIn(req shared.SignInRequest, customHost string) (*shared.SessionResponse, *shared.ApiError) {
|
| 1258 |
+
host := customHost
|
| 1259 |
+
if host == "" {
|
| 1260 |
+
host = CloudApiHost
|
| 1261 |
+
}
|
| 1262 |
+
serverUrl := host + "/accounts/sign_in"
|
| 1263 |
+
reqBytes, err := json.Marshal(req)
|
| 1264 |
+
if err != nil {
|
| 1265 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 1266 |
+
}
|
| 1267 |
+
|
| 1268 |
+
resp, err := unauthenticatedClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
|
| 1269 |
+
if err != nil {
|
| 1270 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1271 |
+
}
|
| 1272 |
+
defer resp.Body.Close()
|
| 1273 |
+
|
| 1274 |
+
if resp.StatusCode >= 400 {
|
| 1275 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1276 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1277 |
+
return nil, apiErr
|
| 1278 |
+
}
|
| 1279 |
+
|
| 1280 |
+
var sessionResponse shared.SessionResponse
|
| 1281 |
+
err = json.NewDecoder(resp.Body).Decode(&sessionResponse)
|
| 1282 |
+
if err != nil {
|
| 1283 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 1284 |
+
}
|
| 1285 |
+
|
| 1286 |
+
return &sessionResponse, nil
|
| 1287 |
+
}
|
| 1288 |
+
|
| 1289 |
+
func (a *Api) CreateAccount(req shared.CreateAccountRequest, customHost string) (*shared.SessionResponse, *shared.ApiError) {
|
| 1290 |
+
host := customHost
|
| 1291 |
+
if host == "" {
|
| 1292 |
+
host = CloudApiHost
|
| 1293 |
+
}
|
| 1294 |
+
serverUrl := host + "/accounts"
|
| 1295 |
+
reqBytes, err := json.Marshal(req)
|
| 1296 |
+
if err != nil {
|
| 1297 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 1298 |
+
}
|
| 1299 |
+
|
| 1300 |
+
resp, err := unauthenticatedClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
|
| 1301 |
+
if err != nil {
|
| 1302 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1303 |
+
}
|
| 1304 |
+
defer resp.Body.Close()
|
| 1305 |
+
|
| 1306 |
+
if resp.StatusCode >= 400 {
|
| 1307 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1308 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1309 |
+
return nil, apiErr
|
| 1310 |
+
}
|
| 1311 |
+
|
| 1312 |
+
var sessionResponse shared.SessionResponse
|
| 1313 |
+
err = json.NewDecoder(resp.Body).Decode(&sessionResponse)
|
| 1314 |
+
if err != nil {
|
| 1315 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 1316 |
+
}
|
| 1317 |
+
|
| 1318 |
+
return &sessionResponse, nil
|
| 1319 |
+
}
|
| 1320 |
+
|
| 1321 |
+
func (a *Api) CreateOrg(req shared.CreateOrgRequest) (*shared.CreateOrgResponse, *shared.ApiError) {
|
| 1322 |
+
serverUrl := GetApiHost() + "/orgs"
|
| 1323 |
+
reqBytes, err := json.Marshal(req)
|
| 1324 |
+
if err != nil {
|
| 1325 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 1326 |
+
}
|
| 1327 |
+
|
| 1328 |
+
resp, err := authenticatedFastClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
|
| 1329 |
+
if err != nil {
|
| 1330 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1331 |
+
}
|
| 1332 |
+
defer resp.Body.Close()
|
| 1333 |
+
|
| 1334 |
+
if resp.StatusCode >= 400 {
|
| 1335 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1336 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1337 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1338 |
+
if authRefreshed {
|
| 1339 |
+
return a.CreateOrg(req)
|
| 1340 |
+
}
|
| 1341 |
+
return nil, apiErr
|
| 1342 |
+
}
|
| 1343 |
+
|
| 1344 |
+
var createOrgResponse shared.CreateOrgResponse
|
| 1345 |
+
err = json.NewDecoder(resp.Body).Decode(&createOrgResponse)
|
| 1346 |
+
if err != nil {
|
| 1347 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 1348 |
+
}
|
| 1349 |
+
|
| 1350 |
+
return &createOrgResponse, nil
|
| 1351 |
+
}
|
| 1352 |
+
|
| 1353 |
+
func (a *Api) GetOrgSession() (*shared.Org, *shared.ApiError) {
|
| 1354 |
+
serverUrl := GetApiHost() + "/orgs/session"
|
| 1355 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 1356 |
+
if err != nil {
|
| 1357 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1358 |
+
}
|
| 1359 |
+
defer resp.Body.Close()
|
| 1360 |
+
|
| 1361 |
+
if resp.StatusCode >= 400 {
|
| 1362 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1363 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1364 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1365 |
+
if authRefreshed {
|
| 1366 |
+
return a.GetOrgSession()
|
| 1367 |
+
}
|
| 1368 |
+
return nil, apiErr
|
| 1369 |
+
}
|
| 1370 |
+
|
| 1371 |
+
var org *shared.Org
|
| 1372 |
+
|
| 1373 |
+
err = json.NewDecoder(resp.Body).Decode(&org)
|
| 1374 |
+
|
| 1375 |
+
if err != nil {
|
| 1376 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 1377 |
+
}
|
| 1378 |
+
|
| 1379 |
+
return org, nil
|
| 1380 |
+
}
|
| 1381 |
+
|
| 1382 |
+
func (a *Api) ListOrgs() ([]*shared.Org, *shared.ApiError) {
|
| 1383 |
+
serverUrl := GetApiHost() + "/orgs"
|
| 1384 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 1385 |
+
if err != nil {
|
| 1386 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1387 |
+
}
|
| 1388 |
+
defer resp.Body.Close()
|
| 1389 |
+
|
| 1390 |
+
if resp.StatusCode >= 400 {
|
| 1391 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1392 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1393 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1394 |
+
if authRefreshed {
|
| 1395 |
+
return a.ListOrgs()
|
| 1396 |
+
}
|
| 1397 |
+
return nil, apiErr
|
| 1398 |
+
}
|
| 1399 |
+
|
| 1400 |
+
var orgs []*shared.Org
|
| 1401 |
+
err = json.NewDecoder(resp.Body).Decode(&orgs)
|
| 1402 |
+
if err != nil {
|
| 1403 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 1404 |
+
}
|
| 1405 |
+
|
| 1406 |
+
return orgs, nil
|
| 1407 |
+
}
|
| 1408 |
+
|
| 1409 |
+
func (a *Api) GetOrgUserConfig() (*shared.OrgUserConfig, *shared.ApiError) {
|
| 1410 |
+
serverUrl := GetApiHost() + "/org_user_config"
|
| 1411 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 1412 |
+
if err != nil {
|
| 1413 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1414 |
+
}
|
| 1415 |
+
defer resp.Body.Close()
|
| 1416 |
+
|
| 1417 |
+
if resp.StatusCode >= 400 {
|
| 1418 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1419 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1420 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1421 |
+
if authRefreshed {
|
| 1422 |
+
return a.GetOrgUserConfig()
|
| 1423 |
+
}
|
| 1424 |
+
return nil, apiErr
|
| 1425 |
+
}
|
| 1426 |
+
|
| 1427 |
+
var orgUserConfig shared.OrgUserConfig
|
| 1428 |
+
err = json.NewDecoder(resp.Body).Decode(&orgUserConfig)
|
| 1429 |
+
if err != nil {
|
| 1430 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 1431 |
+
}
|
| 1432 |
+
|
| 1433 |
+
return &orgUserConfig, nil
|
| 1434 |
+
}
|
| 1435 |
+
|
| 1436 |
+
func (a *Api) UpdateOrgUserConfig(c shared.OrgUserConfig) *shared.ApiError {
|
| 1437 |
+
serverUrl := GetApiHost() + "/org_user_config"
|
| 1438 |
+
|
| 1439 |
+
reqBytes, err := json.Marshal(c)
|
| 1440 |
+
|
| 1441 |
+
if err != nil {
|
| 1442 |
+
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 1443 |
+
}
|
| 1444 |
+
|
| 1445 |
+
request, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))
|
| 1446 |
+
if err != nil {
|
| 1447 |
+
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 1448 |
+
}
|
| 1449 |
+
|
| 1450 |
+
request.Header.Set("Content-Type", "application/json")
|
| 1451 |
+
|
| 1452 |
+
resp, err := authenticatedFastClient.Do(request)
|
| 1453 |
+
if err != nil {
|
| 1454 |
+
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1455 |
+
}
|
| 1456 |
+
defer resp.Body.Close()
|
| 1457 |
+
|
| 1458 |
+
if resp.StatusCode >= 400 {
|
| 1459 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1460 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1461 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1462 |
+
if authRefreshed {
|
| 1463 |
+
return a.UpdateOrgUserConfig(c)
|
| 1464 |
+
}
|
| 1465 |
+
return apiErr
|
| 1466 |
+
}
|
| 1467 |
+
|
| 1468 |
+
return nil
|
| 1469 |
+
}
|
| 1470 |
+
|
| 1471 |
+
func (a *Api) DeleteUser(userId string) *shared.ApiError {
|
| 1472 |
+
serverUrl := fmt.Sprintf("%s/orgs/users/%s", GetApiHost(), userId)
|
| 1473 |
+
req, err := http.NewRequest(http.MethodDelete, serverUrl, nil)
|
| 1474 |
+
if err != nil {
|
| 1475 |
+
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 1476 |
+
}
|
| 1477 |
+
|
| 1478 |
+
resp, err := authenticatedFastClient.Do(req)
|
| 1479 |
+
if err != nil {
|
| 1480 |
+
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1481 |
+
}
|
| 1482 |
+
defer resp.Body.Close()
|
| 1483 |
+
|
| 1484 |
+
if resp.StatusCode >= 400 {
|
| 1485 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1486 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1487 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1488 |
+
if authRefreshed {
|
| 1489 |
+
return a.DeleteUser(userId)
|
| 1490 |
+
}
|
| 1491 |
+
return apiErr
|
| 1492 |
+
}
|
| 1493 |
+
|
| 1494 |
+
return nil
|
| 1495 |
+
}
|
| 1496 |
+
|
| 1497 |
+
func (a *Api) ListOrgRoles() ([]*shared.OrgRole, *shared.ApiError) {
|
| 1498 |
+
serverUrl := GetApiHost() + "/orgs/roles"
|
| 1499 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 1500 |
+
if err != nil {
|
| 1501 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %s", err)}
|
| 1502 |
+
}
|
| 1503 |
+
defer resp.Body.Close()
|
| 1504 |
+
|
| 1505 |
+
if resp.StatusCode >= 400 {
|
| 1506 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1507 |
+
|
| 1508 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1509 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1510 |
+
if authRefreshed {
|
| 1511 |
+
return a.ListOrgRoles()
|
| 1512 |
+
}
|
| 1513 |
+
return nil, apiErr
|
| 1514 |
+
}
|
| 1515 |
+
|
| 1516 |
+
var roles []*shared.OrgRole
|
| 1517 |
+
err = json.NewDecoder(resp.Body).Decode(&roles)
|
| 1518 |
+
if err != nil {
|
| 1519 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %s", err)}
|
| 1520 |
+
}
|
| 1521 |
+
|
| 1522 |
+
return roles, nil
|
| 1523 |
+
}
|
| 1524 |
+
|
| 1525 |
+
func (a *Api) InviteUser(req shared.InviteRequest) *shared.ApiError {
|
| 1526 |
+
serverUrl := GetApiHost() + "/invites"
|
| 1527 |
+
reqBytes, err := json.Marshal(req)
|
| 1528 |
+
if err != nil {
|
| 1529 |
+
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 1530 |
+
}
|
| 1531 |
+
|
| 1532 |
+
resp, err := authenticatedFastClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
|
| 1533 |
+
if err != nil {
|
| 1534 |
+
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1535 |
+
}
|
| 1536 |
+
defer resp.Body.Close()
|
| 1537 |
+
|
| 1538 |
+
if resp.StatusCode >= 400 {
|
| 1539 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1540 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1541 |
+
|
| 1542 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1543 |
+
if authRefreshed {
|
| 1544 |
+
return a.InviteUser(req)
|
| 1545 |
+
}
|
| 1546 |
+
return apiErr
|
| 1547 |
+
}
|
| 1548 |
+
|
| 1549 |
+
return nil
|
| 1550 |
+
}
|
| 1551 |
+
|
| 1552 |
+
func (a *Api) ListPendingInvites() ([]*shared.Invite, *shared.ApiError) {
|
| 1553 |
+
serverUrl := GetApiHost() + "/invites/pending"
|
| 1554 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 1555 |
+
if err != nil {
|
| 1556 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1557 |
+
}
|
| 1558 |
+
defer resp.Body.Close()
|
| 1559 |
+
|
| 1560 |
+
if resp.StatusCode >= 400 {
|
| 1561 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1562 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1563 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1564 |
+
if authRefreshed {
|
| 1565 |
+
return a.ListPendingInvites()
|
| 1566 |
+
}
|
| 1567 |
+
return nil, apiErr
|
| 1568 |
+
}
|
| 1569 |
+
|
| 1570 |
+
var invites []*shared.Invite
|
| 1571 |
+
err = json.NewDecoder(resp.Body).Decode(&invites)
|
| 1572 |
+
if err != nil {
|
| 1573 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 1574 |
+
}
|
| 1575 |
+
|
| 1576 |
+
return invites, nil
|
| 1577 |
+
}
|
| 1578 |
+
|
| 1579 |
+
func (a *Api) ListAcceptedInvites() ([]*shared.Invite, *shared.ApiError) {
|
| 1580 |
+
serverUrl := GetApiHost() + "/invites/accepted"
|
| 1581 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 1582 |
+
if err != nil {
|
| 1583 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1584 |
+
}
|
| 1585 |
+
defer resp.Body.Close()
|
| 1586 |
+
|
| 1587 |
+
if resp.StatusCode >= 400 {
|
| 1588 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1589 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1590 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1591 |
+
if authRefreshed {
|
| 1592 |
+
return a.ListAcceptedInvites()
|
| 1593 |
+
}
|
| 1594 |
+
return nil, apiErr
|
| 1595 |
+
}
|
| 1596 |
+
|
| 1597 |
+
var invites []*shared.Invite
|
| 1598 |
+
err = json.NewDecoder(resp.Body).Decode(&invites)
|
| 1599 |
+
if err != nil {
|
| 1600 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 1601 |
+
}
|
| 1602 |
+
|
| 1603 |
+
return invites, nil
|
| 1604 |
+
}
|
| 1605 |
+
|
| 1606 |
+
func (a *Api) ListAllInvites() ([]*shared.Invite, *shared.ApiError) {
|
| 1607 |
+
serverUrl := GetApiHost() + "/invites/all"
|
| 1608 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 1609 |
+
if err != nil {
|
| 1610 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1611 |
+
}
|
| 1612 |
+
defer resp.Body.Close()
|
| 1613 |
+
|
| 1614 |
+
if resp.StatusCode >= 400 {
|
| 1615 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1616 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1617 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1618 |
+
if authRefreshed {
|
| 1619 |
+
return a.ListAllInvites()
|
| 1620 |
+
}
|
| 1621 |
+
return nil, apiErr
|
| 1622 |
+
}
|
| 1623 |
+
|
| 1624 |
+
var invites []*shared.Invite
|
| 1625 |
+
err = json.NewDecoder(resp.Body).Decode(&invites)
|
| 1626 |
+
if err != nil {
|
| 1627 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 1628 |
+
}
|
| 1629 |
+
|
| 1630 |
+
return invites, nil
|
| 1631 |
+
}
|
| 1632 |
+
|
| 1633 |
+
func (a *Api) DeleteInvite(inviteId string) *shared.ApiError {
|
| 1634 |
+
serverUrl := fmt.Sprintf("%s/invites/%s", GetApiHost(), inviteId)
|
| 1635 |
+
req, err := http.NewRequest(http.MethodDelete, serverUrl, nil)
|
| 1636 |
+
if err != nil {
|
| 1637 |
+
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 1638 |
+
}
|
| 1639 |
+
|
| 1640 |
+
resp, err := authenticatedFastClient.Do(req)
|
| 1641 |
+
if err != nil {
|
| 1642 |
+
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1643 |
+
}
|
| 1644 |
+
defer resp.Body.Close()
|
| 1645 |
+
|
| 1646 |
+
if resp.StatusCode >= 400 {
|
| 1647 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1648 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1649 |
+
|
| 1650 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1651 |
+
if authRefreshed {
|
| 1652 |
+
return a.DeleteInvite(inviteId)
|
| 1653 |
+
}
|
| 1654 |
+
return apiErr
|
| 1655 |
+
}
|
| 1656 |
+
|
| 1657 |
+
return nil
|
| 1658 |
+
}
|
| 1659 |
+
|
| 1660 |
+
func (a *Api) CreateEmailVerification(email, customHost, userId string) (*shared.CreateEmailVerificationResponse, *shared.ApiError) {
|
| 1661 |
+
host := customHost
|
| 1662 |
+
if host == "" {
|
| 1663 |
+
host = CloudApiHost
|
| 1664 |
+
}
|
| 1665 |
+
serverUrl := host + "/accounts/email_verifications"
|
| 1666 |
+
req := shared.CreateEmailVerificationRequest{Email: email, UserId: userId}
|
| 1667 |
+
reqBytes, err := json.Marshal(req)
|
| 1668 |
+
if err != nil {
|
| 1669 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 1670 |
+
}
|
| 1671 |
+
|
| 1672 |
+
resp, err := unauthenticatedClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
|
| 1673 |
+
if err != nil {
|
| 1674 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1675 |
+
}
|
| 1676 |
+
defer resp.Body.Close()
|
| 1677 |
+
|
| 1678 |
+
if resp.StatusCode >= 400 {
|
| 1679 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1680 |
+
return nil, HandleApiError(resp, errorBody)
|
| 1681 |
+
}
|
| 1682 |
+
|
| 1683 |
+
var verificationResponse shared.CreateEmailVerificationResponse
|
| 1684 |
+
err = json.NewDecoder(resp.Body).Decode(&verificationResponse)
|
| 1685 |
+
if err != nil {
|
| 1686 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 1687 |
+
}
|
| 1688 |
+
|
| 1689 |
+
return &verificationResponse, nil
|
| 1690 |
+
}
|
| 1691 |
+
|
| 1692 |
+
func (a *Api) CreateSignInCode() (string, *shared.ApiError) {
|
| 1693 |
+
serverUrl := GetApiHost() + "/accounts/sign_in_codes"
|
| 1694 |
+
resp, err := authenticatedFastClient.Post(serverUrl, "application/json", nil)
|
| 1695 |
+
if err != nil {
|
| 1696 |
+
return "", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1697 |
+
}
|
| 1698 |
+
defer resp.Body.Close()
|
| 1699 |
+
|
| 1700 |
+
if resp.StatusCode >= 400 {
|
| 1701 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1702 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1703 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1704 |
+
if authRefreshed {
|
| 1705 |
+
return a.CreateSignInCode()
|
| 1706 |
+
}
|
| 1707 |
+
return "", apiErr
|
| 1708 |
+
}
|
| 1709 |
+
|
| 1710 |
+
var signInCode string
|
| 1711 |
+
body, err := io.ReadAll(resp.Body)
|
| 1712 |
+
if err != nil {
|
| 1713 |
+
return "", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error reading response body: %v", err)}
|
| 1714 |
+
}
|
| 1715 |
+
signInCode = string(body)
|
| 1716 |
+
|
| 1717 |
+
return signInCode, nil
|
| 1718 |
+
}
|
| 1719 |
+
|
| 1720 |
+
func (a *Api) SignOut() *shared.ApiError {
|
| 1721 |
+
serverUrl := GetApiHost() + "/accounts/sign_out"
|
| 1722 |
+
|
| 1723 |
+
req, err := http.NewRequest(http.MethodPost, serverUrl, nil)
|
| 1724 |
+
if err != nil {
|
| 1725 |
+
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 1726 |
+
}
|
| 1727 |
+
|
| 1728 |
+
resp, err := authenticatedFastClient.Do(req)
|
| 1729 |
+
if err != nil {
|
| 1730 |
+
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1731 |
+
}
|
| 1732 |
+
defer resp.Body.Close()
|
| 1733 |
+
|
| 1734 |
+
if resp.StatusCode >= 400 {
|
| 1735 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1736 |
+
return HandleApiError(resp, errorBody)
|
| 1737 |
+
}
|
| 1738 |
+
|
| 1739 |
+
return nil
|
| 1740 |
+
}
|
| 1741 |
+
|
| 1742 |
+
func (a *Api) ListUsers() (*shared.ListUsersResponse, *shared.ApiError) {
|
| 1743 |
+
serverUrl := GetApiHost() + "/users"
|
| 1744 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 1745 |
+
if err != nil {
|
| 1746 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 1747 |
+
}
|
| 1748 |
+
defer resp.Body.Close()
|
| 1749 |
+
|
| 1750 |
+
if resp.StatusCode >= 400 {
|
| 1751 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1752 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1753 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1754 |
+
if authRefreshed {
|
| 1755 |
+
return a.ListUsers()
|
| 1756 |
+
}
|
| 1757 |
+
return nil, apiErr
|
| 1758 |
+
}
|
| 1759 |
+
|
| 1760 |
+
var r *shared.ListUsersResponse
|
| 1761 |
+
err = json.NewDecoder(resp.Body).Decode(&r)
|
| 1762 |
+
if err != nil {
|
| 1763 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 1764 |
+
}
|
| 1765 |
+
|
| 1766 |
+
return r, nil
|
| 1767 |
+
}
|
| 1768 |
+
|
| 1769 |
+
func (a *Api) ListBranches(planId string) ([]*shared.Branch, *shared.ApiError) {
|
| 1770 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/branches", GetApiHost(), planId)
|
| 1771 |
+
|
| 1772 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 1773 |
+
if err != nil {
|
| 1774 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %s", err)}
|
| 1775 |
+
}
|
| 1776 |
+
defer resp.Body.Close()
|
| 1777 |
+
|
| 1778 |
+
if resp.StatusCode >= 400 {
|
| 1779 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1780 |
+
|
| 1781 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1782 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1783 |
+
if authRefreshed {
|
| 1784 |
+
return a.ListBranches(planId)
|
| 1785 |
+
}
|
| 1786 |
+
return nil, apiErr
|
| 1787 |
+
}
|
| 1788 |
+
|
| 1789 |
+
var branches []*shared.Branch
|
| 1790 |
+
err = json.NewDecoder(resp.Body).Decode(&branches)
|
| 1791 |
+
if err != nil {
|
| 1792 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %s", err)}
|
| 1793 |
+
}
|
| 1794 |
+
|
| 1795 |
+
return branches, nil
|
| 1796 |
+
}
|
| 1797 |
+
|
| 1798 |
+
func (a *Api) CreateBranch(planId, branch string, req shared.CreateBranchRequest) *shared.ApiError {
|
| 1799 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/branches", GetApiHost(), planId, branch)
|
| 1800 |
+
|
| 1801 |
+
reqBytes, err := json.Marshal(req)
|
| 1802 |
+
if err != nil {
|
| 1803 |
+
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %s", err)}
|
| 1804 |
+
}
|
| 1805 |
+
|
| 1806 |
+
resp, err := authenticatedFastClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
|
| 1807 |
+
if err != nil {
|
| 1808 |
+
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %s", err)}
|
| 1809 |
+
}
|
| 1810 |
+
defer resp.Body.Close()
|
| 1811 |
+
|
| 1812 |
+
if resp.StatusCode >= 400 {
|
| 1813 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1814 |
+
|
| 1815 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1816 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1817 |
+
if authRefreshed {
|
| 1818 |
+
return a.CreateBranch(planId, branch, req)
|
| 1819 |
+
}
|
| 1820 |
+
return apiErr
|
| 1821 |
+
}
|
| 1822 |
+
|
| 1823 |
+
return nil
|
| 1824 |
+
}
|
| 1825 |
+
|
| 1826 |
+
func (a *Api) DeleteBranch(planId, branch string) *shared.ApiError {
|
| 1827 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/branches/%s", GetApiHost(), planId, branch)
|
| 1828 |
+
|
| 1829 |
+
req, err := http.NewRequest(http.MethodDelete, serverUrl, nil)
|
| 1830 |
+
if err != nil {
|
| 1831 |
+
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %s", err)}
|
| 1832 |
+
}
|
| 1833 |
+
|
| 1834 |
+
resp, err := authenticatedFastClient.Do(req)
|
| 1835 |
+
if err != nil {
|
| 1836 |
+
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %s", err)}
|
| 1837 |
+
}
|
| 1838 |
+
defer resp.Body.Close()
|
| 1839 |
+
|
| 1840 |
+
if resp.StatusCode >= 400 {
|
| 1841 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1842 |
+
|
| 1843 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1844 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1845 |
+
if authRefreshed {
|
| 1846 |
+
return a.DeleteBranch(planId, branch)
|
| 1847 |
+
}
|
| 1848 |
+
return apiErr
|
| 1849 |
+
}
|
| 1850 |
+
|
| 1851 |
+
return nil
|
| 1852 |
+
}
|
| 1853 |
+
|
| 1854 |
+
func (a *Api) GetSettings(planId, branch string) (*shared.PlanSettings, *shared.ApiError) {
|
| 1855 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/settings", GetApiHost(), planId, branch)
|
| 1856 |
+
|
| 1857 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 1858 |
+
|
| 1859 |
+
if err != nil {
|
| 1860 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %s", err)}
|
| 1861 |
+
}
|
| 1862 |
+
defer resp.Body.Close()
|
| 1863 |
+
|
| 1864 |
+
if resp.StatusCode >= 400 {
|
| 1865 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1866 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1867 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1868 |
+
if authRefreshed {
|
| 1869 |
+
return a.GetSettings(planId, branch)
|
| 1870 |
+
}
|
| 1871 |
+
return nil, apiErr
|
| 1872 |
+
}
|
| 1873 |
+
|
| 1874 |
+
var settings shared.PlanSettings
|
| 1875 |
+
err = json.NewDecoder(resp.Body).Decode(&settings)
|
| 1876 |
+
if err != nil {
|
| 1877 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %s", err)}
|
| 1878 |
+
}
|
| 1879 |
+
|
| 1880 |
+
return &settings, nil
|
| 1881 |
+
}
|
| 1882 |
+
|
| 1883 |
+
func (a *Api) UpdateSettings(planId, branch string, req shared.UpdateSettingsRequest) (*shared.UpdateSettingsResponse, *shared.ApiError) {
|
| 1884 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/settings", GetApiHost(), planId, branch)
|
| 1885 |
+
|
| 1886 |
+
reqBytes, err := json.Marshal(req)
|
| 1887 |
+
if err != nil {
|
| 1888 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %s", err)}
|
| 1889 |
+
}
|
| 1890 |
+
|
| 1891 |
+
// log.Println("UpdateSettings", string(reqBytes))
|
| 1892 |
+
|
| 1893 |
+
request, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))
|
| 1894 |
+
if err != nil {
|
| 1895 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %s", err)}
|
| 1896 |
+
}
|
| 1897 |
+
request.Header.Set("Content-Type", "application/json")
|
| 1898 |
+
|
| 1899 |
+
resp, err := authenticatedFastClient.Do(request)
|
| 1900 |
+
if err != nil {
|
| 1901 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %s", err)}
|
| 1902 |
+
}
|
| 1903 |
+
defer resp.Body.Close()
|
| 1904 |
+
|
| 1905 |
+
if resp.StatusCode >= 400 {
|
| 1906 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1907 |
+
|
| 1908 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1909 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1910 |
+
if authRefreshed {
|
| 1911 |
+
return a.UpdateSettings(planId, branch, req)
|
| 1912 |
+
}
|
| 1913 |
+
return nil, apiErr
|
| 1914 |
+
}
|
| 1915 |
+
|
| 1916 |
+
var updateRes shared.UpdateSettingsResponse
|
| 1917 |
+
err = json.NewDecoder(resp.Body).Decode(&updateRes)
|
| 1918 |
+
if err != nil {
|
| 1919 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %s", err)}
|
| 1920 |
+
}
|
| 1921 |
+
|
| 1922 |
+
return &updateRes, nil
|
| 1923 |
+
|
| 1924 |
+
}
|
| 1925 |
+
|
| 1926 |
+
func (a *Api) GetOrgDefaultSettings() (*shared.PlanSettings, *shared.ApiError) {
|
| 1927 |
+
serverUrl := fmt.Sprintf("%s/default_settings", GetApiHost())
|
| 1928 |
+
|
| 1929 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 1930 |
+
|
| 1931 |
+
if err != nil {
|
| 1932 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %s", err)}
|
| 1933 |
+
}
|
| 1934 |
+
defer resp.Body.Close()
|
| 1935 |
+
|
| 1936 |
+
if resp.StatusCode >= 400 {
|
| 1937 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1938 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1939 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1940 |
+
if authRefreshed {
|
| 1941 |
+
return a.GetOrgDefaultSettings()
|
| 1942 |
+
}
|
| 1943 |
+
return nil, apiErr
|
| 1944 |
+
}
|
| 1945 |
+
|
| 1946 |
+
var settings shared.PlanSettings
|
| 1947 |
+
err = json.NewDecoder(resp.Body).Decode(&settings)
|
| 1948 |
+
if err != nil {
|
| 1949 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %s", err)}
|
| 1950 |
+
}
|
| 1951 |
+
|
| 1952 |
+
return &settings, nil
|
| 1953 |
+
}
|
| 1954 |
+
|
| 1955 |
+
func (a *Api) UpdateOrgDefaultSettings(req shared.UpdateSettingsRequest) (*shared.UpdateSettingsResponse, *shared.ApiError) {
|
| 1956 |
+
serverUrl := fmt.Sprintf("%s/default_settings", GetApiHost())
|
| 1957 |
+
|
| 1958 |
+
reqBytes, err := json.Marshal(req)
|
| 1959 |
+
if err != nil {
|
| 1960 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %s", err)}
|
| 1961 |
+
}
|
| 1962 |
+
|
| 1963 |
+
// log.Println("UpdateSettings", string(reqBytes))
|
| 1964 |
+
|
| 1965 |
+
request, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))
|
| 1966 |
+
if err != nil {
|
| 1967 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %s", err)}
|
| 1968 |
+
}
|
| 1969 |
+
request.Header.Set("Content-Type", "application/json")
|
| 1970 |
+
|
| 1971 |
+
resp, err := authenticatedFastClient.Do(request)
|
| 1972 |
+
if err != nil {
|
| 1973 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %s", err)}
|
| 1974 |
+
}
|
| 1975 |
+
defer resp.Body.Close()
|
| 1976 |
+
|
| 1977 |
+
if resp.StatusCode >= 400 {
|
| 1978 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 1979 |
+
|
| 1980 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 1981 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 1982 |
+
if authRefreshed {
|
| 1983 |
+
return a.UpdateOrgDefaultSettings(req)
|
| 1984 |
+
}
|
| 1985 |
+
return nil, apiErr
|
| 1986 |
+
}
|
| 1987 |
+
|
| 1988 |
+
var updateRes shared.UpdateSettingsResponse
|
| 1989 |
+
err = json.NewDecoder(resp.Body).Decode(&updateRes)
|
| 1990 |
+
if err != nil {
|
| 1991 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %s", err)}
|
| 1992 |
+
}
|
| 1993 |
+
|
| 1994 |
+
return &updateRes, nil
|
| 1995 |
+
}
|
| 1996 |
+
|
| 1997 |
+
func (a *Api) GetPlanConfig(planId string) (*shared.PlanConfig, *shared.ApiError) {
|
| 1998 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/config", GetApiHost(), planId)
|
| 1999 |
+
|
| 2000 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 2001 |
+
if err != nil {
|
| 2002 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 2003 |
+
}
|
| 2004 |
+
defer resp.Body.Close()
|
| 2005 |
+
|
| 2006 |
+
if resp.StatusCode >= 400 {
|
| 2007 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 2008 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 2009 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 2010 |
+
if authRefreshed {
|
| 2011 |
+
return a.GetPlanConfig(planId)
|
| 2012 |
+
}
|
| 2013 |
+
return nil, apiErr
|
| 2014 |
+
}
|
| 2015 |
+
|
| 2016 |
+
var res shared.GetPlanConfigResponse
|
| 2017 |
+
err = json.NewDecoder(resp.Body).Decode(&res)
|
| 2018 |
+
if err != nil {
|
| 2019 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 2020 |
+
}
|
| 2021 |
+
|
| 2022 |
+
return res.Config, nil
|
| 2023 |
+
}
|
| 2024 |
+
|
| 2025 |
+
func (a *Api) UpdatePlanConfig(planId string, req shared.UpdatePlanConfigRequest) *shared.ApiError {
|
| 2026 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/config", GetApiHost(), planId)
|
| 2027 |
+
|
| 2028 |
+
reqBytes, err := json.Marshal(req)
|
| 2029 |
+
if err != nil {
|
| 2030 |
+
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 2031 |
+
}
|
| 2032 |
+
|
| 2033 |
+
request, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))
|
| 2034 |
+
if err != nil {
|
| 2035 |
+
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 2036 |
+
}
|
| 2037 |
+
request.Header.Set("Content-Type", "application/json")
|
| 2038 |
+
|
| 2039 |
+
resp, err := authenticatedFastClient.Do(request)
|
| 2040 |
+
if err != nil {
|
| 2041 |
+
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 2042 |
+
}
|
| 2043 |
+
defer resp.Body.Close()
|
| 2044 |
+
|
| 2045 |
+
if resp.StatusCode >= 400 {
|
| 2046 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 2047 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 2048 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 2049 |
+
if authRefreshed {
|
| 2050 |
+
return a.UpdatePlanConfig(planId, req)
|
| 2051 |
+
}
|
| 2052 |
+
return apiErr
|
| 2053 |
+
}
|
| 2054 |
+
|
| 2055 |
+
return nil
|
| 2056 |
+
}
|
| 2057 |
+
|
| 2058 |
+
func (a *Api) GetDefaultPlanConfig() (*shared.PlanConfig, *shared.ApiError) {
|
| 2059 |
+
serverUrl := fmt.Sprintf("%s/default_plan_config", GetApiHost())
|
| 2060 |
+
|
| 2061 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 2062 |
+
if err != nil {
|
| 2063 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 2064 |
+
}
|
| 2065 |
+
defer resp.Body.Close()
|
| 2066 |
+
|
| 2067 |
+
if resp.StatusCode >= 400 {
|
| 2068 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 2069 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 2070 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 2071 |
+
if authRefreshed {
|
| 2072 |
+
return a.GetDefaultPlanConfig()
|
| 2073 |
+
}
|
| 2074 |
+
return nil, apiErr
|
| 2075 |
+
}
|
| 2076 |
+
|
| 2077 |
+
var res shared.GetDefaultPlanConfigResponse
|
| 2078 |
+
err = json.NewDecoder(resp.Body).Decode(&res)
|
| 2079 |
+
if err != nil {
|
| 2080 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 2081 |
+
}
|
| 2082 |
+
|
| 2083 |
+
return res.Config, nil
|
| 2084 |
+
}
|
| 2085 |
+
|
| 2086 |
+
func (a *Api) UpdateDefaultPlanConfig(req shared.UpdateDefaultPlanConfigRequest) *shared.ApiError {
|
| 2087 |
+
serverUrl := fmt.Sprintf("%s/default_plan_config", GetApiHost())
|
| 2088 |
+
|
| 2089 |
+
reqBytes, err := json.Marshal(req)
|
| 2090 |
+
if err != nil {
|
| 2091 |
+
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 2092 |
+
}
|
| 2093 |
+
|
| 2094 |
+
request, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))
|
| 2095 |
+
if err != nil {
|
| 2096 |
+
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 2097 |
+
}
|
| 2098 |
+
request.Header.Set("Content-Type", "application/json")
|
| 2099 |
+
|
| 2100 |
+
resp, err := authenticatedFastClient.Do(request)
|
| 2101 |
+
if err != nil {
|
| 2102 |
+
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 2103 |
+
}
|
| 2104 |
+
defer resp.Body.Close()
|
| 2105 |
+
|
| 2106 |
+
if resp.StatusCode >= 400 {
|
| 2107 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 2108 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 2109 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 2110 |
+
if authRefreshed {
|
| 2111 |
+
return a.UpdateDefaultPlanConfig(req)
|
| 2112 |
+
}
|
| 2113 |
+
return apiErr
|
| 2114 |
+
}
|
| 2115 |
+
|
| 2116 |
+
return nil
|
| 2117 |
+
}
|
| 2118 |
+
|
| 2119 |
+
func (a *Api) CreateCustomModels(input *shared.ModelsInput) *shared.ApiError {
|
| 2120 |
+
serverUrl := fmt.Sprintf("%s/custom_models", GetApiHost())
|
| 2121 |
+
body, err := json.Marshal(input)
|
| 2122 |
+
if err != nil {
|
| 2123 |
+
return &shared.ApiError{Msg: "Failed to marshal model"}
|
| 2124 |
+
}
|
| 2125 |
+
|
| 2126 |
+
resp, err := authenticatedFastClient.Post(serverUrl, "application/json", bytes.NewBuffer(body))
|
| 2127 |
+
if err != nil {
|
| 2128 |
+
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 2129 |
+
}
|
| 2130 |
+
defer resp.Body.Close()
|
| 2131 |
+
|
| 2132 |
+
if resp.StatusCode >= 400 {
|
| 2133 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 2134 |
+
|
| 2135 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 2136 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 2137 |
+
if authRefreshed {
|
| 2138 |
+
return a.CreateCustomModels(input)
|
| 2139 |
+
}
|
| 2140 |
+
return apiErr
|
| 2141 |
+
}
|
| 2142 |
+
|
| 2143 |
+
return nil
|
| 2144 |
+
}
|
| 2145 |
+
|
| 2146 |
+
func (a *Api) ListCustomModels() ([]*shared.CustomModel, *shared.ApiError) {
|
| 2147 |
+
serverUrl := fmt.Sprintf("%s/custom_models", GetApiHost())
|
| 2148 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 2149 |
+
if err != nil {
|
| 2150 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 2151 |
+
}
|
| 2152 |
+
defer resp.Body.Close()
|
| 2153 |
+
|
| 2154 |
+
if resp.StatusCode >= 400 {
|
| 2155 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 2156 |
+
|
| 2157 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 2158 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 2159 |
+
if authRefreshed {
|
| 2160 |
+
return a.ListCustomModels()
|
| 2161 |
+
}
|
| 2162 |
+
return nil, apiErr
|
| 2163 |
+
}
|
| 2164 |
+
|
| 2165 |
+
var models []*shared.CustomModel
|
| 2166 |
+
err = json.NewDecoder(resp.Body).Decode(&models)
|
| 2167 |
+
if err != nil {
|
| 2168 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 2169 |
+
}
|
| 2170 |
+
|
| 2171 |
+
return models, nil
|
| 2172 |
+
}
|
| 2173 |
+
|
| 2174 |
+
func (a *Api) ListCustomProviders() ([]*shared.CustomProvider, *shared.ApiError) {
|
| 2175 |
+
serverUrl := fmt.Sprintf("%s/custom_providers", GetApiHost())
|
| 2176 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 2177 |
+
if err != nil {
|
| 2178 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 2179 |
+
}
|
| 2180 |
+
defer resp.Body.Close()
|
| 2181 |
+
|
| 2182 |
+
if resp.StatusCode >= 400 {
|
| 2183 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 2184 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 2185 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 2186 |
+
if authRefreshed {
|
| 2187 |
+
return a.ListCustomProviders()
|
| 2188 |
+
}
|
| 2189 |
+
return nil, apiErr
|
| 2190 |
+
}
|
| 2191 |
+
|
| 2192 |
+
var providers []*shared.CustomProvider
|
| 2193 |
+
err = json.NewDecoder(resp.Body).Decode(&providers)
|
| 2194 |
+
if err != nil {
|
| 2195 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 2196 |
+
}
|
| 2197 |
+
|
| 2198 |
+
return providers, nil
|
| 2199 |
+
}
|
| 2200 |
+
|
| 2201 |
+
func (a *Api) ListModelPacks() ([]*shared.ModelPack, *shared.ApiError) {
|
| 2202 |
+
serverUrl := fmt.Sprintf("%s/model_sets", GetApiHost())
|
| 2203 |
+
|
| 2204 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 2205 |
+
if err != nil {
|
| 2206 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 2207 |
+
}
|
| 2208 |
+
defer resp.Body.Close()
|
| 2209 |
+
|
| 2210 |
+
if resp.StatusCode >= 400 {
|
| 2211 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 2212 |
+
|
| 2213 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 2214 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 2215 |
+
if authRefreshed {
|
| 2216 |
+
return a.ListModelPacks()
|
| 2217 |
+
}
|
| 2218 |
+
return nil, apiErr
|
| 2219 |
+
}
|
| 2220 |
+
|
| 2221 |
+
var sets []*shared.ModelPack
|
| 2222 |
+
err = json.NewDecoder(resp.Body).Decode(&sets)
|
| 2223 |
+
if err != nil {
|
| 2224 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 2225 |
+
}
|
| 2226 |
+
|
| 2227 |
+
return sets, nil
|
| 2228 |
+
|
| 2229 |
+
}
|
| 2230 |
+
|
| 2231 |
+
func (a *Api) GetCreditsTransactions(pageSize, pageNum int, req shared.CreditsLogRequest) (*shared.CreditsLogResponse, *shared.ApiError) {
|
| 2232 |
+
serverUrl := fmt.Sprintf("%s/billing/credits_transactions?size=%d&page=%d", GetApiHost(), pageSize, pageNum)
|
| 2233 |
+
|
| 2234 |
+
reqBytes, err := json.Marshal(req)
|
| 2235 |
+
if err != nil {
|
| 2236 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 2237 |
+
}
|
| 2238 |
+
|
| 2239 |
+
resp, err := authenticatedFastClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
|
| 2240 |
+
if err != nil {
|
| 2241 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 2242 |
+
}
|
| 2243 |
+
defer resp.Body.Close()
|
| 2244 |
+
|
| 2245 |
+
if resp.StatusCode >= 400 {
|
| 2246 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 2247 |
+
|
| 2248 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 2249 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 2250 |
+
if authRefreshed {
|
| 2251 |
+
return a.GetCreditsTransactions(pageSize, pageNum, req)
|
| 2252 |
+
}
|
| 2253 |
+
return nil, apiErr
|
| 2254 |
+
}
|
| 2255 |
+
|
| 2256 |
+
var res *shared.CreditsLogResponse
|
| 2257 |
+
err = json.NewDecoder(resp.Body).Decode(&res)
|
| 2258 |
+
if err != nil {
|
| 2259 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 2260 |
+
}
|
| 2261 |
+
|
| 2262 |
+
return res, nil
|
| 2263 |
+
}
|
| 2264 |
+
|
| 2265 |
+
func (a *Api) GetCreditsSummary(req shared.CreditsLogRequest) (*shared.CreditsSummaryResponse, *shared.ApiError) {
|
| 2266 |
+
serverUrl := fmt.Sprintf("%s/billing/credits_summary", GetApiHost())
|
| 2267 |
+
|
| 2268 |
+
reqBytes, err := json.Marshal(req)
|
| 2269 |
+
if err != nil {
|
| 2270 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 2271 |
+
}
|
| 2272 |
+
|
| 2273 |
+
resp, err := authenticatedFastClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
|
| 2274 |
+
if err != nil {
|
| 2275 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 2276 |
+
}
|
| 2277 |
+
defer resp.Body.Close()
|
| 2278 |
+
|
| 2279 |
+
if resp.StatusCode >= 400 {
|
| 2280 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 2281 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 2282 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 2283 |
+
if authRefreshed {
|
| 2284 |
+
return a.GetCreditsSummary(req)
|
| 2285 |
+
}
|
| 2286 |
+
return nil, apiErr
|
| 2287 |
+
}
|
| 2288 |
+
|
| 2289 |
+
var res *shared.CreditsSummaryResponse
|
| 2290 |
+
err = json.NewDecoder(resp.Body).Decode(&res)
|
| 2291 |
+
if err != nil {
|
| 2292 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 2293 |
+
}
|
| 2294 |
+
|
| 2295 |
+
return res, nil
|
| 2296 |
+
}
|
| 2297 |
+
|
| 2298 |
+
func (a *Api) GetBalance() (decimal.Decimal, *shared.ApiError) {
|
| 2299 |
+
serverUrl := fmt.Sprintf("%s/billing/balance", GetApiHost())
|
| 2300 |
+
|
| 2301 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 2302 |
+
if err != nil {
|
| 2303 |
+
return decimal.Zero, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 2304 |
+
}
|
| 2305 |
+
defer resp.Body.Close()
|
| 2306 |
+
|
| 2307 |
+
if resp.StatusCode >= 400 {
|
| 2308 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 2309 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 2310 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 2311 |
+
if authRefreshed {
|
| 2312 |
+
return a.GetBalance()
|
| 2313 |
+
}
|
| 2314 |
+
return decimal.Zero, apiErr
|
| 2315 |
+
}
|
| 2316 |
+
|
| 2317 |
+
var res *shared.GetBalanceResponse
|
| 2318 |
+
err = json.NewDecoder(resp.Body).Decode(&res)
|
| 2319 |
+
if err != nil {
|
| 2320 |
+
return decimal.Zero, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 2321 |
+
}
|
| 2322 |
+
|
| 2323 |
+
return res.Balance, nil
|
| 2324 |
+
}
|
| 2325 |
+
|
| 2326 |
+
func (a *Api) GetFileMap(req shared.GetFileMapRequest) (*shared.GetFileMapResponse, *shared.ApiError) {
|
| 2327 |
+
serverUrl := fmt.Sprintf("%s/file_map", GetApiHost())
|
| 2328 |
+
reqBytes, err := json.Marshal(req)
|
| 2329 |
+
if err != nil {
|
| 2330 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 2331 |
+
}
|
| 2332 |
+
|
| 2333 |
+
resp, err := authenticatedSlowClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
|
| 2334 |
+
if err != nil {
|
| 2335 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 2336 |
+
}
|
| 2337 |
+
defer resp.Body.Close()
|
| 2338 |
+
|
| 2339 |
+
if resp.StatusCode >= 400 {
|
| 2340 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 2341 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 2342 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 2343 |
+
if authRefreshed {
|
| 2344 |
+
return a.GetFileMap(req)
|
| 2345 |
+
}
|
| 2346 |
+
return nil, apiErr
|
| 2347 |
+
}
|
| 2348 |
+
|
| 2349 |
+
var respBody shared.GetFileMapResponse
|
| 2350 |
+
err = json.NewDecoder(resp.Body).Decode(&respBody)
|
| 2351 |
+
if err != nil {
|
| 2352 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 2353 |
+
}
|
| 2354 |
+
|
| 2355 |
+
return &respBody, nil
|
| 2356 |
+
}
|
| 2357 |
+
|
| 2358 |
+
func (a *Api) GetContextBody(planId, branch, contextId string) (*shared.GetContextBodyResponse, *shared.ApiError) {
|
| 2359 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/context/%s/body", GetApiHost(), planId, branch, contextId)
|
| 2360 |
+
|
| 2361 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 2362 |
+
if err != nil {
|
| 2363 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 2364 |
+
}
|
| 2365 |
+
defer resp.Body.Close()
|
| 2366 |
+
|
| 2367 |
+
if resp.StatusCode >= 400 {
|
| 2368 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 2369 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 2370 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 2371 |
+
if authRefreshed {
|
| 2372 |
+
return a.GetContextBody(planId, branch, contextId)
|
| 2373 |
+
}
|
| 2374 |
+
return nil, apiErr
|
| 2375 |
+
}
|
| 2376 |
+
|
| 2377 |
+
var respBody shared.GetContextBodyResponse
|
| 2378 |
+
err = json.NewDecoder(resp.Body).Decode(&respBody)
|
| 2379 |
+
if err != nil {
|
| 2380 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 2381 |
+
}
|
| 2382 |
+
|
| 2383 |
+
return &respBody, nil
|
| 2384 |
+
}
|
| 2385 |
+
|
| 2386 |
+
func (a *Api) AutoLoadContext(ctx context.Context, planId, branch string, req shared.LoadContextRequest) (*shared.LoadContextResponse, *shared.ApiError) {
|
| 2387 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/auto_load_context", GetApiHost(), planId, branch)
|
| 2388 |
+
reqBytes, err := json.Marshal(req)
|
| 2389 |
+
if err != nil {
|
| 2390 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
|
| 2391 |
+
}
|
| 2392 |
+
|
| 2393 |
+
// Create a new request with context
|
| 2394 |
+
httpReq, err := http.NewRequestWithContext(ctx, "POST", serverUrl, bytes.NewBuffer(reqBytes))
|
| 2395 |
+
if err != nil {
|
| 2396 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %v", err)}
|
| 2397 |
+
}
|
| 2398 |
+
|
| 2399 |
+
// Set the content type header
|
| 2400 |
+
httpReq.Header.Set("Content-Type", "application/json")
|
| 2401 |
+
|
| 2402 |
+
// Use the slow client since we may be uploading relatively large files
|
| 2403 |
+
resp, err := authenticatedSlowClient.Do(httpReq)
|
| 2404 |
+
if err != nil {
|
| 2405 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 2406 |
+
}
|
| 2407 |
+
defer resp.Body.Close()
|
| 2408 |
+
|
| 2409 |
+
if resp.StatusCode >= 400 {
|
| 2410 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 2411 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 2412 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 2413 |
+
if authRefreshed {
|
| 2414 |
+
return a.LoadContext(planId, branch, req)
|
| 2415 |
+
}
|
| 2416 |
+
return nil, apiErr
|
| 2417 |
+
}
|
| 2418 |
+
|
| 2419 |
+
var loadContextResponse shared.LoadContextResponse
|
| 2420 |
+
err = json.NewDecoder(resp.Body).Decode(&loadContextResponse)
|
| 2421 |
+
if err != nil {
|
| 2422 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 2423 |
+
}
|
| 2424 |
+
|
| 2425 |
+
return &loadContextResponse, nil
|
| 2426 |
+
}
|
| 2427 |
+
|
| 2428 |
+
func (a *Api) GetBuildStatus(planId, branch string) (*shared.GetBuildStatusResponse, *shared.ApiError) {
|
| 2429 |
+
serverUrl := fmt.Sprintf("%s/plans/%s/%s/build_status", GetApiHost(), planId, branch)
|
| 2430 |
+
|
| 2431 |
+
resp, err := authenticatedFastClient.Get(serverUrl)
|
| 2432 |
+
if err != nil {
|
| 2433 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
|
| 2434 |
+
}
|
| 2435 |
+
defer resp.Body.Close()
|
| 2436 |
+
|
| 2437 |
+
if resp.StatusCode >= 400 {
|
| 2438 |
+
errorBody, _ := io.ReadAll(resp.Body)
|
| 2439 |
+
apiErr := HandleApiError(resp, errorBody)
|
| 2440 |
+
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
|
| 2441 |
+
if authRefreshed {
|
| 2442 |
+
return a.GetBuildStatus(planId, branch)
|
| 2443 |
+
}
|
| 2444 |
+
return nil, apiErr
|
| 2445 |
+
}
|
| 2446 |
+
|
| 2447 |
+
var respBody shared.GetBuildStatusResponse
|
| 2448 |
+
err = json.NewDecoder(resp.Body).Decode(&respBody)
|
| 2449 |
+
if err != nil {
|
| 2450 |
+
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
|
| 2451 |
+
}
|
| 2452 |
+
|
| 2453 |
+
return &respBody, nil
|
| 2454 |
+
}
|
app/cli/api/stream.go
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package api
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bufio"
|
| 5 |
+
"bytes"
|
| 6 |
+
"encoding/json"
|
| 7 |
+
"fmt"
|
| 8 |
+
"io"
|
| 9 |
+
"log"
|
| 10 |
+
"plandex-cli/types"
|
| 11 |
+
"time"
|
| 12 |
+
|
| 13 |
+
shared "plandex-shared"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
// 3 heartbeat misses = timeout
|
| 17 |
+
const HeartbeatTimeout = 16 * time.Second
|
| 18 |
+
|
| 19 |
+
func connectPlanRespStream(body io.ReadCloser, onStream types.OnStreamPlan) {
|
| 20 |
+
reader := bufio.NewReader(body)
|
| 21 |
+
timer := time.NewTimer(HeartbeatTimeout)
|
| 22 |
+
defer timer.Stop()
|
| 23 |
+
|
| 24 |
+
go func() {
|
| 25 |
+
for {
|
| 26 |
+
select {
|
| 27 |
+
case <-timer.C:
|
| 28 |
+
log.Println("Connection to plan stream timed out due to missing heartbeats")
|
| 29 |
+
onStream(types.OnStreamPlanParams{Msg: nil, Err: fmt.Errorf("connection to plan stream timed out due to missing heartbeats")})
|
| 30 |
+
body.Close()
|
| 31 |
+
return
|
| 32 |
+
default:
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
s, err := readUntilSeparator(reader, shared.STREAM_MESSAGE_SEPARATOR)
|
| 36 |
+
if err != nil {
|
| 37 |
+
log.Println("Error reading line:", err)
|
| 38 |
+
onStream(types.OnStreamPlanParams{Msg: nil, Err: err})
|
| 39 |
+
body.Close()
|
| 40 |
+
return
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
timer.Reset(HeartbeatTimeout)
|
| 44 |
+
|
| 45 |
+
// ignore heartbeats
|
| 46 |
+
if s == string(shared.StreamMessageHeartbeat) {
|
| 47 |
+
continue
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
var msg shared.StreamMessage
|
| 51 |
+
err = json.Unmarshal([]byte(s), &msg)
|
| 52 |
+
if err != nil {
|
| 53 |
+
log.Println("Error unmarshalling message:", err)
|
| 54 |
+
onStream(types.OnStreamPlanParams{Msg: nil, Err: err})
|
| 55 |
+
body.Close()
|
| 56 |
+
return
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
// log.Println("connectPlanRespStream: received message:", msg)
|
| 60 |
+
|
| 61 |
+
onStream(types.OnStreamPlanParams{Msg: &msg, Err: nil})
|
| 62 |
+
|
| 63 |
+
if msg.Type == shared.StreamMessageFinished || msg.Type == shared.StreamMessageError || msg.Type == shared.StreamMessageAborted {
|
| 64 |
+
body.Close()
|
| 65 |
+
return
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
}
|
| 69 |
+
}()
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
func readUntilSeparator(reader *bufio.Reader, separator string) (string, error) {
|
| 73 |
+
var result []byte
|
| 74 |
+
sepBytes := []byte(separator)
|
| 75 |
+
for {
|
| 76 |
+
b, err := reader.ReadByte()
|
| 77 |
+
if err != nil {
|
| 78 |
+
return string(result), err
|
| 79 |
+
}
|
| 80 |
+
result = append(result, b)
|
| 81 |
+
if len(result) >= len(sepBytes) && bytes.HasSuffix(result, sepBytes) {
|
| 82 |
+
return string(result[:len(result)-len(separator)]), nil
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
}
|
app/cli/auth/account.go
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package auth
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"plandex-cli/term"
|
| 6 |
+
|
| 7 |
+
shared "plandex-shared"
|
| 8 |
+
|
| 9 |
+
"github.com/fatih/color"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
const AddAccountOption = "Add another account"
|
| 13 |
+
|
| 14 |
+
func SelectOrSignInOrCreate() error {
|
| 15 |
+
accounts, err := loadAccounts()
|
| 16 |
+
|
| 17 |
+
if err != nil {
|
| 18 |
+
return fmt.Errorf("error loading accounts: %v", err)
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
if len(accounts) == 0 {
|
| 22 |
+
err := promptSignInNewAccount()
|
| 23 |
+
if err != nil {
|
| 24 |
+
return fmt.Errorf("error signing in to new account: %v", err)
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
return nil
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
var options []string
|
| 31 |
+
for _, account := range accounts {
|
| 32 |
+
options = append(options, fmt.Sprintf("<%s> %s", account.UserName, account.Email))
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
options = append(options, AddAccountOption)
|
| 36 |
+
|
| 37 |
+
// either select from existing accounts or sign in/create account
|
| 38 |
+
|
| 39 |
+
selectedOpt, err := term.SelectFromList("Select an account:", options)
|
| 40 |
+
|
| 41 |
+
if err != nil {
|
| 42 |
+
return fmt.Errorf("error selecting account: %v", err)
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
if selectedOpt == AddAccountOption {
|
| 46 |
+
err := promptSignInNewAccount()
|
| 47 |
+
if err != nil {
|
| 48 |
+
return fmt.Errorf("error prompting for sign in to new account: %v", err)
|
| 49 |
+
}
|
| 50 |
+
return nil
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
var selected *shared.ClientAccount
|
| 54 |
+
for i, opt := range options {
|
| 55 |
+
if selectedOpt == opt {
|
| 56 |
+
selected = accounts[i]
|
| 57 |
+
break
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
if selected == nil {
|
| 62 |
+
return fmt.Errorf("error selecting account: account not found")
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
selectedAuth := *selected
|
| 66 |
+
|
| 67 |
+
setAuth(&shared.ClientAuth{
|
| 68 |
+
ClientAccount: selectedAuth,
|
| 69 |
+
})
|
| 70 |
+
|
| 71 |
+
term.StartSpinner("")
|
| 72 |
+
orgs, apiErr := apiClient.ListOrgs()
|
| 73 |
+
term.StopSpinner()
|
| 74 |
+
|
| 75 |
+
if apiErr != nil {
|
| 76 |
+
return fmt.Errorf("error listing orgs: %v", apiErr.Msg)
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
org, err := resolveOrgAuth(orgs, selectedAuth.IsLocalMode)
|
| 80 |
+
|
| 81 |
+
if err != nil {
|
| 82 |
+
return fmt.Errorf("error resolving org: %v", err)
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
err = setAuth(&shared.ClientAuth{
|
| 86 |
+
ClientAccount: *selected,
|
| 87 |
+
OrgId: org.Id,
|
| 88 |
+
OrgName: org.Name,
|
| 89 |
+
OrgIsTrial: org.IsTrial,
|
| 90 |
+
IntegratedModelsMode: org.IntegratedModelsMode,
|
| 91 |
+
})
|
| 92 |
+
|
| 93 |
+
if err != nil {
|
| 94 |
+
return fmt.Errorf("error setting auth: %v", err)
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
_, apiErr = apiClient.GetOrgSession()
|
| 98 |
+
|
| 99 |
+
if apiErr != nil {
|
| 100 |
+
return fmt.Errorf("error getting org session: %v", apiErr.Msg)
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
fmt.Printf("✅ Signed in as %s | Org: %s\n", color.New(color.Bold, term.ColorHiGreen).Sprintf("<%s> %s", Current.UserName, Current.Email), color.New(term.ColorHiCyan).Sprint(Current.OrgName))
|
| 104 |
+
fmt.Println()
|
| 105 |
+
|
| 106 |
+
if !term.IsRepl {
|
| 107 |
+
term.PrintCmds("", "")
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
return nil
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
func SignInWithCode(code, host string) error {
|
| 114 |
+
term.StartSpinner("")
|
| 115 |
+
res, apiErr := apiClient.SignIn(shared.SignInRequest{
|
| 116 |
+
Pin: code,
|
| 117 |
+
IsSignInCode: true,
|
| 118 |
+
}, host)
|
| 119 |
+
term.StopSpinner()
|
| 120 |
+
|
| 121 |
+
if apiErr != nil {
|
| 122 |
+
return fmt.Errorf("error signing in: %v", apiErr.Msg)
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
return handleSignInResponse(res, host)
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
func promptInitialAuth() error {
|
| 129 |
+
fmt.Println("👋 Hey there!\nIt looks like this is your first time using Plandex on this computer.")
|
| 130 |
+
|
| 131 |
+
err := SelectOrSignInOrCreate()
|
| 132 |
+
|
| 133 |
+
if err != nil {
|
| 134 |
+
return fmt.Errorf("error selecting or signing in to account: %v", err)
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
return nil
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
const (
|
| 141 |
+
// SignInCloudOption = "Plandex Cloud"
|
| 142 |
+
SignInLocalOption = "Local mode host"
|
| 143 |
+
SignInOtherOption = "Another host"
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
func promptSignInNewAccount() error {
|
| 147 |
+
selected, err := term.SelectFromList("Use local mode or another host?", []string{SignInLocalOption, SignInOtherOption})
|
| 148 |
+
|
| 149 |
+
if err != nil {
|
| 150 |
+
return fmt.Errorf("error selecting sign in option: %v", err)
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
var host string
|
| 154 |
+
var email string
|
| 155 |
+
|
| 156 |
+
if selected == SignInLocalOption {
|
| 157 |
+
host, err = term.GetRequiredUserStringInputWithDefault("Host:", "http://localhost:8099")
|
| 158 |
+
} else {
|
| 159 |
+
host, err = term.GetRequiredUserStringInput("Host:")
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
if err != nil {
|
| 163 |
+
return fmt.Errorf("error prompting host: %v", err)
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
if selected == SignInLocalOption {
|
| 167 |
+
email = "local-admin@plandex.ai"
|
| 168 |
+
} else {
|
| 169 |
+
email, err = term.GetRequiredUserStringInput("Your email:")
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
if err != nil {
|
| 173 |
+
return fmt.Errorf("error prompting email: %v", err)
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
res, err := verifyEmail(email, host)
|
| 177 |
+
|
| 178 |
+
if err != nil {
|
| 179 |
+
return fmt.Errorf("error verifying email: %v", err)
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
if res.hasAccount {
|
| 183 |
+
err := signIn(email, res.pin, host)
|
| 184 |
+
if err != nil {
|
| 185 |
+
return fmt.Errorf("error signing in: %v", err)
|
| 186 |
+
}
|
| 187 |
+
} else {
|
| 188 |
+
err := createAccount(email, res.pin, host, res.isLocalMode)
|
| 189 |
+
if err != nil {
|
| 190 |
+
return fmt.Errorf("error creating account: %v", err)
|
| 191 |
+
}
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
if !term.IsRepl {
|
| 195 |
+
term.PrintCmds("", "")
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
return nil
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
type verifyEmailRes struct {
|
| 202 |
+
hasAccount bool
|
| 203 |
+
isLocalMode bool
|
| 204 |
+
pin string
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
func verifyEmail(email, host string) (*verifyEmailRes, error) {
|
| 208 |
+
term.StartSpinner("")
|
| 209 |
+
res, apiErr := apiClient.CreateEmailVerification(email, host, "")
|
| 210 |
+
term.StopSpinner()
|
| 211 |
+
|
| 212 |
+
if apiErr != nil {
|
| 213 |
+
return nil, fmt.Errorf("error creating email verification: %v", apiErr.Msg)
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
if res.IsLocalMode {
|
| 217 |
+
return &verifyEmailRes{
|
| 218 |
+
hasAccount: res.HasAccount,
|
| 219 |
+
isLocalMode: true,
|
| 220 |
+
pin: "",
|
| 221 |
+
}, nil
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
fmt.Println("✉️ You'll now receive a 6 character pin by email. It will be valid for 5 minutes.")
|
| 225 |
+
|
| 226 |
+
pin, err := term.GetUserPasswordInput("Please enter your pin:")
|
| 227 |
+
|
| 228 |
+
if err != nil {
|
| 229 |
+
return nil, fmt.Errorf("error prompting pin: %v", err)
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
return &verifyEmailRes{
|
| 233 |
+
hasAccount: res.HasAccount,
|
| 234 |
+
isLocalMode: false,
|
| 235 |
+
pin: pin,
|
| 236 |
+
}, nil
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
func signIn(email, pin, host string) error {
|
| 240 |
+
term.StartSpinner("")
|
| 241 |
+
res, apiErr := apiClient.SignIn(shared.SignInRequest{
|
| 242 |
+
Email: email,
|
| 243 |
+
Pin: pin,
|
| 244 |
+
}, host)
|
| 245 |
+
term.StopSpinner()
|
| 246 |
+
|
| 247 |
+
if apiErr != nil {
|
| 248 |
+
return fmt.Errorf("error signing in: %v", apiErr.Msg)
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
return handleSignInResponse(res, host)
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
func handleSignInResponse(res *shared.SessionResponse, host string) error {
|
| 255 |
+
isLocalMode := host != "" && res.IsLocalMode
|
| 256 |
+
|
| 257 |
+
err := setAuth(&shared.ClientAuth{
|
| 258 |
+
ClientAccount: shared.ClientAccount{
|
| 259 |
+
Email: res.Email,
|
| 260 |
+
UserId: res.UserId,
|
| 261 |
+
UserName: res.UserName,
|
| 262 |
+
Token: res.Token,
|
| 263 |
+
IsTrial: false,
|
| 264 |
+
IsCloud: host == "",
|
| 265 |
+
Host: host,
|
| 266 |
+
IsLocalMode: isLocalMode,
|
| 267 |
+
},
|
| 268 |
+
})
|
| 269 |
+
|
| 270 |
+
if err != nil {
|
| 271 |
+
return fmt.Errorf("error setting auth: %v", err)
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
org, err := resolveOrgAuth(res.Orgs, isLocalMode)
|
| 275 |
+
|
| 276 |
+
if err != nil {
|
| 277 |
+
return fmt.Errorf("error resolving org: %v", err)
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
Current.OrgId = org.Id
|
| 281 |
+
Current.OrgName = org.Name
|
| 282 |
+
Current.IntegratedModelsMode = org.IntegratedModelsMode
|
| 283 |
+
|
| 284 |
+
err = writeCurrentAuth()
|
| 285 |
+
|
| 286 |
+
if err != nil {
|
| 287 |
+
return fmt.Errorf("error writing auth: %v", err)
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
fmt.Printf("✅ Signed in as %s | Org: %s\n", color.New(color.Bold, term.ColorHiGreen).Sprintf("<%s> %s", Current.UserName, Current.Email), color.New(term.ColorHiCyan).Sprint(Current.OrgName))
|
| 291 |
+
fmt.Println()
|
| 292 |
+
|
| 293 |
+
return nil
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
func createAccount(email, pin, host string, isLocalMode bool) error {
|
| 297 |
+
var name string
|
| 298 |
+
|
| 299 |
+
if isLocalMode {
|
| 300 |
+
name = "Local Admin"
|
| 301 |
+
} else {
|
| 302 |
+
var err error
|
| 303 |
+
name, err = term.GetUserStringInput("Your name:")
|
| 304 |
+
|
| 305 |
+
if err != nil {
|
| 306 |
+
return fmt.Errorf("error prompting name: %v", err)
|
| 307 |
+
}
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
term.StartSpinner("🌟 Creating account...")
|
| 311 |
+
res, apiErr := apiClient.CreateAccount(shared.CreateAccountRequest{
|
| 312 |
+
Email: email,
|
| 313 |
+
UserName: name,
|
| 314 |
+
Pin: pin,
|
| 315 |
+
}, host)
|
| 316 |
+
term.StopSpinner()
|
| 317 |
+
|
| 318 |
+
if apiErr != nil {
|
| 319 |
+
return fmt.Errorf("error creating account: %v", apiErr.Msg)
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
if res.IsLocalMode {
|
| 323 |
+
isLocalMode = true
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
err := setAuth(&shared.ClientAuth{
|
| 327 |
+
ClientAccount: shared.ClientAccount{
|
| 328 |
+
Email: res.Email,
|
| 329 |
+
UserId: res.UserId,
|
| 330 |
+
UserName: res.UserName,
|
| 331 |
+
Token: res.Token,
|
| 332 |
+
IsTrial: false,
|
| 333 |
+
IsCloud: host == "",
|
| 334 |
+
Host: host,
|
| 335 |
+
IsLocalMode: isLocalMode,
|
| 336 |
+
},
|
| 337 |
+
})
|
| 338 |
+
|
| 339 |
+
if err != nil {
|
| 340 |
+
return fmt.Errorf("error setting auth: %v", err)
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
org, err := resolveOrgAuth(res.Orgs, isLocalMode)
|
| 344 |
+
|
| 345 |
+
if err != nil {
|
| 346 |
+
return fmt.Errorf("error resolving org: %v", err)
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
if org == nil {
|
| 350 |
+
return fmt.Errorf("no org selected")
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
Current.OrgId = org.Id
|
| 354 |
+
Current.OrgName = org.Name
|
| 355 |
+
Current.IntegratedModelsMode = org.IntegratedModelsMode
|
| 356 |
+
|
| 357 |
+
err = writeCurrentAuth()
|
| 358 |
+
|
| 359 |
+
if err != nil {
|
| 360 |
+
return fmt.Errorf("error writing auth: %v", err)
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
fmt.Printf("✅ Signed in as %s | Org: %s\n", color.New(color.Bold, term.ColorHiGreen).Sprintf("<%s> %s", Current.UserName, Current.Email), color.New(term.ColorHiCyan).Sprint(Current.OrgName))
|
| 364 |
+
fmt.Println()
|
| 365 |
+
|
| 366 |
+
return nil
|
| 367 |
+
}
|
app/cli/auth/api.go
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package auth
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/base64"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"fmt"
|
| 7 |
+
"net/http"
|
| 8 |
+
"plandex-cli/types"
|
| 9 |
+
"plandex-cli/version"
|
| 10 |
+
|
| 11 |
+
shared "plandex-shared"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
var apiClient types.ApiClient
|
| 15 |
+
|
| 16 |
+
func SetApiClient(client types.ApiClient) {
|
| 17 |
+
apiClient = client
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
func SetAuthHeader(req *http.Request) error {
|
| 21 |
+
if Current == nil {
|
| 22 |
+
return fmt.Errorf("error setting auth header: auth not loaded")
|
| 23 |
+
}
|
| 24 |
+
hash := Current.ToHash()
|
| 25 |
+
|
| 26 |
+
authHeader := shared.AuthHeader{
|
| 27 |
+
Token: Current.Token,
|
| 28 |
+
OrgId: Current.OrgId,
|
| 29 |
+
Hash: hash,
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
bytes, err := json.Marshal(authHeader)
|
| 33 |
+
|
| 34 |
+
if err != nil {
|
| 35 |
+
return fmt.Errorf("error marshalling auth header: %v", err)
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// base64 encode
|
| 39 |
+
token := base64.URLEncoding.EncodeToString(bytes)
|
| 40 |
+
|
| 41 |
+
req.Header.Set("Authorization", "Bearer "+token)
|
| 42 |
+
|
| 43 |
+
return nil
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
func SetVersionHeader(req *http.Request) {
|
| 47 |
+
req.Header.Set("X-Client-Version", version.Version)
|
| 48 |
+
}
|
app/cli/auth/auth.go
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package auth
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"fmt"
|
| 6 |
+
"os"
|
| 7 |
+
"plandex-cli/fs"
|
| 8 |
+
"plandex-cli/term"
|
| 9 |
+
|
| 10 |
+
shared "plandex-shared"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
var openUnauthenticatedCloudURL func(msg, path string)
|
| 14 |
+
var openAuthenticatedURL func(msg, path string)
|
| 15 |
+
|
| 16 |
+
func SetOpenUnauthenticatedCloudURLFn(fn func(msg, path string)) {
|
| 17 |
+
openUnauthenticatedCloudURL = fn
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
func SetOpenAuthenticatedURLFn(fn func(msg, path string)) {
|
| 21 |
+
openAuthenticatedURL = fn
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
func MustResolveAuthWithOrg() {
|
| 25 |
+
MustResolveAuth(true)
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
func MustResolveAuth(requireOrg bool) {
|
| 29 |
+
if apiClient == nil {
|
| 30 |
+
term.OutputErrorAndExit("error resolving auth: api client not set")
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// load HomeAuthPath file into ClientAuth struct
|
| 34 |
+
bytes, err := os.ReadFile(fs.HomeAuthPath)
|
| 35 |
+
|
| 36 |
+
if err != nil {
|
| 37 |
+
if os.IsNotExist(err) {
|
| 38 |
+
err = promptInitialAuth()
|
| 39 |
+
|
| 40 |
+
if err != nil {
|
| 41 |
+
term.OutputErrorAndExit("error resolving auth: %v", err)
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
return
|
| 45 |
+
} else {
|
| 46 |
+
term.OutputErrorAndExit("error reading auth.json: %v", err)
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
var auth shared.ClientAuth
|
| 51 |
+
err = json.Unmarshal(bytes, &auth)
|
| 52 |
+
if err != nil {
|
| 53 |
+
term.OutputErrorAndExit("error unmarshalling auth.json: %v", err)
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
Current = &auth
|
| 57 |
+
|
| 58 |
+
if requireOrg && Current.OrgId == "" {
|
| 59 |
+
term.StartSpinner("")
|
| 60 |
+
orgs, apiErr := apiClient.ListOrgs()
|
| 61 |
+
term.StopSpinner()
|
| 62 |
+
|
| 63 |
+
if apiErr != nil {
|
| 64 |
+
term.OutputErrorAndExit("Error listing orgs: %v", apiErr.Msg)
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
org, err := resolveOrgAuth(orgs, Current.IsLocalMode)
|
| 68 |
+
|
| 69 |
+
if err != nil {
|
| 70 |
+
term.OutputErrorAndExit("Error resolving org: %v", err)
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
if org.Id == "" {
|
| 74 |
+
// still no org--exit now
|
| 75 |
+
term.OutputErrorAndExit("No org")
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
Current.OrgId = org.Id
|
| 79 |
+
Current.OrgName = org.Name
|
| 80 |
+
Current.IntegratedModelsMode = org.IntegratedModelsMode
|
| 81 |
+
|
| 82 |
+
err = writeCurrentAuth()
|
| 83 |
+
|
| 84 |
+
if err != nil {
|
| 85 |
+
term.OutputErrorAndExit("Error writing auth: %v", err)
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
func RefreshInvalidToken() error {
|
| 91 |
+
if Current == nil {
|
| 92 |
+
return fmt.Errorf("error refreshing token: auth not loaded")
|
| 93 |
+
}
|
| 94 |
+
res, err := verifyEmail(Current.Email, Current.Host)
|
| 95 |
+
|
| 96 |
+
if err != nil {
|
| 97 |
+
return fmt.Errorf("error verifying email: %v", err)
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
if res.hasAccount {
|
| 101 |
+
return signIn(Current.Email, res.pin, Current.Host)
|
| 102 |
+
} else {
|
| 103 |
+
host := Current.Host
|
| 104 |
+
if host == "" {
|
| 105 |
+
host = "Plandex Cloud"
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
term.OutputErrorAndExit("Account %s not found on %s", Current.Email, host)
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
return nil
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
func RefreshAuth() error {
|
| 115 |
+
if Current == nil {
|
| 116 |
+
return fmt.Errorf("error refreshing auth: auth not loaded")
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
org, apiErr := apiClient.GetOrgSession()
|
| 120 |
+
|
| 121 |
+
if apiErr != nil {
|
| 122 |
+
return fmt.Errorf("error getting org session: %v", apiErr.Msg)
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
Current.OrgName = org.Name
|
| 126 |
+
Current.OrgIsTrial = org.IsTrial
|
| 127 |
+
Current.IntegratedModelsMode = org.IntegratedModelsMode
|
| 128 |
+
|
| 129 |
+
err := writeCurrentAuth()
|
| 130 |
+
|
| 131 |
+
if err != nil {
|
| 132 |
+
return fmt.Errorf("error writing auth: %v", err)
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
return nil
|
| 136 |
+
}
|
app/cli/auth/org.go
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package auth
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"plandex-cli/term"
|
| 6 |
+
"strings"
|
| 7 |
+
|
| 8 |
+
shared "plandex-shared"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
func resolveOrgAuth(orgs []*shared.Org, isLocalMode bool) (*shared.Org, error) {
|
| 12 |
+
var org *shared.Org
|
| 13 |
+
var err error
|
| 14 |
+
|
| 15 |
+
if len(orgs) == 0 {
|
| 16 |
+
if isLocalMode {
|
| 17 |
+
org, err = createOrg(isLocalMode)
|
| 18 |
+
} else {
|
| 19 |
+
org, err = promptNoOrgs()
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
if err != nil {
|
| 23 |
+
return nil, fmt.Errorf("error prompting no orgs: %v", err)
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
} else if len(orgs) == 1 {
|
| 27 |
+
org = orgs[0]
|
| 28 |
+
} else {
|
| 29 |
+
org, err = selectOrg(orgs, isLocalMode)
|
| 30 |
+
|
| 31 |
+
if err != nil {
|
| 32 |
+
return nil, fmt.Errorf("error selecting org: %v", err)
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
return org, nil
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
func promptNoOrgs() (*shared.Org, error) {
|
| 40 |
+
fmt.Println("🧐 You don't have access to any orgs yet.\n\nTo join an existing org, ask an admin to either invite you directly or give your whole email domain access.\n\nOtherwise, you can go ahead and create a new org.")
|
| 41 |
+
|
| 42 |
+
shouldCreate, err := term.ConfirmYesNo("Create a new org now?")
|
| 43 |
+
|
| 44 |
+
if err != nil {
|
| 45 |
+
return nil, fmt.Errorf("error prompting create org: %v", err)
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
if shouldCreate {
|
| 49 |
+
return createOrg(false)
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
return nil, nil
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
func createOrg(isLocalMode bool) (*shared.Org, error) {
|
| 56 |
+
var err error
|
| 57 |
+
var name string
|
| 58 |
+
var autoAddDomainUsers bool
|
| 59 |
+
|
| 60 |
+
if isLocalMode {
|
| 61 |
+
name = "Local Org"
|
| 62 |
+
} else {
|
| 63 |
+
name, err = term.GetRequiredUserStringInput("Org name:")
|
| 64 |
+
}
|
| 65 |
+
if err != nil {
|
| 66 |
+
return nil, fmt.Errorf("error prompting org name: %v", err)
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
if !isLocalMode {
|
| 70 |
+
autoAddDomainUsers, err = promptAutoAddUsersIfValid(Current.Email)
|
| 71 |
+
if err != nil {
|
| 72 |
+
return nil, fmt.Errorf("error prompting auto add domain users: %v", err)
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
term.StartSpinner("")
|
| 77 |
+
res, apiErr := apiClient.CreateOrg(shared.CreateOrgRequest{
|
| 78 |
+
Name: name,
|
| 79 |
+
AutoAddDomainUsers: autoAddDomainUsers,
|
| 80 |
+
})
|
| 81 |
+
term.StopSpinner()
|
| 82 |
+
|
| 83 |
+
if apiErr != nil {
|
| 84 |
+
return nil, fmt.Errorf("error creating org: %v", apiErr.Msg)
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
return &shared.Org{Id: res.Id, Name: name}, nil
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
func promptAutoAddUsersIfValid(email string) (bool, error) {
|
| 91 |
+
userDomain := strings.Split(email, "@")[1]
|
| 92 |
+
var autoAddDomainUsers bool
|
| 93 |
+
var err error
|
| 94 |
+
if !shared.IsEmailServiceDomain(userDomain) {
|
| 95 |
+
fmt.Println("With domain auto-join, you can allow any user with an email ending in @"+userDomain, "to auto-join this org.")
|
| 96 |
+
autoAddDomainUsers, err = term.ConfirmYesNo(fmt.Sprintf("Enable auto-join for %s?", userDomain))
|
| 97 |
+
|
| 98 |
+
if err != nil {
|
| 99 |
+
return false, err
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
return autoAddDomainUsers, nil
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
const CreateOrgOption = "Create a new org"
|
| 106 |
+
|
| 107 |
+
func selectOrg(orgs []*shared.Org, isLocalMode bool) (*shared.Org, error) {
|
| 108 |
+
var options []string
|
| 109 |
+
for _, org := range orgs {
|
| 110 |
+
options = append(options, org.Name)
|
| 111 |
+
}
|
| 112 |
+
options = append(options, CreateOrgOption)
|
| 113 |
+
|
| 114 |
+
selected, err := term.SelectFromList("Select an org:", options)
|
| 115 |
+
|
| 116 |
+
if err != nil {
|
| 117 |
+
return nil, fmt.Errorf("error selecting org: %v", err)
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
if selected == CreateOrgOption {
|
| 121 |
+
return createOrg(isLocalMode)
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
var selectedOrg *shared.Org
|
| 125 |
+
for _, org := range orgs {
|
| 126 |
+
if org.Name == selected {
|
| 127 |
+
selectedOrg = org
|
| 128 |
+
break
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
if selectedOrg == nil {
|
| 133 |
+
return nil, fmt.Errorf("error selecting org: org not found")
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
return selectedOrg, nil
|
| 137 |
+
}
|
app/cli/auth/state.go
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package auth
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"fmt"
|
| 6 |
+
"os"
|
| 7 |
+
"plandex-cli/fs"
|
| 8 |
+
|
| 9 |
+
shared "plandex-shared"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
var Current *shared.ClientAuth
|
| 13 |
+
|
| 14 |
+
func loadAccounts() ([]*shared.ClientAccount, error) {
|
| 15 |
+
bytes, err := os.ReadFile(fs.HomeAccountsPath)
|
| 16 |
+
|
| 17 |
+
if err != nil {
|
| 18 |
+
if os.IsNotExist(err) {
|
| 19 |
+
// no accounts
|
| 20 |
+
return []*shared.ClientAccount{}, nil
|
| 21 |
+
} else {
|
| 22 |
+
return nil, fmt.Errorf("error reading accounts.json: %v", err)
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
var accounts []*shared.ClientAccount
|
| 27 |
+
err = json.Unmarshal(bytes, &accounts)
|
| 28 |
+
|
| 29 |
+
if err != nil {
|
| 30 |
+
return nil, fmt.Errorf("error unmarshalling accounts.json: %v", err)
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
return accounts, nil
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
func setAuth(auth *shared.ClientAuth) error {
|
| 37 |
+
err := storeAccount(&auth.ClientAccount)
|
| 38 |
+
|
| 39 |
+
if err != nil {
|
| 40 |
+
return fmt.Errorf("error storing account: %v", err)
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
Current = auth
|
| 44 |
+
|
| 45 |
+
err = writeCurrentAuth()
|
| 46 |
+
|
| 47 |
+
if err != nil {
|
| 48 |
+
return fmt.Errorf("error writing auth: %v", err)
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
return nil
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
func storeAccount(toStore *shared.ClientAccount) error {
|
| 55 |
+
accounts, err := loadAccounts()
|
| 56 |
+
|
| 57 |
+
if err != nil {
|
| 58 |
+
return fmt.Errorf("error loading accounts: %v", err)
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
found := false
|
| 62 |
+
for i, account := range accounts {
|
| 63 |
+
if account.UserId == toStore.UserId {
|
| 64 |
+
accounts[i] = toStore
|
| 65 |
+
found = true
|
| 66 |
+
break
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
if !found {
|
| 71 |
+
accounts = append(accounts, toStore)
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
bytes, err := json.Marshal(accounts)
|
| 75 |
+
|
| 76 |
+
if err != nil {
|
| 77 |
+
return fmt.Errorf("error marshalling accounts: %v", err)
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
err = os.WriteFile(fs.HomeAccountsPath, bytes, os.ModePerm)
|
| 81 |
+
|
| 82 |
+
if err != nil {
|
| 83 |
+
return fmt.Errorf("error writing accounts: %v", err)
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
return nil
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
func writeCurrentAuth() error {
|
| 90 |
+
if Current == nil {
|
| 91 |
+
return fmt.Errorf("error writing auth: auth not loaded")
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
bytes, err := json.Marshal(Current)
|
| 95 |
+
|
| 96 |
+
if err != nil {
|
| 97 |
+
return fmt.Errorf("error marshalling auth: %v", err)
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
err = os.WriteFile(fs.HomeAuthPath, bytes, os.ModePerm)
|
| 101 |
+
|
| 102 |
+
if err != nil {
|
| 103 |
+
return fmt.Errorf("error writing auth: %v", err)
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
return nil
|
| 107 |
+
}
|
app/cli/auth/trial.go
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package auth
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"plandex-cli/term"
|
| 6 |
+
"time"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
func ConvertTrial() {
|
| 10 |
+
openAuthenticatedURL("Opening Plandex Cloud upgrade flow in your browser.", "/settings/billing?upgrade=1&cliUpgrade=1")
|
| 11 |
+
|
| 12 |
+
fmt.Println("\nCommand will continue automatically once you've upgraded...")
|
| 13 |
+
fmt.Println()
|
| 14 |
+
term.StartSpinner("")
|
| 15 |
+
|
| 16 |
+
startTime := time.Now()
|
| 17 |
+
expirationTime := startTime.Add(1 * time.Hour)
|
| 18 |
+
|
| 19 |
+
for time.Now().Before(expirationTime) {
|
| 20 |
+
org, apiErr := apiClient.GetOrgSession()
|
| 21 |
+
|
| 22 |
+
if apiErr != nil {
|
| 23 |
+
term.StopSpinner()
|
| 24 |
+
term.OutputErrorAndExit("error getting org session: %s", apiErr.Msg)
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
if org != nil && !org.IsTrial {
|
| 28 |
+
term.StopSpinner()
|
| 29 |
+
fmt.Println("🚀 Trial upgraded")
|
| 30 |
+
fmt.Println()
|
| 31 |
+
return
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
time.Sleep(1500 * time.Millisecond)
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
term.StopSpinner()
|
| 38 |
+
term.OutputErrorAndExit("Timed out waiting for upgrade. Please try again. Email support@plandex.ai if the problem persists.")
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
func startTrial() {
|
| 42 |
+
term.StartSpinner("")
|
| 43 |
+
cliTrialToken, apiErr := apiClient.CreateCliTrialSession()
|
| 44 |
+
term.StopSpinner()
|
| 45 |
+
|
| 46 |
+
if apiErr != nil {
|
| 47 |
+
term.OutputErrorAndExit("error starting trial: %s", apiErr.Msg)
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
openUnauthenticatedCloudURL(
|
| 51 |
+
"Opening Plandex Cloud trial flow in your browser.",
|
| 52 |
+
fmt.Sprintf("/start?cliTrialToken=%s", cliTrialToken),
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
fmt.Println("\nCommand will continue automatically once you've started your trial...")
|
| 56 |
+
fmt.Println()
|
| 57 |
+
term.StartSpinner("")
|
| 58 |
+
|
| 59 |
+
startTime := time.Now()
|
| 60 |
+
expirationTime := startTime.Add(1 * time.Hour)
|
| 61 |
+
|
| 62 |
+
for time.Now().Before(expirationTime) {
|
| 63 |
+
cliTrialSession, apiErr := apiClient.GetCliTrialSession(cliTrialToken)
|
| 64 |
+
|
| 65 |
+
if apiErr != nil {
|
| 66 |
+
term.StopSpinner()
|
| 67 |
+
term.OutputErrorAndExit("error getting cli trial session: %s", apiErr.Msg)
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
if cliTrialSession != nil {
|
| 71 |
+
// Trial session is valid, break the loop and sign in
|
| 72 |
+
term.StopSpinner()
|
| 73 |
+
fmt.Println("🚀 Trial started")
|
| 74 |
+
fmt.Println()
|
| 75 |
+
err := handleSignInResponse(cliTrialSession, "")
|
| 76 |
+
if err != nil {
|
| 77 |
+
term.OutputErrorAndExit("error signing in after trial started: %s", err)
|
| 78 |
+
}
|
| 79 |
+
return
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
time.Sleep(1500 * time.Millisecond)
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
term.StopSpinner()
|
| 86 |
+
term.OutputErrorAndExit("Timed out waiting for trial to start. Please try again. Email support@plandex.ai if the problem persists.")
|
| 87 |
+
}
|
app/cli/cmd/apply.go
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"plandex-cli/auth"
|
| 5 |
+
"plandex-cli/lib"
|
| 6 |
+
"plandex-cli/plan_exec"
|
| 7 |
+
"plandex-cli/term"
|
| 8 |
+
"plandex-cli/types"
|
| 9 |
+
|
| 10 |
+
"github.com/spf13/cobra"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
var autoCommit, skipCommit, autoExec bool
|
| 14 |
+
|
| 15 |
+
func init() {
|
| 16 |
+
initApplyFlags(applyCmd, false)
|
| 17 |
+
initExecScriptFlags(applyCmd)
|
| 18 |
+
RootCmd.AddCommand(applyCmd)
|
| 19 |
+
|
| 20 |
+
applyCmd.Flags().BoolVar(&fullAuto, "full", false, "Apply the plan and debug in full auto mode")
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
var applyCmd = &cobra.Command{
|
| 24 |
+
Use: "apply",
|
| 25 |
+
Aliases: []string{"ap"},
|
| 26 |
+
Short: "Apply a plan to the project",
|
| 27 |
+
Run: apply,
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
func apply(cmd *cobra.Command, args []string) {
|
| 31 |
+
auth.MustResolveAuthWithOrg()
|
| 32 |
+
lib.MustResolveProject()
|
| 33 |
+
|
| 34 |
+
if fullAuto {
|
| 35 |
+
term.StartSpinner("")
|
| 36 |
+
config := lib.MustGetCurrentPlanConfig()
|
| 37 |
+
_, updatedConfig, printFn := resolveAutoModeSilent(config)
|
| 38 |
+
lib.SetCachedPlanConfig(updatedConfig)
|
| 39 |
+
term.StopSpinner()
|
| 40 |
+
printFn()
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
mustSetPlanExecFlags(cmd, true)
|
| 44 |
+
|
| 45 |
+
if lib.CurrentPlanId == "" {
|
| 46 |
+
term.OutputNoCurrentPlanErrorAndExit()
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
applyFlags := types.ApplyFlags{
|
| 50 |
+
AutoConfirm: true,
|
| 51 |
+
AutoCommit: autoCommit,
|
| 52 |
+
NoCommit: skipCommit,
|
| 53 |
+
AutoExec: autoExec,
|
| 54 |
+
NoExec: noExec,
|
| 55 |
+
AutoDebug: autoDebug,
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
tellFlags := types.TellFlags{
|
| 59 |
+
TellBg: tellBg,
|
| 60 |
+
TellStop: tellStop,
|
| 61 |
+
TellNoBuild: tellNoBuild,
|
| 62 |
+
AutoContext: tellAutoContext,
|
| 63 |
+
ExecEnabled: !noExec,
|
| 64 |
+
AutoApply: tellAutoApply,
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
lib.MustApplyPlan(lib.ApplyPlanParams{
|
| 68 |
+
PlanId: lib.CurrentPlanId,
|
| 69 |
+
Branch: lib.CurrentBranch,
|
| 70 |
+
ApplyFlags: applyFlags,
|
| 71 |
+
TellFlags: tellFlags,
|
| 72 |
+
OnExecFail: plan_exec.GetOnApplyExecFail(applyFlags, tellFlags),
|
| 73 |
+
})
|
| 74 |
+
}
|
app/cli/cmd/archive.go
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"plandex-cli/api"
|
| 6 |
+
"plandex-cli/auth"
|
| 7 |
+
"plandex-cli/lib"
|
| 8 |
+
"plandex-cli/term"
|
| 9 |
+
"strconv"
|
| 10 |
+
"strings"
|
| 11 |
+
|
| 12 |
+
shared "plandex-shared"
|
| 13 |
+
|
| 14 |
+
"github.com/fatih/color"
|
| 15 |
+
"github.com/spf13/cobra"
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
var archiveCmd = &cobra.Command{
|
| 19 |
+
Use: "archive [name-or-index]",
|
| 20 |
+
Aliases: []string{"arc"},
|
| 21 |
+
Short: "Archive a plan",
|
| 22 |
+
Args: cobra.MaximumNArgs(1),
|
| 23 |
+
Run: archive,
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
func init() {
|
| 27 |
+
RootCmd.AddCommand(archiveCmd)
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
func archive(cmd *cobra.Command, args []string) {
|
| 31 |
+
auth.MustResolveAuthWithOrg()
|
| 32 |
+
lib.MustResolveProject()
|
| 33 |
+
|
| 34 |
+
var nameOrIdx string
|
| 35 |
+
if len(args) > 0 {
|
| 36 |
+
nameOrIdx = strings.TrimSpace(args[0])
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
var plan *shared.Plan
|
| 40 |
+
|
| 41 |
+
term.StartSpinner("")
|
| 42 |
+
plans, apiErr := api.Client.ListPlans([]string{lib.CurrentProjectId})
|
| 43 |
+
term.StopSpinner()
|
| 44 |
+
|
| 45 |
+
if apiErr != nil {
|
| 46 |
+
term.OutputErrorAndExit("Error getting plans: %v", apiErr)
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
if len(plans) == 0 {
|
| 50 |
+
fmt.Println("🤷♂️ No plans available to archive")
|
| 51 |
+
return
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
if nameOrIdx == "" {
|
| 55 |
+
opts := make([]string, len(plans))
|
| 56 |
+
for i, p := range plans {
|
| 57 |
+
opts[i] = p.Name
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
selected, err := term.SelectFromList("Select a plan to archive", opts)
|
| 61 |
+
if err != nil {
|
| 62 |
+
term.OutputErrorAndExit("Error selecting plan: %v", err)
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
for _, p := range plans {
|
| 66 |
+
if p.Name == selected {
|
| 67 |
+
plan = p
|
| 68 |
+
break
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
} else {
|
| 72 |
+
idx, err := strconv.Atoi(nameOrIdx)
|
| 73 |
+
if err == nil && idx > 0 && idx <= len(plans) {
|
| 74 |
+
plan = plans[idx-1]
|
| 75 |
+
} else {
|
| 76 |
+
for _, p := range plans {
|
| 77 |
+
if p.Name == nameOrIdx {
|
| 78 |
+
plan = p
|
| 79 |
+
break
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
if plan == nil {
|
| 86 |
+
term.OutputErrorAndExit("Plan not found")
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
err := api.Client.ArchivePlan(plan.Id)
|
| 90 |
+
if err != nil {
|
| 91 |
+
term.OutputErrorAndExit("Error archiving plan: %v", err)
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
fmt.Printf("✅ Plan %s archived\n", color.New(color.Bold, term.ColorHiYellow).Sprint(plan.Name))
|
| 95 |
+
|
| 96 |
+
fmt.Println()
|
| 97 |
+
|
| 98 |
+
term.PrintCmds("", "plans --archived", "unarchive")
|
| 99 |
+
}
|
app/cli/cmd/billing.go
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"plandex-cli/auth"
|
| 5 |
+
"plandex-cli/term"
|
| 6 |
+
"plandex-cli/ui"
|
| 7 |
+
|
| 8 |
+
"github.com/spf13/cobra"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
var billingCmd = &cobra.Command{
|
| 12 |
+
Use: "billing",
|
| 13 |
+
Short: "Open the billing page in the browser",
|
| 14 |
+
Run: billing,
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
func init() {
|
| 18 |
+
RootCmd.AddCommand(billingCmd)
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
func billing(cmd *cobra.Command, args []string) {
|
| 22 |
+
auth.MustResolveAuthWithOrg()
|
| 23 |
+
|
| 24 |
+
if !auth.Current.IsCloud {
|
| 25 |
+
term.OutputErrorAndExit("This command is only available for Plandex Cloud accounts.")
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
ui.OpenAuthenticatedURL("Opening billing page in your default browser...", "/settings/billing")
|
| 29 |
+
}
|
app/cli/cmd/branches.go
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"os"
|
| 6 |
+
"plandex-cli/api"
|
| 7 |
+
"plandex-cli/auth"
|
| 8 |
+
"plandex-cli/format"
|
| 9 |
+
"plandex-cli/lib"
|
| 10 |
+
"plandex-cli/term"
|
| 11 |
+
"strconv"
|
| 12 |
+
|
| 13 |
+
"github.com/fatih/color"
|
| 14 |
+
"github.com/olekukonko/tablewriter"
|
| 15 |
+
"github.com/spf13/cobra"
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
var branchesCmd = &cobra.Command{
|
| 19 |
+
Use: "branches",
|
| 20 |
+
Aliases: []string{"br"},
|
| 21 |
+
Short: "List plan branches",
|
| 22 |
+
Run: branches,
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
func init() {
|
| 26 |
+
RootCmd.AddCommand(branchesCmd)
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
func branches(cmd *cobra.Command, args []string) {
|
| 30 |
+
auth.MustResolveAuthWithOrg()
|
| 31 |
+
lib.MustResolveProject()
|
| 32 |
+
|
| 33 |
+
if lib.CurrentPlanId == "" {
|
| 34 |
+
term.OutputNoCurrentPlanErrorAndExit()
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
term.StartSpinner("")
|
| 38 |
+
|
| 39 |
+
branches, apiErr := api.Client.ListBranches(lib.CurrentPlanId)
|
| 40 |
+
|
| 41 |
+
term.StopSpinner()
|
| 42 |
+
|
| 43 |
+
if apiErr != nil {
|
| 44 |
+
term.OutputErrorAndExit("Error getting branches: %v", apiErr)
|
| 45 |
+
return
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
table := tablewriter.NewWriter(os.Stdout)
|
| 49 |
+
table.SetAutoWrapText(false)
|
| 50 |
+
table.SetHeader([]string{"#", "Name", "Updated" /* "Created",*/, "Context", "Convo"})
|
| 51 |
+
|
| 52 |
+
for i, b := range branches {
|
| 53 |
+
num := strconv.Itoa(i + 1)
|
| 54 |
+
if b.Name == lib.CurrentBranch {
|
| 55 |
+
num = color.New(color.Bold, term.ColorHiGreen).Sprint(num)
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
var name string
|
| 59 |
+
if b.Name == lib.CurrentBranch {
|
| 60 |
+
name = color.New(color.Bold, term.ColorHiGreen).Sprint(b.Name) + " 👈"
|
| 61 |
+
} else {
|
| 62 |
+
name = b.Name
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
row := []string{
|
| 66 |
+
num,
|
| 67 |
+
name,
|
| 68 |
+
format.Time(b.UpdatedAt),
|
| 69 |
+
// format.Time(b.CreatedAt),
|
| 70 |
+
strconv.Itoa(b.ContextTokens) + " 🪙",
|
| 71 |
+
strconv.Itoa(b.ConvoTokens) + " 🪙",
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
var style []tablewriter.Colors
|
| 75 |
+
if b.Name == lib.CurrentPlanId {
|
| 76 |
+
style = []tablewriter.Colors{
|
| 77 |
+
{tablewriter.FgGreenColor, tablewriter.Bold},
|
| 78 |
+
}
|
| 79 |
+
} else {
|
| 80 |
+
style = []tablewriter.Colors{
|
| 81 |
+
{tablewriter.Bold},
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
table.Rich(row, style)
|
| 86 |
+
|
| 87 |
+
}
|
| 88 |
+
table.Render()
|
| 89 |
+
fmt.Println()
|
| 90 |
+
term.PrintCmds("", "checkout", "delete-branch")
|
| 91 |
+
|
| 92 |
+
}
|
app/cli/cmd/browser.go
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"errors"
|
| 6 |
+
"fmt"
|
| 7 |
+
"log"
|
| 8 |
+
"os"
|
| 9 |
+
"os/exec"
|
| 10 |
+
"os/signal"
|
| 11 |
+
"runtime"
|
| 12 |
+
"syscall"
|
| 13 |
+
"time"
|
| 14 |
+
|
| 15 |
+
chrome_log "github.com/chromedp/cdproto/log"
|
| 16 |
+
chrome_runtime "github.com/chromedp/cdproto/runtime"
|
| 17 |
+
"github.com/chromedp/cdproto/target"
|
| 18 |
+
"github.com/chromedp/chromedp"
|
| 19 |
+
"github.com/spf13/cobra"
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
var timeoutSeconds int
|
| 23 |
+
|
| 24 |
+
// browserCmd is our cobra command
|
| 25 |
+
var browserCmd = &cobra.Command{
|
| 26 |
+
Use: "browser [urls...]",
|
| 27 |
+
Short: "Open browser windows with given URLs, capturing console logs and exiting on JS errors",
|
| 28 |
+
RunE: browser,
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
func init() {
|
| 32 |
+
RootCmd.AddCommand(browserCmd)
|
| 33 |
+
browserCmd.Flags().IntVar(&timeoutSeconds, "timeout", 10, "Timeout in seconds for browser to load")
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// browser is the main function for our command.
|
| 37 |
+
func browser(cmd *cobra.Command, args []string) error {
|
| 38 |
+
if len(args) == 0 {
|
| 39 |
+
return errors.New("no URLs provided")
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// See if we can find Chrome or Firefox in PATH:
|
| 43 |
+
chromePath, _ := findChrome()
|
| 44 |
+
|
| 45 |
+
if chromePath != "" {
|
| 46 |
+
fmt.Println("Using Chrome (not default browser, but found in PATH).")
|
| 47 |
+
return openChromeWithLogs(args)
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
// Fallback: open with OS default tool (open/xdg-open)
|
| 51 |
+
fmt.Println("No Chrome or Firefox found; falling back to OS default opener.")
|
| 52 |
+
return openWithOSDefault(args)
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// findChrome returns the path to Chrome/Chromium if found, or "" if not found.
|
| 56 |
+
func findChrome() (string, error) {
|
| 57 |
+
switch runtime.GOOS {
|
| 58 |
+
case "darwin":
|
| 59 |
+
// macOS standard installation paths
|
| 60 |
+
macPaths := []string{
|
| 61 |
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
| 62 |
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
| 63 |
+
}
|
| 64 |
+
for _, p := range macPaths {
|
| 65 |
+
if _, err := os.Stat(p); err == nil {
|
| 66 |
+
return p, nil
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
case "linux", "freebsd":
|
| 70 |
+
// Linux/FreeBSD (usually in PATH)
|
| 71 |
+
candidates := []string{
|
| 72 |
+
"google-chrome",
|
| 73 |
+
"google-chrome-stable",
|
| 74 |
+
"chromium",
|
| 75 |
+
"chromium-browser",
|
| 76 |
+
}
|
| 77 |
+
for _, c := range candidates {
|
| 78 |
+
if path, err := exec.LookPath(c); err == nil {
|
| 79 |
+
return path, nil
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
return "", errors.New("Chrome/Chromium not found")
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
var pages = map[target.ID]string{}
|
| 87 |
+
|
| 88 |
+
// openChromeWithLogs uses chromedp to open each URL in a visible Chrome browser,
|
| 89 |
+
// logs JS console messages, and exits on the first JS error (or if user presses Ctrl+C).
|
| 90 |
+
func openChromeWithLogs(urls []string) error {
|
| 91 |
+
if len(urls) == 0 {
|
| 92 |
+
return errors.New("no URLs provided")
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
fmt.Println("Launching Chrome with console log capture...")
|
| 96 |
+
// 1) Create a cancellable context that sets up a Chrome ExecAllocator
|
| 97 |
+
rootCtx, cancelAllocator := chromedp.NewExecAllocator(context.Background(),
|
| 98 |
+
chromedp.Flag("headless", false), // Visible, not headless
|
| 99 |
+
chromedp.Flag("disable-gpu", false),
|
| 100 |
+
chromedp.Flag("no-first-run", true), // Avoid "Welcome" dialog
|
| 101 |
+
chromedp.Flag("no-default-browser-check", true), // Avoid default browser dialog
|
| 102 |
+
chromedp.Flag("disable-default-apps", true), // Prevent default apps/extensions from loading
|
| 103 |
+
chromedp.Flag("remote-debugging-port", "0"), // Ensures separate debug session
|
| 104 |
+
)
|
| 105 |
+
defer cancelAllocator()
|
| 106 |
+
|
| 107 |
+
// 2) Create a new browser context from the allocator
|
| 108 |
+
browserCtx, cancelBrowser := chromedp.NewContext(rootCtx)
|
| 109 |
+
defer cancelBrowser()
|
| 110 |
+
|
| 111 |
+
// Add this to get the initial target
|
| 112 |
+
var initialTargetID target.ID
|
| 113 |
+
chromedp.ListenTarget(browserCtx, func(ev interface{}) {
|
| 114 |
+
if e, ok := ev.(*target.EventTargetCreated); ok {
|
| 115 |
+
// fmt.Printf("[DEBUG] Got event: %#v\n", e)
|
| 116 |
+
if e.TargetInfo.Type == "page" {
|
| 117 |
+
initialTargetID = e.TargetInfo.TargetID
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
})
|
| 121 |
+
|
| 122 |
+
if err := chromedp.Run(
|
| 123 |
+
browserCtx,
|
| 124 |
+
chrome_runtime.Enable(),
|
| 125 |
+
chrome_log.Enable(),
|
| 126 |
+
); err != nil {
|
| 127 |
+
log.Fatal(err)
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
// Listen for console API calls & JS exceptions
|
| 131 |
+
chromedp.ListenBrowser(browserCtx, func(ev interface{}) {
|
| 132 |
+
// fmt.Printf("[DEBUG] Got event: %#v\n", ev)
|
| 133 |
+
switch e := ev.(type) {
|
| 134 |
+
|
| 135 |
+
case *target.EventTargetCreated:
|
| 136 |
+
case *target.EventAttachedToTarget:
|
| 137 |
+
// This sometimes also includes target info
|
| 138 |
+
info := e.TargetInfo
|
| 139 |
+
if info.Type == "page" {
|
| 140 |
+
url := info.URL
|
| 141 |
+
if url == "about:blank" {
|
| 142 |
+
url = urls[0]
|
| 143 |
+
}
|
| 144 |
+
pages[info.TargetID] = url
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
case *target.EventTargetDestroyed:
|
| 148 |
+
// Something was destroyed
|
| 149 |
+
destroyedID := e.TargetID
|
| 150 |
+
// Check if it was a top-level page
|
| 151 |
+
if url, ok := pages[destroyedID]; ok {
|
| 152 |
+
fmt.Printf("[CHROME] Closed page: %s\n", url)
|
| 153 |
+
// remove from map
|
| 154 |
+
delete(pages, destroyedID)
|
| 155 |
+
// If that was your last page, do something:
|
| 156 |
+
if len(pages) == 0 {
|
| 157 |
+
cancelBrowser()
|
| 158 |
+
cancelAllocator()
|
| 159 |
+
os.Exit(0)
|
| 160 |
+
} else {
|
| 161 |
+
fmt.Printf("num pages left: %d\n", len(pages))
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
})
|
| 166 |
+
|
| 167 |
+
// Channels to handle signals & error detection
|
| 168 |
+
sigChan := make(chan os.Signal, 1)
|
| 169 |
+
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
| 170 |
+
|
| 171 |
+
errChan := make(chan bool, 1)
|
| 172 |
+
|
| 173 |
+
// 3) Open all URLs in new tabs
|
| 174 |
+
for i, url := range urls {
|
| 175 |
+
// Reuse the initial browser tab for the first URL
|
| 176 |
+
var ctx context.Context
|
| 177 |
+
if i == 0 {
|
| 178 |
+
ctx, _ = chromedp.NewContext(browserCtx, chromedp.WithTargetID(initialTargetID))
|
| 179 |
+
} else {
|
| 180 |
+
// Create a new tab for each additional URL
|
| 181 |
+
ctx, _ = chromedp.NewContext(browserCtx)
|
| 182 |
+
}
|
| 183 |
+
tabCtx, _ := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
| 184 |
+
|
| 185 |
+
chromedp.ListenTarget(tabCtx, func(ev interface{}) {
|
| 186 |
+
switch e := ev.(type) {
|
| 187 |
+
case *chrome_runtime.EventConsoleAPICalled:
|
| 188 |
+
logType := e.Type.String()
|
| 189 |
+
for _, arg := range e.Args {
|
| 190 |
+
val := arg.Value
|
| 191 |
+
fmt.Printf("[CHROME:%s] %v\n", logType, val)
|
| 192 |
+
if logType == "error" {
|
| 193 |
+
fmt.Println("❌ Chrome console error")
|
| 194 |
+
errChan <- true
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
case *chrome_log.EventEntryAdded:
|
| 198 |
+
fmt.Printf("[CHROME:Log:%s] %s\n", e.Entry.Level, e.Entry.Text)
|
| 199 |
+
case *chrome_runtime.EventExceptionThrown:
|
| 200 |
+
fmt.Printf("[CHROME:Exception] %s\n", e.ExceptionDetails.Error())
|
| 201 |
+
fmt.Println("❌ JavaScript exception")
|
| 202 |
+
errChan <- true
|
| 203 |
+
}
|
| 204 |
+
})
|
| 205 |
+
|
| 206 |
+
if err := chromedp.Run(
|
| 207 |
+
tabCtx,
|
| 208 |
+
chrome_runtime.Enable(),
|
| 209 |
+
chrome_log.Enable(),
|
| 210 |
+
chromedp.Navigate(url),
|
| 211 |
+
chromedp.WaitReady("body", chromedp.ByQuery),
|
| 212 |
+
); err != nil {
|
| 213 |
+
fmt.Printf("Failed to load url %s: %v", url, err)
|
| 214 |
+
cancelBrowser()
|
| 215 |
+
cancelAllocator()
|
| 216 |
+
os.Exit(1)
|
| 217 |
+
} else {
|
| 218 |
+
fmt.Printf("Opened (%d/%d): %s\n", i+1, len(urls), url)
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
fmt.Println("Chrome is running, waiting for error or interrupt...")
|
| 223 |
+
|
| 224 |
+
// 4) Wait forever, or until an error or signal
|
| 225 |
+
select {
|
| 226 |
+
case <-sigChan:
|
| 227 |
+
fmt.Println("\n⚠️ Plandex browser process interrupted")
|
| 228 |
+
return nil
|
| 229 |
+
case <-rootCtx.Done():
|
| 230 |
+
fmt.Println("\n⚠️ Plandex browser process closed")
|
| 231 |
+
return nil
|
| 232 |
+
case <-errChan:
|
| 233 |
+
fmt.Println("\n❌ Plandex browser process exited due to JavaScript error")
|
| 234 |
+
cancelBrowser()
|
| 235 |
+
cancelAllocator()
|
| 236 |
+
os.Exit(1)
|
| 237 |
+
}
|
| 238 |
+
return nil
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
func openWithOSDefault(urls []string) error {
|
| 242 |
+
var openCmd string
|
| 243 |
+
switch runtime.GOOS {
|
| 244 |
+
case "darwin":
|
| 245 |
+
openCmd = "open"
|
| 246 |
+
case "linux", "freebsd":
|
| 247 |
+
openCmd = "xdg-open"
|
| 248 |
+
default:
|
| 249 |
+
return errors.New("unsupported OS for fallback")
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
for _, url := range urls {
|
| 253 |
+
cmd := exec.Command(openCmd, url)
|
| 254 |
+
if err := cmd.Start(); err != nil {
|
| 255 |
+
log.Printf("Failed to open %s with %s: %v", url, openCmd, err)
|
| 256 |
+
}
|
| 257 |
+
}
|
| 258 |
+
return nil
|
| 259 |
+
}
|
app/cli/cmd/build.go
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"plandex-cli/auth"
|
| 6 |
+
"plandex-cli/lib"
|
| 7 |
+
"plandex-cli/plan_exec"
|
| 8 |
+
"plandex-cli/term"
|
| 9 |
+
"plandex-cli/types"
|
| 10 |
+
|
| 11 |
+
shared "plandex-shared"
|
| 12 |
+
|
| 13 |
+
"github.com/spf13/cobra"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
var buildCmd = &cobra.Command{
|
| 17 |
+
Use: "build",
|
| 18 |
+
Aliases: []string{"b"},
|
| 19 |
+
Short: "Build pending changes",
|
| 20 |
+
// Long: ``,
|
| 21 |
+
Args: cobra.NoArgs,
|
| 22 |
+
Run: build,
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
func init() {
|
| 26 |
+
RootCmd.AddCommand(buildCmd)
|
| 27 |
+
|
| 28 |
+
initExecFlags(buildCmd, initExecFlagsParams{
|
| 29 |
+
omitFile: true,
|
| 30 |
+
omitNoBuild: true,
|
| 31 |
+
omitEditor: true,
|
| 32 |
+
omitStop: true,
|
| 33 |
+
omitAutoContext: true,
|
| 34 |
+
omitSmartContext: true,
|
| 35 |
+
})
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
func build(cmd *cobra.Command, args []string) {
|
| 39 |
+
auth.MustResolveAuthWithOrg()
|
| 40 |
+
lib.MustResolveProject()
|
| 41 |
+
|
| 42 |
+
mustSetPlanExecFlags(cmd, false)
|
| 43 |
+
|
| 44 |
+
didBuild, err := plan_exec.Build(plan_exec.ExecParams{
|
| 45 |
+
CurrentPlanId: lib.CurrentPlanId,
|
| 46 |
+
CurrentBranch: lib.CurrentBranch,
|
| 47 |
+
AuthVars: lib.MustVerifyAuthVars(auth.Current.IntegratedModelsMode),
|
| 48 |
+
CheckOutdatedContext: func(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (bool, bool, error) {
|
| 49 |
+
auto := autoConfirm || tellAutoApply || tellAutoContext
|
| 50 |
+
return lib.CheckOutdatedContextWithOutput(auto, auto, maybeContexts, projectPaths)
|
| 51 |
+
},
|
| 52 |
+
}, types.BuildFlags{
|
| 53 |
+
BuildBg: tellBg,
|
| 54 |
+
AutoApply: tellAutoApply,
|
| 55 |
+
})
|
| 56 |
+
|
| 57 |
+
if err != nil {
|
| 58 |
+
term.OutputErrorAndExit("Error building plan: %v", err)
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
if !didBuild {
|
| 62 |
+
fmt.Println()
|
| 63 |
+
term.PrintCmds("", "log", "tell", "continue")
|
| 64 |
+
return
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
if tellBg {
|
| 68 |
+
fmt.Println("🏗️ Building plan in the background")
|
| 69 |
+
fmt.Println()
|
| 70 |
+
term.PrintCmds("", "ps", "connect", "stop")
|
| 71 |
+
} else if tellAutoApply {
|
| 72 |
+
applyFlags := types.ApplyFlags{
|
| 73 |
+
AutoConfirm: true,
|
| 74 |
+
AutoCommit: autoCommit,
|
| 75 |
+
NoCommit: !autoCommit,
|
| 76 |
+
NoExec: noExec,
|
| 77 |
+
AutoExec: autoExec,
|
| 78 |
+
AutoDebug: autoDebug,
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
tellFlags := types.TellFlags{
|
| 82 |
+
AutoContext: tellAutoContext,
|
| 83 |
+
ExecEnabled: !noExec,
|
| 84 |
+
AutoApply: tellAutoApply,
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
lib.MustApplyPlan(lib.ApplyPlanParams{
|
| 88 |
+
PlanId: lib.CurrentPlanId,
|
| 89 |
+
Branch: lib.CurrentBranch,
|
| 90 |
+
ApplyFlags: applyFlags,
|
| 91 |
+
TellFlags: tellFlags,
|
| 92 |
+
OnExecFail: plan_exec.GetOnApplyExecFail(applyFlags, tellFlags),
|
| 93 |
+
})
|
| 94 |
+
} else {
|
| 95 |
+
fmt.Println()
|
| 96 |
+
term.PrintCmds("", "diff", "diff --ui", "apply", "reject", "log")
|
| 97 |
+
}
|
| 98 |
+
}
|
app/cli/cmd/cd.go
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"plandex-cli/api"
|
| 6 |
+
"plandex-cli/auth"
|
| 7 |
+
"plandex-cli/lib"
|
| 8 |
+
"plandex-cli/term"
|
| 9 |
+
"strconv"
|
| 10 |
+
"strings"
|
| 11 |
+
"time"
|
| 12 |
+
|
| 13 |
+
shared "plandex-shared"
|
| 14 |
+
|
| 15 |
+
"github.com/fatih/color"
|
| 16 |
+
"github.com/spf13/cobra"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
func init() {
|
| 20 |
+
RootCmd.AddCommand(cdCmd)
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
var cdCmd = &cobra.Command{
|
| 24 |
+
Use: "cd [name-or-index]",
|
| 25 |
+
Aliases: []string{"set-plan"},
|
| 26 |
+
Short: "Set current plan by name or index",
|
| 27 |
+
Args: cobra.MaximumNArgs(1),
|
| 28 |
+
Run: cd,
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
func cd(cmd *cobra.Command, args []string) {
|
| 32 |
+
auth.MustResolveAuthWithOrg()
|
| 33 |
+
lib.MustResolveProject()
|
| 34 |
+
|
| 35 |
+
var nameOrIdx string
|
| 36 |
+
if len(args) > 0 {
|
| 37 |
+
nameOrIdx = strings.TrimSpace(args[0])
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
var plan *shared.Plan
|
| 41 |
+
|
| 42 |
+
term.StartSpinner("")
|
| 43 |
+
plans, apiErr := api.Client.ListPlans([]string{lib.CurrentProjectId})
|
| 44 |
+
term.StopSpinner()
|
| 45 |
+
|
| 46 |
+
if apiErr != nil {
|
| 47 |
+
term.OutputErrorAndExit("Error getting plans: %v", apiErr)
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
if len(plans) == 0 {
|
| 51 |
+
fmt.Println("🤷♂️ No plans")
|
| 52 |
+
fmt.Println()
|
| 53 |
+
term.PrintCmds("", "new")
|
| 54 |
+
return
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
if nameOrIdx == "" {
|
| 58 |
+
|
| 59 |
+
opts := make([]string, len(plans))
|
| 60 |
+
for i, plan := range plans {
|
| 61 |
+
opts[i] = plan.Name
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
selected, err := term.SelectFromList("Select a plan", opts)
|
| 65 |
+
|
| 66 |
+
if err != nil {
|
| 67 |
+
term.OutputErrorAndExit("Error selecting plan: %v", err)
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
for _, p := range plans {
|
| 71 |
+
if p.Name == selected {
|
| 72 |
+
plan = p
|
| 73 |
+
break
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
} else {
|
| 77 |
+
// see if it's an index
|
| 78 |
+
idx, err := strconv.Atoi(nameOrIdx)
|
| 79 |
+
|
| 80 |
+
if err == nil {
|
| 81 |
+
if idx > 0 && idx <= len(plans) {
|
| 82 |
+
plan = plans[idx-1]
|
| 83 |
+
} else {
|
| 84 |
+
term.OutputErrorAndExit("Plan index out of range")
|
| 85 |
+
}
|
| 86 |
+
} else {
|
| 87 |
+
for _, p := range plans {
|
| 88 |
+
if p.Name == nameOrIdx {
|
| 89 |
+
plan = p
|
| 90 |
+
break
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
if plan == nil {
|
| 97 |
+
term.OutputErrorAndExit("Plan not found")
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
err := lib.WriteCurrentPlan(plan.Id)
|
| 101 |
+
if err != nil {
|
| 102 |
+
term.OutputErrorAndExit("Error setting current plan: %v", err)
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// reload current plan, which will also handle setting the right branch
|
| 106 |
+
lib.MustLoadCurrentPlan()
|
| 107 |
+
|
| 108 |
+
// fire and forget SetProjectPlan request (we don't care about the response or errors)
|
| 109 |
+
// this only matters for setting the current plan on a new device (i.e. when the current plan is not set)
|
| 110 |
+
go api.Client.SetProjectPlan(lib.CurrentProjectId, shared.SetProjectPlanRequest{PlanId: plan.Id})
|
| 111 |
+
|
| 112 |
+
// give the SetProjectPlan request some time to be sent before exiting
|
| 113 |
+
time.Sleep(50 * time.Millisecond)
|
| 114 |
+
|
| 115 |
+
fmt.Println("✅ Changed current plan to " + color.New(term.ColorHiGreen, color.Bold).Sprint(plan.Name))
|
| 116 |
+
|
| 117 |
+
fmt.Println()
|
| 118 |
+
term.PrintCmds("", "current")
|
| 119 |
+
}
|
app/cli/cmd/chat.go
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"plandex-cli/auth"
|
| 6 |
+
"plandex-cli/lib"
|
| 7 |
+
"plandex-cli/plan_exec"
|
| 8 |
+
"plandex-cli/types"
|
| 9 |
+
|
| 10 |
+
shared "plandex-shared"
|
| 11 |
+
|
| 12 |
+
"github.com/spf13/cobra"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
var chatCmd = &cobra.Command{
|
| 16 |
+
Use: "chat [prompt]",
|
| 17 |
+
Aliases: []string{"c"},
|
| 18 |
+
Short: "Chat without making changes",
|
| 19 |
+
// Long: ``,
|
| 20 |
+
Args: cobra.RangeArgs(0, 1),
|
| 21 |
+
Run: doChat,
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
func init() {
|
| 25 |
+
RootCmd.AddCommand(chatCmd)
|
| 26 |
+
|
| 27 |
+
initExecFlags(chatCmd, initExecFlagsParams{
|
| 28 |
+
omitNoBuild: true,
|
| 29 |
+
omitStop: true,
|
| 30 |
+
omitBg: true,
|
| 31 |
+
omitApply: true,
|
| 32 |
+
omitExec: true,
|
| 33 |
+
omitSmartContext: true,
|
| 34 |
+
omitSkipMenu: true,
|
| 35 |
+
})
|
| 36 |
+
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
func doChat(cmd *cobra.Command, args []string) {
|
| 40 |
+
auth.MustResolveAuthWithOrg()
|
| 41 |
+
lib.MustResolveProject()
|
| 42 |
+
mustSetPlanExecFlags(cmd, false)
|
| 43 |
+
|
| 44 |
+
prompt := getTellPrompt(args)
|
| 45 |
+
|
| 46 |
+
if prompt == "" {
|
| 47 |
+
fmt.Println("🤷♂️ No prompt to send")
|
| 48 |
+
return
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
plan_exec.TellPlan(plan_exec.ExecParams{
|
| 52 |
+
CurrentPlanId: lib.CurrentPlanId,
|
| 53 |
+
CurrentBranch: lib.CurrentBranch,
|
| 54 |
+
AuthVars: lib.MustVerifyAuthVars(auth.Current.IntegratedModelsMode),
|
| 55 |
+
CheckOutdatedContext: func(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (bool, bool, error) {
|
| 56 |
+
auto := autoConfirm || tellAutoApply || tellAutoContext
|
| 57 |
+
return lib.CheckOutdatedContextWithOutput(auto, auto, maybeContexts, projectPaths)
|
| 58 |
+
},
|
| 59 |
+
}, prompt, types.TellFlags{
|
| 60 |
+
IsChatOnly: true,
|
| 61 |
+
AutoContext: tellAutoContext,
|
| 62 |
+
SkipChangesMenu: tellSkipMenu,
|
| 63 |
+
})
|
| 64 |
+
}
|
app/cli/cmd/checkout.go
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"plandex-cli/api"
|
| 6 |
+
"plandex-cli/auth"
|
| 7 |
+
"plandex-cli/lib"
|
| 8 |
+
"plandex-cli/term"
|
| 9 |
+
"strconv"
|
| 10 |
+
"strings"
|
| 11 |
+
|
| 12 |
+
shared "plandex-shared"
|
| 13 |
+
|
| 14 |
+
"github.com/fatih/color"
|
| 15 |
+
"github.com/spf13/cobra"
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
const (
|
| 19 |
+
OptCreateNewBranch = "Create a new branch"
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
var confirmCreateBranch bool
|
| 23 |
+
|
| 24 |
+
var checkoutCmd = &cobra.Command{
|
| 25 |
+
Use: "checkout [name-or-index]",
|
| 26 |
+
Aliases: []string{"co"},
|
| 27 |
+
Short: "Checkout an existing plan branch or create a new one",
|
| 28 |
+
Run: checkout,
|
| 29 |
+
Args: cobra.MaximumNArgs(1),
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
func init() {
|
| 33 |
+
RootCmd.AddCommand(checkoutCmd)
|
| 34 |
+
checkoutCmd.Flags().BoolVarP(&confirmCreateBranch, "yes", "y", false, "Confirm creating a new branch")
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
func checkout(cmd *cobra.Command, args []string) {
|
| 38 |
+
auth.MustResolveAuthWithOrg()
|
| 39 |
+
lib.MustResolveProject()
|
| 40 |
+
|
| 41 |
+
if lib.CurrentPlanId == "" {
|
| 42 |
+
term.OutputNoCurrentPlanErrorAndExit()
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
branchName := ""
|
| 46 |
+
willCreate := false
|
| 47 |
+
|
| 48 |
+
var nameOrIdx string
|
| 49 |
+
if len(args) > 0 {
|
| 50 |
+
nameOrIdx = strings.TrimSpace(args[0])
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
term.StartSpinner("")
|
| 54 |
+
branches, apiErr := api.Client.ListBranches(lib.CurrentPlanId)
|
| 55 |
+
term.StopSpinner()
|
| 56 |
+
|
| 57 |
+
if apiErr != nil {
|
| 58 |
+
term.OutputErrorAndExit("Error getting branches: %v", apiErr)
|
| 59 |
+
return
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
if nameOrIdx != "" {
|
| 63 |
+
idx, err := strconv.Atoi(nameOrIdx)
|
| 64 |
+
|
| 65 |
+
if err == nil {
|
| 66 |
+
if idx > 0 && idx <= len(branches) {
|
| 67 |
+
branchName = branches[idx-1].Name
|
| 68 |
+
} else {
|
| 69 |
+
term.OutputErrorAndExit("Branch %d not found", idx)
|
| 70 |
+
}
|
| 71 |
+
} else {
|
| 72 |
+
for _, b := range branches {
|
| 73 |
+
if b.Name == nameOrIdx {
|
| 74 |
+
branchName = b.Name
|
| 75 |
+
break
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
if branchName == "" {
|
| 81 |
+
fmt.Printf("🌱 Branch %s not found\n", color.New(color.Bold, term.ColorHiCyan).Sprint(nameOrIdx))
|
| 82 |
+
|
| 83 |
+
if confirmCreateBranch {
|
| 84 |
+
fmt.Println("✅ --yes flag set, will create branch")
|
| 85 |
+
branchName = nameOrIdx
|
| 86 |
+
willCreate = true
|
| 87 |
+
} else {
|
| 88 |
+
res, err := term.ConfirmYesNo("Create it now?")
|
| 89 |
+
|
| 90 |
+
if err != nil {
|
| 91 |
+
term.OutputErrorAndExit("Error getting user input: %v", err)
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
if res {
|
| 95 |
+
branchName = nameOrIdx
|
| 96 |
+
willCreate = true
|
| 97 |
+
} else {
|
| 98 |
+
return
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
if nameOrIdx == "" {
|
| 106 |
+
opts := make([]string, len(branches))
|
| 107 |
+
for i, branch := range branches {
|
| 108 |
+
opts[i] = branch.Name
|
| 109 |
+
}
|
| 110 |
+
opts = append(opts, OptCreateNewBranch)
|
| 111 |
+
|
| 112 |
+
selected, err := term.SelectFromList("Select a branch", opts)
|
| 113 |
+
|
| 114 |
+
if err != nil {
|
| 115 |
+
term.OutputErrorAndExit("Error selecting branch: %v", err)
|
| 116 |
+
return
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
if selected == OptCreateNewBranch {
|
| 120 |
+
branchName, err = term.GetRequiredUserStringInput("Branch name")
|
| 121 |
+
if err != nil {
|
| 122 |
+
term.OutputErrorAndExit("Error getting branch name: %v", err)
|
| 123 |
+
return
|
| 124 |
+
}
|
| 125 |
+
willCreate = true
|
| 126 |
+
} else {
|
| 127 |
+
branchName = selected
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
if branchName == "" {
|
| 132 |
+
term.OutputErrorAndExit("Branch not found")
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
term.StartSpinner("")
|
| 136 |
+
if willCreate {
|
| 137 |
+
err := api.Client.CreateBranch(lib.CurrentPlanId, lib.CurrentBranch, shared.CreateBranchRequest{Name: branchName})
|
| 138 |
+
|
| 139 |
+
if err != nil {
|
| 140 |
+
term.OutputErrorAndExit("Error creating branch: %v", err)
|
| 141 |
+
return
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
// fmt.Printf("✅ Created branch %s\n", color.New(color.Bold, term.ColorHiGreen).Sprint(branchName))
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
err := lib.WriteCurrentBranch(branchName)
|
| 148 |
+
|
| 149 |
+
if err != nil {
|
| 150 |
+
term.OutputErrorAndExit("Error setting current branch: %v", err)
|
| 151 |
+
return
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
updatedModelSettings, err := lib.SaveLatestPlanModelSettingsIfNeeded()
|
| 155 |
+
term.StopSpinner()
|
| 156 |
+
if err != nil {
|
| 157 |
+
term.OutputErrorAndExit("Error saving model settings: %v", err)
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
term.StopSpinner()
|
| 161 |
+
|
| 162 |
+
fmt.Printf("✅ Checked out branch %s\n", color.New(color.Bold, term.ColorHiGreen).Sprint(branchName))
|
| 163 |
+
|
| 164 |
+
if updatedModelSettings {
|
| 165 |
+
fmt.Println()
|
| 166 |
+
fmt.Println("🧠 Model settings file updated → ", lib.GetPlanModelSettingsPath(lib.CurrentPlanId))
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
fmt.Println()
|
| 170 |
+
term.PrintCmds("", "load", "tell", "branches", "delete-branch")
|
| 171 |
+
|
| 172 |
+
}
|
app/cli/cmd/claude_max.go
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"plandex-cli/auth"
|
| 6 |
+
"plandex-cli/lib"
|
| 7 |
+
"plandex-cli/term"
|
| 8 |
+
|
| 9 |
+
"github.com/fatih/color"
|
| 10 |
+
"github.com/spf13/cobra"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
var connectClaudeCmd = &cobra.Command{
|
| 14 |
+
Use: "connect-claude",
|
| 15 |
+
Short: "Connect your Claude Pro or Max subscription",
|
| 16 |
+
Run: connectClaude,
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
var disconnectClaudeCmd = &cobra.Command{
|
| 20 |
+
Use: "disconnect-claude",
|
| 21 |
+
Short: "Disconnect your Claude Pro or Max subscription",
|
| 22 |
+
Run: disconnectClaude,
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
var claudeStatusCmd = &cobra.Command{
|
| 26 |
+
Use: "claude-status",
|
| 27 |
+
Short: "Check the status of your Claude Pro or Max subscription",
|
| 28 |
+
Run: claudeStatus,
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
func connectClaude(cmd *cobra.Command, args []string) {
|
| 32 |
+
auth.MustResolveAuthWithOrg()
|
| 33 |
+
lib.ConnectClaudeMax()
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
func disconnectClaude(cmd *cobra.Command, args []string) {
|
| 37 |
+
auth.MustResolveAuthWithOrg()
|
| 38 |
+
lib.DisconnectClaudeMax()
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
func claudeStatus(cmd *cobra.Command, args []string) {
|
| 42 |
+
auth.MustResolveAuthWithOrg()
|
| 43 |
+
|
| 44 |
+
creds, err := lib.GetAccountCredentials()
|
| 45 |
+
if err != nil {
|
| 46 |
+
term.OutputErrorAndExit("Error getting account credentials: %v", err)
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
orgUserConfig := lib.MustGetOrgUserConfig()
|
| 50 |
+
|
| 51 |
+
connected := creds.ClaudeMax != nil && orgUserConfig.UseClaudeSubscription
|
| 52 |
+
|
| 53 |
+
if connected {
|
| 54 |
+
fmt.Println("✅ Claude Pro or Max subscription is connected")
|
| 55 |
+
|
| 56 |
+
// if orgUserConfig.IsClaudeSubscriptionCooldownActive() {
|
| 57 |
+
if true {
|
| 58 |
+
fmt.Println()
|
| 59 |
+
color.New(term.ColorHiYellow, color.Bold).Println("⏳ You've reached your Claude Pro or Max subscription quota")
|
| 60 |
+
fmt.Println("The next provider with valid credentials will be used for Anthropic models until the quota resets")
|
| 61 |
+
fmt.Println()
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
term.PrintCmds("", "disconnect-claude")
|
| 65 |
+
} else {
|
| 66 |
+
fmt.Println("❌ No Claude Pro or Max subscription is connected")
|
| 67 |
+
term.PrintCmds("", "connect-claude")
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
func init() {
|
| 72 |
+
RootCmd.AddCommand(connectClaudeCmd)
|
| 73 |
+
RootCmd.AddCommand(disconnectClaudeCmd)
|
| 74 |
+
RootCmd.AddCommand(claudeStatusCmd)
|
| 75 |
+
}
|
app/cli/cmd/clear.go
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"plandex-cli/api"
|
| 6 |
+
"plandex-cli/auth"
|
| 7 |
+
"plandex-cli/lib"
|
| 8 |
+
"plandex-cli/term"
|
| 9 |
+
|
| 10 |
+
shared "plandex-shared"
|
| 11 |
+
|
| 12 |
+
"github.com/spf13/cobra"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
var clearCmd = &cobra.Command{
|
| 16 |
+
Use: "clear",
|
| 17 |
+
Short: "Clear all context",
|
| 18 |
+
Long: `Clear all context.`,
|
| 19 |
+
Run: clearAllContext,
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
func clearAllContext(cmd *cobra.Command, args []string) {
|
| 23 |
+
auth.MustResolveAuthWithOrg()
|
| 24 |
+
lib.MustResolveProject()
|
| 25 |
+
|
| 26 |
+
if lib.CurrentPlanId == "" {
|
| 27 |
+
term.OutputNoCurrentPlanErrorAndExit()
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
term.StartSpinner("")
|
| 31 |
+
contexts, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)
|
| 32 |
+
term.StopSpinner()
|
| 33 |
+
|
| 34 |
+
if err != nil {
|
| 35 |
+
term.OutputErrorAndExit("Error retrieving context: %v", err)
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
deleteIds := map[string]bool{}
|
| 39 |
+
|
| 40 |
+
for _, context := range contexts {
|
| 41 |
+
deleteIds[context.Id] = true
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
if len(deleteIds) > 0 {
|
| 45 |
+
res, err := api.Client.DeleteContext(lib.CurrentPlanId, lib.CurrentBranch, shared.DeleteContextRequest{
|
| 46 |
+
Ids: deleteIds,
|
| 47 |
+
})
|
| 48 |
+
|
| 49 |
+
if err != nil {
|
| 50 |
+
term.OutputErrorAndExit("Error deleting context: %v", err)
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
fmt.Println("✅ " + res.Msg)
|
| 54 |
+
} else {
|
| 55 |
+
fmt.Println("🤷♂️ No context removed")
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
func init() {
|
| 61 |
+
RootCmd.AddCommand(clearCmd)
|
| 62 |
+
}
|
app/cli/cmd/config.go
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"plandex-cli/api"
|
| 6 |
+
"plandex-cli/auth"
|
| 7 |
+
"plandex-cli/lib"
|
| 8 |
+
"plandex-cli/term"
|
| 9 |
+
|
| 10 |
+
"github.com/fatih/color"
|
| 11 |
+
"github.com/spf13/cobra"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
func init() {
|
| 15 |
+
RootCmd.AddCommand(configCmd)
|
| 16 |
+
configCmd.AddCommand(defaultConfigCmd)
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
var configCmd = &cobra.Command{
|
| 20 |
+
Use: "config",
|
| 21 |
+
Short: "Show plan config",
|
| 22 |
+
Run: config,
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
var defaultConfigCmd = &cobra.Command{
|
| 26 |
+
Use: "default",
|
| 27 |
+
Short: "Show default config for new plans",
|
| 28 |
+
Run: defaultConfig,
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
func config(cmd *cobra.Command, args []string) {
|
| 32 |
+
auth.MustResolveAuthWithOrg()
|
| 33 |
+
lib.MustResolveProject()
|
| 34 |
+
|
| 35 |
+
if lib.CurrentPlanId == "" {
|
| 36 |
+
term.OutputNoCurrentPlanErrorAndExit()
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
term.StartSpinner("")
|
| 40 |
+
|
| 41 |
+
config, apiErr := api.Client.GetPlanConfig(lib.CurrentPlanId)
|
| 42 |
+
if apiErr != nil {
|
| 43 |
+
term.StopSpinner()
|
| 44 |
+
term.OutputErrorAndExit("Error getting config: %v", apiErr.Msg)
|
| 45 |
+
return
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
term.StopSpinner()
|
| 49 |
+
|
| 50 |
+
color.New(color.Bold, term.ColorHiCyan).Println("⚙️ Plan Config")
|
| 51 |
+
lib.ShowPlanConfig(config, "")
|
| 52 |
+
fmt.Println()
|
| 53 |
+
|
| 54 |
+
term.PrintCmds("", "set-config", "config default", "set-config default")
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
func defaultConfig(cmd *cobra.Command, args []string) {
|
| 58 |
+
auth.MustResolveAuth(false)
|
| 59 |
+
|
| 60 |
+
term.StartSpinner("")
|
| 61 |
+
config, err := api.Client.GetDefaultPlanConfig()
|
| 62 |
+
term.StopSpinner()
|
| 63 |
+
|
| 64 |
+
if err != nil {
|
| 65 |
+
term.OutputErrorAndExit("Error getting default config: %v", err)
|
| 66 |
+
return
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
color.New(color.Bold, term.ColorHiCyan).Println("⚙️ Default Config")
|
| 70 |
+
lib.ShowPlanConfig(config, "")
|
| 71 |
+
fmt.Println()
|
| 72 |
+
|
| 73 |
+
term.PrintCmds("", "set-config default", "config", "set-config")
|
| 74 |
+
}
|
app/cli/cmd/connect.go
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"os"
|
| 6 |
+
"plandex-cli/api"
|
| 7 |
+
"plandex-cli/auth"
|
| 8 |
+
"plandex-cli/lib"
|
| 9 |
+
"plandex-cli/stream"
|
| 10 |
+
streamtui "plandex-cli/stream_tui"
|
| 11 |
+
"plandex-cli/term"
|
| 12 |
+
|
| 13 |
+
"github.com/spf13/cobra"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
var connectCmd = &cobra.Command{
|
| 17 |
+
Use: "connect [stream-id-or-plan] [branch]",
|
| 18 |
+
Aliases: []string{"conn"},
|
| 19 |
+
Short: "Connect to an active stream",
|
| 20 |
+
// Long: ``,
|
| 21 |
+
Args: cobra.MaximumNArgs(2),
|
| 22 |
+
Run: connect,
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
func init() {
|
| 26 |
+
RootCmd.AddCommand(connectCmd)
|
| 27 |
+
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
func connect(cmd *cobra.Command, args []string) {
|
| 31 |
+
auth.MustResolveAuthWithOrg()
|
| 32 |
+
lib.MustResolveProject()
|
| 33 |
+
|
| 34 |
+
if lib.CurrentPlanId == "" {
|
| 35 |
+
term.OutputNoCurrentPlanErrorAndExit()
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
planId, branch, shouldContinue := lib.SelectActiveStream(args)
|
| 39 |
+
|
| 40 |
+
if !shouldContinue {
|
| 41 |
+
return
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
term.StartSpinner("")
|
| 45 |
+
apiErr := api.Client.ConnectPlan(planId, branch, stream.OnStreamPlan)
|
| 46 |
+
term.StopSpinner()
|
| 47 |
+
|
| 48 |
+
if apiErr != nil {
|
| 49 |
+
term.OutputErrorAndExit("Error connecting to stream: %v", apiErr)
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
go func() {
|
| 53 |
+
err := streamtui.StartStreamUI("", false, true)
|
| 54 |
+
|
| 55 |
+
if err != nil {
|
| 56 |
+
term.OutputErrorAndExit("Error starting stream UI", err)
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
fmt.Println()
|
| 60 |
+
term.PrintCmds("", "diff", "diff --ui", "apply", "reject", "log")
|
| 61 |
+
|
| 62 |
+
os.Exit(0)
|
| 63 |
+
}()
|
| 64 |
+
|
| 65 |
+
// Wait for the stream to finish
|
| 66 |
+
select {}
|
| 67 |
+
}
|
app/cli/cmd/context_show.go
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"log"
|
| 6 |
+
"plandex-cli/api"
|
| 7 |
+
"plandex-cli/auth"
|
| 8 |
+
"plandex-cli/lib"
|
| 9 |
+
"strconv"
|
| 10 |
+
|
| 11 |
+
"github.com/spf13/cobra"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
func init() {
|
| 15 |
+
RootCmd.AddCommand(contextShowCmd)
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
var contextShowCmd = &cobra.Command{
|
| 19 |
+
Use: "show [name-or-index]",
|
| 20 |
+
Short: "Show the body of a context by name or list index",
|
| 21 |
+
Args: cobra.ExactArgs(1),
|
| 22 |
+
RunE: func(cmd *cobra.Command, args []string) error {
|
| 23 |
+
auth.MustResolveAuthWithOrg()
|
| 24 |
+
lib.MustResolveProject()
|
| 25 |
+
|
| 26 |
+
nameOrIndex := args[0]
|
| 27 |
+
|
| 28 |
+
// Get list of contexts first
|
| 29 |
+
contexts, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)
|
| 30 |
+
if err != nil {
|
| 31 |
+
log.Printf("Error listing contexts: %v\n", err)
|
| 32 |
+
return fmt.Errorf("error listing contexts: %v", err)
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
var contextId string
|
| 36 |
+
|
| 37 |
+
// Try parsing as index first
|
| 38 |
+
if idx, err := strconv.Atoi(nameOrIndex); err == nil {
|
| 39 |
+
// Convert to 0-based index
|
| 40 |
+
idx--
|
| 41 |
+
if idx < 0 || idx >= len(contexts) {
|
| 42 |
+
return fmt.Errorf("invalid context index: %s", nameOrIndex)
|
| 43 |
+
}
|
| 44 |
+
contextId = contexts[idx].Id
|
| 45 |
+
} else {
|
| 46 |
+
// Try finding by name
|
| 47 |
+
found := false
|
| 48 |
+
for _, ctx := range contexts {
|
| 49 |
+
if ctx.Name == nameOrIndex || ctx.FilePath == nameOrIndex {
|
| 50 |
+
contextId = ctx.Id
|
| 51 |
+
found = true
|
| 52 |
+
break
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
if !found {
|
| 56 |
+
return fmt.Errorf("no context found with name: %s", nameOrIndex)
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
res, apiErr := api.Client.GetContextBody(lib.CurrentPlanId, lib.CurrentBranch, contextId)
|
| 61 |
+
if apiErr != nil {
|
| 62 |
+
log.Printf("Error getting context body: %v\n", apiErr)
|
| 63 |
+
return fmt.Errorf("error getting context body: %v", apiErr)
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
fmt.Println(res.Body)
|
| 67 |
+
return nil
|
| 68 |
+
},
|
| 69 |
+
}
|
app/cli/cmd/continue.go
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"plandex-cli/auth"
|
| 5 |
+
"plandex-cli/lib"
|
| 6 |
+
"plandex-cli/plan_exec"
|
| 7 |
+
"plandex-cli/types"
|
| 8 |
+
|
| 9 |
+
shared "plandex-shared"
|
| 10 |
+
|
| 11 |
+
"github.com/spf13/cobra"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
var (
|
| 15 |
+
chatOnly bool
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
var continueCmd = &cobra.Command{
|
| 19 |
+
Use: "continue",
|
| 20 |
+
Aliases: []string{"c"},
|
| 21 |
+
Short: "Continue the plan",
|
| 22 |
+
Run: doContinue,
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
func init() {
|
| 26 |
+
RootCmd.AddCommand(continueCmd)
|
| 27 |
+
|
| 28 |
+
continueCmd.Flags().BoolVar(&chatOnly, "chat", false, "Continue in chat mode (no file changes)")
|
| 29 |
+
|
| 30 |
+
initExecFlags(continueCmd, initExecFlagsParams{
|
| 31 |
+
omitFile: true,
|
| 32 |
+
omitEditor: true,
|
| 33 |
+
})
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
func doContinue(cmd *cobra.Command, args []string) {
|
| 37 |
+
auth.MustResolveAuthWithOrg()
|
| 38 |
+
lib.MustResolveProject()
|
| 39 |
+
mustSetPlanExecFlags(cmd, false)
|
| 40 |
+
|
| 41 |
+
tellFlags := types.TellFlags{
|
| 42 |
+
TellBg: tellBg,
|
| 43 |
+
TellStop: tellStop,
|
| 44 |
+
TellNoBuild: tellNoBuild,
|
| 45 |
+
IsUserContinue: true,
|
| 46 |
+
ExecEnabled: !noExec,
|
| 47 |
+
AutoContext: tellAutoContext,
|
| 48 |
+
SmartContext: tellSmartContext,
|
| 49 |
+
AutoApply: tellAutoApply,
|
| 50 |
+
IsChatOnly: chatOnly,
|
| 51 |
+
SkipChangesMenu: tellSkipMenu,
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
plan_exec.TellPlan(plan_exec.ExecParams{
|
| 55 |
+
CurrentPlanId: lib.CurrentPlanId,
|
| 56 |
+
CurrentBranch: lib.CurrentBranch,
|
| 57 |
+
AuthVars: lib.MustVerifyAuthVars(auth.Current.IntegratedModelsMode),
|
| 58 |
+
CheckOutdatedContext: func(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (bool, bool, error) {
|
| 59 |
+
auto := autoConfirm || tellAutoApply || tellAutoContext
|
| 60 |
+
|
| 61 |
+
return lib.CheckOutdatedContextWithOutput(auto, auto, maybeContexts, projectPaths)
|
| 62 |
+
},
|
| 63 |
+
}, "", tellFlags)
|
| 64 |
+
|
| 65 |
+
if tellAutoApply {
|
| 66 |
+
applyFlags := types.ApplyFlags{
|
| 67 |
+
AutoConfirm: true,
|
| 68 |
+
AutoCommit: autoCommit,
|
| 69 |
+
NoCommit: !autoCommit,
|
| 70 |
+
AutoExec: autoExec,
|
| 71 |
+
NoExec: noExec,
|
| 72 |
+
AutoDebug: autoDebug,
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
lib.MustApplyPlan(lib.ApplyPlanParams{
|
| 76 |
+
PlanId: lib.CurrentPlanId,
|
| 77 |
+
Branch: lib.CurrentBranch,
|
| 78 |
+
ApplyFlags: applyFlags,
|
| 79 |
+
TellFlags: tellFlags,
|
| 80 |
+
OnExecFail: plan_exec.GetOnApplyExecFail(applyFlags, tellFlags),
|
| 81 |
+
})
|
| 82 |
+
}
|
| 83 |
+
}
|
app/cli/cmd/convo.go
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"plandex-cli/api"
|
| 6 |
+
"plandex-cli/auth"
|
| 7 |
+
"plandex-cli/lib"
|
| 8 |
+
"plandex-cli/term"
|
| 9 |
+
"regexp"
|
| 10 |
+
"strings"
|
| 11 |
+
"time"
|
| 12 |
+
|
| 13 |
+
"github.com/fatih/color"
|
| 14 |
+
"github.com/spf13/cobra"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
var plainTextOutput bool
|
| 18 |
+
var convoRaw bool
|
| 19 |
+
|
| 20 |
+
// convoCmd represents the convo command
|
| 21 |
+
var convoCmd = &cobra.Command{
|
| 22 |
+
Use: "convo [msg-range]",
|
| 23 |
+
Short: "Display complete conversation history",
|
| 24 |
+
Long: `Display complete conversation history. Optionally specify a message number or range of messages (e.g. '1' or '5' or '1-5' or '5-')`,
|
| 25 |
+
Run: convo,
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
func init() {
|
| 29 |
+
RootCmd.AddCommand(convoCmd)
|
| 30 |
+
|
| 31 |
+
convoCmd.Flags().BoolVarP(&plainTextOutput, "plain", "p", false, "Output conversation in plain text with no ANSI codes")
|
| 32 |
+
|
| 33 |
+
// for debugging output
|
| 34 |
+
convoCmd.Flags().BoolVar(&convoRaw, "raw", false, "Output conversation in raw format")
|
| 35 |
+
convoCmd.Flags().MarkHidden("raw")
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
const stoppedEarlyMsg = "You stopped the reply early"
|
| 39 |
+
|
| 40 |
+
func convo(cmd *cobra.Command, args []string) {
|
| 41 |
+
auth.MustResolveAuthWithOrg()
|
| 42 |
+
lib.MustResolveProject()
|
| 43 |
+
|
| 44 |
+
term.StartSpinner("")
|
| 45 |
+
conversation, apiErr := api.Client.ListConvo(lib.CurrentPlanId, lib.CurrentBranch)
|
| 46 |
+
term.StopSpinner()
|
| 47 |
+
|
| 48 |
+
if apiErr != nil {
|
| 49 |
+
term.OutputErrorAndExit("Error loading conversation: %v", apiErr.Msg)
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
if len(conversation) == 0 {
|
| 53 |
+
fmt.Println("🤷♂️ No conversation history")
|
| 54 |
+
return
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
var msgRange string
|
| 58 |
+
var msgRangeStart, msgRangeEnd int
|
| 59 |
+
if len(args) > 0 {
|
| 60 |
+
msgRange = args[0]
|
| 61 |
+
}
|
| 62 |
+
if msgRange != "" {
|
| 63 |
+
// validate either a number or a range of numbers
|
| 64 |
+
if strings.Contains(msgRange, "-") {
|
| 65 |
+
_, err := fmt.Sscanf(msgRange, "%d-%d", &msgRangeStart, &msgRangeEnd)
|
| 66 |
+
if err != nil {
|
| 67 |
+
_, err := fmt.Sscanf(msgRange, "%d-", &msgRangeStart)
|
| 68 |
+
|
| 69 |
+
if err != nil {
|
| 70 |
+
term.OutputErrorAndExit("Invalid message range: %s", msgRange)
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
msgRangeEnd = len(conversation)
|
| 74 |
+
}
|
| 75 |
+
} else {
|
| 76 |
+
_, err := fmt.Sscanf(msgRange, "%d", &msgRangeStart)
|
| 77 |
+
if err != nil {
|
| 78 |
+
term.OutputErrorAndExit("Invalid message number: %s", msgRange)
|
| 79 |
+
}
|
| 80 |
+
msgRangeEnd = msgRangeStart
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
var convo string
|
| 85 |
+
var totalTokens int
|
| 86 |
+
var didCut bool
|
| 87 |
+
for i, msg := range conversation {
|
| 88 |
+
if msgRangeStart > 0 && msg.Num < msgRangeStart {
|
| 89 |
+
didCut = true
|
| 90 |
+
continue
|
| 91 |
+
}
|
| 92 |
+
if msgRangeEnd > 0 && msg.Num > msgRangeEnd {
|
| 93 |
+
didCut = true
|
| 94 |
+
break
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
var author string
|
| 98 |
+
if msg.Role == "assistant" {
|
| 99 |
+
author = "🤖 Plandex"
|
| 100 |
+
} else if msg.Role == "user" {
|
| 101 |
+
author = "💬 You"
|
| 102 |
+
} else {
|
| 103 |
+
author = msg.Role
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
replyTags := msg.Flags.GetReplyTags()
|
| 107 |
+
|
| 108 |
+
// format as above but start with day of week
|
| 109 |
+
formattedTs := msg.CreatedAt.Local().Format("Mon Jan 2, 2006 | 3:04pm MST")
|
| 110 |
+
|
| 111 |
+
// if it's today then use 'Today' instead of the date
|
| 112 |
+
if msg.CreatedAt.Day() == time.Now().Day() {
|
| 113 |
+
formattedTs = msg.CreatedAt.Local().Format("Today | 3:04pm MST")
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
// if it's yesterday then use 'Yesterday' instead of the date
|
| 117 |
+
if msg.CreatedAt.Day() == time.Now().AddDate(0, 0, -1).Day() {
|
| 118 |
+
formattedTs = msg.CreatedAt.Local().Format("Yesterday | 3:04pm MST")
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
var header string
|
| 122 |
+
if len(replyTags) > 0 {
|
| 123 |
+
header = fmt.Sprintf("#### %d | %s | %s | %s | %d 🪙 ", i+1,
|
| 124 |
+
author, strings.Join(replyTags, " | "), formattedTs, msg.Tokens)
|
| 125 |
+
} else {
|
| 126 |
+
header = fmt.Sprintf("#### %d | %s | %s | %d 🪙 ", i+1,
|
| 127 |
+
author, formattedTs, msg.Tokens)
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
txt := msg.Message
|
| 131 |
+
if !convoRaw {
|
| 132 |
+
txt = convertCodeBlocks(msg.Message)
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
if plainTextOutput {
|
| 136 |
+
convo += header + "\n" + txt + "\n\n"
|
| 137 |
+
} else {
|
| 138 |
+
md, err := term.GetMarkdown(header + "\n" + txt + "\n\n")
|
| 139 |
+
if err != nil {
|
| 140 |
+
term.OutputErrorAndExit("Error creating markdown representation: %v", err)
|
| 141 |
+
}
|
| 142 |
+
convo += md
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
if !didCut && msg.Stopped {
|
| 146 |
+
if plainTextOutput {
|
| 147 |
+
convo += fmt.Sprintf(" 🛑 %s\n\n", stoppedEarlyMsg)
|
| 148 |
+
} else {
|
| 149 |
+
convo += fmt.Sprintf(" 🛑 %s\n\n", color.New(color.Bold).Sprint(stoppedEarlyMsg))
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
totalTokens += msg.Tokens
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
if !plainTextOutput {
|
| 157 |
+
convo = strings.ReplaceAll(convo, stoppedEarlyMsg, color.New(term.ColorHiRed).Sprint(stoppedEarlyMsg))
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
output :=
|
| 161 |
+
fmt.Sprintf("\n%s", convo)
|
| 162 |
+
|
| 163 |
+
if !plainTextOutput && !didCut {
|
| 164 |
+
output += term.GetDivisionLine() +
|
| 165 |
+
color.New(color.Bold, term.ColorHiCyan).Sprint(" Conversation size →") + fmt.Sprintf(" %d 🪙", totalTokens) + "\n\n"
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
if plainTextOutput {
|
| 169 |
+
fmt.Println(output)
|
| 170 |
+
} else {
|
| 171 |
+
term.PageOutput(output)
|
| 172 |
+
|
| 173 |
+
fmt.Println()
|
| 174 |
+
term.PrintCmds("", "convo 1", "convo 2-5", "convo --plain", "log")
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
var codeBlockPattern = regexp.MustCompile(`<PlandexBlock\s+lang="(.+?)".*?>([\s\S]+?)</PlandexBlock>`)
|
| 179 |
+
|
| 180 |
+
func convertCodeBlocks(msg string) string {
|
| 181 |
+
return codeBlockPattern.ReplaceAllStringFunc(msg, func(match string) string {
|
| 182 |
+
// Extract language and content from the match
|
| 183 |
+
submatches := codeBlockPattern.FindStringSubmatch(match)
|
| 184 |
+
lang := submatches[1]
|
| 185 |
+
content := submatches[2]
|
| 186 |
+
|
| 187 |
+
// Escape any backticks in the content
|
| 188 |
+
content = strings.ReplaceAll(content, "```", "\\`\\`\\`")
|
| 189 |
+
|
| 190 |
+
// Return markdown code block format
|
| 191 |
+
return fmt.Sprintf("```%s%s```", lang, content)
|
| 192 |
+
})
|
| 193 |
+
}
|
app/cli/cmd/current.go
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"plandex-cli/api"
|
| 6 |
+
"plandex-cli/auth"
|
| 7 |
+
"plandex-cli/lib"
|
| 8 |
+
"plandex-cli/term"
|
| 9 |
+
|
| 10 |
+
shared "plandex-shared"
|
| 11 |
+
|
| 12 |
+
"github.com/spf13/cobra"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
var currentCmd = &cobra.Command{
|
| 16 |
+
Use: "current",
|
| 17 |
+
Aliases: []string{"cu"},
|
| 18 |
+
Short: "Get the current plan",
|
| 19 |
+
Run: current,
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
func init() {
|
| 23 |
+
RootCmd.AddCommand(currentCmd)
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
func current(cmd *cobra.Command, args []string) {
|
| 27 |
+
auth.MustResolveAuthWithOrg()
|
| 28 |
+
lib.MaybeResolveProject()
|
| 29 |
+
|
| 30 |
+
if lib.CurrentPlanId == "" {
|
| 31 |
+
term.OutputNoCurrentPlanErrorAndExit()
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
term.StartSpinner("")
|
| 35 |
+
plan, err := api.Client.GetPlan(lib.CurrentPlanId)
|
| 36 |
+
term.StopSpinner()
|
| 37 |
+
|
| 38 |
+
if err != nil {
|
| 39 |
+
term.OutputErrorAndExit("Error getting plan: %v", err)
|
| 40 |
+
return
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
currentBranchesByPlanId, err := api.Client.GetCurrentBranchByPlanId(lib.CurrentProjectId, shared.GetCurrentBranchByPlanIdRequest{
|
| 44 |
+
CurrentBranchByPlanId: map[string]string{
|
| 45 |
+
lib.CurrentPlanId: lib.CurrentBranch,
|
| 46 |
+
},
|
| 47 |
+
})
|
| 48 |
+
|
| 49 |
+
if err != nil {
|
| 50 |
+
term.OutputErrorAndExit("Error getting current branches: %v", err)
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
table := lib.GetCurrentPlanTable(plan, currentBranchesByPlanId, nil)
|
| 54 |
+
fmt.Println(table)
|
| 55 |
+
|
| 56 |
+
term.PrintCmds("", "tell", "ls", "plans")
|
| 57 |
+
|
| 58 |
+
}
|
app/cli/cmd/debug.go
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bufio"
|
| 5 |
+
"context"
|
| 6 |
+
"fmt"
|
| 7 |
+
"log"
|
| 8 |
+
"os"
|
| 9 |
+
"os/exec"
|
| 10 |
+
"os/signal"
|
| 11 |
+
"plandex-cli/auth"
|
| 12 |
+
"plandex-cli/lib"
|
| 13 |
+
"plandex-cli/plan_exec"
|
| 14 |
+
"plandex-cli/term"
|
| 15 |
+
"plandex-cli/types"
|
| 16 |
+
"strconv"
|
| 17 |
+
"strings"
|
| 18 |
+
"sync"
|
| 19 |
+
"sync/atomic"
|
| 20 |
+
"syscall"
|
| 21 |
+
"time"
|
| 22 |
+
|
| 23 |
+
shared "plandex-shared"
|
| 24 |
+
|
| 25 |
+
"github.com/fatih/color"
|
| 26 |
+
"github.com/spf13/cobra"
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
const DebugDefaultTries = 5
|
| 30 |
+
|
| 31 |
+
var debugCmd = &cobra.Command{
|
| 32 |
+
Use: "debug [tries] <cmd>",
|
| 33 |
+
Aliases: []string{"db"},
|
| 34 |
+
Short: "Debug a failing command with Plandex",
|
| 35 |
+
Args: cobra.MinimumNArgs(1),
|
| 36 |
+
Run: doDebug,
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
func init() {
|
| 40 |
+
RootCmd.AddCommand(debugCmd)
|
| 41 |
+
debugCmd.Flags().BoolVarP(&autoCommit, "commit", "c", false, "Commit changes after successful execution")
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
func doDebug(cmd *cobra.Command, args []string) {
|
| 45 |
+
auth.MustResolveAuthWithOrg()
|
| 46 |
+
lib.MustResolveProject()
|
| 47 |
+
mustSetPlanExecFlags(cmd, false)
|
| 48 |
+
|
| 49 |
+
if lib.CurrentPlanId == "" {
|
| 50 |
+
term.OutputNoCurrentPlanErrorAndExit()
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// Parse tries and command
|
| 54 |
+
tries := DebugDefaultTries
|
| 55 |
+
cmdArgs := args
|
| 56 |
+
|
| 57 |
+
// Check if first arg is tries count
|
| 58 |
+
if val, err := strconv.Atoi(args[0]); err == nil {
|
| 59 |
+
if val <= 0 {
|
| 60 |
+
term.OutputErrorAndExit("Tries must be greater than 0")
|
| 61 |
+
}
|
| 62 |
+
tries = val
|
| 63 |
+
cmdArgs = args[1:]
|
| 64 |
+
if len(cmdArgs) == 0 {
|
| 65 |
+
term.OutputErrorAndExit("No command specified")
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
// Get current working directory
|
| 70 |
+
cwd, err := os.Getwd()
|
| 71 |
+
if err != nil {
|
| 72 |
+
term.OutputErrorAndExit("Failed to get working directory: %v", err)
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
cmdStr := strings.Join(cmdArgs, " ")
|
| 76 |
+
|
| 77 |
+
// Execute command and handle retries
|
| 78 |
+
for attempt := 0; attempt < tries; attempt++ {
|
| 79 |
+
// Use shell to handle operators like && and |
|
| 80 |
+
shellCmdStr := "set -euo pipefail; " + cmdStr
|
| 81 |
+
execCmd := exec.Command("sh", "-c", shellCmdStr)
|
| 82 |
+
execCmd.Dir = cwd
|
| 83 |
+
execCmd.Env = os.Environ()
|
| 84 |
+
lib.SetPlatformSpecificAttrs(execCmd)
|
| 85 |
+
|
| 86 |
+
pipe, err := execCmd.StdoutPipe()
|
| 87 |
+
if err != nil {
|
| 88 |
+
term.StopSpinner()
|
| 89 |
+
term.OutputErrorAndExit("Failed to create pipe: %v", err)
|
| 90 |
+
}
|
| 91 |
+
execCmd.Stderr = execCmd.Stdout
|
| 92 |
+
|
| 93 |
+
if err := execCmd.Start(); err != nil {
|
| 94 |
+
term.StopSpinner()
|
| 95 |
+
term.OutputErrorAndExit("Failed to start command: %v", err)
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
maybeDeleteCgroup := lib.MaybeIsolateCgroup(execCmd)
|
| 99 |
+
|
| 100 |
+
ctx, cancel := context.WithCancel(context.Background())
|
| 101 |
+
var interrupted atomic.Bool
|
| 102 |
+
var interruptHandled atomic.Bool
|
| 103 |
+
var interruptWG sync.WaitGroup
|
| 104 |
+
|
| 105 |
+
sigChan := make(chan os.Signal, 1)
|
| 106 |
+
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
|
| 107 |
+
|
| 108 |
+
interruptWG.Add(1)
|
| 109 |
+
go func() {
|
| 110 |
+
defer interruptWG.Done()
|
| 111 |
+
for {
|
| 112 |
+
select {
|
| 113 |
+
case sig := <-sigChan:
|
| 114 |
+
if interruptHandled.CompareAndSwap(false, true) {
|
| 115 |
+
fmt.Println()
|
| 116 |
+
color.New(term.ColorHiYellow, color.Bold).Println("\n👉 Caught interrupt. Exiting gracefully...")
|
| 117 |
+
interrupted.Store(true)
|
| 118 |
+
|
| 119 |
+
var sysSig syscall.Signal
|
| 120 |
+
|
| 121 |
+
switch sig {
|
| 122 |
+
case os.Interrupt:
|
| 123 |
+
// user pressed Ctrl+C
|
| 124 |
+
sysSig = syscall.SIGINT
|
| 125 |
+
case syscall.SIGTERM:
|
| 126 |
+
// a polite "kill" request
|
| 127 |
+
sysSig = syscall.SIGTERM
|
| 128 |
+
case syscall.SIGHUP:
|
| 129 |
+
sysSig = syscall.SIGHUP
|
| 130 |
+
case syscall.SIGQUIT:
|
| 131 |
+
sysSig = syscall.SIGQUIT
|
| 132 |
+
default:
|
| 133 |
+
sysSig = syscall.SIGINT
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
if err := lib.KillProcessGroup(execCmd, sysSig); err != nil {
|
| 137 |
+
log.Printf("Failed to send signal %s to process group: %v", sysSig, err)
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
select {
|
| 141 |
+
case <-time.After(2 * time.Second):
|
| 142 |
+
color.New(term.ColorHiYellow, color.Bold).Println("👉 Commands didn't exit after 2 seconds. Sending SIGKILL.")
|
| 143 |
+
if err := lib.KillProcessGroup(execCmd, syscall.SIGKILL); err != nil {
|
| 144 |
+
log.Printf("Failed to send SIGKILL to process group: %v", err)
|
| 145 |
+
}
|
| 146 |
+
maybeDeleteCgroup()
|
| 147 |
+
case <-ctx.Done():
|
| 148 |
+
maybeDeleteCgroup()
|
| 149 |
+
return
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
case <-ctx.Done():
|
| 153 |
+
maybeDeleteCgroup()
|
| 154 |
+
return
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
}()
|
| 158 |
+
|
| 159 |
+
var outputBuilder strings.Builder
|
| 160 |
+
scanner := bufio.NewScanner(pipe)
|
| 161 |
+
go func() {
|
| 162 |
+
for scanner.Scan() {
|
| 163 |
+
line := scanner.Text()
|
| 164 |
+
fmt.Println(line)
|
| 165 |
+
outputBuilder.WriteString(line + "\n")
|
| 166 |
+
}
|
| 167 |
+
}()
|
| 168 |
+
|
| 169 |
+
waitErr := execCmd.Wait()
|
| 170 |
+
|
| 171 |
+
cancel()
|
| 172 |
+
interruptWG.Wait()
|
| 173 |
+
signal.Stop(sigChan)
|
| 174 |
+
close(sigChan)
|
| 175 |
+
|
| 176 |
+
if scanErr := scanner.Err(); scanErr != nil {
|
| 177 |
+
log.Printf("⚠️ Scanner error reading subprocess output: %v", scanErr)
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
term.StopSpinner()
|
| 181 |
+
|
| 182 |
+
outputStr := outputBuilder.String()
|
| 183 |
+
if outputStr == "" && waitErr != nil {
|
| 184 |
+
outputStr = waitErr.Error()
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
if outputStr != "" {
|
| 188 |
+
fmt.Println(outputStr)
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
didSucceed := waitErr == nil
|
| 192 |
+
|
| 193 |
+
if interrupted.Load() {
|
| 194 |
+
color.New(term.ColorHiYellow, color.Bold).Println("👉 Execution interrupted")
|
| 195 |
+
|
| 196 |
+
res, canceled, err := term.ConfirmYesNoCancel("Did the command succeed?")
|
| 197 |
+
|
| 198 |
+
if err != nil {
|
| 199 |
+
term.OutputErrorAndExit("Failed to get confirmation user input: %s", err)
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
didSucceed = res
|
| 203 |
+
|
| 204 |
+
if canceled {
|
| 205 |
+
os.Exit(0)
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
if didSucceed {
|
| 210 |
+
if attempt == 0 {
|
| 211 |
+
fmt.Printf("✅ Command %s succeeded on first try\n", color.New(color.Bold, term.ColorHiCyan).Sprintf(cmdStr))
|
| 212 |
+
} else {
|
| 213 |
+
lbl := "attempts"
|
| 214 |
+
if attempt == 1 {
|
| 215 |
+
lbl = "attempt"
|
| 216 |
+
}
|
| 217 |
+
fmt.Printf("✅ Command %s succeeded after %d fix %s\n", color.New(color.Bold, term.ColorHiCyan).Sprintf(cmdStr), attempt, lbl)
|
| 218 |
+
}
|
| 219 |
+
return
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
if attempt == tries-1 {
|
| 223 |
+
fmt.Printf("Command failed after %d tries\n", tries)
|
| 224 |
+
os.Exit(1)
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
// Prepare prompt for TellPlan
|
| 228 |
+
exitErr, ok := waitErr.(*exec.ExitError)
|
| 229 |
+
status := -1
|
| 230 |
+
if ok {
|
| 231 |
+
status = exitErr.ExitCode()
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
prompt := fmt.Sprintf("'%s' failed with exit status %d. Output:\n\n%s\n\n--\n\n",
|
| 235 |
+
strings.Join(cmdArgs, " "), status, outputStr)
|
| 236 |
+
|
| 237 |
+
tellFlags := types.TellFlags{
|
| 238 |
+
AutoContext: tellAutoContext,
|
| 239 |
+
ExecEnabled: false,
|
| 240 |
+
IsUserDebug: true,
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
plan_exec.TellPlan(plan_exec.ExecParams{
|
| 244 |
+
CurrentPlanId: lib.CurrentPlanId,
|
| 245 |
+
CurrentBranch: lib.CurrentBranch,
|
| 246 |
+
AuthVars: lib.MustVerifyAuthVars(auth.Current.IntegratedModelsMode),
|
| 247 |
+
CheckOutdatedContext: func(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (bool, bool, error) {
|
| 248 |
+
return lib.CheckOutdatedContextWithOutput(true, true, maybeContexts, projectPaths)
|
| 249 |
+
},
|
| 250 |
+
}, prompt, tellFlags)
|
| 251 |
+
|
| 252 |
+
applyFlags := types.ApplyFlags{
|
| 253 |
+
AutoConfirm: true,
|
| 254 |
+
AutoCommit: autoCommit,
|
| 255 |
+
NoCommit: !autoCommit,
|
| 256 |
+
NoExec: false,
|
| 257 |
+
AutoExec: true,
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
lib.MustApplyPlan(lib.ApplyPlanParams{
|
| 261 |
+
PlanId: lib.CurrentPlanId,
|
| 262 |
+
Branch: lib.CurrentBranch,
|
| 263 |
+
ApplyFlags: applyFlags,
|
| 264 |
+
TellFlags: tellFlags,
|
| 265 |
+
OnExecFail: plan_exec.GetOnApplyExecFailWithCommand(applyFlags, tellFlags, cmdStr),
|
| 266 |
+
ExecCommand: cmdStr,
|
| 267 |
+
})
|
| 268 |
+
}
|
| 269 |
+
}
|
app/cli/cmd/delete_branch.go
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"plandex-cli/api"
|
| 6 |
+
"plandex-cli/auth"
|
| 7 |
+
"plandex-cli/lib"
|
| 8 |
+
"plandex-cli/term"
|
| 9 |
+
"strconv"
|
| 10 |
+
"strings"
|
| 11 |
+
|
| 12 |
+
"github.com/fatih/color"
|
| 13 |
+
"github.com/spf13/cobra"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
var deleteBranchCmd = &cobra.Command{
|
| 17 |
+
Use: "delete-branch",
|
| 18 |
+
Aliases: []string{"dlb"},
|
| 19 |
+
Short: "Delete a plan branch by name or index",
|
| 20 |
+
Run: deleteBranch,
|
| 21 |
+
Args: cobra.MaximumNArgs(1),
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
func init() {
|
| 25 |
+
RootCmd.AddCommand(deleteBranchCmd)
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
func deleteBranch(cmd *cobra.Command, args []string) {
|
| 29 |
+
auth.MustResolveAuthWithOrg()
|
| 30 |
+
lib.MustResolveProject()
|
| 31 |
+
|
| 32 |
+
if lib.CurrentPlanId == "" {
|
| 33 |
+
term.OutputNoCurrentPlanErrorAndExit()
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
var branch string
|
| 37 |
+
var nameOrIdx string
|
| 38 |
+
|
| 39 |
+
if len(args) > 0 {
|
| 40 |
+
nameOrIdx = strings.TrimSpace(args[0])
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
if nameOrIdx == "main" {
|
| 44 |
+
fmt.Println("🚨 Cannot delete main branch")
|
| 45 |
+
return
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
term.StartSpinner("")
|
| 49 |
+
branches, apiErr := api.Client.ListBranches(lib.CurrentPlanId)
|
| 50 |
+
term.StopSpinner()
|
| 51 |
+
|
| 52 |
+
if apiErr != nil {
|
| 53 |
+
term.OutputErrorAndExit("Error getting branches: %v", apiErr)
|
| 54 |
+
return
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
if nameOrIdx == "" {
|
| 58 |
+
opts := make([]string, len(branches))
|
| 59 |
+
for i, branch := range branches {
|
| 60 |
+
if branch.Name == "main" {
|
| 61 |
+
continue
|
| 62 |
+
}
|
| 63 |
+
opts[i] = branch.Name
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
if len(opts) == 0 {
|
| 67 |
+
fmt.Println("🤷♂️ No branches to delete")
|
| 68 |
+
return
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
sel, err := term.SelectFromList("Select a branch to delete", opts)
|
| 72 |
+
|
| 73 |
+
if err != nil {
|
| 74 |
+
term.OutputErrorAndExit("Error selecting branch: %v", err)
|
| 75 |
+
return
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
branch = sel
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
// see if it's an index
|
| 82 |
+
idx, err := strconv.Atoi(nameOrIdx)
|
| 83 |
+
|
| 84 |
+
if err == nil {
|
| 85 |
+
if idx > 0 && idx <= len(branches) {
|
| 86 |
+
branch = branches[idx-1].Name
|
| 87 |
+
} else {
|
| 88 |
+
term.OutputErrorAndExit("Branch index out of range")
|
| 89 |
+
}
|
| 90 |
+
} else {
|
| 91 |
+
for _, b := range branches {
|
| 92 |
+
if b.Name == nameOrIdx {
|
| 93 |
+
branch = b.Name
|
| 94 |
+
break
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
if branch == "" {
|
| 99 |
+
term.OutputErrorAndExit("Branch not found")
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
found := false
|
| 104 |
+
for _, b := range branches {
|
| 105 |
+
if b.Name == branch {
|
| 106 |
+
found = true
|
| 107 |
+
break
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
if !found {
|
| 112 |
+
fmt.Printf("🤷♂️ Branch %s does not exist\n", color.New(color.Bold, term.ColorHiCyan).Sprint(branch))
|
| 113 |
+
return
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
term.StartSpinner("")
|
| 117 |
+
apiErr = api.Client.DeleteBranch(lib.CurrentPlanId, branch)
|
| 118 |
+
term.StopSpinner()
|
| 119 |
+
|
| 120 |
+
if apiErr != nil {
|
| 121 |
+
term.OutputErrorAndExit("Error deleting branch: %v", apiErr)
|
| 122 |
+
return
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
fmt.Printf("✅ Deleted branch %s\n", color.New(color.Bold, term.ColorHiCyan).Sprint(branch))
|
| 126 |
+
|
| 127 |
+
fmt.Println()
|
| 128 |
+
term.PrintCmds("", "branches")
|
| 129 |
+
}
|
app/cli/cmd/delete_plan.go
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"path"
|
| 6 |
+
"strconv"
|
| 7 |
+
"strings"
|
| 8 |
+
|
| 9 |
+
"plandex-cli/api"
|
| 10 |
+
"plandex-cli/auth"
|
| 11 |
+
"plandex-cli/lib"
|
| 12 |
+
"plandex-cli/term"
|
| 13 |
+
|
| 14 |
+
shared "plandex-shared"
|
| 15 |
+
|
| 16 |
+
"github.com/fatih/color"
|
| 17 |
+
"github.com/spf13/cobra"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
var all bool
|
| 21 |
+
|
| 22 |
+
func init() {
|
| 23 |
+
rmCmd.Flags().BoolVar(&all, "all", false, "Delete all plans")
|
| 24 |
+
RootCmd.AddCommand(rmCmd)
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// rmCmd represents the rm command
|
| 28 |
+
var rmCmd = &cobra.Command{
|
| 29 |
+
Use: "delete-plan [name-or-index]",
|
| 30 |
+
Aliases: []string{"dp"},
|
| 31 |
+
Short: "Delete a plan by name, index, range, or pattern, or select from a list. Delete all plans with --all flag.",
|
| 32 |
+
Args: cobra.RangeArgs(0, 1),
|
| 33 |
+
Run: del,
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
func matchPlansByPattern(pattern string, plans []*shared.Plan) []*shared.Plan {
|
| 37 |
+
var matched []*shared.Plan
|
| 38 |
+
for _, plan := range plans {
|
| 39 |
+
if isMatched, err := path.Match(pattern, plan.Name); err == nil && isMatched {
|
| 40 |
+
matched = append(matched, plan)
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
return matched
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
func del(cmd *cobra.Command, args []string) {
|
| 47 |
+
auth.MustResolveAuthWithOrg()
|
| 48 |
+
lib.MustResolveProject()
|
| 49 |
+
|
| 50 |
+
if all {
|
| 51 |
+
delAll()
|
| 52 |
+
return
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
term.StartSpinner("")
|
| 56 |
+
plans, apiErr := api.Client.ListPlans([]string{lib.CurrentProjectId})
|
| 57 |
+
term.StopSpinner()
|
| 58 |
+
|
| 59 |
+
if apiErr != nil {
|
| 60 |
+
term.OutputErrorAndExit("Error getting plans: %v", apiErr)
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
if len(plans) == 0 {
|
| 64 |
+
fmt.Println("🤷♂️ No plans")
|
| 65 |
+
fmt.Println()
|
| 66 |
+
term.PrintCmds("", "new")
|
| 67 |
+
return
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
var plansToDelete []*shared.Plan
|
| 71 |
+
|
| 72 |
+
if len(args) == 0 {
|
| 73 |
+
// Interactive selection
|
| 74 |
+
opts := make([]string, len(plans))
|
| 75 |
+
for i, plan := range plans {
|
| 76 |
+
opts[i] = plan.Name
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
selected, err := term.SelectFromList("Select a plan:", opts)
|
| 80 |
+
if err != nil {
|
| 81 |
+
term.OutputErrorAndExit("Error selecting plan: %v", err)
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
for _, p := range plans {
|
| 85 |
+
if p.Name == selected {
|
| 86 |
+
plansToDelete = append(plansToDelete, p)
|
| 87 |
+
break
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
} else {
|
| 91 |
+
nameOrPattern := strings.TrimSpace(args[0])
|
| 92 |
+
|
| 93 |
+
// Check if it's a range of indices
|
| 94 |
+
if strings.Contains(nameOrPattern, "-") {
|
| 95 |
+
// Create single-element slice with the range pattern
|
| 96 |
+
rangeArgs := []string{nameOrPattern}
|
| 97 |
+
indices := parseIndices(rangeArgs)
|
| 98 |
+
for idx := range indices {
|
| 99 |
+
if idx >= 0 && idx < len(plans) {
|
| 100 |
+
plansToDelete = append(plansToDelete, plans[idx])
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
} else if strings.Contains(nameOrPattern, "*") {
|
| 104 |
+
// Wildcard pattern matching
|
| 105 |
+
plansToDelete = matchPlansByPattern(nameOrPattern, plans)
|
| 106 |
+
} else {
|
| 107 |
+
// Try as index first
|
| 108 |
+
idx, err := strconv.Atoi(nameOrPattern)
|
| 109 |
+
if err == nil {
|
| 110 |
+
if idx > 0 && idx <= len(plans) {
|
| 111 |
+
plansToDelete = append(plansToDelete, plans[idx-1])
|
| 112 |
+
} else {
|
| 113 |
+
term.OutputErrorAndExit("Plan index out of range")
|
| 114 |
+
}
|
| 115 |
+
} else {
|
| 116 |
+
// Try exact name match
|
| 117 |
+
for _, p := range plans {
|
| 118 |
+
if p.Name == nameOrPattern {
|
| 119 |
+
plansToDelete = append(plansToDelete, p)
|
| 120 |
+
break
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
if len(plansToDelete) == 0 {
|
| 128 |
+
term.OutputErrorAndExit("No matching plans found")
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
// Show confirmation with list of plans to be deleted
|
| 132 |
+
fmt.Printf("\nThe following %d plan(s) will be deleted:\n", len(plansToDelete))
|
| 133 |
+
for _, p := range plansToDelete {
|
| 134 |
+
fmt.Printf(" - %s\n", color.New(color.Bold, term.ColorHiCyan).Sprint(p.Name))
|
| 135 |
+
}
|
| 136 |
+
fmt.Println()
|
| 137 |
+
|
| 138 |
+
confirmed, err := term.ConfirmYesNo("Are you sure you want to delete these plans?")
|
| 139 |
+
if err != nil {
|
| 140 |
+
term.OutputErrorAndExit("Error getting confirmation: %v", err)
|
| 141 |
+
}
|
| 142 |
+
if !confirmed {
|
| 143 |
+
fmt.Println("Operation cancelled")
|
| 144 |
+
return
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
// Delete the plans
|
| 148 |
+
term.StartSpinner("")
|
| 149 |
+
for _, p := range plansToDelete {
|
| 150 |
+
apiErr = api.Client.DeletePlan(p.Id)
|
| 151 |
+
if apiErr != nil {
|
| 152 |
+
term.StopSpinner()
|
| 153 |
+
term.OutputErrorAndExit("Error deleting plan %s: %s", p.Name, apiErr.Msg)
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
if lib.CurrentPlanId == p.Id {
|
| 157 |
+
err := lib.ClearCurrentPlan()
|
| 158 |
+
if err != nil {
|
| 159 |
+
term.OutputErrorAndExit("Error clearing current plan: %v", err)
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
term.StopSpinner()
|
| 164 |
+
|
| 165 |
+
if len(plansToDelete) == 1 {
|
| 166 |
+
fmt.Printf("✅ Deleted plan '%s'\n", plansToDelete[0].Name)
|
| 167 |
+
} else {
|
| 168 |
+
fmt.Printf("✅ Deleted %d plans\n", len(plansToDelete))
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
func delAll() {
|
| 173 |
+
term.StartSpinner("")
|
| 174 |
+
err := api.Client.DeleteAllPlans(lib.CurrentProjectId)
|
| 175 |
+
term.StopSpinner()
|
| 176 |
+
|
| 177 |
+
if err != nil {
|
| 178 |
+
term.OutputErrorAndExit("Error deleting all plans: %v", err)
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
fmt.Println("✅ Deleted all plans")
|
| 182 |
+
}
|
app/cli/cmd/diffs.go
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"fmt"
|
| 6 |
+
"html/template"
|
| 7 |
+
"net"
|
| 8 |
+
"net/http"
|
| 9 |
+
"os"
|
| 10 |
+
"plandex-cli/api"
|
| 11 |
+
"plandex-cli/auth"
|
| 12 |
+
"plandex-cli/lib"
|
| 13 |
+
"plandex-cli/term"
|
| 14 |
+
"plandex-cli/ui"
|
| 15 |
+
|
| 16 |
+
"github.com/eiannone/keyboard"
|
| 17 |
+
"github.com/fatih/color"
|
| 18 |
+
"github.com/spf13/cobra"
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
var showDiffUi bool = true
|
| 22 |
+
var diffUiSideBySide = true
|
| 23 |
+
var diffUiLineByLine bool
|
| 24 |
+
var diffGit bool
|
| 25 |
+
|
| 26 |
+
var fromTellMenu bool
|
| 27 |
+
|
| 28 |
+
var diffsCmd = &cobra.Command{
|
| 29 |
+
Use: "diff",
|
| 30 |
+
Aliases: []string{"diffs"},
|
| 31 |
+
Short: "Review pending changes",
|
| 32 |
+
Run: diffs,
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
func init() {
|
| 36 |
+
RootCmd.AddCommand(diffsCmd)
|
| 37 |
+
|
| 38 |
+
diffsCmd.Flags().BoolVarP(&plainTextOutput, "plain", "p", false, "Output diffs in plain text with no ANSI codes")
|
| 39 |
+
diffsCmd.Flags().BoolVar(&showDiffUi, "ui", false, "Show diffs in a browser UI")
|
| 40 |
+
diffsCmd.Flags().BoolVar(&diffGit, "git", true, "Show diffs in git diff format")
|
| 41 |
+
diffsCmd.Flags().BoolVarP(&diffUiSideBySide, "side", "s", true, "Show diffs UI in side-by-side view")
|
| 42 |
+
diffsCmd.Flags().BoolVarP(&diffUiLineByLine, "line", "l", false, "Show diffs UI in line-by-line view")
|
| 43 |
+
|
| 44 |
+
diffsCmd.Flags().BoolVar(&fromTellMenu, "from-tell-menu", false, "Show diffs from the tell menu")
|
| 45 |
+
diffsCmd.Flags().MarkHidden("from-tell-menu")
|
| 46 |
+
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
func diffs(cmd *cobra.Command, args []string) {
|
| 50 |
+
auth.MustResolveAuthWithOrg()
|
| 51 |
+
lib.MaybeResolveProject()
|
| 52 |
+
|
| 53 |
+
if lib.CurrentPlanId == "" {
|
| 54 |
+
term.OutputNoCurrentPlanErrorAndExit()
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
term.StartSpinner("")
|
| 58 |
+
|
| 59 |
+
if showDiffUi {
|
| 60 |
+
diffGit = false
|
| 61 |
+
} else if diffGit || plainTextOutput {
|
| 62 |
+
showDiffUi = false
|
| 63 |
+
} else {
|
| 64 |
+
diffGit = true
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
diffs, err := api.Client.GetPlanDiffs(lib.CurrentPlanId, lib.CurrentBranch, plainTextOutput || showDiffUi)
|
| 68 |
+
term.StopSpinner()
|
| 69 |
+
if err != nil {
|
| 70 |
+
term.OutputErrorAndExit("Error getting plan diffs: %v", err)
|
| 71 |
+
return
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
if len(diffs) == 0 {
|
| 75 |
+
fmt.Println("🤷♂️ No pending changes")
|
| 76 |
+
return
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
if showDiffUi {
|
| 80 |
+
getNewListener := func() net.Listener {
|
| 81 |
+
outputFormat := "line-by-line"
|
| 82 |
+
if diffUiSideBySide {
|
| 83 |
+
outputFormat = "side-by-side"
|
| 84 |
+
} else if diffUiLineByLine {
|
| 85 |
+
outputFormat = "line-by-line"
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// Properly escape the diff content for JavaScript
|
| 89 |
+
diffJSON, err := json.Marshal(diffs)
|
| 90 |
+
if err != nil {
|
| 91 |
+
term.OutputErrorAndExit("Error encoding diff content: %v", err)
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// Create template data
|
| 95 |
+
data := struct {
|
| 96 |
+
DiffContent template.JS
|
| 97 |
+
OutputFormat string
|
| 98 |
+
}{
|
| 99 |
+
DiffContent: template.JS(diffJSON),
|
| 100 |
+
OutputFormat: outputFormat,
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// Parse and execute the template
|
| 104 |
+
tmpl, err := template.New("diff").Parse(htmlTemplate)
|
| 105 |
+
if err != nil {
|
| 106 |
+
term.OutputErrorAndExit("Error parsing template: %v", err)
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// Use :0 to let the OS pick an available port
|
| 110 |
+
listener, err := net.Listen("tcp", ":0")
|
| 111 |
+
if err != nil {
|
| 112 |
+
term.OutputErrorAndExit("Error starting server: %v", err)
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// Get the actual port chosen
|
| 116 |
+
port := listener.Addr().(*net.TCPAddr).Port
|
| 117 |
+
|
| 118 |
+
// Start web server
|
| 119 |
+
go func() {
|
| 120 |
+
http.HandleFunc("/"+outputFormat, func(w http.ResponseWriter, r *http.Request) {
|
| 121 |
+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
| 122 |
+
err := tmpl.Execute(w, data)
|
| 123 |
+
if err != nil {
|
| 124 |
+
http.Error(w, err.Error(), http.StatusInternalServerError)
|
| 125 |
+
}
|
| 126 |
+
})
|
| 127 |
+
http.Serve(listener, nil)
|
| 128 |
+
}()
|
| 129 |
+
|
| 130 |
+
ui.OpenURL("Showing "+outputFormat+" diffs in your default browser...", fmt.Sprintf("http://localhost:%d/%s", port, outputFormat))
|
| 131 |
+
|
| 132 |
+
fmt.Println()
|
| 133 |
+
|
| 134 |
+
return listener
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
listener := getNewListener()
|
| 138 |
+
defer listener.Close()
|
| 139 |
+
|
| 140 |
+
var relaunch bool
|
| 141 |
+
|
| 142 |
+
for {
|
| 143 |
+
if relaunch {
|
| 144 |
+
listener.Close()
|
| 145 |
+
listener = getNewListener()
|
| 146 |
+
defer listener.Close()
|
| 147 |
+
relaunch = false
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
if diffUiLineByLine {
|
| 151 |
+
fmt.Printf("%s for side-by-side view\n", color.New(color.Bold, term.ColorHiGreen).Sprintf("(s)"))
|
| 152 |
+
} else {
|
| 153 |
+
fmt.Printf("%s for line-by-line view\n", color.New(color.Bold, term.ColorHiGreen).Sprintf("(l)"))
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
fmt.Printf("%s for git diff format\n", color.New(color.Bold, term.ColorHiGreen).Sprintf("(g)"))
|
| 157 |
+
// fmt.Printf("%s to quit\n", color.New(color.Bold, term.ColorHiGreen).Sprintf("(q)"))
|
| 158 |
+
|
| 159 |
+
s := "to exit menu/continue"
|
| 160 |
+
if fromTellMenu {
|
| 161 |
+
s = "to go back"
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
fmt.Printf("%s %s %s %s %s",
|
| 165 |
+
color.New(term.ColorHiMagenta, color.Bold).Sprint("Press a hotkey,"),
|
| 166 |
+
color.New(color.FgHiWhite, color.Bold).Sprintf("↓"),
|
| 167 |
+
color.New(term.ColorHiMagenta, color.Bold).Sprintf("to select, or"),
|
| 168 |
+
color.New(color.FgHiWhite, color.Bold).Sprintf("enter"),
|
| 169 |
+
color.New(term.ColorHiMagenta, color.Bold).Sprintf("%s>", s),
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
char, key, err := term.GetUserKeyInput()
|
| 173 |
+
if err != nil {
|
| 174 |
+
term.OutputErrorAndExit("Error getting key: %v", err)
|
| 175 |
+
return
|
| 176 |
+
}
|
| 177 |
+
fmt.Println()
|
| 178 |
+
|
| 179 |
+
if key == keyboard.KeyArrowDown {
|
| 180 |
+
options := []string{}
|
| 181 |
+
if diffUiLineByLine {
|
| 182 |
+
options = append(options, "side-by-side")
|
| 183 |
+
} else {
|
| 184 |
+
options = append(options, "line-by-line")
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
options = append(options, "git diff")
|
| 188 |
+
options = append(options, "exit menu")
|
| 189 |
+
|
| 190 |
+
selected, err := term.SelectFromList(
|
| 191 |
+
"Select an action",
|
| 192 |
+
options,
|
| 193 |
+
)
|
| 194 |
+
if err != nil {
|
| 195 |
+
term.OutputErrorAndExit("Error selecting action: %v", err)
|
| 196 |
+
return
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
if selected == "side-by-side" {
|
| 200 |
+
diffUiSideBySide = true
|
| 201 |
+
diffUiLineByLine = false
|
| 202 |
+
relaunch = true
|
| 203 |
+
} else if selected == "line-by-line" {
|
| 204 |
+
diffUiSideBySide = false
|
| 205 |
+
diffUiLineByLine = true
|
| 206 |
+
relaunch = true
|
| 207 |
+
} else if selected == "git diff" {
|
| 208 |
+
showGitDiff()
|
| 209 |
+
} else if selected == "exit menu" {
|
| 210 |
+
fmt.Println()
|
| 211 |
+
break
|
| 212 |
+
}
|
| 213 |
+
} else if string(char) == "g" {
|
| 214 |
+
showGitDiff()
|
| 215 |
+
} else if string(char) == "s" {
|
| 216 |
+
diffUiSideBySide = true
|
| 217 |
+
diffUiLineByLine = false
|
| 218 |
+
relaunch = true
|
| 219 |
+
} else if string(char) == "l" {
|
| 220 |
+
diffUiSideBySide = false
|
| 221 |
+
diffUiLineByLine = true
|
| 222 |
+
relaunch = true
|
| 223 |
+
} else if key == 13 || key == 10 || string(char) == "q" { // Check raw key codes for Enter/Return
|
| 224 |
+
if term.IsRepl {
|
| 225 |
+
fmt.Println()
|
| 226 |
+
break
|
| 227 |
+
} else {
|
| 228 |
+
fmt.Println()
|
| 229 |
+
break
|
| 230 |
+
}
|
| 231 |
+
} else if string(char) == "\x03" { // Ctrl+C
|
| 232 |
+
os.Exit(0)
|
| 233 |
+
} else {
|
| 234 |
+
fmt.Println()
|
| 235 |
+
term.OutputSimpleError("Invalid hotkey")
|
| 236 |
+
fmt.Println()
|
| 237 |
+
}
|
| 238 |
+
}
|
| 239 |
+
} else {
|
| 240 |
+
if plainTextOutput {
|
| 241 |
+
fmt.Println(diffs)
|
| 242 |
+
} else {
|
| 243 |
+
term.PageOutput(diffs)
|
| 244 |
+
}
|
| 245 |
+
fmt.Println()
|
| 246 |
+
}
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
func showGitDiff() {
|
| 250 |
+
_, err := lib.ExecPlandexCommandWithParams([]string{"diff", "--git"}, lib.ExecPlandexCommandParams{
|
| 251 |
+
DisableSuggestions: true,
|
| 252 |
+
})
|
| 253 |
+
if err != nil {
|
| 254 |
+
term.OutputErrorAndExit("Error showing git diff: %v", err)
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
var htmlTemplate = `<!doctype html>
|
| 259 |
+
<html lang="en-us">
|
| 260 |
+
<head>
|
| 261 |
+
<meta charset="utf-8" />
|
| 262 |
+
<!-- Make sure to load the highlight.js CSS file before the Diff2Html CSS file -->
|
| 263 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs.min.css" />
|
| 264 |
+
<link
|
| 265 |
+
rel="stylesheet"
|
| 266 |
+
type="text/css"
|
| 267 |
+
href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css"
|
| 268 |
+
/>
|
| 269 |
+
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js"></script>
|
| 270 |
+
</head>
|
| 271 |
+
<script>
|
| 272 |
+
// Parse the JSON-encoded diff content
|
| 273 |
+
const diffString = {{.DiffContent}};
|
| 274 |
+
|
| 275 |
+
document.addEventListener('DOMContentLoaded', function () {
|
| 276 |
+
var targetElement = document.getElementById('myDiffElement');
|
| 277 |
+
var configuration = {
|
| 278 |
+
outputFormat: '{{.OutputFormat}}'
|
| 279 |
+
};
|
| 280 |
+
var diff2htmlUi = new Diff2HtmlUI(targetElement, diffString, configuration);
|
| 281 |
+
diff2htmlUi.draw();
|
| 282 |
+
diff2htmlUi.highlightCode();
|
| 283 |
+
});
|
| 284 |
+
</script>
|
| 285 |
+
<body>
|
| 286 |
+
<div id="myDiffElement"></div>
|
| 287 |
+
</body>
|
| 288 |
+
</html>`
|
app/cli/cmd/invite.go
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"plandex-cli/api"
|
| 6 |
+
"plandex-cli/auth"
|
| 7 |
+
"plandex-cli/term"
|
| 8 |
+
|
| 9 |
+
shared "plandex-shared"
|
| 10 |
+
|
| 11 |
+
"github.com/spf13/cobra"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
var inviteCmd = &cobra.Command{
|
| 15 |
+
Use: "invite [email] [name] [org-role]",
|
| 16 |
+
Short: "Invite a new user to the org",
|
| 17 |
+
Run: invite,
|
| 18 |
+
Args: cobra.MaximumNArgs(3),
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
func init() {
|
| 22 |
+
RootCmd.AddCommand(inviteCmd)
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
func invite(cmd *cobra.Command, args []string) {
|
| 26 |
+
auth.MustResolveAuthWithOrg()
|
| 27 |
+
|
| 28 |
+
email, name, orgRoleName := "", "", ""
|
| 29 |
+
if len(args) >= 1 {
|
| 30 |
+
email = args[0]
|
| 31 |
+
}
|
| 32 |
+
if len(args) >= 2 {
|
| 33 |
+
name = args[1]
|
| 34 |
+
}
|
| 35 |
+
if len(args) == 3 {
|
| 36 |
+
orgRoleName = args[2]
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
term.StartSpinner("")
|
| 40 |
+
orgRoles, err := api.Client.ListOrgRoles()
|
| 41 |
+
term.StopSpinner()
|
| 42 |
+
|
| 43 |
+
if err != nil {
|
| 44 |
+
term.OutputErrorAndExit("Failed to list org roles: %v", err)
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
if email == "" {
|
| 48 |
+
var err error
|
| 49 |
+
email, err = term.GetRequiredUserStringInput("Email:")
|
| 50 |
+
if err != nil {
|
| 51 |
+
term.OutputErrorAndExit("Failed to get email: %v", err)
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
if name == "" {
|
| 55 |
+
var err error
|
| 56 |
+
name, err = term.GetRequiredUserStringInput("Name:")
|
| 57 |
+
if err != nil {
|
| 58 |
+
term.OutputErrorAndExit("Failed to get name: %v", err)
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
if orgRoleName == "" {
|
| 63 |
+
var orgRoleNames []string
|
| 64 |
+
for _, orgRole := range orgRoles {
|
| 65 |
+
orgRoleNames = append(orgRoleNames, orgRole.Label)
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
var err error
|
| 69 |
+
orgRoleName, err = term.SelectFromList("Org role:", orgRoleNames)
|
| 70 |
+
|
| 71 |
+
if err != nil {
|
| 72 |
+
term.OutputErrorAndExit("Failed to select org role: %v", err)
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
var orgRoleId string
|
| 77 |
+
for _, orgRole := range orgRoles {
|
| 78 |
+
if orgRole.Label == orgRoleName {
|
| 79 |
+
orgRoleId = orgRole.Id
|
| 80 |
+
break
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
if orgRoleId == "" {
|
| 85 |
+
term.OutputErrorAndExit("Org role '%s' not found", orgRoleName)
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
inviteRequest := shared.InviteRequest{
|
| 89 |
+
Email: email,
|
| 90 |
+
Name: name,
|
| 91 |
+
OrgRoleId: orgRoleId,
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
term.StartSpinner("")
|
| 95 |
+
apiErr := api.Client.InviteUser(inviteRequest)
|
| 96 |
+
term.StopSpinner()
|
| 97 |
+
if apiErr != nil {
|
| 98 |
+
term.OutputErrorAndExit("Failed to invite user: %s", apiErr.Msg)
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
fmt.Println("✅ Invite sent")
|
| 102 |
+
}
|
app/cli/cmd/load.go
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"os"
|
| 6 |
+
"plandex-cli/auth"
|
| 7 |
+
"plandex-cli/lib"
|
| 8 |
+
"plandex-cli/term"
|
| 9 |
+
"plandex-cli/types"
|
| 10 |
+
|
| 11 |
+
"github.com/sashabaranov/go-openai"
|
| 12 |
+
"github.com/spf13/cobra"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
var (
|
| 16 |
+
recursive bool
|
| 17 |
+
namesOnly bool
|
| 18 |
+
note string
|
| 19 |
+
forceSkipIgnore bool
|
| 20 |
+
imageDetail string
|
| 21 |
+
defsOnly bool
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
var contextLoadCmd = &cobra.Command{
|
| 25 |
+
Use: "load [files-or-urls...]",
|
| 26 |
+
Aliases: []string{"l", "add"},
|
| 27 |
+
Short: "Load context from various inputs",
|
| 28 |
+
Long: `Load context from a file path, a directory, a URL, an image, a note, or piped data.`,
|
| 29 |
+
Run: contextLoad,
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
func init() {
|
| 33 |
+
contextLoadCmd.Flags().StringVarP(¬e, "note", "n", "", "Add a note to the context")
|
| 34 |
+
contextLoadCmd.Flags().BoolVarP(&recursive, "recursive", "r", false, "Search directories recursively")
|
| 35 |
+
contextLoadCmd.Flags().BoolVar(&namesOnly, "tree", false, "Load directory tree with file names only")
|
| 36 |
+
contextLoadCmd.Flags().BoolVarP(&forceSkipIgnore, "force", "f", false, "Load files even when ignored by .gitignore or .plandexignore")
|
| 37 |
+
contextLoadCmd.Flags().StringVarP(&imageDetail, "detail", "d", "high", "Image detail level (high or low)")
|
| 38 |
+
contextLoadCmd.Flags().BoolVar(&defsOnly, "map", false, "Load file maps (function/method/class signatures, variable names, types, etc.)")
|
| 39 |
+
RootCmd.AddCommand(contextLoadCmd)
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
func contextLoad(cmd *cobra.Command, args []string) {
|
| 43 |
+
auth.MustResolveAuthWithOrg()
|
| 44 |
+
lib.MustResolveProject()
|
| 45 |
+
|
| 46 |
+
if lib.CurrentPlanId == "" {
|
| 47 |
+
term.OutputNoCurrentPlanErrorAndExit()
|
| 48 |
+
return
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
lib.MustLoadContext(args, &types.LoadContextParams{
|
| 52 |
+
Note: note,
|
| 53 |
+
Recursive: recursive,
|
| 54 |
+
NamesOnly: namesOnly,
|
| 55 |
+
ForceSkipIgnore: forceSkipIgnore,
|
| 56 |
+
ImageDetail: openai.ImageURLDetail(imageDetail),
|
| 57 |
+
DefsOnly: defsOnly,
|
| 58 |
+
SessionId: os.Getenv("PLANDEX_REPL_SESSION_ID"),
|
| 59 |
+
})
|
| 60 |
+
|
| 61 |
+
fmt.Println()
|
| 62 |
+
term.PrintCmds("", "ls", "tell", "debug")
|
| 63 |
+
}
|
app/cli/cmd/log.go
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"plandex-cli/api"
|
| 6 |
+
"plandex-cli/auth"
|
| 7 |
+
"plandex-cli/lib"
|
| 8 |
+
"plandex-cli/term"
|
| 9 |
+
"time"
|
| 10 |
+
|
| 11 |
+
"github.com/spf13/cobra"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
// logCmd represents the log command
|
| 15 |
+
var logCmd = &cobra.Command{
|
| 16 |
+
Use: "log",
|
| 17 |
+
Aliases: []string{"history", "logs"},
|
| 18 |
+
Short: "Show plan history",
|
| 19 |
+
Long: `Show plan history`,
|
| 20 |
+
Args: cobra.NoArgs,
|
| 21 |
+
Run: runLog,
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
func init() {
|
| 25 |
+
// Add log command
|
| 26 |
+
RootCmd.AddCommand(logCmd)
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
func runLog(cmd *cobra.Command, args []string) {
|
| 30 |
+
auth.MustResolveAuthWithOrg()
|
| 31 |
+
lib.MustResolveProject()
|
| 32 |
+
|
| 33 |
+
if lib.CurrentPlanId == "" {
|
| 34 |
+
term.OutputNoCurrentPlanErrorAndExit()
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
term.StartSpinner("")
|
| 38 |
+
res, apiErr := api.Client.ListLogs(lib.CurrentPlanId, lib.CurrentBranch)
|
| 39 |
+
term.StopSpinner()
|
| 40 |
+
|
| 41 |
+
if apiErr != nil {
|
| 42 |
+
term.OutputErrorAndExit("Error getting logs: %v", apiErr)
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
withLocalTimestamps, err := convertTimestampsToLocal(res.Body)
|
| 46 |
+
|
| 47 |
+
if err != nil {
|
| 48 |
+
term.OutputErrorAndExit("Error converting timestamps: %v", err)
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
term.PageOutput(withLocalTimestamps)
|
| 52 |
+
|
| 53 |
+
fmt.Println()
|
| 54 |
+
term.PrintCmds("", "rewind", "continue", "convo", "convo 1", "convo 2-5")
|
| 55 |
+
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
func convertTimestampsToLocal(input string) (string, error) {
|
| 59 |
+
t := time.Now()
|
| 60 |
+
zone, _ := t.Zone()
|
| 61 |
+
re := lib.GitLogTimestampRegex
|
| 62 |
+
|
| 63 |
+
// Function to convert matched timestamps assuming they are in UTC to local time.
|
| 64 |
+
replaceFunc := func(match string) string {
|
| 65 |
+
t, err := time.Parse(lib.GitLogTimestampFormat, match)
|
| 66 |
+
if err != nil {
|
| 67 |
+
// In case of an error, return the original match.
|
| 68 |
+
return match
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
localDt := t.Local()
|
| 72 |
+
formattedTs := localDt.Format("Mon Jan 2, 2006 | 3:04:05pm")
|
| 73 |
+
|
| 74 |
+
if localDt.Day() == time.Now().Day() {
|
| 75 |
+
formattedTs = localDt.Format("Today | 3:04:05pm")
|
| 76 |
+
} else if localDt.Day() == time.Now().AddDate(0, 0, -1).Day() {
|
| 77 |
+
formattedTs = localDt.Format("Yesterday | 3:04:05pm")
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// Convert to local time and format back to a string without the timezone to match the original format.
|
| 81 |
+
return formattedTs + " " + zone
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// Find all matches and replace them.
|
| 85 |
+
result := re.ReplaceAllStringFunc(input, replaceFunc)
|
| 86 |
+
|
| 87 |
+
return result, nil
|
| 88 |
+
}
|
app/cli/cmd/ls.go
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"os"
|
| 6 |
+
"plandex-cli/api"
|
| 7 |
+
"plandex-cli/auth"
|
| 8 |
+
"plandex-cli/format"
|
| 9 |
+
"plandex-cli/lib"
|
| 10 |
+
"plandex-cli/term"
|
| 11 |
+
"strconv"
|
| 12 |
+
|
| 13 |
+
shared "plandex-shared"
|
| 14 |
+
|
| 15 |
+
"github.com/fatih/color"
|
| 16 |
+
"github.com/olekukonko/tablewriter"
|
| 17 |
+
"github.com/spf13/cobra"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
var contextCmd = &cobra.Command{
|
| 21 |
+
Use: "ls",
|
| 22 |
+
Aliases: []string{"list-context"},
|
| 23 |
+
Short: "List everything in context",
|
| 24 |
+
Run: listContext,
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
func listContext(cmd *cobra.Command, args []string) {
|
| 28 |
+
auth.MustResolveAuthWithOrg()
|
| 29 |
+
lib.MustResolveProject()
|
| 30 |
+
|
| 31 |
+
term.StartSpinner("")
|
| 32 |
+
contexts, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)
|
| 33 |
+
|
| 34 |
+
if err != nil {
|
| 35 |
+
term.OutputErrorAndExit("Error listing context: %v", err)
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
planConfig, err := api.Client.GetPlanConfig(lib.CurrentPlanId)
|
| 39 |
+
if err != nil {
|
| 40 |
+
term.OutputErrorAndExit("Error getting plan config: %v", err)
|
| 41 |
+
}
|
| 42 |
+
term.StopSpinner()
|
| 43 |
+
|
| 44 |
+
totalTokens := 0
|
| 45 |
+
totalPlannerTokens := 0
|
| 46 |
+
totalMapTokens := 0
|
| 47 |
+
table := tablewriter.NewWriter(os.Stdout)
|
| 48 |
+
table.SetHeader([]string{"#", "Name", "Type", "🪙", "Added", "Updated"})
|
| 49 |
+
table.SetAutoWrapText(false)
|
| 50 |
+
|
| 51 |
+
if len(contexts) == 0 {
|
| 52 |
+
fmt.Println("🤷♂️ No context")
|
| 53 |
+
fmt.Println()
|
| 54 |
+
term.PrintCmds("", "load")
|
| 55 |
+
return
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
for i, context := range contexts {
|
| 59 |
+
totalTokens += context.NumTokens
|
| 60 |
+
|
| 61 |
+
if context.ContextType == shared.ContextMapType {
|
| 62 |
+
totalMapTokens += context.NumTokens
|
| 63 |
+
} else {
|
| 64 |
+
totalPlannerTokens += context.NumTokens
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
t, icon := context.TypeAndIcon()
|
| 68 |
+
|
| 69 |
+
name := context.Name
|
| 70 |
+
if name == "" {
|
| 71 |
+
name = context.FilePath
|
| 72 |
+
}
|
| 73 |
+
if len(name) > 40 {
|
| 74 |
+
name = name[:20] + "⋯" + name[len(name)-20:]
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
row := []string{
|
| 78 |
+
strconv.Itoa(i + 1),
|
| 79 |
+
" " + icon + " " + name,
|
| 80 |
+
t,
|
| 81 |
+
strconv.Itoa(context.NumTokens), //+ " 🪙",
|
| 82 |
+
format.Time(context.CreatedAt),
|
| 83 |
+
format.Time(context.UpdatedAt),
|
| 84 |
+
}
|
| 85 |
+
table.Rich(row, []tablewriter.Colors{
|
| 86 |
+
{tablewriter.Bold},
|
| 87 |
+
{tablewriter.FgHiGreenColor, tablewriter.Bold},
|
| 88 |
+
})
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
table.Render()
|
| 92 |
+
|
| 93 |
+
tokensTbl := tablewriter.NewWriter(os.Stdout)
|
| 94 |
+
tokensTbl.SetAutoWrapText(false)
|
| 95 |
+
|
| 96 |
+
if planConfig.AutoLoadContext {
|
| 97 |
+
tokensTbl.Append([]string{
|
| 98 |
+
color.New(term.ColorHiCyan, color.Bold).Sprintf("Map tokens →") + color.New(color.Bold).Sprintf(" %d 🪙", totalMapTokens),
|
| 99 |
+
color.New(term.ColorHiCyan, color.Bold).Sprintf("Context tokens →") + color.New(color.Bold).Sprintf(" %d 🪙", totalPlannerTokens),
|
| 100 |
+
})
|
| 101 |
+
} else {
|
| 102 |
+
|
| 103 |
+
tokensTbl.Append([]string{color.New(term.ColorHiCyan, color.Bold).Sprintf("Total tokens →") + color.New(color.Bold).Sprintf(" %d 🪙", totalTokens)})
|
| 104 |
+
}
|
| 105 |
+
tokensTbl.Render()
|
| 106 |
+
|
| 107 |
+
fmt.Println()
|
| 108 |
+
term.PrintCmds("", "load", "rm", "clear")
|
| 109 |
+
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
func init() {
|
| 113 |
+
RootCmd.AddCommand(contextCmd)
|
| 114 |
+
|
| 115 |
+
}
|
app/cli/cmd/model_helpers.go
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"os"
|
| 6 |
+
"plandex-cli/lib"
|
| 7 |
+
"plandex-cli/term"
|
| 8 |
+
shared "plandex-shared"
|
| 9 |
+
|
| 10 |
+
"github.com/fatih/color"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
func warnModelsFileLocalChanges(path, cmd string) (bool, error) {
|
| 14 |
+
cmdPrefix := "\\"
|
| 15 |
+
if !term.IsRepl {
|
| 16 |
+
cmdPrefix = "plandex "
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
color.New(color.Bold, term.ColorHiYellow).Println("⚠️ The models file has local changes")
|
| 20 |
+
|
| 21 |
+
fmt.Println()
|
| 22 |
+
fmt.Println("Path → " + path)
|
| 23 |
+
fmt.Println()
|
| 24 |
+
|
| 25 |
+
fmt.Println("If you continue, local changes will be dropped in favor of the latest server state")
|
| 26 |
+
|
| 27 |
+
fmt.Println()
|
| 28 |
+
fmt.Printf("To keep the local version instead, quit and run %s\n", color.New(color.Bold, color.BgCyan, color.FgHiWhite).
|
| 29 |
+
Sprintf(" %s%s --save ", cmdPrefix, cmd))
|
| 30 |
+
fmt.Println()
|
| 31 |
+
return term.ConfirmYesNo("Drop local changes and continue?")
|
| 32 |
+
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
type maybePromptAndOpenModelsFileResult struct {
|
| 36 |
+
shouldReturn bool
|
| 37 |
+
jsonData []byte
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
func maybePromptAndOpenModelsFile(filePath, pathArg, cmd string, defaultConfig *shared.PlanConfig, planConfig *shared.PlanConfig) maybePromptAndOpenModelsFileResult {
|
| 41 |
+
|
| 42 |
+
printManual := func() {
|
| 43 |
+
cmdPrefix := "\\"
|
| 44 |
+
if !term.IsRepl {
|
| 45 |
+
cmdPrefix = "plandex "
|
| 46 |
+
}
|
| 47 |
+
fmt.Println("To save changes, run " +
|
| 48 |
+
fmt.Sprintf(" %s ", color.New(color.Bold, color.BgCyan, color.FgHiWhite).
|
| 49 |
+
Sprintf(" %s%s --save%s ", cmdPrefix, cmd, pathArg)))
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
selectedEditor := lib.MaybePromptAndOpen(filePath, defaultConfig, planConfig)
|
| 53 |
+
|
| 54 |
+
if selectedEditor {
|
| 55 |
+
fmt.Println("📝 Opened in editor")
|
| 56 |
+
fmt.Println()
|
| 57 |
+
|
| 58 |
+
confirmed, err := term.ConfirmYesNo("Ready to save?")
|
| 59 |
+
if err != nil {
|
| 60 |
+
term.OutputErrorAndExit("Error confirming: %v", err)
|
| 61 |
+
return maybePromptAndOpenModelsFileResult{shouldReturn: true}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
if !confirmed {
|
| 65 |
+
fmt.Println("🙅♂️ Update canceled")
|
| 66 |
+
fmt.Println()
|
| 67 |
+
|
| 68 |
+
printManual()
|
| 69 |
+
return maybePromptAndOpenModelsFileResult{shouldReturn: true}
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// get updated file state
|
| 73 |
+
jsonData, err := os.ReadFile(filePath)
|
| 74 |
+
if err != nil {
|
| 75 |
+
term.OutputErrorAndExit("Error reading JSON file: %v", err)
|
| 76 |
+
return maybePromptAndOpenModelsFileResult{shouldReturn: true}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
return maybePromptAndOpenModelsFileResult{shouldReturn: false, jsonData: jsonData}
|
| 80 |
+
} else {
|
| 81 |
+
// No editor available or user chose manual
|
| 82 |
+
fmt.Println("👨💻 Edit the file in your JSON editor of choice")
|
| 83 |
+
fmt.Println()
|
| 84 |
+
|
| 85 |
+
printManual()
|
| 86 |
+
return maybePromptAndOpenModelsFileResult{shouldReturn: true}
|
| 87 |
+
}
|
| 88 |
+
}
|
app/cli/cmd/model_packs.go
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"os"
|
| 6 |
+
"plandex-cli/api"
|
| 7 |
+
"plandex-cli/auth"
|
| 8 |
+
"plandex-cli/term"
|
| 9 |
+
|
| 10 |
+
shared "plandex-shared"
|
| 11 |
+
|
| 12 |
+
"github.com/fatih/color"
|
| 13 |
+
"github.com/olekukonko/tablewriter"
|
| 14 |
+
"github.com/spf13/cobra"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
var customModelPacksOnly bool
|
| 18 |
+
|
| 19 |
+
var modelPacksCmd = &cobra.Command{
|
| 20 |
+
Use: "model-packs",
|
| 21 |
+
Short: "List all model packs",
|
| 22 |
+
Run: listModelPacks,
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
var createModelPackCmd = &cobra.Command{
|
| 26 |
+
Use: "create",
|
| 27 |
+
Short: "Create a model pack",
|
| 28 |
+
Run: customModelsNotImplemented,
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
var deleteModelPackCmd = &cobra.Command{
|
| 32 |
+
Use: "delete",
|
| 33 |
+
Aliases: []string{"rm"},
|
| 34 |
+
Short: "Delete a model pack by name or index",
|
| 35 |
+
Run: customModelsNotImplemented,
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
var updateModelPackCmd = &cobra.Command{
|
| 39 |
+
Use: "update",
|
| 40 |
+
Short: "Update a model pack by name",
|
| 41 |
+
Run: customModelsNotImplemented,
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
var showModelPackCmd = &cobra.Command{
|
| 45 |
+
Use: "show [name]",
|
| 46 |
+
Short: "Show a model pack by name",
|
| 47 |
+
Args: cobra.MaximumNArgs(1),
|
| 48 |
+
Run: showModelPack,
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
func init() {
|
| 52 |
+
RootCmd.AddCommand(modelPacksCmd)
|
| 53 |
+
modelPacksCmd.AddCommand(createModelPackCmd)
|
| 54 |
+
modelPacksCmd.AddCommand(deleteModelPackCmd)
|
| 55 |
+
modelPacksCmd.AddCommand(updateModelPackCmd)
|
| 56 |
+
modelPacksCmd.AddCommand(showModelPackCmd)
|
| 57 |
+
modelPacksCmd.Flags().BoolVarP(&customModelPacksOnly, "custom", "c", false, "Only show custom model packs")
|
| 58 |
+
modelPacksCmd.Flags().BoolVarP(&allProperties, "all", "a", false, "Show all properties")
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
func listModelPacks(cmd *cobra.Command, args []string) {
|
| 62 |
+
auth.MustResolveAuthWithOrg()
|
| 63 |
+
|
| 64 |
+
term.StartSpinner("")
|
| 65 |
+
builtInModelPacks := shared.BuiltInModelPacks
|
| 66 |
+
|
| 67 |
+
if auth.Current.IsCloud {
|
| 68 |
+
filtered := []*shared.ModelPack{}
|
| 69 |
+
for _, mp := range builtInModelPacks {
|
| 70 |
+
if mp.LocalProvider == "" {
|
| 71 |
+
filtered = append(filtered, mp)
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
builtInModelPacks = filtered
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
customModelPacks, err := api.Client.ListModelPacks()
|
| 78 |
+
term.StopSpinner()
|
| 79 |
+
|
| 80 |
+
if err != nil {
|
| 81 |
+
term.OutputErrorAndExit("Error fetching model packs: %v", err)
|
| 82 |
+
return
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
if !customModelPacksOnly {
|
| 86 |
+
color.New(color.Bold, term.ColorHiCyan).Println("🏠 Built-in Model Packs")
|
| 87 |
+
table := tablewriter.NewWriter(os.Stdout)
|
| 88 |
+
table.SetAutoWrapText(true)
|
| 89 |
+
table.SetRowLine(true)
|
| 90 |
+
table.SetHeader([]string{"Name", "Description"})
|
| 91 |
+
for _, set := range builtInModelPacks {
|
| 92 |
+
table.Append([]string{set.Name, set.Description})
|
| 93 |
+
}
|
| 94 |
+
table.Render()
|
| 95 |
+
fmt.Println()
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
if len(customModelPacks) > 0 {
|
| 99 |
+
color.New(color.Bold, term.ColorHiCyan).Println("🛠️ Custom Model Packs")
|
| 100 |
+
table := tablewriter.NewWriter(os.Stdout)
|
| 101 |
+
table.SetAutoWrapText(true)
|
| 102 |
+
table.SetRowLine(true)
|
| 103 |
+
table.SetHeader([]string{"#", "Name", "Description"})
|
| 104 |
+
for i, set := range customModelPacks {
|
| 105 |
+
table.Append([]string{fmt.Sprintf("%d", i+1), set.Name, set.Description})
|
| 106 |
+
}
|
| 107 |
+
table.Render()
|
| 108 |
+
|
| 109 |
+
fmt.Println()
|
| 110 |
+
} else if customModelPacksOnly {
|
| 111 |
+
fmt.Println("🤷♂️ No custom model packs")
|
| 112 |
+
fmt.Println()
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
if customModelPacksOnly && len(customModelPacks) > 0 {
|
| 116 |
+
term.PrintCmds("", "model-packs show", "models custom")
|
| 117 |
+
} else if len(customModelPacks) > 0 {
|
| 118 |
+
term.PrintCmds("", "model-packs --custom", "model-packs show", "models custom")
|
| 119 |
+
} else {
|
| 120 |
+
term.PrintCmds("", "model-packs show", "models custom")
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
func showModelPack(cmd *cobra.Command, args []string) {
|
| 126 |
+
auth.MustResolveAuthWithOrg()
|
| 127 |
+
|
| 128 |
+
term.StartSpinner("")
|
| 129 |
+
customModelPacks, apiErr := api.Client.ListModelPacks()
|
| 130 |
+
if apiErr != nil {
|
| 131 |
+
term.OutputErrorAndExit("Error fetching models: %v", apiErr)
|
| 132 |
+
}
|
| 133 |
+
customModels, err := api.Client.ListCustomModels()
|
| 134 |
+
if err != nil {
|
| 135 |
+
term.OutputErrorAndExit("Error fetching custom models: %v", err)
|
| 136 |
+
}
|
| 137 |
+
customModelsById := make(map[shared.ModelId]*shared.CustomModel)
|
| 138 |
+
for _, m := range customModels {
|
| 139 |
+
customModelsById[m.ModelId] = m
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
term.StopSpinner()
|
| 143 |
+
|
| 144 |
+
modelPacks := []*shared.ModelPack{}
|
| 145 |
+
modelPacks = append(modelPacks, customModelPacks...)
|
| 146 |
+
|
| 147 |
+
builtInModelPacks := shared.BuiltInModelPacks
|
| 148 |
+
if auth.Current.IsCloud {
|
| 149 |
+
filtered := []*shared.ModelPack{}
|
| 150 |
+
for _, mp := range builtInModelPacks {
|
| 151 |
+
if mp.LocalProvider == "" {
|
| 152 |
+
filtered = append(filtered, mp)
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
builtInModelPacks = filtered
|
| 156 |
+
}
|
| 157 |
+
modelPacks = append(modelPacks, builtInModelPacks...)
|
| 158 |
+
|
| 159 |
+
var name string
|
| 160 |
+
if len(args) > 0 {
|
| 161 |
+
name = args[0]
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
var modelPack *shared.ModelPack
|
| 165 |
+
|
| 166 |
+
if name == "" {
|
| 167 |
+
opts := make([]string, len(modelPacks))
|
| 168 |
+
for i, mp := range modelPacks {
|
| 169 |
+
opts[i] = mp.Name
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
selected, err := term.SelectFromList("Select a model pack:", opts)
|
| 173 |
+
if err != nil {
|
| 174 |
+
term.OutputErrorAndExit("Error selecting model pack: %v", err)
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
for _, mp := range modelPacks {
|
| 178 |
+
if mp.Name == selected {
|
| 179 |
+
modelPack = mp
|
| 180 |
+
break
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
} else {
|
| 184 |
+
for _, mp := range modelPacks {
|
| 185 |
+
if mp.Name == name {
|
| 186 |
+
modelPack = mp
|
| 187 |
+
break
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
if modelPack == nil {
|
| 193 |
+
term.OutputErrorAndExit("Model pack not found")
|
| 194 |
+
return
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
renderModelPack(modelPack, customModelsById, allProperties)
|
| 198 |
+
|
| 199 |
+
fmt.Println()
|
| 200 |
+
|
| 201 |
+
term.PrintCmds("", "set-model", "set-model default", "models custom")
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
// func getModelRoleConfig(customModels []*shared.CustomModel, modelRole shared.ModelRole) shared.ModelRoleConfig {
|
| 205 |
+
// _, modelConfig := getModelWithRoleConfig(customModels, modelRole)
|
| 206 |
+
// return modelConfig
|
| 207 |
+
// }
|
| 208 |
+
|
| 209 |
+
// func getModelWithRoleConfig(customModels []*shared.CustomModel, modelRole shared.ModelRole) (*shared.CustomModel, shared.ModelRoleConfig) {
|
| 210 |
+
// role := string(modelRole)
|
| 211 |
+
|
| 212 |
+
// modelId := getModelIdForRole(customModels, modelRole)
|
| 213 |
+
|
| 214 |
+
// temperatureStr, err := term.GetUserStringInputWithDefault("Temperature for "+role+":", fmt.Sprintf("%.1f", shared.DefaultConfigByRole[modelRole].Temperature))
|
| 215 |
+
// if err != nil {
|
| 216 |
+
// term.OutputErrorAndExit("Error reading temperature: %v", err)
|
| 217 |
+
// }
|
| 218 |
+
// temperature, err := strconv.ParseFloat(temperatureStr, 32)
|
| 219 |
+
// if err != nil {
|
| 220 |
+
// term.OutputErrorAndExit("Invalid number for temperature: %v", err)
|
| 221 |
+
// }
|
| 222 |
+
|
| 223 |
+
// topPStr, err := term.GetUserStringInputWithDefault("Top P for "+role+":", fmt.Sprintf("%.1f", shared.DefaultConfigByRole[modelRole].TopP))
|
| 224 |
+
// if err != nil {
|
| 225 |
+
// term.OutputErrorAndExit("Error reading top P: %v", err)
|
| 226 |
+
// }
|
| 227 |
+
// topP, err := strconv.ParseFloat(topPStr, 32)
|
| 228 |
+
// if err != nil {
|
| 229 |
+
// term.OutputErrorAndExit("Invalid number for top P: %v", err)
|
| 230 |
+
// }
|
| 231 |
+
|
| 232 |
+
// var reservedOutputTokens int
|
| 233 |
+
// if modelRole == shared.ModelRoleBuilder || modelRole == shared.ModelRolePlanner || modelRole == shared.ModelRoleWholeFileBuilder {
|
| 234 |
+
// reservedOutputTokensStr, err := term.GetUserStringInputWithDefault("Reserved output tokens for "+role+":", fmt.Sprintf("%d", model.ReservedOutputTokens))
|
| 235 |
+
// if err != nil {
|
| 236 |
+
// term.OutputErrorAndExit("Error reading reserved output tokens: %v", err)
|
| 237 |
+
// }
|
| 238 |
+
// reservedOutputTokens, err = strconv.Atoi(reservedOutputTokensStr)
|
| 239 |
+
// if err != nil {
|
| 240 |
+
// term.OutputErrorAndExit("Invalid number for reserved output tokens: %v", err)
|
| 241 |
+
// }
|
| 242 |
+
// }
|
| 243 |
+
|
| 244 |
+
// return model, shared.ModelRoleConfig{
|
| 245 |
+
// ModelId: model.ModelId,
|
| 246 |
+
// Role: modelRole,
|
| 247 |
+
// Temperature: float32(temperature),
|
| 248 |
+
// TopP: float32(topP),
|
| 249 |
+
// ReservedOutputTokens: reservedOutputTokens,
|
| 250 |
+
// }
|
| 251 |
+
// }
|
| 252 |
+
|
| 253 |
+
// func getPlannerRoleConfig(customModels []*shared.CustomModel) shared.PlannerRoleConfig {
|
| 254 |
+
// model, modelConfig := getModelWithRoleConfig(customModels, shared.ModelRolePlanner)
|
| 255 |
+
|
| 256 |
+
// return shared.PlannerRoleConfig{
|
| 257 |
+
// ModelRoleConfig: modelConfig,
|
| 258 |
+
// PlannerModelConfig: shared.PlannerModelConfig{
|
| 259 |
+
// MaxConvoTokens: model.DefaultMaxConvoTokens,
|
| 260 |
+
// },
|
| 261 |
+
// }
|
| 262 |
+
// }
|
| 263 |
+
|
| 264 |
+
// func getModelIdForRole(customModels []*shared.CustomModel, role shared.ModelRole) shared.ModelId {
|
| 265 |
+
// color.New(color.Bold).Printf("Select a model for the %s role 👇\n", role)
|
| 266 |
+
// return lib.SelectModelIdForRole(customModels, role)
|
| 267 |
+
// }
|
app/cli/cmd/model_providers.go
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"os"
|
| 6 |
+
"strconv"
|
| 7 |
+
"strings"
|
| 8 |
+
|
| 9 |
+
"plandex-cli/api"
|
| 10 |
+
"plandex-cli/auth"
|
| 11 |
+
"plandex-cli/term"
|
| 12 |
+
|
| 13 |
+
shared "plandex-shared"
|
| 14 |
+
|
| 15 |
+
"github.com/fatih/color"
|
| 16 |
+
"github.com/olekukonko/tablewriter"
|
| 17 |
+
"github.com/spf13/cobra"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
var customProvidersOnly bool
|
| 21 |
+
|
| 22 |
+
var providersCmd = &cobra.Command{
|
| 23 |
+
Use: "providers",
|
| 24 |
+
Short: "List built-in and custom model providers",
|
| 25 |
+
Run: listProviders,
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
var addProviderCmd = &cobra.Command{
|
| 29 |
+
Use: "add",
|
| 30 |
+
Aliases: []string{"create"},
|
| 31 |
+
Short: "Add a custom model provider",
|
| 32 |
+
Run: customModelsNotImplemented,
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
var updateProviderCmd = &cobra.Command{
|
| 36 |
+
Use: "update",
|
| 37 |
+
Aliases: []string{"edit"},
|
| 38 |
+
Short: "Update a custom model provider",
|
| 39 |
+
Run: customModelsNotImplemented,
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
func init() {
|
| 43 |
+
RootCmd.AddCommand(providersCmd)
|
| 44 |
+
providersCmd.Flags().BoolVarP(&customProvidersOnly, "custom", "c", false, "List custom providers only")
|
| 45 |
+
providersCmd.AddCommand(addProviderCmd)
|
| 46 |
+
providersCmd.AddCommand(updateProviderCmd)
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
func listProviders(cmd *cobra.Command, args []string) {
|
| 50 |
+
auth.MustResolveAuthWithOrg()
|
| 51 |
+
|
| 52 |
+
var customProviders []*shared.CustomProvider
|
| 53 |
+
var apiErr *shared.ApiError
|
| 54 |
+
|
| 55 |
+
if customProvidersOnly && auth.Current.IsCloud {
|
| 56 |
+
term.OutputErrorAndExit("Custom providers are not supported on Plandex Cloud")
|
| 57 |
+
return
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
if !auth.Current.IsCloud {
|
| 61 |
+
term.StartSpinner("")
|
| 62 |
+
customProviders, apiErr = api.Client.ListCustomProviders()
|
| 63 |
+
term.StopSpinner()
|
| 64 |
+
if apiErr != nil {
|
| 65 |
+
term.OutputErrorAndExit("Error fetching providers: %v", apiErr.Msg)
|
| 66 |
+
return
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
if customProvidersOnly && len(customProviders) == 0 {
|
| 71 |
+
fmt.Println("🤷♂️ No custom providers")
|
| 72 |
+
fmt.Println()
|
| 73 |
+
term.PrintCmds("", "models custom")
|
| 74 |
+
return
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
color.New(color.Bold, term.ColorHiCyan).Println("🏠 Built-in Providers")
|
| 78 |
+
table := tablewriter.NewWriter(os.Stdout)
|
| 79 |
+
table.SetAutoWrapText(true)
|
| 80 |
+
|
| 81 |
+
var header []string
|
| 82 |
+
if auth.Current.IsCloud {
|
| 83 |
+
header = []string{"ID", "API Key", "Other Vars"}
|
| 84 |
+
} else {
|
| 85 |
+
header = []string{"ID", "Base URL", "API Key", "Other Vars"}
|
| 86 |
+
}
|
| 87 |
+
table.SetHeader(header)
|
| 88 |
+
for _, p := range shared.AllModelProviders {
|
| 89 |
+
if p == shared.ModelProviderCustom {
|
| 90 |
+
continue
|
| 91 |
+
}
|
| 92 |
+
config := shared.BuiltInModelProviderConfigs[p]
|
| 93 |
+
if config.LocalOnly && auth.Current.IsCloud {
|
| 94 |
+
continue
|
| 95 |
+
}
|
| 96 |
+
var apiKey string
|
| 97 |
+
if config.ApiKeyEnvVar != "" {
|
| 98 |
+
apiKey = config.ApiKeyEnvVar
|
| 99 |
+
} else if config.SkipAuth {
|
| 100 |
+
apiKey = "No Auth"
|
| 101 |
+
} else if config.HasClaudeMaxAuth {
|
| 102 |
+
apiKey = "Claude Max Oauth"
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
extraVars := []string{}
|
| 106 |
+
if config.Provider == shared.ModelProviderAmazonBedrock {
|
| 107 |
+
extraVars = append(extraVars, "PLANDEX_AWS_PROFILE")
|
| 108 |
+
}
|
| 109 |
+
for _, v := range config.ExtraAuthVars {
|
| 110 |
+
extraVars = append(extraVars, v.Var)
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
if auth.Current.IsCloud {
|
| 114 |
+
table.Append([]string{
|
| 115 |
+
string(p),
|
| 116 |
+
apiKey,
|
| 117 |
+
strings.Join(extraVars, "\n"),
|
| 118 |
+
})
|
| 119 |
+
} else {
|
| 120 |
+
table.Append([]string{
|
| 121 |
+
string(p),
|
| 122 |
+
config.BaseUrl,
|
| 123 |
+
apiKey,
|
| 124 |
+
strings.Join(extraVars, "\n"),
|
| 125 |
+
})
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
table.Render()
|
| 129 |
+
fmt.Println()
|
| 130 |
+
|
| 131 |
+
if len(customProviders) > 0 {
|
| 132 |
+
color.New(color.Bold, term.ColorHiCyan).Println("🛠️ Custom Providers")
|
| 133 |
+
table := tablewriter.NewWriter(os.Stdout)
|
| 134 |
+
table.SetAutoWrapText(false)
|
| 135 |
+
table.SetHeader([]string{"#", "Name", "Base URL", "API Key", "Other Vars"})
|
| 136 |
+
for i, p := range customProviders {
|
| 137 |
+
extraVars := []string{}
|
| 138 |
+
for _, v := range p.ExtraAuthVars {
|
| 139 |
+
extraVars = append(extraVars, v.Var)
|
| 140 |
+
}
|
| 141 |
+
apiKey := p.ApiKeyEnvVar
|
| 142 |
+
if apiKey == "" && p.SkipAuth {
|
| 143 |
+
apiKey = "No Auth"
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
table.Append([]string{
|
| 147 |
+
strconv.Itoa(i + 1),
|
| 148 |
+
p.Name,
|
| 149 |
+
p.BaseUrl,
|
| 150 |
+
apiKey,
|
| 151 |
+
strings.Join(extraVars, "\n"),
|
| 152 |
+
})
|
| 153 |
+
}
|
| 154 |
+
table.Render()
|
| 155 |
+
fmt.Println()
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
fmt.Println(color.New(color.Bold, term.ColorHiCyan).Sprint("\n📖 Per-provider instructions"))
|
| 159 |
+
fmt.Println("Go to → " + color.New(color.Bold).Sprint("https://docs.plandex.ai/models/model-providers"))
|
| 160 |
+
fmt.Println()
|
| 161 |
+
|
| 162 |
+
term.PrintCmds("", "models custom")
|
| 163 |
+
}
|
app/cli/cmd/models.go
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"fmt"
|
| 6 |
+
"os"
|
| 7 |
+
"path/filepath"
|
| 8 |
+
|
| 9 |
+
"plandex-cli/api"
|
| 10 |
+
"plandex-cli/auth"
|
| 11 |
+
"plandex-cli/fs"
|
| 12 |
+
"plandex-cli/lib"
|
| 13 |
+
"plandex-cli/term"
|
| 14 |
+
"strconv"
|
| 15 |
+
|
| 16 |
+
shared "plandex-shared"
|
| 17 |
+
|
| 18 |
+
"github.com/fatih/color"
|
| 19 |
+
"github.com/olekukonko/tablewriter"
|
| 20 |
+
"github.com/spf13/cobra"
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
var customModelsOnly bool
|
| 24 |
+
var allProperties bool
|
| 25 |
+
var saveCustomModels bool
|
| 26 |
+
var customModelsPath string
|
| 27 |
+
|
| 28 |
+
func init() {
|
| 29 |
+
RootCmd.AddCommand(modelsCmd)
|
| 30 |
+
|
| 31 |
+
modelsCmd.Flags().BoolVarP(&allProperties, "all", "a", false, "Show all properties")
|
| 32 |
+
|
| 33 |
+
modelsCmd.AddCommand(listAvailableModelsCmd)
|
| 34 |
+
modelsCmd.AddCommand(addCustomModelCmd)
|
| 35 |
+
modelsCmd.AddCommand(updateCustomModelCmd)
|
| 36 |
+
modelsCmd.AddCommand(manageCustomModelsCmd)
|
| 37 |
+
modelsCmd.AddCommand(deleteCustomModelCmd)
|
| 38 |
+
modelsCmd.AddCommand(defaultModelsCmd)
|
| 39 |
+
|
| 40 |
+
manageCustomModelsCmd.Flags().BoolVar(&saveCustomModels, "save", false, "Save custom models")
|
| 41 |
+
manageCustomModelsCmd.Flags().StringVarP(&customModelsPath, "file", "f", "", "Path to custom models file")
|
| 42 |
+
|
| 43 |
+
listAvailableModelsCmd.Flags().BoolVarP(&customModelsOnly, "custom", "c", false, "List custom models only")
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
var modelsCmd = &cobra.Command{
|
| 47 |
+
Use: "models",
|
| 48 |
+
Short: "Show plan model settings",
|
| 49 |
+
Run: models,
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
var defaultModelsCmd = &cobra.Command{
|
| 53 |
+
Use: "default",
|
| 54 |
+
Short: "Show default model settings for new plans",
|
| 55 |
+
Run: defaultModels,
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
var listAvailableModelsCmd = &cobra.Command{
|
| 59 |
+
Use: "available",
|
| 60 |
+
Aliases: []string{"avail"},
|
| 61 |
+
Short: "List all available models",
|
| 62 |
+
Run: listAvailableModels,
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
var manageCustomModelsCmd = &cobra.Command{
|
| 66 |
+
Use: "custom",
|
| 67 |
+
Short: "Manage custom models, providers, and model packs",
|
| 68 |
+
Run: manageCustomModels,
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
var addCustomModelCmd = &cobra.Command{
|
| 72 |
+
Use: "add",
|
| 73 |
+
Aliases: []string{"create"},
|
| 74 |
+
Short: "Add a custom model",
|
| 75 |
+
Run: customModelsNotImplemented,
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
var updateCustomModelCmd = &cobra.Command{
|
| 79 |
+
Use: "update",
|
| 80 |
+
Aliases: []string{"edit"},
|
| 81 |
+
Short: "Update a custom model",
|
| 82 |
+
Run: customModelsNotImplemented,
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
var deleteCustomModelCmd = &cobra.Command{
|
| 86 |
+
Use: "rm",
|
| 87 |
+
Aliases: []string{"remove", "delete"},
|
| 88 |
+
Short: "Remove a custom model",
|
| 89 |
+
Args: cobra.MaximumNArgs(1),
|
| 90 |
+
Run: customModelsNotImplemented,
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
func manageCustomModels(cmd *cobra.Command, args []string) {
|
| 94 |
+
auth.MustResolveAuthWithOrg()
|
| 95 |
+
|
| 96 |
+
term.StartSpinner("")
|
| 97 |
+
|
| 98 |
+
var serverModelsInput *shared.ModelsInput
|
| 99 |
+
var defaultConfig *shared.PlanConfig
|
| 100 |
+
|
| 101 |
+
errCh := make(chan error, 2)
|
| 102 |
+
|
| 103 |
+
go func() {
|
| 104 |
+
var err error
|
| 105 |
+
serverModelsInput, err = lib.GetServerModelsInput()
|
| 106 |
+
if err != nil {
|
| 107 |
+
errCh <- fmt.Errorf("error getting server models input: %v", err)
|
| 108 |
+
return
|
| 109 |
+
}
|
| 110 |
+
errCh <- nil
|
| 111 |
+
}()
|
| 112 |
+
|
| 113 |
+
go func() {
|
| 114 |
+
var apiErr *shared.ApiError
|
| 115 |
+
defaultConfig, apiErr = api.Client.GetDefaultPlanConfig()
|
| 116 |
+
if apiErr != nil {
|
| 117 |
+
errCh <- fmt.Errorf("error getting default config: %v", apiErr.Msg)
|
| 118 |
+
return
|
| 119 |
+
}
|
| 120 |
+
errCh <- nil
|
| 121 |
+
}()
|
| 122 |
+
|
| 123 |
+
for i := 0; i < 2; i++ {
|
| 124 |
+
err := <-errCh
|
| 125 |
+
if err != nil {
|
| 126 |
+
term.OutputErrorAndExit(err.Error())
|
| 127 |
+
return
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
usingDefaultPath := false
|
| 132 |
+
if customModelsPath == "" {
|
| 133 |
+
usingDefaultPath = true
|
| 134 |
+
customModelsPath = lib.GetCustomModelsPath(auth.Current.UserId)
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
exists, err := fs.FileExists(customModelsPath)
|
| 138 |
+
if err != nil {
|
| 139 |
+
term.OutputErrorAndExit("Error checking custom models file: %v", err)
|
| 140 |
+
return
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
if saveCustomModels {
|
| 144 |
+
if !exists {
|
| 145 |
+
term.OutputErrorAndExit("File not found: %s", customModelsPath)
|
| 146 |
+
return
|
| 147 |
+
}
|
| 148 |
+
} else {
|
| 149 |
+
if serverModelsInput.IsEmpty() {
|
| 150 |
+
jsonData, err := json.MarshalIndent(getExampleTemplate(auth.Current.IsCloud, auth.Current.IntegratedModelsMode), "", " ")
|
| 151 |
+
if err != nil {
|
| 152 |
+
term.OutputErrorAndExit("Error marshalling template: %v", err)
|
| 153 |
+
return
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
err = os.MkdirAll(filepath.Dir(customModelsPath), 0755)
|
| 157 |
+
if err != nil {
|
| 158 |
+
term.OutputErrorAndExit("Error creating directory: %v", err)
|
| 159 |
+
return
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
err = os.WriteFile(customModelsPath, jsonData, 0644)
|
| 163 |
+
if err != nil {
|
| 164 |
+
term.OutputErrorAndExit("Error writing template file: %v", err)
|
| 165 |
+
return
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
term.StopSpinner()
|
| 169 |
+
|
| 170 |
+
fmt.Printf("🧠 Example models file → %s\n", customModelsPath)
|
| 171 |
+
fmt.Println("👨💻 Edit it, then come back here to save")
|
| 172 |
+
fmt.Println()
|
| 173 |
+
} else {
|
| 174 |
+
serverClientModelsInput := serverModelsInput.ToClientModelsInput()
|
| 175 |
+
serverClientModelsInput.PrepareUpdate()
|
| 176 |
+
|
| 177 |
+
var localModelsInput shared.ModelsInput
|
| 178 |
+
|
| 179 |
+
if exists {
|
| 180 |
+
// we only do a conflict check on the default path in the home dir
|
| 181 |
+
// if user specifies the path and the file exists, just open the file without checking for conflicts
|
| 182 |
+
if usingDefaultPath {
|
| 183 |
+
res, err := lib.CustomModelsCheckLocalChanges(customModelsPath)
|
| 184 |
+
if err != nil {
|
| 185 |
+
term.OutputErrorAndExit("Error checking local changes: %v", err)
|
| 186 |
+
return
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
localModelsInput = res.LocalModelsInput
|
| 190 |
+
if res.HasLocalChanges {
|
| 191 |
+
term.StopSpinner()
|
| 192 |
+
|
| 193 |
+
res, err := warnModelsFileLocalChanges(customModelsPath, "models custom")
|
| 194 |
+
if err != nil {
|
| 195 |
+
term.OutputErrorAndExit("Error confirming: %v", err)
|
| 196 |
+
return
|
| 197 |
+
}
|
| 198 |
+
if !res {
|
| 199 |
+
return
|
| 200 |
+
}
|
| 201 |
+
fmt.Println()
|
| 202 |
+
term.StartSpinner("")
|
| 203 |
+
}
|
| 204 |
+
}
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
if !serverModelsInput.Equals(localModelsInput) {
|
| 208 |
+
err := lib.WriteCustomModelsFile(customModelsPath, serverModelsInput)
|
| 209 |
+
if err != nil {
|
| 210 |
+
term.OutputErrorAndExit("Error saving custom models file: %v", err)
|
| 211 |
+
return
|
| 212 |
+
}
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
term.StopSpinner()
|
| 216 |
+
|
| 217 |
+
fmt.Printf("🧠 %s → %s\n", color.New(color.Bold, term.ColorHiCyan).Sprint("Models file"), customModelsPath)
|
| 218 |
+
fmt.Println("👨💻 Edit it, then come back here to save")
|
| 219 |
+
fmt.Println()
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
pathArg := ""
|
| 223 |
+
if !usingDefaultPath {
|
| 224 |
+
pathArg = fmt.Sprintf(" --file %s", customModelsPath)
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
res := maybePromptAndOpenModelsFile(customModelsPath, pathArg, "models custom", defaultConfig, nil)
|
| 228 |
+
if res.shouldReturn {
|
| 229 |
+
return
|
| 230 |
+
}
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
didUpdate := lib.MustSyncCustomModels(customModelsPath, serverModelsInput)
|
| 234 |
+
|
| 235 |
+
if !didUpdate {
|
| 236 |
+
fmt.Println("🤷♂️ No changes to custom models/providers/model packs")
|
| 237 |
+
}
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
func models(cmd *cobra.Command, args []string) {
|
| 241 |
+
auth.MustResolveAuthWithOrg()
|
| 242 |
+
lib.MustResolveProject()
|
| 243 |
+
|
| 244 |
+
if lib.CurrentPlanId == "" {
|
| 245 |
+
term.OutputNoCurrentPlanErrorAndExit()
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
term.StartSpinner("")
|
| 249 |
+
|
| 250 |
+
plan, err := api.Client.GetPlan(lib.CurrentPlanId)
|
| 251 |
+
|
| 252 |
+
if err != nil {
|
| 253 |
+
term.StopSpinner()
|
| 254 |
+
term.OutputErrorAndExit("Error getting plan: %v", err)
|
| 255 |
+
return
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
settings, err := api.Client.GetSettings(lib.CurrentPlanId, lib.CurrentBranch)
|
| 259 |
+
term.StopSpinner()
|
| 260 |
+
|
| 261 |
+
if err != nil {
|
| 262 |
+
term.OutputErrorAndExit("Error getting settings: %v", err)
|
| 263 |
+
return
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
title := fmt.Sprintf("%s Model Settings", color.New(color.Bold, term.ColorHiGreen).Sprint(plan.Name))
|
| 267 |
+
|
| 268 |
+
table := tablewriter.NewWriter(os.Stdout)
|
| 269 |
+
table.SetAutoWrapText(false)
|
| 270 |
+
table.Append([]string{title})
|
| 271 |
+
table.Render()
|
| 272 |
+
fmt.Println()
|
| 273 |
+
|
| 274 |
+
renderSettings(settings, allProperties)
|
| 275 |
+
|
| 276 |
+
term.PrintCmds("", "set-model", "models available", "models default", "models custom")
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
func defaultModels(cmd *cobra.Command, args []string) {
|
| 280 |
+
auth.MustResolveAuthWithOrg()
|
| 281 |
+
|
| 282 |
+
term.StartSpinner("")
|
| 283 |
+
settings, err := api.Client.GetOrgDefaultSettings()
|
| 284 |
+
|
| 285 |
+
if err != nil {
|
| 286 |
+
term.OutputErrorAndExit("Error getting default model settings: %v", err)
|
| 287 |
+
return
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
term.StopSpinner()
|
| 291 |
+
|
| 292 |
+
title := fmt.Sprintf("%s Model Settings", color.New(color.Bold, term.ColorHiGreen).Sprint("Org-Wide Default"))
|
| 293 |
+
table := tablewriter.NewWriter(os.Stdout)
|
| 294 |
+
table.SetAutoWrapText(false)
|
| 295 |
+
table.Append([]string{title})
|
| 296 |
+
table.Render()
|
| 297 |
+
fmt.Println()
|
| 298 |
+
|
| 299 |
+
renderSettings(settings, allProperties)
|
| 300 |
+
|
| 301 |
+
term.PrintCmds("", "set-model default", "models available", "models")
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
func listAvailableModels(cmd *cobra.Command, args []string) {
|
| 305 |
+
auth.MustResolveAuthWithOrg()
|
| 306 |
+
|
| 307 |
+
if customModelsOnly && auth.Current.IntegratedModelsMode {
|
| 308 |
+
term.OutputErrorAndExit("Custom models are not supported in Integrated Models mode on Plandex Cloud")
|
| 309 |
+
return
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
term.StartSpinner("")
|
| 313 |
+
|
| 314 |
+
customModels, err := api.Client.ListCustomModels()
|
| 315 |
+
|
| 316 |
+
term.StopSpinner()
|
| 317 |
+
|
| 318 |
+
if err != nil {
|
| 319 |
+
term.OutputErrorAndExit("Error fetching custom models: %v", err)
|
| 320 |
+
return
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
if !customModelsOnly {
|
| 324 |
+
color.New(color.Bold, term.ColorHiCyan).Println("🏠 Built-in Models")
|
| 325 |
+
table := tablewriter.NewWriter(os.Stdout)
|
| 326 |
+
table.SetAutoWrapText(false)
|
| 327 |
+
table.SetHeader([]string{"Model", "Input", "Output", "Reserved"})
|
| 328 |
+
for _, model := range shared.BuiltInBaseModels {
|
| 329 |
+
if auth.Current.IsCloud && model.IsLocalOnly() {
|
| 330 |
+
continue
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
table.Append([]string{string(model.ModelId), fmt.Sprintf("%d 🪙", model.MaxTokens), fmt.Sprintf("%d 🪙", model.MaxOutputTokens), fmt.Sprintf("%d 🪙", model.ReservedOutputTokens)})
|
| 334 |
+
}
|
| 335 |
+
table.Render()
|
| 336 |
+
fmt.Println()
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
if len(customModels) > 0 {
|
| 340 |
+
color.New(color.Bold, term.ColorHiCyan).Println("🛠️ Custom Models")
|
| 341 |
+
table := tablewriter.NewWriter(os.Stdout)
|
| 342 |
+
table.SetAutoWrapText(false)
|
| 343 |
+
table.SetHeader([]string{"#", "ID", "🪙"})
|
| 344 |
+
for i, model := range customModels {
|
| 345 |
+
table.Append([]string{fmt.Sprintf("%d", i+1), string(model.ModelId), strconv.Itoa(model.MaxTokens)})
|
| 346 |
+
}
|
| 347 |
+
table.Render()
|
| 348 |
+
} else if customModelsOnly {
|
| 349 |
+
fmt.Println("🤷♂️ No custom models")
|
| 350 |
+
}
|
| 351 |
+
fmt.Println()
|
| 352 |
+
|
| 353 |
+
if customModelsOnly {
|
| 354 |
+
if len(customModels) > 0 {
|
| 355 |
+
term.PrintCmds("", "models", "set-model", "models custom")
|
| 356 |
+
} else {
|
| 357 |
+
term.PrintCmds("", "models custom")
|
| 358 |
+
}
|
| 359 |
+
} else {
|
| 360 |
+
term.PrintCmds("", "models available --custom", "models", "set-model", "models custom")
|
| 361 |
+
}
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
func renderSettings(settings *shared.PlanSettings, allProperties bool) {
|
| 365 |
+
modelPack := settings.GetModelPack()
|
| 366 |
+
|
| 367 |
+
color.New(color.Bold, term.ColorHiCyan).Println("🎛️ Current Model Pack")
|
| 368 |
+
renderModelPack(modelPack, settings.CustomModelsById, allProperties)
|
| 369 |
+
|
| 370 |
+
if allProperties {
|
| 371 |
+
color.New(color.Bold, term.ColorHiCyan).Println("🧠 Planner Defaults")
|
| 372 |
+
table := tablewriter.NewWriter(os.Stdout)
|
| 373 |
+
table.SetAutoWrapText(false)
|
| 374 |
+
table.SetHeader([]string{"Max Tokens", "Max Convo Tokens"})
|
| 375 |
+
table.Append([]string{
|
| 376 |
+
fmt.Sprintf("%d", modelPack.Planner.GetFinalLargeContextFallback().GetSharedBaseConfig(settings).MaxTokens),
|
| 377 |
+
fmt.Sprintf("%d", modelPack.Planner.GetMaxConvoTokens(settings)),
|
| 378 |
+
})
|
| 379 |
+
table.Render()
|
| 380 |
+
fmt.Println()
|
| 381 |
+
}
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
func renderModelPack(modelPack *shared.ModelPack, customModelsById map[shared.ModelId]*shared.CustomModel, allProperties bool) {
|
| 385 |
+
table := tablewriter.NewWriter(os.Stdout)
|
| 386 |
+
table.SetAutoFormatHeaders(false)
|
| 387 |
+
table.SetAutoWrapText(true)
|
| 388 |
+
table.SetColWidth(64)
|
| 389 |
+
table.SetHeader([]string{modelPack.Name})
|
| 390 |
+
table.Append([]string{modelPack.Description})
|
| 391 |
+
table.Render()
|
| 392 |
+
fmt.Println()
|
| 393 |
+
|
| 394 |
+
color.New(color.Bold, term.ColorHiCyan).Println("🤖 Models")
|
| 395 |
+
table = tablewriter.NewWriter(os.Stdout)
|
| 396 |
+
table.SetAutoWrapText(false)
|
| 397 |
+
cols := []string{
|
| 398 |
+
"Role",
|
| 399 |
+
"Model",
|
| 400 |
+
}
|
| 401 |
+
align := []int{
|
| 402 |
+
tablewriter.ALIGN_LEFT, // Role
|
| 403 |
+
tablewriter.ALIGN_LEFT, // Model
|
| 404 |
+
}
|
| 405 |
+
if allProperties {
|
| 406 |
+
cols = append(cols, []string{
|
| 407 |
+
"Temperature",
|
| 408 |
+
"Top P",
|
| 409 |
+
"Max Input",
|
| 410 |
+
}...)
|
| 411 |
+
align = append(align, []int{
|
| 412 |
+
tablewriter.ALIGN_RIGHT, // Temperature
|
| 413 |
+
tablewriter.ALIGN_RIGHT, // Top P
|
| 414 |
+
tablewriter.ALIGN_RIGHT, // Max Input
|
| 415 |
+
}...)
|
| 416 |
+
}
|
| 417 |
+
table.SetHeader(cols)
|
| 418 |
+
table.SetColumnAlignment(align)
|
| 419 |
+
|
| 420 |
+
anyRoleParamsDisabled := false
|
| 421 |
+
|
| 422 |
+
var addModelRow func(role string, config shared.ModelRoleConfig, indent int)
|
| 423 |
+
addModelRow = func(role string, config shared.ModelRoleConfig, indent int) {
|
| 424 |
+
if indent > 0 {
|
| 425 |
+
role = "└─ " + role
|
| 426 |
+
for i := 0; i < indent-1; i++ {
|
| 427 |
+
role = " " + role
|
| 428 |
+
}
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
var temp float32
|
| 432 |
+
var topP float32
|
| 433 |
+
var disabled bool
|
| 434 |
+
|
| 435 |
+
sharedBaseConfig := config.GetSharedBaseConfigWithCustomModels(customModelsById)
|
| 436 |
+
|
| 437 |
+
if sharedBaseConfig.RoleParamsDisabled {
|
| 438 |
+
temp = 1
|
| 439 |
+
topP = 1
|
| 440 |
+
disabled = true
|
| 441 |
+
anyRoleParamsDisabled = true
|
| 442 |
+
} else {
|
| 443 |
+
temp = config.Temperature
|
| 444 |
+
topP = config.TopP
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
tempStr := fmt.Sprintf("%.1f", temp)
|
| 448 |
+
if disabled {
|
| 449 |
+
tempStr = "*" + tempStr
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
topPStr := fmt.Sprintf("%.1f", topP)
|
| 453 |
+
if disabled {
|
| 454 |
+
topPStr = "*" + topPStr
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
row := []string{
|
| 458 |
+
role,
|
| 459 |
+
string(config.GetModelId()),
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
if allProperties {
|
| 463 |
+
row = append(row, []string{
|
| 464 |
+
tempStr,
|
| 465 |
+
topPStr,
|
| 466 |
+
fmt.Sprintf("%d 🪙", sharedBaseConfig.MaxTokens-config.GetReservedOutputTokens(customModelsById)),
|
| 467 |
+
}...)
|
| 468 |
+
}
|
| 469 |
+
table.Append(row)
|
| 470 |
+
|
| 471 |
+
// Add large context and large output fallback(s) if present
|
| 472 |
+
if config.LargeContextFallback != nil {
|
| 473 |
+
addModelRow("large-context", *config.LargeContextFallback, indent+1)
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
if config.LargeOutputFallback != nil {
|
| 477 |
+
addModelRow("large-output", *config.LargeOutputFallback, indent+1)
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
if config.StrongModel != nil {
|
| 481 |
+
addModelRow("strong", *config.StrongModel, indent+1)
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
if config.ErrorFallback != nil {
|
| 485 |
+
addModelRow("error", *config.ErrorFallback, indent+1)
|
| 486 |
+
}
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
addModelRow(string(shared.ModelRolePlanner), modelPack.Planner.ModelRoleConfig, 0)
|
| 490 |
+
|
| 491 |
+
addModelRow(string(shared.ModelRoleArchitect), modelPack.GetArchitect(), 0)
|
| 492 |
+
addModelRow(string(shared.ModelRoleCoder), modelPack.GetCoder(), 0)
|
| 493 |
+
addModelRow(string(shared.ModelRolePlanSummary), modelPack.PlanSummary, 0)
|
| 494 |
+
addModelRow(string(shared.ModelRoleBuilder), modelPack.Builder, 0)
|
| 495 |
+
addModelRow(string(shared.ModelRoleWholeFileBuilder), modelPack.GetWholeFileBuilder(), 0)
|
| 496 |
+
addModelRow(string(shared.ModelRoleName), modelPack.Namer, 0)
|
| 497 |
+
addModelRow(string(shared.ModelRoleCommitMsg), modelPack.CommitMsg, 0)
|
| 498 |
+
addModelRow(string(shared.ModelRoleExecStatus), modelPack.ExecStatus, 0)
|
| 499 |
+
table.Render()
|
| 500 |
+
|
| 501 |
+
if anyRoleParamsDisabled && allProperties {
|
| 502 |
+
fmt.Println("* these models do not support changing temperature or top p")
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
fmt.Println()
|
| 506 |
+
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
func customModelsNotImplemented(cmd *cobra.Command, args []string) {
|
| 510 |
+
color.New(color.Bold, color.FgHiRed).Println("⛔️ Not implemented")
|
| 511 |
+
fmt.Println()
|
| 512 |
+
fmt.Println("Use " + color.New(color.BgCyan, color.FgHiWhite).Sprint(" plandex models custom ") + " to manage custom models, providers, and model packs")
|
| 513 |
+
os.Exit(1)
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
func getExampleTemplate(isCloud, isCloudIntegratedModels bool) shared.ClientModelsInput {
|
| 517 |
+
exampleProviderName := "togetherai"
|
| 518 |
+
|
| 519 |
+
var customProviders []*shared.CustomProvider
|
| 520 |
+
usesProviders := []shared.BaseModelUsesProvider{}
|
| 521 |
+
if !isCloud {
|
| 522 |
+
customProviders = append(customProviders, &shared.CustomProvider{
|
| 523 |
+
Name: exampleProviderName,
|
| 524 |
+
BaseUrl: "https://api.together.xyz/v1",
|
| 525 |
+
ApiKeyEnvVar: "TOGETHER_API_KEY",
|
| 526 |
+
})
|
| 527 |
+
|
| 528 |
+
usesProviders = append(usesProviders, shared.BaseModelUsesProvider{
|
| 529 |
+
Provider: shared.ModelProviderCustom,
|
| 530 |
+
CustomProvider: &exampleProviderName,
|
| 531 |
+
ModelName: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
|
| 532 |
+
})
|
| 533 |
+
}
|
| 534 |
+
usesProviders = append(usesProviders, shared.BaseModelUsesProvider{
|
| 535 |
+
Provider: shared.ModelProviderOpenRouter,
|
| 536 |
+
ModelName: "meta-llama/llama-4-maverick",
|
| 537 |
+
})
|
| 538 |
+
|
| 539 |
+
var customModels []*shared.CustomModel
|
| 540 |
+
if !isCloudIntegratedModels {
|
| 541 |
+
customModels = []*shared.CustomModel{
|
| 542 |
+
{
|
| 543 |
+
ModelId: shared.ModelId("meta-llama/llama-4-maverick"),
|
| 544 |
+
Publisher: shared.ModelPublisher("meta-llama"),
|
| 545 |
+
Description: "Meta Llama 4 Maverick",
|
| 546 |
+
|
| 547 |
+
BaseModelShared: shared.BaseModelShared{
|
| 548 |
+
DefaultMaxConvoTokens: 75000,
|
| 549 |
+
MaxTokens: 1048576,
|
| 550 |
+
MaxOutputTokens: 16000,
|
| 551 |
+
ReservedOutputTokens: 16000,
|
| 552 |
+
ModelCompatibility: shared.FullCompatibility,
|
| 553 |
+
PreferredOutputFormat: shared.ModelOutputFormatXml,
|
| 554 |
+
},
|
| 555 |
+
|
| 556 |
+
Providers: usesProviders,
|
| 557 |
+
},
|
| 558 |
+
}
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
lightModelId := "meta-llama/llama-4-maverick"
|
| 562 |
+
if len(customModels) == 0 {
|
| 563 |
+
lightModelId = "mistral/devstral-small"
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
return shared.ClientModelsInput{
|
| 567 |
+
SchemaUrl: shared.SchemaUrlInputConfig,
|
| 568 |
+
CustomProviders: customProviders,
|
| 569 |
+
CustomModels: customModels,
|
| 570 |
+
CustomModelPacks: []*shared.ClientModelPackSchema{
|
| 571 |
+
{
|
| 572 |
+
Name: "example-model-pack",
|
| 573 |
+
Description: "Example model pack",
|
| 574 |
+
ClientModelPackSchemaRoles: shared.ClientModelPackSchemaRoles{
|
| 575 |
+
Planner: "deepseek/r1",
|
| 576 |
+
Architect: "deepseek/r1",
|
| 577 |
+
Coder: &shared.ModelRoleConfigSchema{
|
| 578 |
+
ModelId: "deepseek/v3",
|
| 579 |
+
LargeContextFallback: &shared.ModelRoleConfigSchema{
|
| 580 |
+
ModelId: "google/gemini-2.5-pro",
|
| 581 |
+
},
|
| 582 |
+
ErrorFallback: &shared.ModelRoleConfigSchema{
|
| 583 |
+
ModelId: "deepseek/r1-hidden",
|
| 584 |
+
},
|
| 585 |
+
},
|
| 586 |
+
PlanSummary: lightModelId,
|
| 587 |
+
Builder: shared.ModelRoleConfigSchema{
|
| 588 |
+
ModelId: "deepseek/r1-hidden",
|
| 589 |
+
StrongModel: &shared.ModelRoleConfigSchema{
|
| 590 |
+
ModelId: "openai/o3-medium",
|
| 591 |
+
},
|
| 592 |
+
},
|
| 593 |
+
WholeFileBuilder: shared.ModelRoleConfigSchema{
|
| 594 |
+
ModelId: "deepseek/r1-hidden",
|
| 595 |
+
LargeContextFallback: &shared.ModelRoleConfigSchema{
|
| 596 |
+
ModelId: "google/gemini-2.5-pro",
|
| 597 |
+
LargeOutputFallback: &shared.ModelRoleConfigSchema{
|
| 598 |
+
ModelId: "openai/o3-low",
|
| 599 |
+
},
|
| 600 |
+
},
|
| 601 |
+
LargeOutputFallback: &shared.ModelRoleConfigSchema{
|
| 602 |
+
ModelId: "openai/o3-low",
|
| 603 |
+
},
|
| 604 |
+
},
|
| 605 |
+
ExecStatus: "deepseek/r1-hidden",
|
| 606 |
+
Namer: lightModelId,
|
| 607 |
+
CommitMsg: lightModelId,
|
| 608 |
+
},
|
| 609 |
+
},
|
| 610 |
+
},
|
| 611 |
+
}
|
| 612 |
+
}
|
app/cli/cmd/new.go
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
|
| 6 |
+
"plandex-cli/api"
|
| 7 |
+
"plandex-cli/auth"
|
| 8 |
+
"plandex-cli/lib"
|
| 9 |
+
"plandex-cli/term"
|
| 10 |
+
"plandex-cli/types"
|
| 11 |
+
|
| 12 |
+
shared "plandex-shared"
|
| 13 |
+
|
| 14 |
+
"github.com/fatih/color"
|
| 15 |
+
"github.com/spf13/cobra"
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
var name string
|
| 19 |
+
var contextBaseDir string
|
| 20 |
+
|
| 21 |
+
// newCmd represents the new command
|
| 22 |
+
var newCmd = &cobra.Command{
|
| 23 |
+
Use: "new",
|
| 24 |
+
Aliases: []string{"n"},
|
| 25 |
+
Short: "Start a new plan",
|
| 26 |
+
// Long: ``,
|
| 27 |
+
Args: cobra.ExactArgs(0),
|
| 28 |
+
Run: new,
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
func init() {
|
| 32 |
+
RootCmd.AddCommand(newCmd)
|
| 33 |
+
newCmd.Flags().StringVarP(&name, "name", "n", "", "Name of the new plan")
|
| 34 |
+
newCmd.Flags().StringVar(&contextBaseDir, "context-dir", ".", "Base directory to auto-load context from")
|
| 35 |
+
|
| 36 |
+
AddNewPlanFlags(newCmd)
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
func new(cmd *cobra.Command, args []string) {
|
| 40 |
+
auth.MustResolveAuthWithOrg()
|
| 41 |
+
lib.MustResolveOrCreateProject()
|
| 42 |
+
|
| 43 |
+
term.StartSpinner("")
|
| 44 |
+
|
| 45 |
+
errCh := make(chan error, 2)
|
| 46 |
+
|
| 47 |
+
var planId string
|
| 48 |
+
var config *shared.PlanConfig
|
| 49 |
+
|
| 50 |
+
go func() {
|
| 51 |
+
res, apiErr := api.Client.CreatePlan(lib.CurrentProjectId, shared.CreatePlanRequest{Name: name})
|
| 52 |
+
if apiErr != nil {
|
| 53 |
+
errCh <- fmt.Errorf("error creating plan: %v", apiErr.Msg)
|
| 54 |
+
return
|
| 55 |
+
}
|
| 56 |
+
planId = res.Id
|
| 57 |
+
errCh <- nil
|
| 58 |
+
}()
|
| 59 |
+
|
| 60 |
+
go func() {
|
| 61 |
+
var apiErr *shared.ApiError
|
| 62 |
+
config, apiErr = api.Client.GetDefaultPlanConfig()
|
| 63 |
+
if apiErr != nil {
|
| 64 |
+
errCh <- fmt.Errorf("error getting plan config: %v", apiErr.Msg)
|
| 65 |
+
return
|
| 66 |
+
}
|
| 67 |
+
errCh <- nil
|
| 68 |
+
}()
|
| 69 |
+
|
| 70 |
+
for i := 0; i < 2; i++ {
|
| 71 |
+
err := <-errCh
|
| 72 |
+
if err != nil {
|
| 73 |
+
term.OutputErrorAndExit("Error: %v", err)
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
err := lib.WriteCurrentPlan(planId)
|
| 78 |
+
|
| 79 |
+
if err != nil {
|
| 80 |
+
term.OutputErrorAndExit("Error setting current plan: %v", err)
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
err = lib.WriteCurrentBranch("main")
|
| 84 |
+
if err != nil {
|
| 85 |
+
term.OutputErrorAndExit("Error setting current branch: %v", err)
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
if name == "" {
|
| 89 |
+
name = "draft"
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
term.StopSpinner()
|
| 93 |
+
|
| 94 |
+
fmt.Printf("✅ Started new plan %s and set it to current plan\n", color.New(color.Bold, term.ColorHiGreen).Sprint(name))
|
| 95 |
+
fmt.Printf("⚙️ Using default config\n")
|
| 96 |
+
|
| 97 |
+
resolveAutoMode(config)
|
| 98 |
+
|
| 99 |
+
resolveModelPack()
|
| 100 |
+
|
| 101 |
+
// autoModeLabel := shared.ConfigSettingsByKey["automode"].KeyToLabel(string(config.AutoMode))
|
| 102 |
+
// fmt.Println("⚡️ Auto-mode:", autoModeLabel)
|
| 103 |
+
|
| 104 |
+
if config.AutoLoadContext {
|
| 105 |
+
fmt.Println("📥 Automatic context loading is enabled")
|
| 106 |
+
|
| 107 |
+
baseDir := contextBaseDir
|
| 108 |
+
if baseDir == "" {
|
| 109 |
+
baseDir = "."
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
lib.MustLoadContext([]string{baseDir}, &types.LoadContextParams{
|
| 113 |
+
DefsOnly: true,
|
| 114 |
+
SkipIgnoreWarning: true,
|
| 115 |
+
AutoLoaded: true,
|
| 116 |
+
})
|
| 117 |
+
} else {
|
| 118 |
+
fmt.Println()
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
var cmds []string
|
| 122 |
+
if term.IsRepl {
|
| 123 |
+
cmds = []string{"config", "plans", "cd", "models"}
|
| 124 |
+
} else {
|
| 125 |
+
cmds = []string{"tell", "chat", "config"}
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
if !config.AutoLoadContext {
|
| 129 |
+
cmds = append([]string{"load"}, cmds...)
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
fmt.Println()
|
| 133 |
+
term.PrintCmds("", cmds...)
|
| 134 |
+
}
|
app/cli/cmd/plan_exec_helpers.go
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"os"
|
| 6 |
+
"plandex-cli/lib"
|
| 7 |
+
"plandex-cli/term"
|
| 8 |
+
"strconv"
|
| 9 |
+
|
| 10 |
+
shared "plandex-shared"
|
| 11 |
+
|
| 12 |
+
"github.com/spf13/cobra"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
const (
|
| 16 |
+
EditorTypeVim string = "vim"
|
| 17 |
+
EditorTypeNano string = "nano"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
var defaultEditor = EditorTypeVim
|
| 21 |
+
|
| 22 |
+
const defaultAutoDebugTries = 5
|
| 23 |
+
|
| 24 |
+
var autoConfirm bool
|
| 25 |
+
|
| 26 |
+
var tellPromptFile string
|
| 27 |
+
var tellBg bool
|
| 28 |
+
var tellStop bool
|
| 29 |
+
var tellNoBuild bool
|
| 30 |
+
var tellAutoApply bool
|
| 31 |
+
var tellAutoContext bool
|
| 32 |
+
var tellSmartContext bool
|
| 33 |
+
var tellSkipMenu bool
|
| 34 |
+
var noExec bool
|
| 35 |
+
var autoDebug int
|
| 36 |
+
|
| 37 |
+
var editor = EditorTypeVim // default to vim
|
| 38 |
+
var editorSetByFlag bool
|
| 39 |
+
|
| 40 |
+
func init() {
|
| 41 |
+
envEditor := os.Getenv("EDITOR")
|
| 42 |
+
if envEditor == "" {
|
| 43 |
+
envEditor = os.Getenv("VISUAL")
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
if envEditor != "" {
|
| 47 |
+
defaultEditor = envEditor
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
type initExecFlagsParams struct {
|
| 52 |
+
omitFile bool
|
| 53 |
+
omitNoBuild bool
|
| 54 |
+
omitEditor bool
|
| 55 |
+
omitStop bool
|
| 56 |
+
omitBg bool
|
| 57 |
+
omitApply bool
|
| 58 |
+
omitExec bool
|
| 59 |
+
omitAutoContext bool
|
| 60 |
+
omitSmartContext bool
|
| 61 |
+
omitSkipMenu bool
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
func initExecFlags(cmd *cobra.Command, params initExecFlagsParams) {
|
| 65 |
+
if !params.omitFile {
|
| 66 |
+
cmd.Flags().StringVarP(&tellPromptFile, "file", "f", "", "File containing prompt")
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
if !params.omitBg {
|
| 70 |
+
cmd.Flags().BoolVar(&tellBg, "bg", false, "Execute autonomously in the background")
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
if !params.omitStop {
|
| 74 |
+
cmd.Flags().BoolVarP(&tellStop, "stop", "s", false, "Stop after a single reply")
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
if !params.omitNoBuild {
|
| 78 |
+
cmd.Flags().BoolVarP(&tellNoBuild, "no-build", "n", false, "Don't build files")
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
cmd.Flags().BoolVar(&autoConfirm, "auto-update-context", false, shared.ConfigSettingsByKey["auto-update-context"].Desc)
|
| 82 |
+
|
| 83 |
+
if !params.omitAutoContext {
|
| 84 |
+
cmd.Flags().BoolVar(&tellAutoContext, "auto-load-context", false, shared.ConfigSettingsByKey["auto-load-context"].Desc)
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
if !params.omitSmartContext {
|
| 88 |
+
cmd.Flags().BoolVar(&tellSmartContext, "smart-context", false, shared.ConfigSettingsByKey["smart-context"].Desc)
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
if !params.omitApply {
|
| 92 |
+
cmd.Flags().BoolVar(&tellAutoApply, "apply", false, "Automatically apply changes")
|
| 93 |
+
initApplyFlags(cmd, true)
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
if !params.omitExec {
|
| 97 |
+
initExecScriptFlags(cmd)
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
if !params.omitEditor {
|
| 101 |
+
cmd.Flags().Var(newEditorValue(&editor), "editor", "Write prompt in system editor")
|
| 102 |
+
cmd.Flag("editor").NoOptDefVal = defaultEditor
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
if !params.omitSkipMenu {
|
| 106 |
+
cmd.Flags().BoolVar(&tellSkipMenu, "skip-menu", false, shared.ConfigSettingsByKey["skip-changes-menu"].Desc)
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
func initApplyFlags(cmd *cobra.Command, applyFlag bool) {
|
| 111 |
+
commitDesc := "Commit changes to git"
|
| 112 |
+
if applyFlag {
|
| 113 |
+
commitDesc += " when --apply is passed"
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
skipCommitDesc := "Skip committing changes to git"
|
| 117 |
+
if applyFlag {
|
| 118 |
+
skipCommitDesc += " when --apply is passed"
|
| 119 |
+
}
|
| 120 |
+
cmd.Flags().BoolVarP(&autoCommit, "commit", "c", false, commitDesc)
|
| 121 |
+
cmd.Flags().BoolVar(&skipCommit, "skip-commit", false, skipCommitDesc)
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
func initExecScriptFlags(cmd *cobra.Command) {
|
| 125 |
+
cmd.Flags().BoolVar(&noExec, "no-exec", false, "Disable command execution")
|
| 126 |
+
cmd.Flags().BoolVar(&autoExec, "auto-exec", false, "Automatically execute commands without confirmation")
|
| 127 |
+
cmd.Flags().Var(newAutoDebugValue(&autoDebug), "debug", "Automatically execute and debug failing commands (optionally specify number of tries—default is 5)")
|
| 128 |
+
cmd.Flag("debug").NoOptDefVal = strconv.Itoa(defaultAutoDebugTries)
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
func validatePlanExecFlags(isApply bool) {
|
| 132 |
+
if autoDebug > 0 && noExec {
|
| 133 |
+
term.OutputErrorAndExit("--debug can't be used with --no-exec")
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
if tellAutoContext && tellBg {
|
| 137 |
+
term.OutputErrorAndExit("--auto-context/-c can't be used with --bg")
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
if !isApply {
|
| 141 |
+
if autoDebug > 0 && !tellAutoApply {
|
| 142 |
+
term.OutputErrorAndExit("--debug can only be used with --apply")
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
if autoExec && !tellAutoApply {
|
| 146 |
+
term.OutputErrorAndExit("--auto-exec can only be used with --apply")
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
if tellAutoApply && tellNoBuild {
|
| 150 |
+
term.OutputErrorAndExit("--apply can't be used with --no-build/-n")
|
| 151 |
+
}
|
| 152 |
+
if tellAutoApply && tellBg {
|
| 153 |
+
term.OutputErrorAndExit("--apply can't be used with --bg")
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
func mustSetPlanExecFlags(cmd *cobra.Command, isApply bool) {
|
| 159 |
+
if lib.CurrentPlanId == "" {
|
| 160 |
+
term.OutputNoCurrentPlanErrorAndExit()
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
config := lib.MustGetCurrentPlanConfig()
|
| 164 |
+
|
| 165 |
+
// Set flag vars from config when flags aren't explicitly set
|
| 166 |
+
if !cmd.Flags().Changed("stop") {
|
| 167 |
+
tellStop = !config.AutoContinue
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
if !cmd.Flags().Changed("no-build") {
|
| 171 |
+
tellNoBuild = !config.AutoBuild
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
if !cmd.Flags().Changed("auto-update-context") {
|
| 175 |
+
autoConfirm = config.AutoUpdateContext
|
| 176 |
+
}
|
| 177 |
+
if !cmd.Flags().Changed("apply") {
|
| 178 |
+
tellAutoApply = config.AutoApply
|
| 179 |
+
}
|
| 180 |
+
if !cmd.Flags().Changed("skip-commit") {
|
| 181 |
+
skipCommit = config.SkipCommit
|
| 182 |
+
}
|
| 183 |
+
if !cmd.Flags().Changed("commit") {
|
| 184 |
+
autoCommit = config.AutoCommit
|
| 185 |
+
}
|
| 186 |
+
if !cmd.Flags().Changed("auto-load-context") {
|
| 187 |
+
tellAutoContext = config.AutoLoadContext
|
| 188 |
+
}
|
| 189 |
+
if !cmd.Flags().Changed("smart-context") {
|
| 190 |
+
tellSmartContext = config.SmartContext
|
| 191 |
+
}
|
| 192 |
+
if !cmd.Flags().Changed("no-exec") {
|
| 193 |
+
noExec = !config.CanExec
|
| 194 |
+
}
|
| 195 |
+
if !cmd.Flags().Changed("auto-exec") {
|
| 196 |
+
autoExec = config.AutoExec
|
| 197 |
+
}
|
| 198 |
+
if !cmd.Flags().Changed("debug") {
|
| 199 |
+
autoDebug = config.AutoDebugTries
|
| 200 |
+
// Only set autoDebug if AutoDebug is enabled in config
|
| 201 |
+
if !config.AutoDebug {
|
| 202 |
+
autoDebug = 0
|
| 203 |
+
}
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
if !cmd.Flags().Changed("skip-menu") {
|
| 207 |
+
tellSkipMenu = config.SkipChangesMenu
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
// tell command editor is no longer tied to config *unless* it's set to vim or nano
|
| 211 |
+
// otherwise, the flag or EDITOR env var are used
|
| 212 |
+
// config.Editor is now used for mainly for JSON editing (and perhaps other purposes)
|
| 213 |
+
// this is because it's pretty rare to use the editor for writing prompts now rather than the REPL
|
| 214 |
+
if !editorSetByFlag && (config.Editor == shared.EditorTypeVim || config.Editor == shared.EditorTypeNano) {
|
| 215 |
+
editor = config.Editor
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
validatePlanExecFlags(isApply)
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
// AutoDebugValue implements the flag.Value interface
|
| 222 |
+
type autoDebugValue struct {
|
| 223 |
+
value *int
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
func newAutoDebugValue(p *int) *autoDebugValue {
|
| 227 |
+
*p = 0 // Default to 0 (disabled)
|
| 228 |
+
return &autoDebugValue{p}
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
func (f *autoDebugValue) Set(s string) error {
|
| 232 |
+
if s == "" {
|
| 233 |
+
*f.value = defaultAutoDebugTries
|
| 234 |
+
return nil
|
| 235 |
+
}
|
| 236 |
+
v, err := strconv.Atoi(s)
|
| 237 |
+
if err != nil {
|
| 238 |
+
return fmt.Errorf("invalid value for --debug: %v", err)
|
| 239 |
+
}
|
| 240 |
+
if v <= 0 {
|
| 241 |
+
return fmt.Errorf("--debug value must be greater than 0")
|
| 242 |
+
}
|
| 243 |
+
*f.value = v
|
| 244 |
+
return nil
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
func (f *autoDebugValue) String() string {
|
| 248 |
+
if f.value == nil {
|
| 249 |
+
return "0"
|
| 250 |
+
}
|
| 251 |
+
return strconv.Itoa(*f.value)
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
func (f *autoDebugValue) Type() string {
|
| 255 |
+
return "int"
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
// EditorValue implements the flag.Value interface
|
| 259 |
+
type editorValue struct {
|
| 260 |
+
value *string
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
func newEditorValue(p *string) *editorValue {
|
| 264 |
+
*p = defaultEditor
|
| 265 |
+
return &editorValue{p}
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
func (f *editorValue) Set(s string) error {
|
| 269 |
+
if s == "" {
|
| 270 |
+
*f.value = defaultEditor
|
| 271 |
+
return nil
|
| 272 |
+
}
|
| 273 |
+
*f.value = s
|
| 274 |
+
editorSetByFlag = true
|
| 275 |
+
return nil
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
func (f *editorValue) String() string {
|
| 279 |
+
if f.value == nil {
|
| 280 |
+
return ""
|
| 281 |
+
}
|
| 282 |
+
return *f.value
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
func (f *editorValue) Type() string {
|
| 286 |
+
return "string"
|
| 287 |
+
}
|