mirror of
https://github.com/plandex-ai/plandex.git
synced 2024-04-04 10:47:51 +03:00
lots of work on user management, various fixes, usage guide update
This commit is contained in:
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
const (
|
||||
AuthFreeTrialOption = "Start an anonymous trial on Plandex Cloud (no email required)"
|
||||
AuthAccountOption = "Sign in or create an account"
|
||||
AuthAccountOption = "Sign in, accept an invite, or create an account"
|
||||
)
|
||||
|
||||
func promptInitialAuth() error {
|
||||
@@ -185,9 +185,9 @@ func verifyEmail(email, host string) (bool, string, error) {
|
||||
return false, "", fmt.Errorf("error creating email verification: %v", apiErr.Msg)
|
||||
}
|
||||
|
||||
fmt.Println("✉️ You'll now receive a 6 character pin by email")
|
||||
fmt.Println("✉️ You'll now receive a 6 character pin by email. It will be valid for 5 minutes.")
|
||||
|
||||
pin, err := term.GetUserPasswordInput("Please enter it here:")
|
||||
pin, err := term.GetUserPasswordInput("Please enter your pin:")
|
||||
|
||||
if err != nil {
|
||||
return false, "", fmt.Errorf("error prompting pin: %v", err)
|
||||
|
||||
@@ -89,7 +89,7 @@ func promptAutoAddUsersIfValid(email string) (bool, error) {
|
||||
var autoAddDomainUsers bool
|
||||
var err error
|
||||
if !shared.IsEmailServiceDomain(userDomain) {
|
||||
fmt.Println("With domain auto-join, you can allow any user with an email ending in @"+userDomain, "to auto-join this org")
|
||||
fmt.Println("With domain auto-join, you can allow any user with an email ending in @"+userDomain, "to auto-join this org.")
|
||||
autoAddDomainUsers, err = term.ConfirmYesNo(fmt.Sprintf("Enable auto-join for %s?", userDomain))
|
||||
|
||||
if err != nil {
|
||||
|
||||
101
app/cli/cmd/invite.go
Normal file
101
app/cli/cmd/invite.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"plandex/api"
|
||||
"plandex/auth"
|
||||
"plandex/term"
|
||||
|
||||
"github.com/plandex/plandex/shared"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var inviteCmd = &cobra.Command{
|
||||
Use: "invite [email] [name] [org-role]",
|
||||
Short: "Invite a new user to the org",
|
||||
Run: invite,
|
||||
Args: cobra.MaximumNArgs(3),
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(inviteCmd)
|
||||
}
|
||||
|
||||
func invite(cmd *cobra.Command, args []string) {
|
||||
auth.MustResolveAuthWithOrg()
|
||||
|
||||
email, name, orgRoleName := "", "", ""
|
||||
if len(args) >= 1 {
|
||||
email = args[0]
|
||||
}
|
||||
if len(args) >= 2 {
|
||||
name = args[1]
|
||||
}
|
||||
if len(args) == 3 {
|
||||
orgRoleName = args[2]
|
||||
}
|
||||
|
||||
term.StartSpinner("")
|
||||
orgRoles, err := api.Client.ListOrgRoles()
|
||||
term.StopSpinner()
|
||||
|
||||
if err != nil {
|
||||
term.OutputErrorAndExit("Failed to list org roles: %v", err)
|
||||
}
|
||||
|
||||
if email == "" {
|
||||
var err error
|
||||
email, err = term.GetUserStringInput("Email:")
|
||||
if err != nil {
|
||||
term.OutputErrorAndExit("Failed to get email: %v", err)
|
||||
}
|
||||
}
|
||||
if name == "" {
|
||||
var err error
|
||||
name, err = term.GetUserStringInput("Name:")
|
||||
if err != nil {
|
||||
term.OutputErrorAndExit("Failed to get name: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if orgRoleName == "" {
|
||||
var orgRoleNames []string
|
||||
for _, orgRole := range orgRoles {
|
||||
orgRoleNames = append(orgRoleNames, orgRole.Label)
|
||||
}
|
||||
|
||||
var err error
|
||||
orgRoleName, err = term.SelectFromList("Org role:", orgRoleNames)
|
||||
|
||||
if err != nil {
|
||||
term.OutputErrorAndExit("Failed to select org role: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var orgRoleId string
|
||||
for _, orgRole := range orgRoles {
|
||||
if orgRole.Label == orgRoleName {
|
||||
orgRoleId = orgRole.Id
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if orgRoleId == "" {
|
||||
term.OutputErrorAndExit("Org role '%s' not found", orgRoleName)
|
||||
}
|
||||
|
||||
inviteRequest := shared.InviteRequest{
|
||||
Email: email,
|
||||
Name: name,
|
||||
OrgRoleId: orgRoleId,
|
||||
}
|
||||
|
||||
term.StartSpinner("")
|
||||
apiErr := api.Client.InviteUser(inviteRequest)
|
||||
term.StopSpinner()
|
||||
if apiErr != nil {
|
||||
term.OutputErrorAndExit("Failed to invite user: %s", apiErr.Msg)
|
||||
}
|
||||
|
||||
fmt.Println("✅ Invite sent")
|
||||
}
|
||||
116
app/cli/cmd/revoke.go
Normal file
116
app/cli/cmd/revoke.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"plandex/api"
|
||||
"plandex/auth"
|
||||
"plandex/term"
|
||||
|
||||
"github.com/plandex/plandex/shared"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var revokeCmd = &cobra.Command{
|
||||
Use: "revoke [email]",
|
||||
Short: "Revoke an invite or remove a user from the org",
|
||||
Run: revoke,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(revokeCmd)
|
||||
}
|
||||
|
||||
func revoke(cmd *cobra.Command, args []string) {
|
||||
auth.MustResolveAuthWithOrg()
|
||||
|
||||
email := ""
|
||||
if len(args) > 0 {
|
||||
email = args[0]
|
||||
}
|
||||
|
||||
var users []*shared.User
|
||||
var pendingInvites []*shared.Invite
|
||||
errCh := make(chan error)
|
||||
|
||||
term.StartSpinner("")
|
||||
|
||||
go func() {
|
||||
var err *shared.ApiError
|
||||
users, err = api.Client.ListUsers()
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("error fetching users: %s", err.Msg)
|
||||
return
|
||||
}
|
||||
errCh <- nil
|
||||
}()
|
||||
|
||||
go func() {
|
||||
var err *shared.ApiError
|
||||
pendingInvites, err = api.Client.ListPendingInvites()
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("error fetching pending invites: %s", err.Msg)
|
||||
return
|
||||
}
|
||||
errCh <- nil
|
||||
}()
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
err := <-errCh
|
||||
if err != nil {
|
||||
term.StopSpinner()
|
||||
term.OutputErrorAndExit(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
term.StopSpinner()
|
||||
|
||||
type userInfo struct {
|
||||
Id string
|
||||
IsInvite bool
|
||||
}
|
||||
|
||||
emailToUserMap := make(map[string]userInfo)
|
||||
labelToEmail := make(map[string]string)
|
||||
|
||||
// Combine users and invites for selection
|
||||
combinedList := make([]string, 0, len(users)+len(pendingInvites))
|
||||
for _, user := range users {
|
||||
label := fmt.Sprintf("%s <%s>", user.Name, user.Email)
|
||||
labelToEmail[label] = user.Email
|
||||
combinedList = append(combinedList, label)
|
||||
emailToUserMap[user.Email] = userInfo{Id: user.Id, IsInvite: false}
|
||||
}
|
||||
for _, invite := range pendingInvites {
|
||||
label := fmt.Sprintf("%s <%s> (invite pending)", invite.Name, invite.Email)
|
||||
labelToEmail[label] = invite.Email
|
||||
combinedList = append(combinedList, label)
|
||||
emailToUserMap[invite.Email] = userInfo{Id: invite.Id, IsInvite: true}
|
||||
}
|
||||
|
||||
if email == "" {
|
||||
selected, err := term.SelectFromList("Select a user or invite:", combinedList)
|
||||
if err != nil {
|
||||
term.OutputErrorAndExit("Error selecting item to revoke: %v", err)
|
||||
}
|
||||
|
||||
email = labelToEmail[selected]
|
||||
}
|
||||
|
||||
// Determine if email belongs to a user or an invite and revoke accordingly
|
||||
if userInfo, exists := emailToUserMap[email]; exists {
|
||||
if userInfo.IsInvite {
|
||||
if err := api.Client.DeleteInvite(userInfo.Id); err != nil {
|
||||
term.OutputErrorAndExit("Failed to revoke invite: %v", err)
|
||||
}
|
||||
fmt.Println("✅ Invite revoked")
|
||||
} else {
|
||||
if err := api.Client.DeleteUser(userInfo.Id); err != nil {
|
||||
term.OutputErrorAndExit("Failed to remove user: %v", err)
|
||||
}
|
||||
fmt.Println("✅ User removed")
|
||||
}
|
||||
} else {
|
||||
term.OutputErrorAndExit("No user or pending invite found for email '%s'", email)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"plandex/auth"
|
||||
"plandex/lib"
|
||||
"plandex/term"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -24,5 +26,19 @@ func update(cmd *cobra.Command, args []string) {
|
||||
auth.MustResolveAuthWithOrg()
|
||||
lib.MustResolveProject()
|
||||
|
||||
term.StartSpinner("")
|
||||
outdated, err := lib.CheckOutdatedContext(nil)
|
||||
|
||||
if err != nil {
|
||||
term.StopSpinner()
|
||||
term.OutputErrorAndExit("failed to check outdated context: %s", err)
|
||||
}
|
||||
|
||||
if len(outdated.UpdatedContexts) == 0 {
|
||||
term.StopSpinner()
|
||||
fmt.Println("✅ Context is up to date")
|
||||
return
|
||||
}
|
||||
|
||||
lib.MustUpdateContext(nil)
|
||||
}
|
||||
|
||||
95
app/cli/cmd/users.go
Normal file
95
app/cli/cmd/users.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"plandex/api"
|
||||
"plandex/auth"
|
||||
"plandex/term"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/plandex/plandex/shared"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var usersCmd = &cobra.Command{
|
||||
Use: "users",
|
||||
Short: "List all users and pending invites and the current org",
|
||||
Run: listUsersAndInvites,
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(usersCmd)
|
||||
}
|
||||
|
||||
func listUsersAndInvites(cmd *cobra.Command, args []string) {
|
||||
auth.MustResolveAuthWithOrg()
|
||||
|
||||
var users []*shared.User
|
||||
var pendingInvites []*shared.Invite
|
||||
var orgRoles []*shared.OrgRole
|
||||
|
||||
errCh := make(chan error)
|
||||
|
||||
term.StartSpinner("")
|
||||
|
||||
go func() {
|
||||
var err *shared.ApiError
|
||||
users, err = api.Client.ListUsers()
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("error fetching users: %s", err.Msg)
|
||||
return
|
||||
}
|
||||
errCh <- nil
|
||||
}()
|
||||
|
||||
go func() {
|
||||
var err *shared.ApiError
|
||||
pendingInvites, err = api.Client.ListPendingInvites()
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("error fetching pending invites: %s", err.Msg)
|
||||
return
|
||||
}
|
||||
errCh <- nil
|
||||
}()
|
||||
|
||||
go func() {
|
||||
var err *shared.ApiError
|
||||
orgRoles, err = api.Client.ListOrgRoles()
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("error fetching org roles: %s", err.Msg)
|
||||
return
|
||||
}
|
||||
errCh <- nil
|
||||
|
||||
}()
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
err := <-errCh
|
||||
if err != nil {
|
||||
term.StopSpinner()
|
||||
term.OutputErrorAndExit("%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
term.StopSpinner()
|
||||
|
||||
orgRolesById := make(map[string]*shared.OrgRole)
|
||||
for _, role := range orgRoles {
|
||||
orgRolesById[role.Id] = role
|
||||
}
|
||||
|
||||
// Display users and pending invites in a table
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Type", "Email", "Name", "Role", "Status"})
|
||||
|
||||
for _, user := range users {
|
||||
table.Append([]string{"User", user.Email, user.Name, orgRolesById[user.OrgRoleId].Label, "Active"})
|
||||
}
|
||||
|
||||
for _, invite := range pendingInvites {
|
||||
table.Append([]string{"Invite", invite.Email, invite.Name, orgRolesById[invite.OrgRoleId].Label, "Pending"})
|
||||
}
|
||||
|
||||
table.Render()
|
||||
}
|
||||
@@ -178,9 +178,9 @@ func MustApplyPlan(planId, branch string, autoConfirm bool) {
|
||||
return
|
||||
} else {
|
||||
if isRepo {
|
||||
fmt.Println("✏️ Plandex can commit these updates with an automatically generated message.")
|
||||
fmt.Println("✏️ Plandex can commit these updates with an automatically generated message.")
|
||||
fmt.Println()
|
||||
fmt.Println("ℹ️ Only the files that Plandex is updating will be included the commit. Any other changes, staged or unstaged, will remain exactly as they are.")
|
||||
fmt.Println("ℹ️ Only the files that Plandex is updating will be included the commit. Any other changes, staged or unstaged, will remain exactly as they are.")
|
||||
fmt.Println()
|
||||
|
||||
confirmed, err := term.ConfirmYesNo("Commit Plandex updates now?")
|
||||
|
||||
@@ -144,10 +144,11 @@ func MustLoadContext(resources []string, params *types.LoadContextParams) {
|
||||
}
|
||||
|
||||
contextCh <- &shared.LoadContextParams{
|
||||
ContextType: shared.ContextDirectoryTreeType,
|
||||
Name: name,
|
||||
Body: body,
|
||||
FilePath: inputFilePath,
|
||||
ContextType: shared.ContextDirectoryTreeType,
|
||||
Name: name,
|
||||
Body: body,
|
||||
FilePath: inputFilePath,
|
||||
ForceSkipIgnore: params.ForceSkipIgnore,
|
||||
}
|
||||
}(inputFilePath)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"plandex/api"
|
||||
"plandex/fs"
|
||||
"plandex/term"
|
||||
"plandex/types"
|
||||
"plandex/url"
|
||||
@@ -29,9 +30,7 @@ func MustCheckOutdatedContext(quiet bool, maybeContexts []*shared.Context) (cont
|
||||
term.OutputErrorAndExit("failed to check outdated context: %s", err)
|
||||
}
|
||||
|
||||
if !quiet {
|
||||
term.StopSpinner()
|
||||
}
|
||||
term.StopSpinner()
|
||||
|
||||
if len(outdatedRes.UpdatedContexts) == 0 {
|
||||
if !quiet {
|
||||
@@ -155,6 +154,25 @@ func checkOutdatedAndMaybeUpdateContext(doUpdate bool, maybeContexts []*shared.C
|
||||
var wg sync.WaitGroup
|
||||
contextsById := map[string]*shared.Context{}
|
||||
|
||||
var paths *fs.ProjectPaths
|
||||
var hasDirectoryTreeWithIgnoredPaths bool
|
||||
|
||||
for _, context := range contexts {
|
||||
if context.ContextType == shared.ContextDirectoryTreeType && !context.ForceSkipIgnore {
|
||||
hasDirectoryTreeWithIgnoredPaths = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hasDirectoryTreeWithIgnoredPaths {
|
||||
baseDir := fs.GetBaseDirForContexts(contexts)
|
||||
var err error
|
||||
paths, err = fs.GetProjectPaths(baseDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get project paths: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, context := range contexts {
|
||||
contextsById[context.Id] = context
|
||||
|
||||
@@ -198,7 +216,8 @@ func checkOutdatedAndMaybeUpdateContext(doUpdate bool, maybeContexts []*shared.C
|
||||
go func(context *shared.Context) {
|
||||
defer wg.Done()
|
||||
flattenedPaths, err := ParseInputPaths([]string{context.FilePath}, &types.LoadContextParams{
|
||||
NamesOnly: true,
|
||||
NamesOnly: true,
|
||||
ForceSkipIgnore: context.ForceSkipIgnore,
|
||||
})
|
||||
|
||||
mu.Lock()
|
||||
@@ -208,6 +227,22 @@ func checkOutdatedAndMaybeUpdateContext(doUpdate bool, maybeContexts []*shared.C
|
||||
errs = append(errs, fmt.Errorf("failed to get the directory tree %s: %v", context.FilePath, err))
|
||||
return
|
||||
}
|
||||
|
||||
if !context.ForceSkipIgnore {
|
||||
if paths == nil {
|
||||
errs = append(errs, fmt.Errorf("project paths are nil"))
|
||||
return
|
||||
}
|
||||
|
||||
var filteredPaths []string
|
||||
for _, path := range flattenedPaths {
|
||||
if _, ok := paths.ActivePaths[path]; ok {
|
||||
filteredPaths = append(filteredPaths, path)
|
||||
}
|
||||
}
|
||||
flattenedPaths = filteredPaths
|
||||
}
|
||||
|
||||
body := strings.Join(flattenedPaths, "\n")
|
||||
bytes := []byte(body)
|
||||
|
||||
|
||||
@@ -26,11 +26,29 @@ func OutputErrorAndExit(msg string, args ...interface{}) {
|
||||
displayMsg := ""
|
||||
errorParts := strings.Split(msg, ": ")
|
||||
|
||||
addedErrors := map[string]bool{}
|
||||
|
||||
if len(errorParts) > 1 {
|
||||
for i, part := range errorParts {
|
||||
var lastPart string
|
||||
i := 0
|
||||
for _, part := range errorParts {
|
||||
// don't repeat the same error message
|
||||
if _, ok := addedErrors[strings.ToLower(part)]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(lastPart) < 10 && i > 0 {
|
||||
lastPart = lastPart + ": " + part
|
||||
displayMsg += ": " + part
|
||||
addedErrors[strings.ToLower(lastPart)] = true
|
||||
addedErrors[strings.ToLower(part)] = true
|
||||
continue
|
||||
}
|
||||
|
||||
if i != 0 {
|
||||
displayMsg += "\n"
|
||||
}
|
||||
|
||||
// indent the error message
|
||||
for n := 0; n < i; n++ {
|
||||
displayMsg += " "
|
||||
@@ -45,6 +63,10 @@ func OutputErrorAndExit(msg string, args ...interface{}) {
|
||||
}
|
||||
|
||||
displayMsg += s
|
||||
|
||||
addedErrors[strings.ToLower(part)] = true
|
||||
lastPart = part
|
||||
i++
|
||||
}
|
||||
} else {
|
||||
displayMsg = color.New(ColorHiRed, color.Bold).Sprint("🚨 " + msg)
|
||||
|
||||
@@ -39,7 +39,7 @@ var CmdDesc = map[string][2]string{
|
||||
"ps": {"", "list active and recently finished plan streams"},
|
||||
"stop": {"", "stop an active plan stream"},
|
||||
"connect": {"conn", "connect to an active plan stream"},
|
||||
"sign-in": {"", "sign in to an existing account or create a new one"},
|
||||
"sign-in": {"", "sign in, accept an invite, or create an account"},
|
||||
}
|
||||
|
||||
func PrintCmds(prefix string, cmds ...string) {
|
||||
|
||||
@@ -87,7 +87,7 @@ func ValidateEmailVerification(email, pin string) (id string, err error) {
|
||||
err = Conn.QueryRow(query, pinHash, email, time.Now().Add(-emailVerificationExpirationMinutes*time.Minute)).Scan(&id, &authTokenId)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return "", errors.New("invalid pin")
|
||||
return "", errors.New("invalid or expired pin")
|
||||
}
|
||||
return "", fmt.Errorf("error validating email verification: %v", err)
|
||||
}
|
||||
|
||||
@@ -123,12 +123,16 @@ func ListPlanBranches(orgId, planId string) ([]*Branch, error) {
|
||||
return nil, fmt.Errorf("error listing branches: %v", err)
|
||||
}
|
||||
|
||||
// log.Println("branches: ", spew.Sdump(branches))
|
||||
|
||||
gitBranches, err := GitListBranches(orgId, planId)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error listing git branches: %v", err)
|
||||
}
|
||||
|
||||
// log.Println("gitBranches: ", spew.Sdump(gitBranches))
|
||||
|
||||
var nameSet = make(map[string]bool)
|
||||
for _, name := range gitBranches {
|
||||
nameSet[name] = true
|
||||
|
||||
@@ -253,16 +253,17 @@ func LoadContexts(params LoadContextsParams) (*shared.LoadContextResponse, []*Co
|
||||
|
||||
context := Context{
|
||||
// Id generated by db layer
|
||||
OrgId: orgId,
|
||||
OwnerId: userId,
|
||||
PlanId: planId,
|
||||
ContextType: params.ContextType,
|
||||
Name: params.Name,
|
||||
Url: params.Url,
|
||||
FilePath: params.FilePath,
|
||||
NumTokens: numTokensByTempId[tempId],
|
||||
Sha: sha,
|
||||
Body: params.Body,
|
||||
OrgId: orgId,
|
||||
OwnerId: userId,
|
||||
PlanId: planId,
|
||||
ContextType: params.ContextType,
|
||||
Name: params.Name,
|
||||
Url: params.Url,
|
||||
FilePath: params.FilePath,
|
||||
NumTokens: numTokensByTempId[tempId],
|
||||
Sha: sha,
|
||||
Body: params.Body,
|
||||
ForceSkipIgnore: params.ForceSkipIgnore,
|
||||
}
|
||||
|
||||
err := StoreContext(&context)
|
||||
|
||||
@@ -298,34 +298,36 @@ type repoLock struct {
|
||||
// This allows us to store them in a git repo and use git to manage history.
|
||||
|
||||
type Context struct {
|
||||
Id string `json:"id"`
|
||||
OrgId string `json:"orgId"`
|
||||
OwnerId string `json:"ownerId"`
|
||||
PlanId string `json:"planId"`
|
||||
ContextType shared.ContextType `json:"contextType"`
|
||||
Name string `json:"name"`
|
||||
Url string `json:"url"`
|
||||
FilePath string `json:"filePath"`
|
||||
Sha string `json:"sha"`
|
||||
NumTokens int `json:"numTokens"`
|
||||
Body string `json:"body,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
Id string `json:"id"`
|
||||
OrgId string `json:"orgId"`
|
||||
OwnerId string `json:"ownerId"`
|
||||
PlanId string `json:"planId"`
|
||||
ContextType shared.ContextType `json:"contextType"`
|
||||
Name string `json:"name"`
|
||||
Url string `json:"url"`
|
||||
FilePath string `json:"filePath"`
|
||||
Sha string `json:"sha"`
|
||||
NumTokens int `json:"numTokens"`
|
||||
Body string `json:"body,omitempty"`
|
||||
ForceSkipIgnore bool `json:"forceSkipIgnore"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (context *Context) ToApi() *shared.Context {
|
||||
return &shared.Context{
|
||||
Id: context.Id,
|
||||
OwnerId: context.OwnerId,
|
||||
ContextType: context.ContextType,
|
||||
Name: context.Name,
|
||||
Url: context.Url,
|
||||
FilePath: context.FilePath,
|
||||
Sha: context.Sha,
|
||||
NumTokens: context.NumTokens,
|
||||
Body: context.Body,
|
||||
CreatedAt: context.CreatedAt,
|
||||
UpdatedAt: context.UpdatedAt,
|
||||
Id: context.Id,
|
||||
OwnerId: context.OwnerId,
|
||||
ContextType: context.ContextType,
|
||||
Name: context.Name,
|
||||
Url: context.Url,
|
||||
FilePath: context.FilePath,
|
||||
Sha: context.Sha,
|
||||
NumTokens: context.NumTokens,
|
||||
Body: context.Body,
|
||||
ForceSkipIgnore: context.ForceSkipIgnore,
|
||||
CreatedAt: context.CreatedAt,
|
||||
UpdatedAt: context.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -120,6 +120,10 @@ func GitListBranches(orgId, planId string) ([]string, error) {
|
||||
|
||||
branches := strings.Split(strings.TrimSpace(out.String()), "\n")
|
||||
|
||||
if len(branches) == 0 || (len(branches) == 1 && branches[0] == "") {
|
||||
return []string{"main"}, nil
|
||||
}
|
||||
|
||||
return branches, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,9 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CreateInvite(invite *Invite) error {
|
||||
_, err := Conn.NamedExec(`INSERT INTO invites (id, org_id, email, name, inviter_id) VALUES (:id, :org_id, :email, :name, :inviter_id)`, invite)
|
||||
func CreateInvite(invite *Invite, tx *sql.Tx) error {
|
||||
err := tx.QueryRow("INSERT INTO invites (org_id, email, name, inviter_id) RETURNING id", invite.OrgId, invite.Email, invite.Name, invite.InviterId).Scan(&invite.Id)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating invite: %v", err)
|
||||
}
|
||||
@@ -21,6 +22,10 @@ func GetInvite(id string) (*Invite, error) {
|
||||
err := Conn.Get(&invite, "SELECT * FROM invites WHERE id = $1", id)
|
||||
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("error getting invite: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ func CreateOrg(req *shared.CreateOrgRequest, userId string, domain *string, tx *
|
||||
OwnerId: userId,
|
||||
}
|
||||
|
||||
err := tx.QueryRow("INSERT INTO orgs (name, domain, auto_add_domain_users, owner_id) VALUES ($1, $2, $3, $4) RETURNING id", req.Name, domain, req.AutoAddDomainUsers, userId).Scan(&org.Id)
|
||||
err := tx.QueryRow("INSERT INTO orgs (name, domain, auto_add_domain_users, owner_id, is_trial) VALUES ($1, $2, $3, $4, false) RETURNING id", req.Name, domain, req.AutoAddDomainUsers, userId).Scan(&org.Id)
|
||||
|
||||
if err != nil {
|
||||
if IsNonUniqueErr(err) {
|
||||
|
||||
@@ -60,3 +60,14 @@ func ListUsers(orgId string) ([]*User, error) {
|
||||
func CreateUser(user *User, tx *sql.Tx) error {
|
||||
return tx.QueryRow("INSERT INTO users (name, email, domain, is_trial) VALUES ($1, $2, $3, $4) RETURNING id", user.Name, user.Email, user.Domain, user.IsTrial).Scan(&user.Id)
|
||||
}
|
||||
|
||||
func NumUsersWithRole(orgId, roleId string) (int, error) {
|
||||
var count int
|
||||
err := Conn.Get(&count, "SELECT COUNT(*) FROM orgs_users WHERE org_id = $1 AND org_role_id = $2", orgId, roleId)
|
||||
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error counting users with role: %v", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
@@ -5,46 +5,11 @@ import (
|
||||
"net/smtp"
|
||||
"os"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/ses"
|
||||
"github.com/gen2brain/beeep"
|
||||
)
|
||||
|
||||
func SendVerificationEmail(email string, pin string) error {
|
||||
// Check if the environment is production
|
||||
if os.Getenv("GOENV") == "production" {
|
||||
// Production environment - send email using AWS SES
|
||||
subject := "Your Plandex Pin"
|
||||
htmlBody := fmt.Sprintf("<p>Hi there,</p><p>Your pin is: <strong>%s</strong></p>", pin)
|
||||
textBody := fmt.Sprintf("Hi there,\n\nYour pin is: %s", pin)
|
||||
|
||||
if os.Getenv("IS_CLOUD") == "" {
|
||||
return sendEmailViaSMTP(email, subject, htmlBody, textBody)
|
||||
} else {
|
||||
return sendEmailViaSES(email, subject, htmlBody, textBody)
|
||||
}
|
||||
}
|
||||
|
||||
if os.Getenv("GOENV") == "development" {
|
||||
// Development environment
|
||||
// Copy pin to clipboard
|
||||
if err := clipboard.WriteAll(pin); err != nil {
|
||||
return fmt.Errorf("error copying pin to clipboard in dev: %v", err)
|
||||
}
|
||||
|
||||
// Send notification
|
||||
err := beeep.Notify("Verification Pin", fmt.Sprintf("Verification pin %s copied to clipboard %s", pin, email), "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error sending notification in dev: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendEmailViaSES sends an email using AWS SES
|
||||
func sendEmailViaSES(recipient, subject, htmlBody, textBody string) error {
|
||||
sess, err := session.NewSession()
|
||||
@@ -78,7 +43,7 @@ func sendEmailViaSES(recipient, subject, htmlBody, textBody string) error {
|
||||
Data: aws.String(subject),
|
||||
},
|
||||
},
|
||||
Source: aws.String("support@plandex.ai"),
|
||||
Source: aws.String("Plandex <support@plandex.ai>"),
|
||||
}
|
||||
|
||||
// Attempt to send the email.
|
||||
|
||||
34
app/server/email/invite.go
Normal file
34
app/server/email/invite.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/gen2brain/beeep"
|
||||
)
|
||||
|
||||
func SendInviteEmail(email, inviteeFirstName, inviterName, orgName string) error {
|
||||
// Check if the environment is production
|
||||
if os.Getenv("GOENV") == "production" {
|
||||
// Production environment - send email using AWS SES
|
||||
subject := fmt.Sprintf("%s, you've been invited to join %s on Plandex", inviteeFirstName, orgName)
|
||||
|
||||
htmlBody := fmt.Sprintf(`<p>Hi %s,</p><p>%s has invited you to join the org <strong>%s</strong> on <a href="https://plandex.ai">Plandex.</a></p><p>Plandex is a terminal-based AI programming engine for complex tasks.</p><p>To accept the invite, first <a href="https://github.com/plandex-ai/plandex?tab=readme-ov-file#install">install Plandex</a>, then open a terminal and run 'plandex sign-in'. Enter '%s' when asked for your email and follow the prompts from there.</p><p>If you have questions, feedback, or run into a problem, you can reply directly to this email, <a href="https://github.com/plandex-ai/plandex/discussions">start a discussion</a>, or <a href="https://github.com/plandex-ai/plandex/issues">open an issue.</a></p>`, inviteeFirstName, inviterName, orgName, email)
|
||||
|
||||
textBody := fmt.Sprintf(`Hi %s,\n\n%s has invited you to join the org %s on Plandex.\n\nPlandex is a terminal-based AI programming engine for complex tasks.\n\nTo accept the invite, first install Plandex (https://github.com/plandex-ai/plandex?tab=readme-ov-file#install), then open a terminal and run 'plandex sign-in'. Enter '%s' when asked for your email and follow the prompts from there.\n\nIf you have questions, feedback, or run into a problem, you can reply directly to this email, start a discussion (https://github.com/plandex-ai/plandex/discussions), or open an issue (https://github.com/plandex-ai/plandex/issues).`, inviteeFirstName, inviterName, orgName, email)
|
||||
|
||||
if os.Getenv("IS_CLOUD") == "" {
|
||||
return sendEmailViaSMTP(email, subject, htmlBody, textBody)
|
||||
} else {
|
||||
return sendEmailViaSES(email, subject, htmlBody, textBody)
|
||||
}
|
||||
} else {
|
||||
// Send notification
|
||||
err := beeep.Notify("Invite Sent", fmt.Sprintf("Invite sent to %s (email not sent in development)", email), "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error sending notification in dev: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
42
app/server/email/verification.go
Normal file
42
app/server/email/verification.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/gen2brain/beeep"
|
||||
)
|
||||
|
||||
func SendVerificationEmail(email string, pin string) error {
|
||||
// Check if the environment is production
|
||||
if os.Getenv("GOENV") == "production" {
|
||||
// Production environment - send email using AWS SES
|
||||
subject := "Your Plandex Pin"
|
||||
htmlBody := fmt.Sprintf("<p>Hi there,</p><p>Welcome to Plandex!</p><p>Your pin is:<br><strong>%s</strong></p><p>It will be valid for the next 5 minutes. Please return to the terminal and paste in your pin.</p>", pin)
|
||||
textBody := fmt.Sprintf("Hi there,\n\nWelcome to Plandex!\n\nYour pin is:\n%s\n\nIt will be valid for the next 5 minutes. Please return to the terminal and paste in your pin.", pin)
|
||||
|
||||
if os.Getenv("IS_CLOUD") == "" {
|
||||
return sendEmailViaSMTP(email, subject, htmlBody, textBody)
|
||||
} else {
|
||||
return sendEmailViaSES(email, subject, htmlBody, textBody)
|
||||
}
|
||||
}
|
||||
|
||||
if os.Getenv("GOENV") == "development" {
|
||||
// Development environment
|
||||
// Copy pin to clipboard
|
||||
if err := clipboard.WriteAll(pin); err != nil {
|
||||
return fmt.Errorf("error copying pin to clipboard in dev: %v", err)
|
||||
}
|
||||
|
||||
// Send notification
|
||||
err := beeep.Notify("Verification Pin", fmt.Sprintf("Verification pin %s copied to clipboard %s", pin, email), "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error sending notification in dev: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -53,7 +53,7 @@ func lockRepo(w http.ResponseWriter, r *http.Request, auth *types.ServerAuth, sc
|
||||
http.Error(w, "Error locking repo: "+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
log.Println("Rolling back repo if error")
|
||||
// log.Println("Rolling back repo if error")
|
||||
err = RollbackRepoIfErr(auth.OrgId, planId, err)
|
||||
if err != nil {
|
||||
log.Printf("Error rolling back repo: %v\n", err)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"plandex-server/db"
|
||||
"plandex-server/email"
|
||||
"plandex-server/types"
|
||||
"strings"
|
||||
|
||||
@@ -110,13 +111,32 @@ func InviteUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// start a transaction
|
||||
tx, err := db.Conn.Begin()
|
||||
if err != nil {
|
||||
log.Printf("Error starting transaction: %v\n", err)
|
||||
http.Error(w, "Error starting transaction: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure that rollback is attempted in case of failure
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if rbErr := tx.Rollback(); rbErr != nil {
|
||||
log.Printf("transaction rollback error: %v\n", rbErr)
|
||||
} else {
|
||||
log.Println("transaction rolled back")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err = db.CreateInvite(&db.Invite{
|
||||
OrgId: auth.OrgId,
|
||||
OrgRoleId: req.OrgRoleId,
|
||||
Email: req.Email,
|
||||
Name: req.Name,
|
||||
InviterId: currentUserId,
|
||||
})
|
||||
}, tx)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error creating invite: %v\n", err)
|
||||
@@ -124,6 +144,22 @@ func InviteUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
err = email.SendInviteEmail(req.Email, req.Name, auth.User.Name, org.Name)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error sending invite email: %v\n", err)
|
||||
http.Error(w, "Error sending invite email: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// commit transaction
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
log.Printf("Error committing transaction: %v\n", err)
|
||||
http.Error(w, "Error committing transaction: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Successfully created invite")
|
||||
}
|
||||
|
||||
@@ -277,9 +313,9 @@ func DeleteInviteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if invite.OrgId != auth.OrgId {
|
||||
log.Printf("Invite does not belong to org: %v\n", inviteId)
|
||||
http.Error(w, "Invite does not belong to org: "+inviteId, http.StatusBadRequest)
|
||||
if invite == nil || invite.OrgId != auth.OrgId {
|
||||
log.Printf("Invite not found: %v\n", inviteId)
|
||||
http.Error(w, "Invite not found: "+inviteId, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"plandex-server/db"
|
||||
"plandex-server/host"
|
||||
"time"
|
||||
|
||||
"github.com/plandex/plandex/shared"
|
||||
)
|
||||
@@ -54,7 +55,9 @@ func proxyActivePlanMethod(w http.ResponseWriter, r *http.Request, planId, branc
|
||||
}
|
||||
|
||||
func proxyRequest(w http.ResponseWriter, originalRequest *http.Request, url string) {
|
||||
client := &http.Client{}
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
}
|
||||
|
||||
// Create a new request based on the original request
|
||||
req, err := http.NewRequest(originalRequest.Method, url, originalRequest.Body)
|
||||
|
||||
@@ -98,6 +98,30 @@ func DeleteOrgUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// verify user isn't the only org owner
|
||||
ownerRoleId, err := db.GetOrgOwnerRoleId()
|
||||
if err != nil {
|
||||
log.Printf("Error getting org owner role id: %v\n", err)
|
||||
http.Error(w, "Error getting org owner role id: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if user.OrgRoleId == ownerRoleId {
|
||||
numOwners, err := db.NumUsersWithRole(auth.OrgId, ownerRoleId)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error getting number of org owners: %v\n", err)
|
||||
http.Error(w, "Error getting number of org owners: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if numOwners == 1 {
|
||||
log.Println("Cannot delete the only org owner")
|
||||
http.Error(w, "Cannot delete the only org owner", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// start a transaction
|
||||
tx, err := db.Conn.Begin()
|
||||
if err != nil {
|
||||
|
||||
@@ -28,19 +28,19 @@ func getPlanResult(params planResultParams) (*db.PlanFileResult, bool) {
|
||||
filePath := params.filePath
|
||||
currentState := params.currentState
|
||||
streamedChanges := params.streamedChanges
|
||||
fileContent := params.fileContent
|
||||
// fileContent := params.fileContent
|
||||
|
||||
currentStateLines := strings.Split(currentState, "\n")
|
||||
// fileContentLines := strings.Split(fileContent, "\n")
|
||||
|
||||
log.Printf("\n\ngetPlanResult - path: %s\n", filePath)
|
||||
log.Println("getPlanResult - currentState:")
|
||||
log.Println(currentState)
|
||||
log.Println("getPlanResult - currentStateLines:")
|
||||
log.Println(currentStateLines)
|
||||
log.Println("getPlanResult - fileContent:")
|
||||
log.Println(fileContent)
|
||||
log.Print("\n\n")
|
||||
// log.Printf("\n\ngetPlanResult - path: %s\n", filePath)
|
||||
// log.Println("getPlanResult - currentState:")
|
||||
// log.Println(currentState)
|
||||
// log.Println("getPlanResult - currentStateLines:")
|
||||
// log.Println(currentStateLines)
|
||||
// log.Println("getPlanResult - fileContent:")
|
||||
// log.Println(fileContent)
|
||||
// log.Print("\n\n")
|
||||
|
||||
var replacements []*shared.Replacement
|
||||
for _, streamedChange := range streamedChanges {
|
||||
|
||||
@@ -78,17 +78,18 @@ const (
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
Id string `json:"id"`
|
||||
OwnerId string `json:"ownerId"`
|
||||
ContextType ContextType `json:"contextType"`
|
||||
Name string `json:"name"`
|
||||
Url string `json:"url"`
|
||||
FilePath string `json:"file_path"`
|
||||
Sha string `json:"sha"`
|
||||
NumTokens int `json:"numTokens"`
|
||||
Body string `json:"body,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
Id string `json:"id"`
|
||||
OwnerId string `json:"ownerId"`
|
||||
ContextType ContextType `json:"contextType"`
|
||||
Name string `json:"name"`
|
||||
Url string `json:"url"`
|
||||
FilePath string `json:"file_path"`
|
||||
Sha string `json:"sha"`
|
||||
NumTokens int `json:"numTokens"`
|
||||
Body string `json:"body,omitempty"`
|
||||
ForceSkipIgnore bool `json:"forceSkipIgnore"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type ConvoMessage struct {
|
||||
|
||||
@@ -139,11 +139,12 @@ type RespondMissingFileRequest struct {
|
||||
}
|
||||
|
||||
type LoadContextParams struct {
|
||||
ContextType ContextType `json:"contextType"`
|
||||
Name string `json:"name"`
|
||||
Url string `json:"url"`
|
||||
FilePath string `json:"file_path"`
|
||||
Body string `json:"body"`
|
||||
ContextType ContextType `json:"contextType"`
|
||||
Name string `json:"name"`
|
||||
Url string `json:"url"`
|
||||
FilePath string `json:"file_path"`
|
||||
Body string `json:"body"`
|
||||
ForceSkipIgnore bool `json:"forceSkipIgnore"`
|
||||
}
|
||||
|
||||
type LoadContextRequest []*LoadContextParams
|
||||
|
||||
@@ -171,7 +171,37 @@ Model changes are versioned and can be rewound or applied to a branch just like
|
||||
|
||||
### .plandex and teams
|
||||
|
||||
When you run `plandex new` for the first time in any directory, Plandex will create a `.plandex` directory there for light project-level config. While the `.plandex` directory can be safely added to version control, **if multiple developers are using Plandex with the same project, then for now you should add `.plandex/` to .gitignore (or the ignore file for whatever VCS you use).** When orgs and teams are fully implemented (currently a WIP), it can be re-added to version control so that all team members can work from the same org and project.
|
||||
When you run `plandex new` for the first time in any directory, Plandex will create a `.plandex` directory there for light project-level config. The `.plandex` directory can be safely added to version control.
|
||||
|
||||
### Orgs
|
||||
|
||||
If multiple people are using Plandex with the same project, they should all join the same **org** so that they aren't overwriting each other's `.plandex` directories.
|
||||
|
||||
When creating a new org, you have the option of automatically granting access to anyone with an email address on your domain. If you choose not to do this, or you want to invite someone from outside your email domain, you can use `plandex invite`.
|
||||
|
||||
```bash
|
||||
plandex invite # follow prompts to invite a new user
|
||||
```
|
||||
|
||||
To join an org you've been invited to, use `plandex sign-in`.
|
||||
|
||||
```bash
|
||||
plandex sign-in # follow the prompts to sign in to your org
|
||||
```
|
||||
|
||||
To list users and pending invites:
|
||||
|
||||
```bash
|
||||
plandex users
|
||||
```
|
||||
|
||||
To revoke an invite or remove a user:
|
||||
|
||||
```bash
|
||||
plandex revoke # follow prompts to revoke an invite or remove a user
|
||||
```
|
||||
|
||||
Orgs will be the basis for plan sharing and collaboriation in future releases.
|
||||
|
||||
### Directories
|
||||
|
||||
@@ -179,7 +209,7 @@ So far, we've assumed you're running `plandex new` to create plans in your proje
|
||||
|
||||
When you run `plandex plans`, in addition to showing you plans in the current directory, Plandex will also show you plans in nearby parent directories or subdirectories. This helps you keep track of what plans you're working on and where they are in your project hierarchy. If you want to switch to a plan in a different directory, first `cd` into that directory, then run `plandex cd` to select the plan.
|
||||
|
||||
```bash
|
||||
<!-- ```bash
|
||||
cd your-project
|
||||
plandex new -n root-project-plan # cwd is 'your-project'
|
||||
plandex current # 'your-project' current plan is root-project-plan
|
||||
@@ -193,16 +223,13 @@ cd ../ # cwd is now 'your-project', current plan is root-project-plan
|
||||
plandex plans # shows root-project-plan in current directory + subdir-plan1 and subdir-plan2 in child directory 'some-subdirectory'
|
||||
cd some-subdirectory # cwd is now 'some-subdirectory', current plan is subdir-plan2
|
||||
plandex cd subdir-plan1 # cwd is still 'some-subdirectory', current plan is now subdir-plan1
|
||||
```
|
||||
``` -->
|
||||
|
||||
One more thing to note on directories: you can load context from parent or sibling directories if needed by using `..` in your load paths.
|
||||
|
||||
```bash
|
||||
cd your-project
|
||||
cd some-subdirectory
|
||||
plandex cd subdir-plan1
|
||||
plandex load ../file.go # loads your-project/file.go into subdir-plan1 context
|
||||
plandex load ../other-subdirectory/test.go # loads your-project/other-subdirectory/test.go into subdir-plan1 context
|
||||
plandex load ../file.go # loads file.go from parent directory
|
||||
plandex load ../sibling-dir/test.go # loads test.go from sibling directory
|
||||
```
|
||||
|
||||
### Ignoring files
|
||||
|
||||
BIN
images/plandex-context-still.png
Normal file
BIN
images/plandex-context-still.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 599 KiB |
14
prompts/invite-commands.txt
Normal file
14
prompts/invite-commands.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
Let's add the following commands to the 'app/cli/cmd' directory.
|
||||
|
||||
For all commands, look at the cli/types/api.go and shared/req_res.go files for the API request and response types.
|
||||
|
||||
Commands:
|
||||
|
||||
invite.go - invite a new user to the org. look at the 'checkout' command on accepting optional parameters or prompting if parameters aren't provided.
|
||||
|
||||
users.go - list all users in the org, as well as all pending invites in the org, then list them in a table like the one in the 'plans' command.
|
||||
|
||||
revoke.go - revoke an invite or remove a user from the org. optionally accept an email parameter. if email is supplied, revoke the user or invite with that email. if email is not supplied, prompt the user to select a user or invite to revoke, similar to how branches are selected in the 'checkout' command.
|
||||
|
||||
|
||||
|
||||
2
releases/cli/versions/0.8.0.md
Normal file
2
releases/cli/versions/0.8.0.md
Normal file
@@ -0,0 +1,2 @@
|
||||
- Fix for context update of directory tree when some paths are ignored
|
||||
- Fix for `plandex branches` command showing no branches immediately after plan creation rather than showing the default 'main' branch
|
||||
4
releases/server/versions/0.8.0.md
Normal file
4
releases/server/versions/0.8.0.md
Normal file
@@ -0,0 +1,4 @@
|
||||
- Improvements to copy for email verification emails
|
||||
- Fix for org creation when creating a new account
|
||||
- Send an email to invited user when they are invited to an org
|
||||
- Add timeout when forwarding requests from one instance to another within a cluster
|
||||
Reference in New Issue
Block a user