Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 37 additions & 22 deletions internal/appsetup/appsetup.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,34 +239,49 @@ func (s *Setup) Run(ctx context.Context, org, role string) (*AppCredentials, err
if err := s.ensureInstalled(ctx, org, recovered.Slug); err != nil {
return nil, fmt.Errorf("ensuring installation: %w", err)
}
// Resolve AppID from the installation so ROLE_APP_IDS gets updated.
if recovered.AppID == 0 {
inst, found, err := s.findExistingInstallation(ctx, org, role, recovered.Slug)
if err == nil && found {
recovered.AppID = inst.AppID
}
}
return recovered, nil
}

// For public app-sets, check if the app already exists globally before
// creating a new one. Public apps (e.g. fullsend-ai-fullsend) are owned
// by another org and just need to be installed — not re-created.
// We do not attempt PEM recovery here; the PEM is owned by the source org
// and is copied separately by copySharedAppPEMs.
if s.publicApps {
clientID, lookupErr := s.client.GetAppClientID(ctx, slug)
if lookupErr != nil && !forge.IsNotFound(lookupErr) {
return nil, fmt.Errorf("checking public app %s: %w", slug, lookupErr)
}
if lookupErr == nil {
if err := s.ensureInstalled(ctx, org, slug); err != nil {
return nil, fmt.Errorf("ensuring installation of public app %s: %w", slug, err)
// Check if the app already exists globally before creating a new one.
// Public apps (e.g. fullsend-ai-triage) are owned by another org and
// just need to be installed — not re-created via the manifest flow.
// When --public was passed, install without prompting. Otherwise, ask
// the user to confirm so they don't accidentally create a duplicate.
clientID, lookupErr := s.client.GetAppClientID(ctx, slug)
if lookupErr != nil && !forge.IsNotFound(lookupErr) {
return nil, fmt.Errorf("checking existing app %s: %w", slug, lookupErr)
}
if lookupErr == nil {
if !s.publicApps {
s.ui.StepDone(fmt.Sprintf("Found existing public app: %s", slug))
install, confirmErr := s.prompter.Confirm(fmt.Sprintf("Public app %s already exists — install it into %s?", slug, org))
if confirmErr != nil {
return nil, fmt.Errorf("confirming public app install: %w", confirmErr)
}
inst, found, err := s.findExistingInstallation(ctx, org, role, slug)
if err != nil {
return nil, fmt.Errorf("looking up installed public app: %w", err)
}
if !found {
return nil, fmt.Errorf("public app %s was installed but not found in installations list", slug)
if !install {
return nil, fmt.Errorf("app %s already exists as a public app; use --public to install it, or choose a different --app-set", slug)
}
s.checkPermissions(inst, org, role)
s.ui.StepDone(fmt.Sprintf("Reusing public app %s (ID: %d)", slug, inst.AppID))
return &AppCredentials{AppID: inst.AppID, Slug: slug, Name: slug, ClientID: clientID}, nil
}
if err := s.ensureInstalled(ctx, org, slug); err != nil {
return nil, fmt.Errorf("ensuring installation of public app %s: %w", slug, err)
}
inst, found, err := s.findExistingInstallation(ctx, org, role, slug)
if err != nil {
return nil, fmt.Errorf("looking up installed public app: %w", err)
}
if !found {
return nil, fmt.Errorf("public app %s was installed but not found in installations list", slug)
}
s.checkPermissions(inst, org, role)
s.ui.StepDone(fmt.Sprintf("Reusing public app %s (ID: %d)", slug, inst.AppID))
return &AppCredentials{AppID: inst.AppID, Slug: slug, Name: slug, ClientID: clientID}, nil
}

// No existing app found — run the manifest flow.
Expand Down
52 changes: 51 additions & 1 deletion internal/appsetup/appsetup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,10 +420,60 @@ func TestSetup_PublicApp_TransientErrorPropagated(t *testing.T) {

_, err := s.Run(context.Background(), "myorg", "coder")
require.Error(t, err)
assert.Contains(t, err.Error(), "checking public app")
assert.Contains(t, err.Error(), "checking existing app")
assert.Contains(t, err.Error(), "rate limit exceeded")
}

func TestSetup_PublicAppFound_WithoutFlag_UserConfirms(t *testing.T) {
// --public NOT set, but app exists globally. User confirms → installs it.
client := &forge.FakeClient{
Installations: []forge.Installation{},
AppClientIDs: map[string]string{"fullsend-ai-coder": "Iv1.public123"},
}
prompter := &fakePrompter{confirmResult: true}
browser := &installOnOpenBrowser{
client: client,
inst: forge.Installation{ID: 1, AppID: 42, AppSlug: "fullsend-ai-coder"},
urlCh: make(chan string, 1),
}
printer := ui.New(&discardWriter{})

s := NewSetup(client, prompter, browser, printer).
WithAppSet("fullsend-ai")
// Note: WithPublicApps NOT called (defaults to false).

creds, err := s.Run(context.Background(), "myorg", "coder")
require.NoError(t, err)

assert.True(t, prompter.confirmCalled, "user should be prompted")
assert.Equal(t, "fullsend-ai-coder", creds.Slug)
assert.Equal(t, "Iv1.public123", creds.ClientID)
assert.Equal(t, 42, creds.AppID)
assert.Empty(t, creds.PEM)
}

func TestSetup_PublicAppFound_WithoutFlag_UserDeclines(t *testing.T) {
// --public NOT set, app exists globally. User says no → helpful error.
client := &forge.FakeClient{
Installations: []forge.Installation{},
AppClientIDs: map[string]string{"fullsend-ai-coder": "Iv1.public123"},
}
prompter := &fakePrompter{confirmResult: false}
browser := newFakeBrowser()
printer := ui.New(&discardWriter{})

s := NewSetup(client, prompter, browser, printer).
WithAppSet("fullsend-ai")

_, err := s.Run(context.Background(), "myorg", "coder")
require.Error(t, err)

assert.True(t, prompter.confirmCalled, "user should be prompted")
assert.Contains(t, err.Error(), "already exists as a public app")
assert.Contains(t, err.Error(), "--public")
assert.Contains(t, err.Error(), "--app-set")
}

func TestSetup_NoExistingApp(t *testing.T) {
client := &forge.FakeClient{
Installations: []forge.Installation{},
Expand Down
12 changes: 6 additions & 6 deletions internal/cli/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -1311,13 +1311,13 @@ func runAppSetup(ctx context.Context, client forge.Client, printer *ui.Printer,
WithStoredAppIDs(storedAppIDs)

// Merge known slugs: config-based first, then shared app overrides.
// Filter config slugs to the requested app-set so that an existing
// install of app-set A doesn't shadow a new install of app-set B.
// Filter both config slugs and shared slugs to the requested app-set
// so that an existing install of app-set A doesn't shadow a new install
// of app-set B. Without this, nonflux-triage (app-set "nonflux") would
// prevent fullsend-ai-triage (app-set "fullsend-ai") from being detected
// and installed.
knownSlugs := filterSlugsByAppSet(loadKnownSlugs(ctx, client, org), appSet)
// Shared slugs are exempt from the prefix filter because they represent
// cross-org shared apps whose slug is authoritative regardless of the
// requested app-set prefix.
for role, slug := range sharedSlugs {
for role, slug := range filterSlugsByAppSet(sharedSlugs, appSet) {
knownSlugs[role] = slug
}
if len(knownSlugs) > 0 {
Expand Down
Loading