Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 3 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ services:
volumes:
- ./config.json:/data/config.json
- sourcebot_data:/data
env_file:
- path: .env
required: false
environment:
- CONFIG_PATH=/data/config.json
- AUTH_URL=${AUTH_URL:-http://localhost:3000}
- AUTH_SECRET=${AUTH_SECRET:-000000000000000000000000000000000} # CHANGEME: generate via `openssl rand -base64 33`
- SOURCEBOT_ENCRYPTION_KEY=${SOURCEBOT_ENCRYPTION_KEY:-000000000000000000000000000000000} # CHANGEME: generate via `openssl rand -base64 24`
- DATABASE_URL=${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/postgres} # CHANGEME
- REDIS_URL=${REDIS_URL:-redis://redis:6379} # CHANGEME
- SOURCEBOT_EE_LICENSE_KEY=${SOURCEBOT_EE_LICENSE_KEY:-}

# For the full list of environment variables see:
# https://docs.sourcebot.dev/docs/configuration/environment-variables
Expand Down
30 changes: 30 additions & 0 deletions packages/setupWizard/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "setup-sourcebot",
"version": "0.1.0",
"description": "CLI wizard for creating a Sourcebot configuration",
"type": "module",
"bin": "./dist/index.js",
"scripts": {
"build": "tsc",
"watch": "tsc --watch",
"dev": "tsx src/index.ts"
},
"dependencies": {
"@inquirer/prompts": "^8.4.3",
"@sourcebot/schemas": "workspace:^",
"chalk": "^5.6.2",
"inquirer-select-pro": "^1.0.0-alpha.9",
"ora": "^9.4.0"
},
"devDependencies": {
"@types/node": "^22.7.5",
"tsx": "^4.21.0",
"typescript": "^5.6.2"
},
"engines": {
"node": ">=18"
},
"files": [
"dist"
]
}
99 changes: 99 additions & 0 deletions packages/setupWizard/src/azuredevops.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { checkbox, confirm, input, password, select } from '@inquirer/prompts';
import type { AzureDevOpsConnectionConfig } from '@sourcebot/schemas/v3/azuredevops.type';
import type { CollectResult, EnvVars } from './utils.js';
import { multiInput, note, toEnvKey } from './utils.js';

export async function collectAzureDevOpsConfig(connectionName: string): Promise<CollectResult> {
const env: EnvVars = {};

const deploymentType = await select<'cloud' | 'server'>({
message: 'Which Azure DevOps deployment?',
choices: [
{ value: 'cloud', name: 'Azure DevOps Cloud', description: 'dev.azure.com' },
{ value: 'server', name: 'Azure DevOps Server', description: 'self-hosted' },
],
});

const config: AzureDevOpsConnectionConfig = {
type: 'azuredevops',
deploymentType,
token: { env: '' },
};

if (deploymentType === 'server') {
const url = await input({
message: 'Azure DevOps Server URL (e.g. https://ado.example.com)',
validate: (v) => {
if (!v?.trim()) {
return 'URL is required';
}
if (!/^https?:\/\//.test(v)) {
return 'Must start with http:// or https://';
}
return true;
},
});
config.url = url;

Comment on lines +24 to +37
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Harden server URL handling for token auth (http:// currently allowed).

Line 30 accepts plain HTTP, which can expose PATs in transit. Require HTTPS by default (or explicitly gate HTTP behind an insecure-confirmation step), and persist the trimmed URL.

🔐 Suggested patch
-        const url = await input({
+        const url = await input({
             message: 'Azure DevOps Server URL (e.g. https://ado.example.com)',
-            validate: (v) => {
-                if (!v?.trim()) {
+            validate: (raw) => {
+                const v = raw?.trim();
+                if (!v) {
                     return 'URL is required';
                 }
-                if (!/^https?:\/\//.test(v)) {
-                    return 'Must start with http:// or https://';
+                if (!/^https:\/\//.test(v)) {
+                    return 'Must start with https://';
                 }
                 return true;
             },
         });
-        config.url = url;
+        config.url = url.trim();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const url = await input({
message: 'Azure DevOps Server URL (e.g. https://ado.example.com)',
validate: (v) => {
if (!v?.trim()) {
return 'URL is required';
}
if (!/^https?:\/\//.test(v)) {
return 'Must start with http:// or https://';
}
return true;
},
});
config.url = url;
const url = await input({
message: 'Azure DevOps Server URL (e.g. https://ado.example.com)',
validate: (raw) => {
const v = raw?.trim();
if (!v) {
return 'URL is required';
}
if (!/^https:\/\//.test(v)) {
return 'Must start with https://';
}
return true;
},
});
config.url = url.trim();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/setupWizard/src/azuredevops.ts` around lines 24 - 37, The URL input
currently allows plain http and doesn't persist a trimmed value; update the
input validation and assignment around the prompt (the call to input(...) that
sets config.url) to require HTTPS by default by changing the validate function
to reject non-https schemes, trim the value before validation/assignment, and
only allow http if the user explicitly confirms an insecure choice (e.g., an
extra confirmation prompt when v startsWith 'http://'); finally assign the
trimmed URL to config.url instead of the raw input.

const useTfsPath = await confirm({
message: 'Use legacy TFS path format (/tfs in API URLs)?',
default: false,
});
if (useTfsPath) {
config.useTfsPath = true;
}
}

note(
[
'Create a Personal Access Token at:',
deploymentType === 'cloud'
? ' https://dev.azure.com/<your-org>/_usersSettings/tokens'
: ' <your-server-url>/_usersSettings/tokens',
'Grant `Code (Read)` scope so Sourcebot can find and clone your repos.',
].join('\n'),
'Azure DevOps Personal Access Token',
);

const envKey = toEnvKey(connectionName, 'TOKEN');
const token = await password({
message: `Azure DevOps Personal Access Token (stored as ${envKey})`,
mask: true,
validate: (v) => !v?.trim() ? 'Token is required' : true,
});
env[envKey] = token;
config.token = { env: envKey };

const orgLabel = deploymentType === 'cloud' ? 'organization' : 'collection';
const orgLabelPlural = deploymentType === 'cloud' ? 'Organizations' : 'Collections';

const targets = await checkbox<string>({
message: 'What do you want to index?',
choices: [
{ value: 'orgs', name: orgLabelPlural, description: `all projects in a ${orgLabel}` },
{ value: 'projects', name: 'Specific projects', description: `${orgLabel}/project format` },
{ value: 'repos', name: 'Specific repositories', description: `${orgLabel}/project/repo format` },
],
required: true,
});

if (targets.includes('orgs')) {
config.orgs = await multiInput({
message: `${orgLabelPlural} to index`,
});
}

if (targets.includes('projects')) {
config.projects = await multiInput({
message: `Projects to index (${orgLabel}/project)`,
});
}

if (targets.includes('repos')) {
config.repos = await multiInput({
message: `Repositories to index (${orgLabel}/project/repo)`,
});
}

return { connections: [{ config }], env };
}
235 changes: 235 additions & 0 deletions packages/setupWizard/src/bitbucket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import { checkbox, confirm, input, password, select } from '@inquirer/prompts';
import type { BitbucketConnectionConfig } from '@sourcebot/schemas/v3/bitbucket.type';
import type { CollectResult, EnvVars } from './utils.js';
import { multiInput, note, toEnvKey } from './utils.js';

export async function collectBitbucketConfig(connectionName: string): Promise<CollectResult> {
const env: EnvVars = {};

const deploymentType = await select<'cloud' | 'server'>({
message: 'Which Bitbucket deployment?',
choices: [
{ value: 'cloud', name: 'Bitbucket Cloud', description: 'bitbucket.org' },
{ value: 'server', name: 'Bitbucket Data Center', description: 'self-hosted' },
],
});

const config: BitbucketConnectionConfig = {
type: 'bitbucket',
deploymentType,
};

if (deploymentType === 'cloud') {
return collectBitbucketCloud(connectionName, config, env);
}
return collectBitbucketServer(connectionName, config, env);
}

async function collectBitbucketCloud(
connectionName: string,
config: BitbucketConnectionConfig,
env: EnvVars,
): Promise<CollectResult> {
const authMethod = await select<'api-token' | 'access-token' | 'app-password'>({
message: 'How will you authenticate?',
choices: [
{ value: 'api-token', name: 'API Token', description: 'Recommended by Atlassian' },
{ value: 'access-token', name: 'Access Token', description: 'Scoped to a repo, project, or workspace' },
{ value: 'app-password', name: 'App Password (deprecated)', description: 'Deprecated by Atlassian' },
],
});

if (authMethod === 'api-token') {
note(
'The email you use to sign in to Atlassian (e.g. you@example.com).',
'Atlassian account email',
);

const email = await input({
message: 'Atlassian account email',
validate: (v) => !v?.trim() ? 'Email is required' : true,
});
config.user = email;

note(
[
'Your Bitbucket username (separate from your Atlassian email).',
' Find it at: https://bitbucket.org/account/settings/',
].join('\n'),
'Bitbucket username',
);

const gitUser = await input({
message: 'Bitbucket username',
validate: (v) => !v?.trim() ? 'Username is required' : true,
});
config.gitUser = gitUser;

note(
[
'Create an API Token at:',
' https://id.atlassian.com/manage-profile/security/api-tokens',
'Click "Create API token with scopes", choose Bitbucket, and grant:',
' read:repository:bitbucket',
' read:workspace:bitbucket',
].join('\n'),
'Bitbucket Cloud API Token',
);

const tokenEnvKey = toEnvKey(connectionName, 'TOKEN');
const token = await password({
message: `API Token (stored as ${tokenEnvKey})`,
mask: true,
validate: (v) => !v?.trim() ? 'Token is required' : true,
});
env[tokenEnvKey] = token;
config.token = { env: tokenEnvKey };
} else if (authMethod === 'access-token') {
note(
[
'Create an Access Token scoped to a repo, project, or workspace.',
' https://support.atlassian.com/bitbucket-cloud/docs/access-tokens/',
].join('\n'),
'Create a Bitbucket Cloud Access Token',
);

const tokenEnvKey = toEnvKey(connectionName, 'TOKEN');
const token = await password({
message: `Access Token (stored as ${tokenEnvKey})`,
mask: true,
validate: (v) => !v?.trim() ? 'Token is required' : true,
});
env[tokenEnvKey] = token;
config.token = { env: tokenEnvKey };
} else {
note(
[
'⚠ App Passwords are deprecated. Prefer an API Token if possible.',
'',
'Create an App Password:',
' https://bitbucket.org/account/settings/app-passwords/new',
' Required permissions: Repositories (read), Workspaces (read)',
].join('\n'),
'Create a Bitbucket Cloud App Password',
);

const username = await input({
message: 'Bitbucket username',
validate: (v) => !v?.trim() ? 'Username is required' : true,
});
config.user = username;

const tokenEnvKey = toEnvKey(connectionName, 'APP_PASSWORD');
const token = await password({
message: `Bitbucket App Password (stored as ${tokenEnvKey})`,
mask: true,
validate: (v) => !v?.trim() ? 'App Password is required' : true,
});
env[tokenEnvKey] = token;
config.token = { env: tokenEnvKey };
}

const targets = await checkbox<string>({
message: 'What do you want to index?',
choices: [
{ value: 'workspaces', name: 'Workspaces', description: 'Index every repo each chosen workspace owns' },
{ value: 'repos', name: 'Specific repositories', description: 'Hand-pick individual repos to index' },
],
required: true,
});

if (targets.includes('workspaces')) {
config.workspaces = await multiInput({
message: 'Workspaces to index',
});
}

if (targets.includes('repos')) {
config.repos = await multiInput({
message: 'Repositories to index (workspace/repo)',
});
}

return { connections: [{ config }], env };
}

async function collectBitbucketServer(
connectionName: string,
config: BitbucketConnectionConfig,
env: EnvVars,
): Promise<CollectResult> {
const url = await input({
message: 'Bitbucket Data Center URL (e.g. https://bitbucket.example.com)',
validate: (v) => {
if (!v?.trim()) {
return 'URL is required';
}
if (!/^https?:\/\//.test(v)) {
return 'Must start with http:// or https://';
}
return true;
},
});
config.url = url;

Comment on lines +161 to +174
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bitbucket Server URL validation should not allow insecure HTTP by default.

Line 167 currently permits http://. Since this flow collects/stores access tokens, enforce HTTPS (or add explicit insecure opt-in) and normalize with trim() before storing.

🔐 Suggested patch
-    const url = await input({
+    const url = await input({
         message: 'Bitbucket Data Center URL (e.g. https://bitbucket.example.com)',
-        validate: (v) => {
-            if (!v?.trim()) {
+        validate: (raw) => {
+            const v = raw?.trim();
+            if (!v) {
                 return 'URL is required';
             }
-            if (!/^https?:\/\//.test(v)) {
-                return 'Must start with http:// or https://';
+            if (!/^https:\/\//.test(v)) {
+                return 'Must start with https://';
             }
             return true;
         },
     });
-    config.url = url;
+    config.url = url.trim();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/setupWizard/src/bitbucket.ts` around lines 161 - 174, The URL input
validation currently allows http and stores the raw value; update the validate
callback passed to input(...) to require HTTPS only (reject if it doesn't start
with https://) and normalize the saved value by trimming whitespace before
assigning to config.url (use the trimmed url variable). If you want to support
insecure HTTP as an opt-in, add an explicit flag (e.g., allowInsecure) or a
separate prompt rather than silently accepting http in the validate function;
locate the validate lambda and the assignment to config.url to make these
changes.

note(
[
'Create an HTTP Access Token:',
' Profile → Manage account → HTTP access tokens',
' Required permissions: Project read, Repository read',
'',
'Use a user-account token for cross-project access,',
'or a project/repository-scoped token for narrower access.',
].join('\n'),
'Create a Bitbucket Data Center HTTP Access Token',
);

const username = await input({
message: 'Bitbucket username (leave blank if using a project/repo-scoped token)',
});
if (username.trim()) {
config.user = username;
}

const tokenEnvKey = toEnvKey(connectionName, 'TOKEN');
const token = await password({
message: `Bitbucket HTTP Access Token (stored as ${tokenEnvKey})`,
mask: true,
validate: (v) => !v?.trim() ? 'Token is required' : true,
});
env[tokenEnvKey] = token;
config.token = { env: tokenEnvKey };

const indexAll = await confirm({
message: 'Index every repository visible to the token?',
default: false,
});

if (indexAll) {
config.all = true;
return { connections: [{ config }], env };
}

const targets = await checkbox<string>({
message: 'What do you want to index?',
choices: [
{ value: 'projects', name: 'Projects', description: 'Index every repo in each chosen project' },
{ value: 'repos', name: 'Specific repositories', description: 'Hand-pick individual repos to index' },
],
required: true,
});

if (targets.includes('projects')) {
config.projects = await multiInput({
message: 'Project keys to index (e.g. MYPROJ)',
});
}

if (targets.includes('repos')) {
config.repos = await multiInput({
message: 'Repositories to index (project/repo)',
});
}

return { connections: [{ config }], env };
}
Loading
Loading