Skip to content

Commit 3ee7914

Browse files
authored
feat(setup-wizard): improve setup wizard UX and Docker conflict handling (#1232)
1 parent 0bf20db commit 3ee7914

12 files changed

Lines changed: 737 additions & 47 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
### Changed
1515
- Redesigned the app layout with a new collapsible sidebar navigation, replacing the previous top navigation bar. [#1097](https://github.com/sourcebot-dev/sourcebot/pull/1097)
1616
- Expired offline license keys no longer crash the process. An expired key now degrades to the unlicensed state. [#1109](https://github.com/sourcebot-dev/sourcebot/pull/1109)
17+
- Improved the `setup-sourcebot` wizard: prompts for a setup directory, clarifies that secrets are stored locally in `.env`, switches multi-select to Tab, hides "No results" until a real search runs, and detects/cleans up conflicting Docker deployments and volumes before starting. [#1232](https://github.com/sourcebot-dev/sourcebot/pull/1232)
1718

1819
## [4.17.2] - 2026-05-16
1920

packages/setupWizard/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "setup-sourcebot",
3-
"version": "0.1.3",
3+
"version": "0.1.4",
44
"description": "CLI wizard for creating a Sourcebot configuration.",
55
"type": "module",
66
"bin": "./dist/index.js",

packages/setupWizard/src/azuredevops.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { checkbox, confirm, input, password, select } from '@inquirer/prompts';
1+
import { confirm, input, password, select } from '@inquirer/prompts';
2+
import { tabCheckbox as checkbox } from './tabCheckbox.js';
23
import type { AzureDevOpsConnectionConfig } from '@sourcebot/schemas/v3/azuredevops.type';
34
import type { CollectResult, EnvVars } from './utils.js';
45
import { multiInput, note, toEnvKey } from './utils.js';
@@ -57,7 +58,7 @@ export async function collectAzureDevOpsConfig(connectionName: string): Promise<
5758

5859
const envKey = toEnvKey(connectionName, 'TOKEN');
5960
const token = await password({
60-
message: `Azure DevOps Personal Access Token (stored as ${envKey})`,
61+
message: `Azure DevOps Personal Access Token (stored locally in .env as ${envKey})`,
6162
mask: true,
6263
validate: (v) => !v?.trim() ? 'Token is required' : true,
6364
});

packages/setupWizard/src/bitbucket.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { checkbox, confirm, input, password, select } from '@inquirer/prompts';
1+
import { confirm, input, password, select } from '@inquirer/prompts';
2+
import { tabCheckbox as checkbox } from './tabCheckbox.js';
23
import type { BitbucketConnectionConfig } from '@sourcebot/schemas/v3/bitbucket.type';
34
import type { CollectResult, EnvVars } from './utils.js';
45
import { multiInput, note, toEnvKey } from './utils.js';
@@ -78,7 +79,7 @@ async function collectBitbucketCloud(
7879

7980
const tokenEnvKey = toEnvKey(connectionName, 'TOKEN');
8081
const token = await password({
81-
message: `API Token (stored as ${tokenEnvKey})`,
82+
message: `API Token (stored locally in .env as ${tokenEnvKey})`,
8283
mask: true,
8384
validate: (v) => !v?.trim() ? 'Token is required' : true,
8485
});
@@ -95,7 +96,7 @@ async function collectBitbucketCloud(
9596

9697
const tokenEnvKey = toEnvKey(connectionName, 'TOKEN');
9798
const token = await password({
98-
message: `Access Token (stored as ${tokenEnvKey})`,
99+
message: `Access Token (stored locally in .env as ${tokenEnvKey})`,
99100
mask: true,
100101
validate: (v) => !v?.trim() ? 'Token is required' : true,
101102
});
@@ -121,7 +122,7 @@ async function collectBitbucketCloud(
121122

122123
const tokenEnvKey = toEnvKey(connectionName, 'APP_PASSWORD');
123124
const token = await password({
124-
message: `Bitbucket App Password (stored as ${tokenEnvKey})`,
125+
message: `Bitbucket App Password (stored locally in .env as ${tokenEnvKey})`,
125126
mask: true,
126127
validate: (v) => !v?.trim() ? 'App Password is required' : true,
127128
});
@@ -193,7 +194,7 @@ async function collectBitbucketServer(
193194

194195
const tokenEnvKey = toEnvKey(connectionName, 'TOKEN');
195196
const token = await password({
196-
message: `Bitbucket HTTP Access Token (stored as ${tokenEnvKey})`,
197+
message: `Bitbucket HTTP Access Token (stored locally in .env as ${tokenEnvKey})`,
197198
mask: true,
198199
validate: (v) => !v?.trim() ? 'Token is required' : true,
199200
});

packages/setupWizard/src/gitea.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { checkbox, input, password } from '@inquirer/prompts';
1+
import { input, password } from '@inquirer/prompts';
2+
import { tabCheckbox as checkbox } from './tabCheckbox.js';
23
import type { GiteaConnectionConfig } from '@sourcebot/schemas/v3/gitea.type';
34
import type { CollectResult, EnvVars } from './utils.js';
4-
import { multiInput, toEnvKey } from './utils.js';
5+
import { INPUT_THEME, multiInput, toEnvKey } from './utils.js';
56

67
export async function collectGiteaConfig(connectionName: string): Promise<CollectResult> {
78
const env: EnvVars = {};
@@ -10,6 +11,7 @@ export async function collectGiteaConfig(connectionName: string): Promise<Collec
1011
const url = await input({
1112
message: 'Gitea URL',
1213
default: 'https://gitea.com',
14+
theme: INPUT_THEME,
1315
validate: (v) => {
1416
if (!v?.trim()) {
1517
return 'URL is required';
@@ -26,7 +28,7 @@ export async function collectGiteaConfig(connectionName: string): Promise<Collec
2628

2729
const giteaEnvKey = toEnvKey(connectionName, 'TOKEN');
2830
const giteaToken = await password({
29-
message: `Gitea Access Token (stored as ${giteaEnvKey}, leave blank for public repos only)`,
31+
message: `Gitea Access Token (stored locally in .env as ${giteaEnvKey}, leave blank for public repos only)`,
3032
mask: true,
3133
});
3234
if (giteaToken.trim()) {

packages/setupWizard/src/github.ts

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { checkbox, input, password } from '@inquirer/prompts';
1+
import { input, password } from '@inquirer/prompts';
2+
import { tabCheckbox as checkbox } from './tabCheckbox.js';
23
import { select as searchSelect, Separator } from 'inquirer-select-pro';
34
import type { GithubConnectionConfig } from '@sourcebot/schemas/v3/github.type';
45
import type { CollectResult, EnvVars } from './utils.js';
5-
import { note, toEnvKey } from './utils.js';
6+
import { createSearchSelectContext, INPUT_THEME, note, toEnvKey } from './utils.js';
67

78
function githubApiBase(url: string): string {
89
try {
@@ -75,6 +76,7 @@ export async function collectGitHubConfig(connectionName: string): Promise<Colle
7576
const url = await input({
7677
message: 'GitHub URL',
7778
default: 'https://github.com',
79+
theme: INPUT_THEME,
7880
validate: (v) => {
7981
if (!v?.trim()) {
8082
return 'URL is required';
@@ -104,7 +106,7 @@ export async function collectGitHubConfig(connectionName: string): Promise<Colle
104106

105107
const tokenEnvKey = toEnvKey(connectionName, 'TOKEN');
106108
const token = await password({
107-
message: `GitHub Personal Access Token (stored as ${tokenEnvKey}, leave blank for public repos only)`,
109+
message: `GitHub Personal Access Token (stored locally in .env as ${tokenEnvKey}, leave blank for public repos only)`,
108110
mask: true,
109111
});
110112
if (token.trim()) {
@@ -125,18 +127,26 @@ export async function collectGitHubConfig(connectionName: string): Promise<Colle
125127
});
126128

127129
if (targets.includes('repos')) {
130+
const ctx = createSearchSelectContext();
128131
const repos = await searchSelect<string, true>({
129132
message: 'Repositories to index (type to search, or type owner/repo)',
130133
multiple: true,
131134
required: true,
132135
loop: false,
133136
clearInputWhenSelected: true,
134-
placeholder: 'Type 2+ characters to search...',
137+
theme: ctx.theme,
138+
placeholder: 'Type to search...',
135139
options: async (search) => {
136-
if (!search || search.length < 2) {
140+
ctx.trackSearch(search);
141+
if (!search) {
137142
return [];
138143
}
139-
return searchGitHub(apiBase, search, token, 'repo');
144+
ctx.setLoading(true);
145+
try {
146+
return await searchGitHub(apiBase, search, token, 'repo');
147+
} finally {
148+
ctx.setLoading(false);
149+
}
140150
},
141151
validate: (selected) => {
142152
for (const opt of selected) {
@@ -151,36 +161,52 @@ export async function collectGitHubConfig(connectionName: string): Promise<Colle
151161
}
152162

153163
if (targets.includes('orgs')) {
164+
const ctx = createSearchSelectContext();
154165
const orgs = await searchSelect<string, true>({
155166
message: 'Organizations to index (type to search)',
156167
multiple: true,
157168
required: true,
158169
loop: false,
159170
clearInputWhenSelected: true,
160-
placeholder: 'Type 2+ characters to search...',
171+
theme: ctx.theme,
172+
placeholder: 'Type to search...',
161173
options: async (search) => {
162-
if (!search || search.length < 2) {
174+
ctx.trackSearch(search);
175+
if (!search) {
163176
return [];
164177
}
165-
return searchGitHub(apiBase, search, token, 'org');
178+
ctx.setLoading(true);
179+
try {
180+
return await searchGitHub(apiBase, search, token, 'org');
181+
} finally {
182+
ctx.setLoading(false);
183+
}
166184
},
167185
});
168186
config.orgs = orgs;
169187
}
170188

171189
if (targets.includes('users')) {
190+
const ctx = createSearchSelectContext();
172191
const users = await searchSelect<string, true>({
173192
message: 'GitHub users to index (type to search)',
174193
multiple: true,
175194
required: true,
176195
loop: false,
177196
clearInputWhenSelected: true,
178-
placeholder: 'Type 2+ characters to search...',
197+
theme: ctx.theme,
198+
placeholder: 'Type to search...',
179199
options: async (search) => {
180-
if (!search || search.length < 2) {
200+
ctx.trackSearch(search);
201+
if (!search) {
181202
return [];
182203
}
183-
return searchGitHub(apiBase, search, token, 'user');
204+
ctx.setLoading(true);
205+
try {
206+
return await searchGitHub(apiBase, search, token, 'user');
207+
} finally {
208+
ctx.setLoading(false);
209+
}
184210
},
185211
});
186212
config.users = users;

packages/setupWizard/src/gitlab.ts

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { checkbox, input, password } from '@inquirer/prompts';
1+
import { input, password } from '@inquirer/prompts';
2+
import { tabCheckbox as checkbox } from './tabCheckbox.js';
23
import { select as searchSelect, Separator } from 'inquirer-select-pro';
34
import type { GitlabConnectionConfig } from '@sourcebot/schemas/v3/gitlab.type';
45
import type { CollectResult, EnvVars } from './utils.js';
5-
import { note, toEnvKey } from './utils.js';
6+
import { createSearchSelectContext, INPUT_THEME, note, toEnvKey } from './utils.js';
67

78
function gitlabApiBase(url: string): string {
89
try {
@@ -89,6 +90,7 @@ export async function collectGitLabConfig(connectionName: string): Promise<Colle
8990
const url = await input({
9091
message: 'GitLab URL',
9192
default: 'https://gitlab.com',
93+
theme: INPUT_THEME,
9294
validate: (v) => {
9395
if (!v?.trim()) {
9496
return 'URL is required';
@@ -114,7 +116,7 @@ export async function collectGitLabConfig(connectionName: string): Promise<Colle
114116

115117
const gitlabEnvKey = toEnvKey(connectionName, 'TOKEN');
116118
const gitlabToken = await password({
117-
message: `GitLab Personal Access Token (stored as ${gitlabEnvKey}, leave blank for public repos only)`,
119+
message: `GitLab Personal Access Token (stored locally in .env as ${gitlabEnvKey}, leave blank for public repos only)`,
118120
mask: true,
119121
});
120122
if (gitlabToken.trim()) {
@@ -143,36 +145,52 @@ export async function collectGitLabConfig(connectionName: string): Promise<Colle
143145
}
144146

145147
if (targets.includes('groups')) {
148+
const ctx = createSearchSelectContext();
146149
const groups = await searchSelect<string, true>({
147150
message: 'Groups to index (type to search)',
148151
multiple: true,
149152
required: true,
150153
loop: false,
151154
clearInputWhenSelected: true,
152-
placeholder: 'Type 2+ characters to search...',
155+
theme: ctx.theme,
156+
placeholder: 'Type to search...',
153157
options: async (search) => {
154-
if (!search || search.length < 2) {
158+
ctx.trackSearch(search);
159+
if (!search) {
155160
return [];
156161
}
157-
return searchGitLab(apiBase, search, gitlabToken, 'group');
162+
ctx.setLoading(true);
163+
try {
164+
return await searchGitLab(apiBase, search, gitlabToken, 'group');
165+
} finally {
166+
ctx.setLoading(false);
167+
}
158168
},
159169
});
160170
config.groups = groups;
161171
}
162172

163173
if (targets.includes('projects')) {
174+
const ctx = createSearchSelectContext();
164175
const projects = await searchSelect<string, true>({
165176
message: 'Projects to index (type to search, or type group/project)',
166177
multiple: true,
167178
required: true,
168179
loop: false,
169180
clearInputWhenSelected: true,
170-
placeholder: 'Type 2+ characters to search...',
181+
theme: ctx.theme,
182+
placeholder: 'Type to search...',
171183
options: async (search) => {
172-
if (!search || search.length < 2) {
184+
ctx.trackSearch(search);
185+
if (!search) {
173186
return [];
174187
}
175-
return searchGitLab(apiBase, search, gitlabToken, 'project');
188+
ctx.setLoading(true);
189+
try {
190+
return await searchGitLab(apiBase, search, gitlabToken, 'project');
191+
} finally {
192+
ctx.setLoading(false);
193+
}
176194
},
177195
validate: (selected) => {
178196
for (const opt of selected) {
@@ -187,18 +205,26 @@ export async function collectGitLabConfig(connectionName: string): Promise<Colle
187205
}
188206

189207
if (targets.includes('users')) {
208+
const ctx = createSearchSelectContext();
190209
const users = await searchSelect<string, true>({
191210
message: 'Users to index (type to search)',
192211
multiple: true,
193212
required: true,
194213
loop: false,
195214
clearInputWhenSelected: true,
196-
placeholder: 'Type 2+ characters to search...',
215+
theme: ctx.theme,
216+
placeholder: 'Type to search...',
197217
options: async (search) => {
198-
if (!search || search.length < 2) {
218+
ctx.trackSearch(search);
219+
if (!search) {
199220
return [];
200221
}
201-
return searchGitLab(apiBase, search, gitlabToken, 'user');
222+
ctx.setLoading(true);
223+
try {
224+
return await searchGitLab(apiBase, search, gitlabToken, 'user');
225+
} finally {
226+
ctx.setLoading(false);
227+
}
202228
},
203229
});
204230
config.users = users;

0 commit comments

Comments
 (0)