diff --git a/docker-compose.yml b/docker-compose.yml
index 5e954067c..e0272ba35 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,6 +15,9 @@ 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}
@@ -22,7 +25,6 @@ services:
- 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
diff --git a/docs/docs/deployment/docker-compose.mdx b/docs/docs/deployment/docker-compose.mdx
index 15055a8cc..8362bbeda 100644
--- a/docs/docs/deployment/docker-compose.mdx
+++ b/docs/docs/deployment/docker-compose.mdx
@@ -2,59 +2,68 @@
title: "Docker Compose"
---
-This guide will walk you through deploying Sourcebot locally or on a VM using Docker Compose. We will use the [docker-compose.yml](https://github.com/sourcebot-dev/sourcebot/blob/main/docker-compose.yml) file from the [Sourcebot repository](https://github.com/sourcebot-dev/sourcebot). This is the simplest way to get started with Sourcebot.
+This guide will walk you through deploying Sourcebot locally or on a VM using [Docker Compose](https://github.com/sourcebot-dev/sourcebot/blob/main/docker-compose.yml). This is the simplest way to get started with Sourcebot.
If you are looking to deploy onto Kubernetes, see the [Kubernetes (Helm)](/docs/deployment/k8s) guide.
-## Get started
-
-
-
- - docker & docker compose. Use [Docker Desktop](https://www.docker.com/products/docker-desktop/) on Mac or Windows.
-
-
- Download the [docker-compose.yml](https://github.com/sourcebot-dev/sourcebot/blob/main/docker-compose.yml) file from the Sourcebot repository.
-
- ```bash wrap icon="terminal"
- curl -o docker-compose.yml https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/docker-compose.yml
- ```
-
-
-
-
- In the same directory as the `docker-compose.yml` file, create a [configuration file](/docs/configuration/config-file). The configuration file is a JSON file that configures Sourcebot's behaviour, including what repositories to index, language model providers, auth providers, and more.
-
- ```bash wrap icon="terminal" Create example config
- touch config.json
- echo '{
- "$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json",
- // Comments are supported.
- // This config creates a single connection to GitHub.com that
- // indexes the Sourcebot repository
- "connections": {
- "starter-connection": {
- "type": "github",
- "repos": [
- "sourcebot-dev/sourcebot"
- ]
- }
- }
- }' > config.json
- ```
-
-
-
- Update the secrets in the `docker-compose.yml` and then run Sourcebot using:
-
- ```bash wrap icon="terminal"
- docker compose up
- ```
-
-
-
- You're all set! Navigate to [http://localhost:3000](http://localhost:3000) to access your Sourcebot instance.
-
-
+## System requirements
+- RAM: Ensure your environment has at least 4GB of RAM. Insufficient memory can cause processes to crash.
+- Docker & Docker Compose: Make sure both are installed and up-to-date.
+- Node.js 18+: Required for the setup CLI
+
+## Option 1: Setup CLI
+
+The setup CLI will guide you through configuring your Sourcebot instance to connect to your code hosts and LLM providers. From a empty folder, run the following command:
+
+```
+npx setup-sourcebot
+```
+
+
+
+
+
+## Option 2: Manual steps
+
+### Obtain the Docker Compose file
+
+Download the [docker-compose.yml](https://github.com/sourcebot-dev/sourcebot/blob/main/docker-compose.yml) file from the Sourcebot repository.
+
+```bash wrap icon="terminal"
+curl -o docker-compose.yml https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/docker-compose.yml
+```
+
+### Create a config.json
+
+In the same directory as the `docker-compose.yml` file, create a [configuration file](/docs/configuration/config-file). The configuration file is a JSON file that configures Sourcebot's behaviour, including what repositories to index, language model providers, auth providers, and more.
+
+```bash wrap icon="terminal" Create example config
+touch config.json
+echo '{
+ "$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json",
+ // Comments are supported.
+ // This config creates a single connection to GitHub.com that
+ // indexes the Sourcebot repository
+ "connections": {
+ "starter-connection": {
+ "type": "github",
+ "repos": [
+ "sourcebot-dev/sourcebot"
+ ]
+ }
+ }
+}' > config.json
+```
+
+### Launch your instance
+
+Update the secrets in the `docker-compose.yml` and then run Sourcebot using:
+
+```bash wrap icon="terminal"
+docker compose up
+```
+
+Navigate to [http://localhost:3000](http://localhost:3000) to access your Sourcebot instance.
## Next steps
diff --git a/docs/images/setup_sourcebot_splash.png b/docs/images/setup_sourcebot_splash.png
new file mode 100644
index 000000000..fadd9cc5b
Binary files /dev/null and b/docs/images/setup_sourcebot_splash.png differ
diff --git a/packages/setupWizard/README.md b/packages/setupWizard/README.md
new file mode 100644
index 000000000..ddd2be912
--- /dev/null
+++ b/packages/setupWizard/README.md
@@ -0,0 +1,25 @@
+# setup-sourcebot
+
+Interactive CLI wizard for setting up a self-hosted [Sourcebot](https://sourcebot.dev) instance.
+
+## Usage
+
+Run from an empty directory:
+
+```bash
+npx setup-sourcebot
+```
+
+The wizard walks you through:
+
+- **Code hosts** — GitHub, GitLab, Bitbucket (Cloud or Data Center), Azure DevOps (Cloud or Server), Gitea, Gerrit, a local folder of cloned repos, or any other git URL.
+- **AI providers** (optional) — Anthropic, OpenAI, Google Gemini, Google Vertex, DeepSeek, Mistral, xAI, OpenRouter, OpenAI-compatible endpoints, Amazon Bedrock, or Azure OpenAI. Powers [Ask](https://docs.sourcebot.dev/docs/features/ask/overview).
+
+## Requirements
+
+- Node.js 18+
+- Docker and Docker Compose
+
+## Docs
+
+Full deployment guide: [docs.sourcebot.dev/docs/deployment/docker-compose](https://docs.sourcebot.dev/docs/deployment/docker-compose)
diff --git a/packages/setupWizard/package.json b/packages/setupWizard/package.json
new file mode 100644
index 000000000..0209ae3ba
--- /dev/null
+++ b/packages/setupWizard/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "setup-sourcebot",
+ "version": "0.1.1",
+ "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",
+ "prepublishOnly": "yarn build"
+ },
+ "dependencies": {
+ "@inquirer/prompts": "^8.4.3",
+ "chalk": "^5.6.2",
+ "inquirer-select-pro": "^1.0.0-alpha.9",
+ "ora": "^9.4.0"
+ },
+ "devDependencies": {
+ "@sourcebot/schemas": "workspace:^",
+ "@types/node": "^22.7.5",
+ "tsx": "^4.21.0",
+ "typescript": "^5.6.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "files": [
+ "dist",
+ "README.md"
+ ]
+}
diff --git a/packages/setupWizard/src/azuredevops.ts b/packages/setupWizard/src/azuredevops.ts
new file mode 100644
index 000000000..085b77d67
--- /dev/null
+++ b/packages/setupWizard/src/azuredevops.ts
@@ -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 {
+ 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;
+
+ 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//_usersSettings/tokens'
+ : ' /_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({
+ 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 };
+}
diff --git a/packages/setupWizard/src/bitbucket.ts b/packages/setupWizard/src/bitbucket.ts
new file mode 100644
index 000000000..83035dc16
--- /dev/null
+++ b/packages/setupWizard/src/bitbucket.ts
@@ -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 {
+ 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 {
+ 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({
+ 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 {
+ 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;
+
+ 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({
+ 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 };
+}
diff --git a/packages/setupWizard/src/genericGit.ts b/packages/setupWizard/src/genericGit.ts
new file mode 100644
index 000000000..5f636220e
--- /dev/null
+++ b/packages/setupWizard/src/genericGit.ts
@@ -0,0 +1,25 @@
+import { input } from '@inquirer/prompts';
+import type { GenericGitHostConnectionConfig } from '@sourcebot/schemas/v3/genericGitHost.type';
+import type { CollectResult } from './utils.js';
+
+export async function collectGenericGitConfig(): Promise {
+ const url = await input({
+ message: 'Git clone URL (e.g. https://github.com/sourcebot-dev/sourcebot)',
+ validate: (v) => {
+ if (!v?.trim()) {
+ return 'URL is required';
+ }
+ if (!/^https?:\/\//.test(v)) {
+ return 'Must start with http:// or https://';
+ }
+ return true;
+ },
+ });
+
+ const config: GenericGitHostConnectionConfig = {
+ type: 'git',
+ url,
+ };
+
+ return { connections: [{ config }], env: {} };
+}
diff --git a/packages/setupWizard/src/gerrit.ts b/packages/setupWizard/src/gerrit.ts
new file mode 100644
index 000000000..51ccd97e0
--- /dev/null
+++ b/packages/setupWizard/src/gerrit.ts
@@ -0,0 +1,37 @@
+import { confirm, input } from '@inquirer/prompts';
+import type { GerritConnectionConfig } from '@sourcebot/schemas/v3/gerrit.type';
+import type { CollectResult } from './utils.js';
+import { multiInput } from './utils.js';
+
+export async function collectGerritConfig(): Promise {
+ const url = await input({
+ message: 'Gerrit URL (e.g. https://gerrit.example.com)',
+ validate: (v) => {
+ if (!v?.trim()) {
+ return 'URL is required';
+ }
+ if (!/^https?:\/\//.test(v)) {
+ return 'Must start with http:// or https://';
+ }
+ return true;
+ },
+ });
+
+ const config: GerritConnectionConfig = {
+ type: 'gerrit',
+ url,
+ };
+
+ const indexAll = await confirm({
+ message: 'Index all projects?',
+ default: true,
+ });
+
+ if (!indexAll) {
+ config.projects = await multiInput({
+ message: 'Projects to index',
+ });
+ }
+
+ return { connections: [{ config }], env: {} };
+}
diff --git a/packages/setupWizard/src/gitea.ts b/packages/setupWizard/src/gitea.ts
new file mode 100644
index 000000000..08d716a5e
--- /dev/null
+++ b/packages/setupWizard/src/gitea.ts
@@ -0,0 +1,66 @@
+import { checkbox, input, password } from '@inquirer/prompts';
+import type { GiteaConnectionConfig } from '@sourcebot/schemas/v3/gitea.type';
+import type { CollectResult, EnvVars } from './utils.js';
+import { multiInput, toEnvKey } from './utils.js';
+
+export async function collectGiteaConfig(connectionName: string): Promise {
+ const env: EnvVars = {};
+ const config: GiteaConnectionConfig = { type: 'gitea' };
+
+ const url = await input({
+ message: 'Gitea URL',
+ default: 'https://gitea.com',
+ validate: (v) => {
+ if (!v?.trim()) {
+ return 'URL is required';
+ }
+ if (!/^https?:\/\//.test(v)) {
+ return 'Must start with http:// or https://';
+ }
+ return true;
+ },
+ });
+ if (url !== 'https://gitea.com') {
+ config.url = url;
+ }
+
+ const giteaEnvKey = toEnvKey(connectionName, 'TOKEN');
+ const giteaToken = await password({
+ message: `Gitea Access Token (stored as ${giteaEnvKey}, leave blank for public repos only)`,
+ mask: true,
+ });
+ if (giteaToken.trim()) {
+ env[giteaEnvKey] = giteaToken;
+ config.token = { env: giteaEnvKey };
+ }
+
+ const targets = await checkbox({
+ message: 'What do you want to index?',
+ choices: [
+ { value: 'orgs', name: 'Organizations' },
+ { value: 'repos', name: 'Specific repositories', description: 'owner/repo format' },
+ { value: 'users', name: 'Users' },
+ ],
+ required: true,
+ });
+
+ if (targets.includes('orgs')) {
+ config.orgs = await multiInput({
+ message: 'Organizations to index',
+ });
+ }
+
+ if (targets.includes('repos')) {
+ config.repos = await multiInput({
+ message: 'Repositories to index (owner/repo)',
+ });
+ }
+
+ if (targets.includes('users')) {
+ config.users = await multiInput({
+ message: 'Users to index',
+ });
+ }
+
+ return { connections: [{ config }], env };
+}
diff --git a/packages/setupWizard/src/github.ts b/packages/setupWizard/src/github.ts
new file mode 100644
index 000000000..73527c865
--- /dev/null
+++ b/packages/setupWizard/src/github.ts
@@ -0,0 +1,190 @@
+import { checkbox, input, password } from '@inquirer/prompts';
+import { select as searchSelect, Separator } from 'inquirer-select-pro';
+import type { GithubConnectionConfig } from '@sourcebot/schemas/v3/github.type';
+import type { CollectResult, EnvVars } from './utils.js';
+import { note, toEnvKey } from './utils.js';
+
+function githubApiBase(url: string): string {
+ try {
+ const u = new URL(url);
+ if (u.hostname === 'github.com') {
+ return 'https://api.github.com';
+ }
+ return `${u.protocol}//${u.hostname}/api/v3`;
+ } catch {
+ return 'https://api.github.com';
+ }
+}
+
+type SearchOption = { name: string; value: string };
+type GitHubSearchType = 'org' | 'user' | 'repo';
+const githubSearchCache = new Map>();
+const REPO_PATTERN = /^[\w.-]+\/[\w.-]+$/;
+
+async function searchGitHub(
+ apiBase: string,
+ query: string,
+ token: string,
+ type: GitHubSearchType,
+): Promise> {
+ const cacheKey = `${apiBase}|${type}|${query}`;
+ const cached = githubSearchCache.get(cacheKey);
+ if (cached) {
+ return cached;
+ }
+
+ const headers: Record = {
+ 'User-Agent': 'setup-sourcebot',
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
+ };
+ const url = type === 'repo'
+ ? `${apiBase}/search/repositories?q=${encodeURIComponent(query)}&per_page=8`
+ : `${apiBase}/search/users?q=${encodeURIComponent(query)}+type:${type}&per_page=8`;
+ const res = await fetch(url, { headers });
+ const data = await res.json() as { items?: Array<{ login?: string; full_name?: string }> };
+
+ const literalFallback = (): SearchOption | null => {
+ return { name: query, value: query };
+ };
+
+ if (!res.ok) {
+ const warning =
+ (res.status === 403 && res.headers.get('x-ratelimit-remaining') === '0')
+ ? '⚠ Autocomplete disabled — GitHub rate limit exceeded.'
+ : '⚠ Autocomplete disabled — authentication failed, check your PAT.';
+ const fallback = literalFallback();
+ return fallback ? [fallback, new Separator(warning)] : [new Separator(warning)];
+ }
+
+ const results: SearchOption[] = (data.items ?? []).map((item) => {
+ const value = type === 'repo' ? item.full_name! : item.login!;
+ return { name: value, value };
+ });
+ if (results.length === 0) {
+ const fallback = literalFallback();
+ return fallback ? [fallback] : [];
+ }
+ githubSearchCache.set(cacheKey, results);
+ return results;
+}
+
+export async function collectGitHubConfig(connectionName: string): Promise {
+ const env: EnvVars = {};
+ const config: GithubConnectionConfig = { type: 'github' };
+
+ const url = await input({
+ message: 'GitHub URL',
+ default: 'https://github.com',
+ validate: (v) => {
+ if (!v?.trim()) {
+ return 'URL is required';
+ }
+ if (!/^https?:\/\//.test(v)) {
+ return 'Must start with http:// or https://';
+ }
+ return true;
+ },
+ });
+ if (url !== 'https://github.com') {
+ config.url = url;
+ }
+
+ note(
+ [
+ 'Fine-grained PAT (recommended):',
+ ` ${url}/settings/personal-access-tokens/new`,
+ ' Required permissions: Contents (read), Metadata (read)',
+ '',
+ 'Classic PAT:',
+ ` ${url}/settings/tokens/new`,
+ ' Required scope: repo',
+ ].join('\n'),
+ 'Create a GitHub Personal Access Token',
+ );
+
+ const tokenEnvKey = toEnvKey(connectionName, 'TOKEN');
+ const token = await password({
+ message: `GitHub Personal Access Token (stored as ${tokenEnvKey}, leave blank for public repos only)`,
+ mask: true,
+ });
+ if (token.trim()) {
+ env[tokenEnvKey] = token;
+ config.token = { env: tokenEnvKey };
+ }
+
+ const apiBase = githubApiBase(url);
+
+ const targets = await checkbox({
+ message: 'What do you want to index?',
+ choices: [
+ { value: 'repos', name: 'Specific repositories', description: 'Hand-pick individual repos to index' },
+ { value: 'orgs', name: 'Organizations', description: 'Index every repo each chosen org owns' },
+ { value: 'users', name: 'Users', description: 'Index every repo each chosen user owns' },
+ ],
+ required: true,
+ });
+
+ if (targets.includes('repos')) {
+ const repos = await searchSelect({
+ message: 'Repositories to index (type to search, or type owner/repo)',
+ multiple: true,
+ required: true,
+ loop: false,
+ clearInputWhenSelected: true,
+ placeholder: 'Type 2+ characters to search...',
+ options: async (search) => {
+ if (!search || search.length < 2) {
+ return [];
+ }
+ return searchGitHub(apiBase, search, token, 'repo');
+ },
+ validate: (selected) => {
+ for (const opt of selected) {
+ if (!REPO_PATTERN.test(opt.value)) {
+ return `Invalid format: "${opt.value}" — expected owner/repo`;
+ }
+ }
+ return true;
+ },
+ });
+ config.repos = repos;
+ }
+
+ if (targets.includes('orgs')) {
+ const orgs = await searchSelect({
+ message: 'Organizations to index (type to search)',
+ multiple: true,
+ required: true,
+ loop: false,
+ clearInputWhenSelected: true,
+ placeholder: 'Type 2+ characters to search...',
+ options: async (search) => {
+ if (!search || search.length < 2) {
+ return [];
+ }
+ return searchGitHub(apiBase, search, token, 'org');
+ },
+ });
+ config.orgs = orgs;
+ }
+
+ if (targets.includes('users')) {
+ const users = await searchSelect({
+ message: 'GitHub users to index (type to search)',
+ multiple: true,
+ required: true,
+ loop: false,
+ clearInputWhenSelected: true,
+ placeholder: 'Type 2+ characters to search...',
+ options: async (search) => {
+ if (!search || search.length < 2) {
+ return [];
+ }
+ return searchGitHub(apiBase, search, token, 'user');
+ },
+ });
+ config.users = users;
+ }
+
+ return { connections: [{ config }], env };
+}
diff --git a/packages/setupWizard/src/gitlab.ts b/packages/setupWizard/src/gitlab.ts
new file mode 100644
index 000000000..f50e5b5cd
--- /dev/null
+++ b/packages/setupWizard/src/gitlab.ts
@@ -0,0 +1,208 @@
+import { checkbox, input, password } from '@inquirer/prompts';
+import { select as searchSelect, Separator } from 'inquirer-select-pro';
+import type { GitlabConnectionConfig } from '@sourcebot/schemas/v3/gitlab.type';
+import type { CollectResult, EnvVars } from './utils.js';
+import { note, toEnvKey } from './utils.js';
+
+function gitlabApiBase(url: string): string {
+ try {
+ const u = new URL(url);
+ return `${u.protocol}//${u.host}/api/v4`;
+ } catch {
+ return 'https://gitlab.com/api/v4';
+ }
+}
+
+type SearchOption = { name: string; value: string };
+type GitLabSearchType = 'group' | 'project' | 'user';
+const gitlabSearchCache = new Map>();
+const PROJECT_PATTERN = /^[\w.-]+(\/[\w.-]+)+$/;
+
+async function searchGitLab(
+ apiBase: string,
+ query: string,
+ token: string,
+ type: GitLabSearchType,
+): Promise> {
+ const cacheKey = `${apiBase}|${type}|${query}`;
+ const cached = gitlabSearchCache.get(cacheKey);
+ if (cached) {
+ return cached;
+ }
+
+ const headers: Record = {
+ 'User-Agent': 'setup-sourcebot',
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
+ };
+
+ const endpoint = type === 'group' ? 'groups' : type === 'project' ? 'projects' : 'users';
+ const extraParams = type === 'project' ? '&simple=true' : '';
+ const url = `${apiBase}/${endpoint}?search=${encodeURIComponent(query)}&per_page=8${extraParams}`;
+ const res = await fetch(url, { headers });
+
+ const literalFallback = (): SearchOption | null => {
+ if (type === 'project') {
+ return PROJECT_PATTERN.test(query) ? { name: query, value: query } : null;
+ }
+ return { name: query, value: query };
+ };
+
+ if (!res.ok) {
+ const warning = res.status === 401
+ ? '⚠ Autocomplete disabled — authentication failed, check your PAT.'
+ : `⚠ Autocomplete disabled — GitLab API error (${res.status}).`;
+ const fallback = literalFallback();
+ return fallback ? [fallback, new Separator(warning)] : [new Separator(warning)];
+ }
+
+ const data = await res.json() as Array<{
+ full_path?: string;
+ path_with_namespace?: string;
+ username?: string;
+ }>;
+
+ const results: SearchOption[] = data.map((item) => {
+ let value: string;
+ if (type === 'group') {
+ value = item.full_path!;
+ } else if (type === 'project') {
+ value = item.path_with_namespace!;
+ } else {
+ value = item.username!;
+ }
+ return { name: value, value };
+ });
+
+ if (results.length === 0) {
+ const fallback = literalFallback();
+ return fallback ? [fallback] : [];
+ }
+
+ gitlabSearchCache.set(cacheKey, results);
+ return results;
+}
+
+export async function collectGitLabConfig(connectionName: string): Promise {
+ const env: EnvVars = {};
+ const config: GitlabConnectionConfig = { type: 'gitlab' };
+
+ const url = await input({
+ message: 'GitLab URL',
+ default: 'https://gitlab.com',
+ validate: (v) => {
+ if (!v?.trim()) {
+ return 'URL is required';
+ }
+ if (!/^https?:\/\//.test(v)) {
+ return 'Must start with http:// or https://';
+ }
+ return true;
+ },
+ });
+ if (url !== 'https://gitlab.com') {
+ config.url = url;
+ }
+
+ note(
+ [
+ 'Create a PAT:',
+ ` ${url}/-/user_settings/personal_access_tokens`,
+ ' Required scope: read_api',
+ ].join('\n'),
+ 'Create a GitLab Personal Access Token',
+ );
+
+ const gitlabEnvKey = toEnvKey(connectionName, 'TOKEN');
+ const gitlabToken = await password({
+ message: `GitLab Personal Access Token (stored as ${gitlabEnvKey}, leave blank for public repos only)`,
+ mask: true,
+ });
+ if (gitlabToken.trim()) {
+ env[gitlabEnvKey] = gitlabToken;
+ config.token = { env: gitlabEnvKey };
+ }
+
+ const apiBase = gitlabApiBase(url);
+ const isSelfHosted = url !== 'https://gitlab.com';
+
+ const targets = await checkbox({
+ message: 'What do you want to index?',
+ choices: [
+ ...(isSelfHosted
+ ? [{ value: 'all', name: 'Everything', description: 'Index every project visible to the token on this self-hosted instance' }]
+ : []),
+ { value: 'groups', name: 'Groups', description: 'Index every project each chosen group owns' },
+ { value: 'projects', name: 'Specific projects', description: 'Hand-pick individual projects to index' },
+ { value: 'users', name: 'Users', description: 'Index every project each chosen user owns' },
+ ],
+ required: true,
+ });
+
+ if (targets.includes('all')) {
+ config.all = true;
+ }
+
+ if (targets.includes('groups')) {
+ const groups = await searchSelect({
+ message: 'Groups to index (type to search)',
+ multiple: true,
+ required: true,
+ loop: false,
+ clearInputWhenSelected: true,
+ placeholder: 'Type 2+ characters to search...',
+ options: async (search) => {
+ if (!search || search.length < 2) {
+ return [];
+ }
+ return searchGitLab(apiBase, search, gitlabToken, 'group');
+ },
+ });
+ config.groups = groups;
+ }
+
+ if (targets.includes('projects')) {
+ const projects = await searchSelect({
+ message: 'Projects to index (type to search, or type group/project)',
+ multiple: true,
+ required: true,
+ loop: false,
+ clearInputWhenSelected: true,
+ placeholder: 'Type 2+ characters to search...',
+ options: async (search) => {
+ if (!search || search.length < 2) {
+ return [];
+ }
+ return searchGitLab(apiBase, search, gitlabToken, 'project');
+ },
+ validate: (selected) => {
+ for (const opt of selected) {
+ if (!PROJECT_PATTERN.test(opt.value)) {
+ return `Invalid format: "${opt.value}" — expected group/project`;
+ }
+ }
+ return true;
+ },
+ });
+ config.projects = projects;
+ }
+
+ if (targets.includes('users')) {
+ const users = await searchSelect({
+ message: 'Users to index (type to search)',
+ multiple: true,
+ required: true,
+ loop: false,
+ clearInputWhenSelected: true,
+ placeholder: 'Type 2+ characters to search...',
+ options: async (search) => {
+ if (!search || search.length < 2) {
+ return [];
+ }
+ return searchGitLab(apiBase, search, gitlabToken, 'user');
+ },
+ });
+ config.users = users;
+ }
+
+ return { connections: [{ config }], env };
+}
diff --git a/packages/setupWizard/src/index.ts b/packages/setupWizard/src/index.ts
new file mode 100644
index 000000000..6c349e3d1
--- /dev/null
+++ b/packages/setupWizard/src/index.ts
@@ -0,0 +1,334 @@
+#!/usr/bin/env node
+import { confirm, select } from '@inquirer/prompts';
+import chalk from 'chalk';
+import ora from 'ora';
+import { spawn } from 'node:child_process';
+import { existsSync, writeFileSync } from 'fs';
+import { writeFile } from 'fs/promises';
+import { collectAzureDevOpsConfig } from './azuredevops.js';
+import { collectBitbucketConfig } from './bitbucket.js';
+import { collectGenericGitConfig } from './genericGit.js';
+import { collectGerritConfig } from './gerrit.js';
+import { collectGiteaConfig } from './gitea.js';
+import { collectGitHubConfig } from './github.js';
+import { collectGitLabConfig } from './gitlab.js';
+import { collectLocalReposConfig } from './localRepos.js';
+import { collectModels, PROVIDER_ENV_KEYS } from './models.js';
+import {
+ type CollectResult,
+ type ConnectionConfig,
+ type EnvVars,
+ generateConnectionName,
+ generateSecret,
+ note,
+} from './utils.js';
+
+// @nocheckin: change this to main
+const DOCKER_COMPOSE_BRANCH = 'v5';
+const DOCKER_COMPOSE_URL = `https://raw.githubusercontent.com/sourcebot-dev/sourcebot/${DOCKER_COMPOSE_BRANCH}/docker-compose.yml`;
+
+const SOURCEBOT_URL = 'http://localhost:3000';
+
+function openBrowser(url: string): void {
+ const cmd = process.platform === 'darwin' ? 'open'
+ : process.platform === 'win32' ? 'cmd'
+ : 'xdg-open';
+ const args = process.platform === 'win32' ? ['/c', 'start', '""', url] : [url];
+ spawn(cmd, args, { stdio: 'ignore', detached: true }).unref();
+}
+
+async function openBrowserWhenReady(url: string, timeoutMs = 120_000): Promise {
+ const start = Date.now();
+ while (Date.now() - start < timeoutMs) {
+ try {
+ const res = await fetch(url, { signal: AbortSignal.timeout(2000) });
+ if (res.status < 500) {
+ openBrowser(url);
+ return;
+ }
+ } catch {
+ // not yet ready
+ }
+ await new Promise((r) => setTimeout(r, 2000));
+ }
+}
+
+const PLATFORM_LABELS: Record = {
+ github: 'GitHub',
+ gitlab: 'GitLab',
+ bitbucket: 'Bitbucket',
+ gitea: 'Gitea',
+ azuredevops: 'Azure DevOps',
+ gerrit: 'Gerrit',
+ local: 'Local Git repositories',
+ git: 'Other Git host',
+};
+
+async function main() {
+ console.log(String.raw`
+███████╗ ██████╗ ██╗ ██╗██████╗ ██████╗███████╗██████╗ ██████╗ ████████╗
+██╔════╝██╔═══██╗██║ ██║██╔══██╗██╔════╝██╔════╝██╔══██╗██╔═══██╗╚══██╔══╝
+███████╗██║ ██║██║ ██║██████╔╝██║ █████╗ ██████╔╝██║ ██║ ██║
+╚════██║██║ ██║██║ ██║██╔══██╗██║ ██╔══╝ ██╔══██╗██║ ██║ ██║
+███████║╚██████╔╝╚██████╔╝██║ ██║╚██████╗███████╗██████╔╝╚██████╔╝ ██║██╗
+╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝╚═════╝ ╚═════╝ ╚═╝╚═╝
+`);
+
+ const connections: Record = {};
+ const allEnv: EnvVars = {};
+ const localRepoIndex = new Map();
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const platform = await select({
+ message: 'Which code host do you want to connect?',
+ loop: false,
+ choices: [
+ { value: 'github', name: 'GitHub', description: 'github.com, GitHub Enterprise Server, or GitHub Enterprise Cloud' },
+ { value: 'gitlab', name: 'GitLab', description: 'gitlab.com, GitLab Self Managed, or GitLab Dedicated' },
+ { value: 'local', name: 'Local git repositories', description: 'git repositories in a local directory' },
+ { value: 'git', name: 'Remote git repository', description: 'Arbitrary git URL' },
+ { value: 'azuredevops', name: 'Azure DevOps', description: 'dev.azure.com or Azure Devops Server' },
+ { value: 'bitbucket', name: 'Bitbucket', description: 'Bitbucket Cloud or Bitbucket Data Center' },
+ { value: 'gitea', name: 'Gitea', description: 'Gitea Cloud or Gitea self-hosted' },
+ { value: 'gerrit', name: 'Gerrit' },
+ ],
+ });
+
+ const connectionName = generateConnectionName(platform, connections);
+
+ note(`Configuring ${PLATFORM_LABELS[platform] ?? platform}`, connectionName);
+
+ let result: CollectResult;
+
+ switch (platform) {
+ case 'github':
+ result = await collectGitHubConfig(connectionName);
+ break;
+ case 'gitlab':
+ result = await collectGitLabConfig(connectionName);
+ break;
+ case 'bitbucket':
+ result = await collectBitbucketConfig(connectionName);
+ break;
+ case 'gitea':
+ result = await collectGiteaConfig(connectionName);
+ break;
+ case 'azuredevops':
+ result = await collectAzureDevOpsConfig(connectionName);
+ break;
+ case 'gerrit':
+ result = await collectGerritConfig();
+ break;
+ case 'local':
+ result = await collectLocalReposConfig(localRepoIndex);
+ break;
+ case 'git':
+ result = await collectGenericGitConfig();
+ break;
+ default:
+ continue;
+ }
+
+ for (const { name, config } of result.connections) {
+ const finalName = name
+ ? generateConnectionName(name, connections)
+ : connectionName;
+ connections[finalName] = config;
+ }
+ Object.assign(allEnv, result.env);
+
+ const addAnother = await confirm({
+ message: 'Add another code host?',
+ default: false,
+ });
+
+ if (!addAnother) {
+ break;
+ }
+ }
+
+ const { models, env: modelEnv } = await collectModels();
+ Object.assign(allEnv, modelEnv);
+
+ if (existsSync('config.json')) {
+ const overwrite = await confirm({
+ message: 'config.json already exists. Overwrite?',
+ default: true,
+ });
+ if (!overwrite) {
+ console.log();
+ console.log(chalk.red('✗ ') + 'config.json was not overwritten.');
+ process.exit(0);
+ }
+ }
+
+ if (existsSync('.env')) {
+ const overwrite = await confirm({
+ message: '.env already exists. Overwrite?',
+ default: true,
+ });
+ if (!overwrite) {
+ console.log();
+ console.log(chalk.red('✗ ') + '.env was not overwritten.');
+ process.exit(0);
+ }
+ }
+
+ if (localRepoIndex.size > 0 && existsSync('docker-compose.override.yml')) {
+ const overwrite = await confirm({
+ message: 'docker-compose.override.yml already exists. Overwrite?',
+ default: true,
+ });
+ if (!overwrite) {
+ console.log();
+ console.log(chalk.red('✗ ') + 'docker-compose.override.yml was not overwritten.');
+ process.exit(0);
+ }
+ }
+
+ const s = ora('Writing configuration files...').start();
+
+ const configOutput: Record = {
+ $schema: 'https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json',
+ connections,
+ };
+ if (models.length > 0) {
+ configOutput.models = models;
+ }
+ const configJson = JSON.stringify(configOutput, null, 4);
+
+ const connectionEnv = Object.fromEntries(
+ Object.entries(allEnv).filter(([k]) => !Object.values(PROVIDER_ENV_KEYS).includes(k) && !['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY'].includes(k))
+ );
+ const aiEnv = Object.fromEntries(
+ Object.entries(allEnv).filter(([k]) => Object.values(PROVIDER_ENV_KEYS).includes(k) || ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY'].includes(k))
+ );
+
+ const envLines: string[] = [
+ '# Generated by setup-sourcebot',
+ '',
+ '# Auto-generated secrets — do not change after first run',
+ `AUTH_SECRET=${generateSecret(33)}`,
+ `SOURCEBOT_ENCRYPTION_KEY=${generateSecret(24)}`,
+ ];
+
+ if (Object.keys(connectionEnv).length > 0) {
+ envLines.push('', '# Code host credentials');
+ for (const [key, value] of Object.entries(connectionEnv)) {
+ envLines.push(`${key}=${value}`);
+ }
+ }
+
+ if (Object.keys(aiEnv).length > 0) {
+ envLines.push('', '# AI provider credentials');
+ for (const [key, value] of Object.entries(aiEnv)) {
+ envLines.push(`${key}=${value}`);
+ }
+ }
+
+ writeFileSync('config.json', configJson + '\n');
+ writeFileSync('.env', envLines.join('\n') + '\n');
+
+ const writtenFiles = ['config.json', '.env'];
+
+ if (localRepoIndex.size > 0) {
+ const mounts = [...localRepoIndex.entries()]
+ .sort((a, b) => a[1] - b[1])
+ .map(([p, i]) => ` - ${p}:/repos/${i}:ro`);
+ const overrideYaml = [
+ '# Generated by setup-sourcebot',
+ '# Merged with docker-compose.yml at `docker compose up` time.',
+ 'services:',
+ ' sourcebot:',
+ ' volumes:',
+ ...mounts,
+ '',
+ ].join('\n');
+ writeFileSync('docker-compose.override.yml', overrideYaml);
+ writtenFiles.push('docker-compose.override.yml');
+ }
+
+ s.succeed(`Wrote ${writtenFiles.join(', ')}`);
+
+ let downloadedCompose = false;
+
+ if (!existsSync('docker-compose.yml')) {
+ const download = await confirm({
+ message: 'Download docker-compose.yml?',
+ default: true,
+ });
+
+ if (download) {
+ const ds = ora('Downloading docker-compose.yml...').start();
+ try {
+ const res = await fetch(DOCKER_COMPOSE_URL);
+ if (!res.ok) {
+ throw new Error(`HTTP ${res.status}`);
+ }
+ await writeFile('docker-compose.yml', await res.text());
+ ds.succeed('Downloaded docker-compose.yml');
+ downloadedCompose = true;
+ } catch {
+ ds.fail('Download failed — you can get it manually (see next steps)');
+ }
+ }
+ } else {
+ downloadedCompose = true;
+ }
+
+ console.log();
+ console.log(chalk.green('✓ ') + chalk.bold('Your Sourcebot configuration is ready!'));
+
+ if (downloadedCompose) {
+ const startNow = await confirm({
+ message: 'Start Sourcebot now? (runs `docker compose up`)',
+ default: true,
+ });
+
+ if (startNow) {
+ note(
+ `Sourcebot will open at ${SOURCEBOT_URL} once it's ready.\nPress Ctrl+C to stop.`,
+ 'Starting Sourcebot',
+ );
+ void openBrowserWhenReady(SOURCEBOT_URL).catch(() => { /* best effort */ });
+ await new Promise((resolve) => {
+ const child = spawn('docker', ['compose', 'up'], { stdio: 'inherit' });
+ child.on('exit', () => resolve());
+ child.on('error', (err) => {
+ console.error(chalk.red('✗ ') + 'Failed to run `docker compose up`: ' + (err instanceof Error ? err.message : String(err)));
+ resolve();
+ });
+ });
+ return;
+ }
+ }
+
+ const nextSteps: string[] = [];
+ let step = 1;
+
+ if (!downloadedCompose) {
+ nextSteps.push(`${step++}. Download docker-compose.yml:`);
+ nextSteps.push(` curl -o docker-compose.yml ${DOCKER_COMPOSE_URL}`);
+ nextSteps.push('');
+ }
+
+ nextSteps.push(`${step++}. Start Sourcebot:`);
+ nextSteps.push(' docker compose up');
+ nextSteps.push('');
+ nextSteps.push(`${step}. Open http://localhost:3000`);
+
+ note(nextSteps.join('\n'), 'Next steps');
+}
+
+main().catch(err => {
+ const isExitPrompt = err instanceof Error
+ && (err.name === 'ExitPromptError' || err.message?.startsWith('User force closed the prompt'));
+ if (isExitPrompt) {
+ console.log();
+ console.log(chalk.red('✗ ') + 'Setup cancelled.');
+ process.exit(0);
+ }
+ console.error(err);
+ process.exit(1);
+});
diff --git a/packages/setupWizard/src/localRepos.ts b/packages/setupWizard/src/localRepos.ts
new file mode 100644
index 000000000..d5c7b6e52
--- /dev/null
+++ b/packages/setupWizard/src/localRepos.ts
@@ -0,0 +1,170 @@
+import { checkbox, input } from '@inquirer/prompts';
+import { existsSync, statSync } from 'fs';
+import { readdir } from 'fs/promises';
+import { homedir } from 'os';
+import { basename, join, relative, resolve } from 'path';
+import ora from 'ora';
+import type { GenericGitHostConnectionConfig } from '@sourcebot/schemas/v3/genericGitHost.type';
+import type { CollectResult } from './utils.js';
+import { note } from './utils.js';
+
+const MAX_DEPTH = 5;
+
+const SKIP_DIRS = new Set([
+ 'node_modules',
+ 'dist',
+ 'build',
+ 'out',
+ 'target',
+ 'vendor',
+ 'coverage',
+ '__pycache__',
+]);
+
+function expandHostPath(p: string): string {
+ const trimmed = p.trim();
+ if (trimmed.startsWith('~')) {
+ return resolve(join(homedir(), trimmed.slice(1)));
+ }
+ return resolve(trimmed);
+}
+
+async function findGitRepos(root: string, maxDepth: number): Promise {
+ const repos: string[] = [];
+
+ async function walk(dir: string, depth: number): Promise {
+ if (existsSync(join(dir, '.git'))) {
+ repos.push(dir);
+ return;
+ }
+ if (depth >= maxDepth) {
+ return;
+ }
+ let entries;
+ try {
+ entries = await readdir(dir, { withFileTypes: true });
+ } catch {
+ return;
+ }
+ for (const entry of entries) {
+ if (!entry.isDirectory()) {
+ continue;
+ }
+ if (entry.name.startsWith('.')) {
+ continue;
+ }
+ if (SKIP_DIRS.has(entry.name)) {
+ continue;
+ }
+ await walk(join(dir, entry.name), depth + 1);
+ }
+ }
+
+ await walk(root, 0);
+ return repos.sort();
+}
+
+export async function collectLocalReposConfig(
+ localRepoIndex: Map,
+): Promise {
+ note(
+ [
+ 'Point at a directory on your machine that contains git repositories.',
+ `The wizard will scan up to ${MAX_DEPTH} levels deep and let you pick which to index.`,
+ 'Local repos are treated as read-only.',
+ ].join('\n'),
+ 'Local Git repositories',
+ );
+
+ let hostPath: string;
+ let repos: string[];
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const rawPath = await input({
+ message: 'Path to your repos directory (e.g. ~/code)',
+ validate: (v) => {
+ if (!v?.trim()) {
+ return 'Path is required';
+ }
+ const resolved = expandHostPath(v);
+ if (!existsSync(resolved)) {
+ return `Path does not exist: ${resolved}`;
+ }
+ if (!statSync(resolved).isDirectory()) {
+ return `Not a directory: ${resolved}`;
+ }
+ return true;
+ },
+ });
+
+ hostPath = expandHostPath(rawPath);
+
+ const spinner = ora(`Scanning ${hostPath} for git repositories...`).start();
+ repos = await findGitRepos(hostPath, MAX_DEPTH);
+ if (repos.length === 0) {
+ spinner.fail(`No git repositories found under ${hostPath}`);
+ continue;
+ }
+ spinner.succeed(`Found ${repos.length} repositor${repos.length === 1 ? 'y' : 'ies'}`);
+ break;
+ }
+
+ let index = localRepoIndex.get(hostPath);
+ if (index === undefined) {
+ index = localRepoIndex.size;
+ localRepoIndex.set(hostPath, index);
+ }
+ const containerRoot = `/repos/${index}`;
+
+ const hostPathIsRepo = repos.length === 1 && repos[0] === hostPath;
+ if (hostPathIsRepo) {
+ return {
+ connections: [{
+ name: basename(hostPath),
+ config: {
+ type: 'git',
+ url: `file://${containerRoot}`,
+ } satisfies GenericGitHostConnectionConfig,
+ }],
+ env: {},
+ localRepoHostPath: hostPath,
+ };
+ }
+
+ const choices = repos.map((repoPath) => ({
+ name: relative(hostPath, repoPath) || basename(repoPath),
+ value: repoPath,
+ checked: true,
+ }));
+
+ const selected = await checkbox({
+ message: 'Which repositories should be indexed?',
+ choices,
+ required: true,
+ pageSize: 15,
+ loop: false,
+ });
+
+ const posixRel = (p: string): string => relative(hostPath, p).split('\\').join('/');
+
+ const allSelected = selected.length === repos.length;
+ const allAtDepthOne = repos.every((p) => !posixRel(p).includes('/'));
+
+ const connections = allSelected && allAtDepthOne
+ ? [{
+ config: {
+ type: 'git',
+ url: `file://${containerRoot}/*`,
+ } satisfies GenericGitHostConnectionConfig,
+ }]
+ : selected.map((repoPath) => {
+ const config: GenericGitHostConnectionConfig = {
+ type: 'git',
+ url: `file://${containerRoot}/${posixRel(repoPath)}`,
+ };
+ return { name: basename(repoPath), config };
+ });
+
+ return { connections, env: {}, localRepoHostPath: hostPath };
+}
diff --git a/packages/setupWizard/src/models.ts b/packages/setupWizard/src/models.ts
new file mode 100644
index 000000000..c7b0d4862
--- /dev/null
+++ b/packages/setupWizard/src/models.ts
@@ -0,0 +1,375 @@
+import { confirm, input, password, select } from '@inquirer/prompts';
+import { select as searchSelect } from 'inquirer-select-pro';
+import type {
+ AmazonBedrockLanguageModel,
+ AzureLanguageModel,
+ GoogleVertexAnthropicLanguageModel,
+ GoogleVertexLanguageModel,
+ LanguageModel,
+ OpenAICompatibleLanguageModel,
+} from '@sourcebot/schemas/v3/languageModel.type';
+import { note, type EnvVars } from './utils.js';
+
+type Provider = LanguageModel['provider'];
+
+export const PROVIDER_ENV_KEYS: Record = {
+ 'anthropic': 'ANTHROPIC_API_KEY',
+ 'openai': 'OPENAI_API_KEY',
+ 'google-generative-ai': 'GOOGLE_GENERATIVE_AI_API_KEY',
+ 'deepseek': 'DEEPSEEK_API_KEY',
+ 'mistral': 'MISTRAL_API_KEY',
+ 'xai': 'XAI_API_KEY',
+ 'openrouter': 'OPENROUTER_API_KEY',
+ 'openai-compatible': 'OPENAI_COMPATIBLE_API_KEY',
+ 'azure': 'AZURE_OPENAI_API_KEY',
+};
+
+// ─── models.dev catalog ────────────────────────────────────────────────────
+
+type ModelsDevModel = {
+ id: string;
+ name?: string;
+ release_date?: string;
+};
+
+type ModelsDevProvider = {
+ id: string;
+ name?: string;
+ models?: Record;
+};
+
+type ModelsDevCatalog = Record;
+
+type ModelOption = {
+ id: string;
+ name: string;
+ releaseDate?: string;
+};
+
+const MODELS_DEV_API_URL = 'https://models.dev/api.json';
+const FETCH_TIMEOUT_MS = 8000;
+
+const PROVIDER_ID_OVERRIDES: Record = {
+ 'google-generative-ai': 'google',
+};
+
+let catalogPromise: Promise | null = null;
+
+async function loadCatalog(): Promise {
+ if (!catalogPromise) {
+ catalogPromise = (async () => {
+ try {
+ const response = await fetch(MODELS_DEV_API_URL, {
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
+ });
+ if (!response.ok) {
+ return null;
+ }
+ return await response.json() as ModelsDevCatalog;
+ } catch {
+ return null;
+ }
+ })();
+ }
+ return catalogPromise;
+}
+
+async function getModelOptionsForProvider(providerKey: string): Promise {
+ const catalog = await loadCatalog();
+ if (!catalog) {
+ return null;
+ }
+ const providerId = PROVIDER_ID_OVERRIDES[providerKey] ?? providerKey;
+ const provider = catalog[providerId];
+ if (!provider || !provider.models) {
+ return null;
+ }
+ const models = Object.values(provider.models);
+ if (models.length === 0) {
+ return null;
+ }
+ return models
+ .map((m) => ({
+ id: m.id,
+ name: m.name || m.id,
+ releaseDate: m.release_date,
+ }))
+ .sort((a, b) => {
+ if (a.releaseDate && b.releaseDate) {
+ return b.releaseDate.localeCompare(a.releaseDate);
+ }
+ if (a.releaseDate) {
+ return -1;
+ }
+ if (b.releaseDate) {
+ return 1;
+ }
+ return a.name.localeCompare(b.name);
+ });
+}
+
+// ─── prompts ───────────────────────────────────────────────────────────────
+
+async function searchModel(options: {
+ message: string;
+ models: ModelOption[];
+}): Promise {
+ const choices = options.models.map((m) => ({
+ name: m.name === m.id ? m.id : `${m.id} · ${m.name}`,
+ value: m.id,
+ }));
+
+ const result = await searchSelect({
+ message: options.message,
+ multiple: false,
+ loop: false,
+ clearInputWhenSelected: false,
+ placeholder: 'Type to search models, or enter a custom name',
+ options: async (search) => {
+ const trimmed = (search ?? '').trim();
+ if (!trimmed) {
+ return choices;
+ }
+ const lowered = trimmed.toLowerCase();
+ const filtered = choices.filter((c) =>
+ c.value.toLowerCase().includes(lowered) || c.name.toLowerCase().includes(lowered),
+ );
+ const hasExact = choices.some((c) => c.value === trimmed);
+ if (!hasExact) {
+ filtered.unshift({ name: `${trimmed} (custom)`, value: trimmed });
+ }
+ return filtered;
+ },
+ });
+ if (result === null) {
+ throw new Error('Model name is required');
+ }
+ return result;
+}
+
+async function ensureApiKey(provider: Provider, env: EnvVars): Promise {
+ const envKey = PROVIDER_ENV_KEYS[provider] ?? `${provider.toUpperCase().replace(/-/g, '_')}_API_KEY`;
+ if (!env[envKey]) {
+ const apiKey = await password({
+ message: `API key (stored as ${envKey})`,
+ mask: true,
+ validate: (v) => !v?.trim() ? 'API key is required' : true,
+ });
+ env[envKey] = apiKey;
+ }
+ return envKey;
+}
+
+async function collectModelConfig(
+ provider: Provider,
+ model: string,
+ env: EnvVars,
+): Promise {
+ switch (provider) {
+ case 'anthropic':
+ case 'openai':
+ case 'google-generative-ai':
+ case 'deepseek':
+ case 'mistral':
+ case 'xai':
+ case 'openrouter': {
+ const envKey = await ensureApiKey(provider, env);
+ return { provider, model, token: { env: envKey } } satisfies LanguageModel;
+ }
+ case 'openai-compatible': {
+ const baseUrl = await input({
+ message: 'Base URL (e.g. https://your-endpoint.example.com/v1)',
+ validate: (v) => {
+ if (!v?.trim()) {
+ return 'Base URL is required';
+ }
+ if (!/^https?:\/\//.test(v)) {
+ return 'Must start with http:// or https://';
+ }
+ return true;
+ },
+ });
+ const envKey = await ensureApiKey(provider, env);
+ const config: OpenAICompatibleLanguageModel = {
+ provider,
+ model,
+ baseUrl,
+ token: { env: envKey },
+ };
+ return config;
+ }
+ case 'azure': {
+ const resourceName = await input({
+ message: 'Azure resource name',
+ validate: (v) => !v?.trim() ? 'Resource name is required' : true,
+ });
+ const apiVersion = await input({
+ message: 'API version',
+ default: '2024-08-01-preview',
+ validate: (v) => !v?.trim() ? 'API version is required' : true,
+ });
+ const envKey = await ensureApiKey(provider, env);
+ const config: AzureLanguageModel = {
+ provider,
+ model,
+ resourceName,
+ apiVersion,
+ token: { env: envKey },
+ };
+ return config;
+ }
+ case 'amazon-bedrock': {
+ const useDefaultChain = await confirm({
+ message: 'Use the default AWS credential chain? (No to provide Access Key ID and Secret explicitly)',
+ default: true,
+ });
+
+ const config: AmazonBedrockLanguageModel = { provider, model };
+
+ if (!useDefaultChain) {
+ if (!env['AWS_ACCESS_KEY_ID']) {
+ env['AWS_ACCESS_KEY_ID'] = await input({
+ message: 'AWS Access Key ID (stored as AWS_ACCESS_KEY_ID)',
+ validate: (v) => !v?.trim() ? 'Access Key ID is required' : true,
+ });
+ }
+ config.accessKeyId = { env: 'AWS_ACCESS_KEY_ID' };
+
+ if (!env['AWS_SECRET_ACCESS_KEY']) {
+ env['AWS_SECRET_ACCESS_KEY'] = await password({
+ message: 'AWS Secret Access Key (stored as AWS_SECRET_ACCESS_KEY)',
+ mask: true,
+ validate: (v) => !v?.trim() ? 'Secret Access Key is required' : true,
+ });
+ }
+ config.accessKeySecret = { env: 'AWS_SECRET_ACCESS_KEY' };
+ }
+
+ config.region = await input({
+ message: 'AWS region',
+ default: 'us-east-1',
+ validate: (v) => !v?.trim() ? 'Region is required' : true,
+ });
+ return config;
+ }
+ case 'google-vertex':
+ case 'google-vertex-anthropic': {
+ if (!env['GOOGLE_VERTEX_PROJECT']) {
+ env['GOOGLE_VERTEX_PROJECT'] = await input({
+ message: 'Google Cloud project ID (stored as GOOGLE_VERTEX_PROJECT)',
+ validate: (v) => !v?.trim() ? 'Project ID is required' : true,
+ });
+ }
+ if (!env['GOOGLE_VERTEX_REGION']) {
+ env['GOOGLE_VERTEX_REGION'] = await input({
+ message: 'Google Cloud region (stored as GOOGLE_VERTEX_REGION)',
+ default: 'us-central1',
+ validate: (v) => !v?.trim() ? 'Region is required' : true,
+ });
+ }
+
+ const useAppDefault = await confirm({
+ message: 'Use Application Default Credentials? (No to provide a service account credentials file path)',
+ default: true,
+ });
+
+ const config: GoogleVertexLanguageModel | GoogleVertexAnthropicLanguageModel = {
+ provider,
+ model,
+ };
+
+ if (!useAppDefault) {
+ if (!env['GOOGLE_APPLICATION_CREDENTIALS']) {
+ env['GOOGLE_APPLICATION_CREDENTIALS'] = await input({
+ message: 'Path to service account credentials JSON (stored as GOOGLE_APPLICATION_CREDENTIALS)',
+ validate: (v) => !v?.trim() ? 'Credentials path is required' : true,
+ });
+ }
+ config.credentials = { env: 'GOOGLE_APPLICATION_CREDENTIALS' };
+ }
+ return config;
+ }
+ }
+}
+
+export async function collectModels(): Promise<{ models: LanguageModel[]; env: EnvVars }> {
+ const models: LanguageModel[] = [];
+ const env: EnvVars = {};
+
+ note(
+ [
+ 'AI features include Ask, which lets you ask questions about your codebase',
+ 'in natural language and get answers grounded in your indexed code.',
+ ' https://docs.sourcebot.dev/docs/features/ask/overview',
+ '',
+ 'You\'ll need an API key from at least one supported provider',
+ '(Anthropic, OpenAI, Google, etc.) to enable these features.',
+ ].join('\n'),
+ 'AI features',
+ );
+
+ const wantsAI = await confirm({
+ message: 'Would you like to configure AI features?',
+ default: true,
+ });
+
+ if (!wantsAI) {
+ return { models, env };
+ }
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const provider = await select({
+ message: 'Which AI provider?',
+ loop: false,
+ choices: [
+ { value: 'anthropic', name: 'Anthropic' },
+ { value: 'openai', name: 'OpenAI' },
+ { value: 'openai-compatible', name: 'OpenAI-compatible', description: 'self-hosted / custom endpoint' },
+ { value: 'amazon-bedrock', name: 'Amazon Bedrock' },
+ { value: 'google-generative-ai', name: 'Google Gemini' },
+ { value: 'google-vertex', name: 'Google Vertex AI', description: 'Gemini via Vertex' },
+ { value: 'google-vertex-anthropic', name: 'Google Vertex AI (Anthropic)', description: 'Claude via Vertex' },
+ { value: 'azure', name: 'Azure OpenAI' },
+ { value: 'deepseek', name: 'DeepSeek' },
+ { value: 'mistral', name: 'Mistral' },
+ { value: 'openrouter', name: 'OpenRouter' },
+ { value: 'xai', name: 'xAI', description: 'Grok' },
+ ],
+ });
+
+ const modelOptions = provider === 'openai-compatible'
+ ? null
+ : await getModelOptionsForProvider(provider);
+ const model = modelOptions && modelOptions.length > 0
+ ? await searchModel({
+ message: 'Model name',
+ models: modelOptions,
+ })
+ : await input({
+ message: 'Model name',
+ validate: (v) => !v?.trim() ? 'Model name is required' : true,
+ });
+
+ const config = await collectModelConfig(provider, model, env);
+
+ const displayName = (await input({
+ message: 'Display name (optional, press enter to skip)',
+ })).trim();
+ if (displayName) {
+ config.displayName = displayName;
+ }
+ models.push(config);
+
+ const addAnother = await confirm({
+ message: 'Add another model?',
+ default: false,
+ });
+
+ if (!addAnother) {
+ break;
+ }
+ }
+
+ return { models, env };
+}
diff --git a/packages/setupWizard/src/utils.ts b/packages/setupWizard/src/utils.ts
new file mode 100644
index 000000000..99a553f92
--- /dev/null
+++ b/packages/setupWizard/src/utils.ts
@@ -0,0 +1,83 @@
+import chalk from 'chalk';
+import { randomBytes } from 'crypto';
+import { select as searchSelect } from 'inquirer-select-pro';
+import type { ConnectionConfig } from '@sourcebot/schemas/v3/index.type';
+
+export type { ConnectionConfig };
+export type EnvVars = Record;
+export type CollectResult = {
+ /**
+ * One or more connections produced by the host's collect function. Single-connection
+ * hosts return a single entry with no `name` (main() uses the platform-derived
+ * connection name). Multi-connection hosts provide a `name` per entry.
+ */
+ connections: Array<{ name?: string; config: ConnectionConfig }>;
+ env: EnvVars;
+ /**
+ * Optional host path that needs to be mounted into the Sourcebot container.
+ * Surfaced in the wizard's next-steps so users get the matching volume mount line.
+ */
+ localRepoHostPath?: string;
+};
+
+export function generateSecret(bytes: number): string {
+ return randomBytes(bytes).toString('base64');
+}
+
+export function toEnvKey(connectionName: string, suffix: string): string {
+ return `${connectionName.toUpperCase().replace(/-/g, '_')}_${suffix}`;
+}
+
+export function generateConnectionName(platform: string, existing: Record): string {
+ if (!existing[platform]) {
+ return platform;
+ }
+ let i = 1;
+ while (existing[`${platform}-${i}`]) {
+ i++;
+ }
+ return `${platform}-${i}`;
+}
+
+export async function multiInput(options: {
+ message: string;
+ placeholder?: string;
+ validate?: (value: string) => string | true;
+}): Promise {
+ return searchSelect({
+ message: options.message,
+ multiple: true,
+ required: true,
+ loop: false,
+ clearInputWhenSelected: true,
+ placeholder: options.placeholder ?? 'Type a value and press space to add, enter to finish',
+ options: async (search) => {
+ if (!search) {
+ return [];
+ }
+ return [{ name: search, value: search }];
+ },
+ validate: options.validate
+ ? (selected) => {
+ for (const opt of selected) {
+ const result = options.validate!(opt.value);
+ if (result !== true) {
+ return result;
+ }
+ }
+ return true;
+ }
+ : undefined,
+ });
+}
+
+export function note(message: string, title?: string): void {
+ console.log();
+ if (title) {
+ console.log(chalk.cyan('◆ ') + chalk.bold(title));
+ }
+ for (const line of message.split('\n')) {
+ console.log(chalk.gray('│ ') + line);
+ }
+ console.log();
+}
diff --git a/packages/setupWizard/tsconfig.json b/packages/setupWizard/tsconfig.json
new file mode 100644
index 000000000..efb889845
--- /dev/null
+++ b/packages/setupWizard/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": "src",
+ "declaration": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "isolatedModules": true,
+ "module": "Node16",
+ "moduleResolution": "Node16",
+ "target": "ES2022",
+ "noEmitOnError": true,
+ "noImplicitAny": true,
+ "pretty": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "lib": ["ES2023"],
+ "types": ["node"],
+ "strict": true
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/yarn.lock b/yarn.lock
index 7be7eb0ae..e4545b495 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2955,6 +2955,284 @@ __metadata:
languageName: node
linkType: hard
+"@inquirer/ansi@npm:^2.0.5":
+ version: 2.0.5
+ resolution: "@inquirer/ansi@npm:2.0.5"
+ checksum: 10c0/ad61532e5bb47473e3d987c32d4015499a8ce5f4f86e46467e8e672fc52670beb303905d6b324e453935a61671f59f3b9b1b6a1edbbe1f64085e2bb87735e295
+ languageName: node
+ linkType: hard
+
+"@inquirer/checkbox@npm:^5.1.5":
+ version: 5.1.5
+ resolution: "@inquirer/checkbox@npm:5.1.5"
+ dependencies:
+ "@inquirer/ansi": "npm:^2.0.5"
+ "@inquirer/core": "npm:^11.1.10"
+ "@inquirer/figures": "npm:^2.0.5"
+ "@inquirer/type": "npm:^4.0.5"
+ peerDependencies:
+ "@types/node": ">=18"
+ peerDependenciesMeta:
+ "@types/node":
+ optional: true
+ checksum: 10c0/6cf3bfbc0e39b80b8a37b69e49231c22616877b31b77507a55be5363d14b33e91cd3c1eb4ebf0ba89435ab5ef8204ce634579a23ab7b186ee8c71d54a7c959ee
+ languageName: node
+ linkType: hard
+
+"@inquirer/confirm@npm:^6.0.13":
+ version: 6.0.13
+ resolution: "@inquirer/confirm@npm:6.0.13"
+ dependencies:
+ "@inquirer/core": "npm:^11.1.10"
+ "@inquirer/type": "npm:^4.0.5"
+ peerDependencies:
+ "@types/node": ">=18"
+ peerDependenciesMeta:
+ "@types/node":
+ optional: true
+ checksum: 10c0/59f3c484f405b3ffe2e97a9e4927111d71d317ea84fa14dc0ffd2d7c902f934ad31f48b380765937f5ff85722ece475edcde8a64b8018c21f2ee3e33082cee8b
+ languageName: node
+ linkType: hard
+
+"@inquirer/core@npm:^11.1.10":
+ version: 11.1.10
+ resolution: "@inquirer/core@npm:11.1.10"
+ dependencies:
+ "@inquirer/ansi": "npm:^2.0.5"
+ "@inquirer/figures": "npm:^2.0.5"
+ "@inquirer/type": "npm:^4.0.5"
+ cli-width: "npm:^4.1.0"
+ fast-wrap-ansi: "npm:^0.2.0"
+ mute-stream: "npm:^3.0.0"
+ signal-exit: "npm:^4.1.0"
+ peerDependencies:
+ "@types/node": ">=18"
+ peerDependenciesMeta:
+ "@types/node":
+ optional: true
+ checksum: 10c0/d1d4081cbb0bd3dc15a3c95c58560a4836079a14161c407bc20f9a1b0fdb93c2228daf61aa60acab7023bc78df1b85875fde94308dcac2c6bd0ffd24b5f05190
+ languageName: node
+ linkType: hard
+
+"@inquirer/core@npm:^8.1.0":
+ version: 8.2.4
+ resolution: "@inquirer/core@npm:8.2.4"
+ dependencies:
+ "@inquirer/figures": "npm:^1.0.3"
+ "@inquirer/type": "npm:^1.3.3"
+ "@types/mute-stream": "npm:^0.0.4"
+ "@types/node": "npm:^20.14.9"
+ "@types/wrap-ansi": "npm:^3.0.0"
+ ansi-escapes: "npm:^4.3.2"
+ cli-spinners: "npm:^2.9.2"
+ cli-width: "npm:^4.1.0"
+ mute-stream: "npm:^1.0.0"
+ picocolors: "npm:^1.0.1"
+ signal-exit: "npm:^4.1.0"
+ strip-ansi: "npm:^6.0.1"
+ wrap-ansi: "npm:^6.2.0"
+ checksum: 10c0/3328ea52823a59cad4bf6c36b143c7322a2e1430ae040717e63c94680f246c0d628aed3032a2f6890652dd4b7fdb0fec7e324059b74173ffa78ae2a485939f33
+ languageName: node
+ linkType: hard
+
+"@inquirer/editor@npm:^5.1.2":
+ version: 5.1.2
+ resolution: "@inquirer/editor@npm:5.1.2"
+ dependencies:
+ "@inquirer/core": "npm:^11.1.10"
+ "@inquirer/external-editor": "npm:^3.0.0"
+ "@inquirer/type": "npm:^4.0.5"
+ peerDependencies:
+ "@types/node": ">=18"
+ peerDependenciesMeta:
+ "@types/node":
+ optional: true
+ checksum: 10c0/5b24700b8d4339d4b9b5fe5991d4c35843c1977595bfc0ab242e95a2bd8c2463c9039050f3b7821e8f49786926c5b68ba4cb20d6d887292fc0b12ccb03bd3ca2
+ languageName: node
+ linkType: hard
+
+"@inquirer/expand@npm:^5.0.14":
+ version: 5.0.14
+ resolution: "@inquirer/expand@npm:5.0.14"
+ dependencies:
+ "@inquirer/core": "npm:^11.1.10"
+ "@inquirer/type": "npm:^4.0.5"
+ peerDependencies:
+ "@types/node": ">=18"
+ peerDependenciesMeta:
+ "@types/node":
+ optional: true
+ checksum: 10c0/efdc9a93d57397f415529ed2b969de6fa78c2ab98901a89efd73f1cfc79eba3ea899c621a63a458c530ca4142c89fd61cfeb873384e906f9cc33c022a5a3f5be
+ languageName: node
+ linkType: hard
+
+"@inquirer/external-editor@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "@inquirer/external-editor@npm:3.0.0"
+ dependencies:
+ chardet: "npm:^2.1.1"
+ iconv-lite: "npm:^0.7.2"
+ peerDependencies:
+ "@types/node": ">=18"
+ peerDependenciesMeta:
+ "@types/node":
+ optional: true
+ checksum: 10c0/120910c954869c73c54aee825abef6f4c4aa8620cafa831d56b218586b1ee02c12ad0a17b8d701784fb9d33fa8fd63ee155d9c718c90533ff4d8086b99986e1d
+ languageName: node
+ linkType: hard
+
+"@inquirer/figures@npm:^1.0.1, @inquirer/figures@npm:^1.0.3":
+ version: 1.0.15
+ resolution: "@inquirer/figures@npm:1.0.15"
+ checksum: 10c0/6e39a040d260ae234ae220180b7994ff852673e20be925f8aa95e78c7934d732b018cbb4d0ec39e600a410461bcb93dca771e7de23caa10630d255692e440f69
+ languageName: node
+ linkType: hard
+
+"@inquirer/figures@npm:^2.0.5":
+ version: 2.0.5
+ resolution: "@inquirer/figures@npm:2.0.5"
+ checksum: 10c0/139671b88f33f059aec85ed3fdf464999115573350c6dea61141adc1cfd43d14742b6cb68150c2ca9baf5a1bae618f990ed89b4430ae768d415bbd19944c56df
+ languageName: node
+ linkType: hard
+
+"@inquirer/input@npm:^5.0.13":
+ version: 5.0.13
+ resolution: "@inquirer/input@npm:5.0.13"
+ dependencies:
+ "@inquirer/core": "npm:^11.1.10"
+ "@inquirer/type": "npm:^4.0.5"
+ peerDependencies:
+ "@types/node": ">=18"
+ peerDependenciesMeta:
+ "@types/node":
+ optional: true
+ checksum: 10c0/df2d67a6f0a1b4cc22dfdc1e78f833560f613647bd75374d4664601b7947106dd977aceb8440a19a17204b58c11611e6084096eae3eb1873260f1f1d029a8a0a
+ languageName: node
+ linkType: hard
+
+"@inquirer/number@npm:^4.0.13":
+ version: 4.0.13
+ resolution: "@inquirer/number@npm:4.0.13"
+ dependencies:
+ "@inquirer/core": "npm:^11.1.10"
+ "@inquirer/type": "npm:^4.0.5"
+ peerDependencies:
+ "@types/node": ">=18"
+ peerDependenciesMeta:
+ "@types/node":
+ optional: true
+ checksum: 10c0/9185d15c8b0ab820dc0456e7db6f189ae84bf8d5f5bce19398f3a858fca0276e66a53922a4ab118dbae65f1f4f6a29f06f3d34c6436ae9e095a98e9862590e2b
+ languageName: node
+ linkType: hard
+
+"@inquirer/password@npm:^5.0.13":
+ version: 5.0.13
+ resolution: "@inquirer/password@npm:5.0.13"
+ dependencies:
+ "@inquirer/ansi": "npm:^2.0.5"
+ "@inquirer/core": "npm:^11.1.10"
+ "@inquirer/type": "npm:^4.0.5"
+ peerDependencies:
+ "@types/node": ">=18"
+ peerDependenciesMeta:
+ "@types/node":
+ optional: true
+ checksum: 10c0/e06aa6ae4344e3e37630655c443001bcf4421b1e4821c15987fc747b8d0362d536a52a115d25d02b023bb7091fc88630a65d7b0ac02e15a2f70d933f6a863da3
+ languageName: node
+ linkType: hard
+
+"@inquirer/prompts@npm:^8.4.3":
+ version: 8.4.3
+ resolution: "@inquirer/prompts@npm:8.4.3"
+ dependencies:
+ "@inquirer/checkbox": "npm:^5.1.5"
+ "@inquirer/confirm": "npm:^6.0.13"
+ "@inquirer/editor": "npm:^5.1.2"
+ "@inquirer/expand": "npm:^5.0.14"
+ "@inquirer/input": "npm:^5.0.13"
+ "@inquirer/number": "npm:^4.0.13"
+ "@inquirer/password": "npm:^5.0.13"
+ "@inquirer/rawlist": "npm:^5.2.9"
+ "@inquirer/search": "npm:^4.1.9"
+ "@inquirer/select": "npm:^5.1.5"
+ peerDependencies:
+ "@types/node": ">=18"
+ peerDependenciesMeta:
+ "@types/node":
+ optional: true
+ checksum: 10c0/fd77efb0b12a9293d6533cf332a1af10f69fefa97202c3790c2a7695a06c443ed5d4877d05efe512803e4a98cce0fa46fed9d372149512c2eccbfcf23f1c44bd
+ languageName: node
+ linkType: hard
+
+"@inquirer/rawlist@npm:^5.2.9":
+ version: 5.2.9
+ resolution: "@inquirer/rawlist@npm:5.2.9"
+ dependencies:
+ "@inquirer/core": "npm:^11.1.10"
+ "@inquirer/type": "npm:^4.0.5"
+ peerDependencies:
+ "@types/node": ">=18"
+ peerDependenciesMeta:
+ "@types/node":
+ optional: true
+ checksum: 10c0/10f1a23e5222a932d9965490beb8b37551b2c68dcb281290d8f0099dc0778b49d4ca671ad8b346c802cbff29924faffd01b62dc88f297020e3d1061eb00b7f78
+ languageName: node
+ linkType: hard
+
+"@inquirer/search@npm:^4.1.9":
+ version: 4.1.9
+ resolution: "@inquirer/search@npm:4.1.9"
+ dependencies:
+ "@inquirer/core": "npm:^11.1.10"
+ "@inquirer/figures": "npm:^2.0.5"
+ "@inquirer/type": "npm:^4.0.5"
+ peerDependencies:
+ "@types/node": ">=18"
+ peerDependenciesMeta:
+ "@types/node":
+ optional: true
+ checksum: 10c0/0e0cd6f2f312cecfa02f7d5556a7ccf5cbffff442fbdd0828f320e0a0239b63d24db5e31e05ec77d2a969900ad40b2d115f6496b2c9c47561a15dc504298d9dc
+ languageName: node
+ linkType: hard
+
+"@inquirer/select@npm:^5.1.5":
+ version: 5.1.5
+ resolution: "@inquirer/select@npm:5.1.5"
+ dependencies:
+ "@inquirer/ansi": "npm:^2.0.5"
+ "@inquirer/core": "npm:^11.1.10"
+ "@inquirer/figures": "npm:^2.0.5"
+ "@inquirer/type": "npm:^4.0.5"
+ peerDependencies:
+ "@types/node": ">=18"
+ peerDependenciesMeta:
+ "@types/node":
+ optional: true
+ checksum: 10c0/871e05266c00151031798dc659acaed3d9c76d0040a25204cbcc99f7104559db1a8bc2a73ee04318a01f06f879a3d7e5986db50f563aeca23ae4cd5e8cff6057
+ languageName: node
+ linkType: hard
+
+"@inquirer/type@npm:^1.3.1, @inquirer/type@npm:^1.3.3":
+ version: 1.5.5
+ resolution: "@inquirer/type@npm:1.5.5"
+ dependencies:
+ mute-stream: "npm:^1.0.0"
+ checksum: 10c0/4c41736c09ba9426b5a9e44993bdd54e8f532e791518802e33866f233a2a6126a25c1c82c19d1abbf1df627e57b1b957dd3f8318ea96073d8bfc32193943bcb3
+ languageName: node
+ linkType: hard
+
+"@inquirer/type@npm:^4.0.5":
+ version: 4.0.5
+ resolution: "@inquirer/type@npm:4.0.5"
+ peerDependencies:
+ "@types/node": ">=18"
+ peerDependenciesMeta:
+ "@types/node":
+ optional: true
+ checksum: 10c0/390edb0fd1f027f9c8dc26bac28486d38bbde6c19974ef1588ea187f54a2cb58db639ebca31fa81a8fe4a4e84c2f0953ab3f5a6768ba86649368c5e806148a6f
+ languageName: node
+ linkType: hard
+
"@ioredis/commands@npm:^1.1.1":
version: 1.2.0
resolution: "@ioredis/commands@npm:1.2.0"
@@ -9013,7 +9291,7 @@ __metadata:
languageName: unknown
linkType: soft
-"@sourcebot/schemas@workspace:*, @sourcebot/schemas@workspace:packages/schemas":
+"@sourcebot/schemas@workspace:*, @sourcebot/schemas@workspace:^, @sourcebot/schemas@workspace:packages/schemas":
version: 0.0.0-use.local
resolution: "@sourcebot/schemas@workspace:packages/schemas"
dependencies:
@@ -9756,6 +10034,15 @@ __metadata:
languageName: node
linkType: hard
+"@types/mute-stream@npm:^0.0.4":
+ version: 0.0.4
+ resolution: "@types/mute-stream@npm:0.0.4"
+ dependencies:
+ "@types/node": "npm:*"
+ checksum: 10c0/944730fd7b398c5078de3c3d4d0afeec8584283bc694da1803fdfca14149ea385e18b1b774326f1601baf53898ce6d121a952c51eb62d188ef6fcc41f725c0dc
+ languageName: node
+ linkType: hard
+
"@types/mysql@npm:2.15.27":
version: 2.15.27
resolution: "@types/mysql@npm:2.15.27"
@@ -9811,6 +10098,15 @@ __metadata:
languageName: node
linkType: hard
+"@types/node@npm:^20.14.9":
+ version: 20.19.41
+ resolution: "@types/node@npm:20.19.41"
+ dependencies:
+ undici-types: "npm:~6.21.0"
+ checksum: 10c0/aa2a07317bbd700bea68d5784b403a738dbcebadbe2d8ef05649f7953065120d5d37f7edfdd7881df3a3bd15328c8a4dc46fdd69732ab540d552c505378c585b
+ languageName: node
+ linkType: hard
+
"@types/node@npm:^20.17.9":
version: 20.19.37
resolution: "@types/node@npm:20.19.37"
@@ -9994,6 +10290,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/wrap-ansi@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "@types/wrap-ansi@npm:3.0.0"
+ checksum: 10c0/8d8f53363f360f38135301a06b596c295433ad01debd082078c33c6ed98b05a5c8fe8853a88265432126096084f4a135ec1564e3daad631b83296905509f90b3
+ languageName: node
+ linkType: hard
+
"@typescript-eslint/eslint-plugin@npm:8.56.1":
version: 8.56.1
resolution: "@typescript-eslint/eslint-plugin@npm:8.56.1"
@@ -10780,6 +11083,24 @@ __metadata:
languageName: node
linkType: hard
+"ansi-escapes@npm:^4.3.2":
+ version: 4.3.2
+ resolution: "ansi-escapes@npm:4.3.2"
+ dependencies:
+ type-fest: "npm:^0.21.3"
+ checksum: 10c0/da917be01871525a3dfcf925ae2977bc59e8c513d4423368645634bf5d4ceba5401574eb705c1e92b79f7292af5a656f78c5725a4b0e1cec97c4b413705c1d50
+ languageName: node
+ linkType: hard
+
+"ansi-escapes@npm:^7.0.0":
+ version: 7.3.0
+ resolution: "ansi-escapes@npm:7.3.0"
+ dependencies:
+ environment: "npm:^1.0.0"
+ checksum: 10c0/068961d99f0ef28b661a4a9f84a5d645df93ccf3b9b93816cc7d46bbe1913321d4cdf156bb842a4e1e4583b7375c631fa963efb43001c4eb7ff9ab8f78fc0679
+ languageName: node
+ linkType: hard
+
"ansi-regex@npm:^5.0.1":
version: 5.0.1
resolution: "ansi-regex@npm:5.0.1"
@@ -10794,6 +11115,13 @@ __metadata:
languageName: node
linkType: hard
+"ansi-regex@npm:^6.2.2":
+ version: 6.2.2
+ resolution: "ansi-regex@npm:6.2.2"
+ checksum: 10c0/05d4acb1d2f59ab2cf4b794339c7b168890d44dda4bf0ce01152a8da0213aca207802f930442ce8cd22d7a92f44907664aac6508904e75e038fa944d2601b30f
+ languageName: node
+ linkType: hard
+
"ansi-styles@npm:^3.2.1":
version: 3.2.1
resolution: "ansi-styles@npm:3.2.1"
@@ -11472,7 +11800,7 @@ __metadata:
languageName: node
linkType: hard
-"chalk@npm:^5.3.0":
+"chalk@npm:^5.3.0, chalk@npm:^5.6.2":
version: 5.6.2
resolution: "chalk@npm:5.6.2"
checksum: 10c0/99a4b0f0e7991796b1e7e3f52dceb9137cae2a9dfc8fc0784a550dc4c558e15ab32ed70b14b21b52beb2679b4892b41a0aa44249bcb996f01e125d58477c6976
@@ -11507,6 +11835,13 @@ __metadata:
languageName: node
linkType: hard
+"chardet@npm:^2.1.1":
+ version: 2.1.1
+ resolution: "chardet@npm:2.1.1"
+ checksum: 10c0/d8391dd412338442b3de0d3a488aa9327f8bcf74b62b8723d6bd0b85c4084d50b731320e0a7c710edb1d44de75969995d2784b80e4c13b004a6c7a0db4c6e793
+ languageName: node
+ linkType: hard
+
"chokidar@npm:^3.5.2, chokidar@npm:^3.6.0":
version: 3.6.0
resolution: "chokidar@npm:3.6.0"
@@ -11597,6 +11932,20 @@ __metadata:
languageName: node
linkType: hard
+"cli-spinners@npm:^3.2.0":
+ version: 3.4.0
+ resolution: "cli-spinners@npm:3.4.0"
+ checksum: 10c0/91296c32e147d5b973c9d439d1512306499215437b92f0c0d8be44ec850b555acb8795c19c606b2f6747f31d50c4e41fdde7dcef653f18f0ae7cdd58e99a4764
+ languageName: node
+ linkType: hard
+
+"cli-width@npm:^4.1.0":
+ version: 4.1.0
+ resolution: "cli-width@npm:4.1.0"
+ checksum: 10c0/1fbd56413578f6117abcaf858903ba1f4ad78370a4032f916745fa2c7e390183a9d9029cf837df320b0fdce8137668e522f60a30a5f3d6529ff3872d265a955f
+ languageName: node
+ linkType: hard
+
"client-only@npm:0.0.1, client-only@npm:^0.0.1":
version: 0.0.1
resolution: "client-only@npm:0.0.1"
@@ -13018,6 +13367,13 @@ __metadata:
languageName: node
linkType: hard
+"environment@npm:^1.0.0":
+ version: 1.1.0
+ resolution: "environment@npm:1.1.0"
+ checksum: 10c0/fb26434b0b581ab397039e51ff3c92b34924a98b2039dcb47e41b7bca577b9dbf134a8eadb364415c74464b682e2d3afe1a4c0eb9873dc44ea814c5d3103331d
+ languageName: node
+ linkType: hard
+
"err-code@npm:^2.0.2":
version: 2.0.3
resolution: "err-code@npm:2.0.3"
@@ -13987,6 +14343,22 @@ __metadata:
languageName: node
linkType: hard
+"fast-string-truncated-width@npm:^3.0.2":
+ version: 3.0.3
+ resolution: "fast-string-truncated-width@npm:3.0.3"
+ checksum: 10c0/043b8663397d14a3880ce4f3407bcda60b40db9bbeafe62863a35d1f9c69ea17c8da3fcd72de235553e6c9cd053128cde9e24ca0d4a7463208f48db3cd23d981
+ languageName: node
+ linkType: hard
+
+"fast-string-width@npm:^3.0.2":
+ version: 3.0.2
+ resolution: "fast-string-width@npm:3.0.2"
+ dependencies:
+ fast-string-truncated-width: "npm:^3.0.2"
+ checksum: 10c0/c8822d175315bb353ebe782b65214ac53b13e3bf704e03b132ea7bdfa8de6a636375b3ab7a4097545393d109381c37c4f387c72a462c90b61412dbc4632f39a7
+ languageName: node
+ linkType: hard
+
"fast-uri@npm:^3.1.2":
version: 3.1.2
resolution: "fast-uri@npm:3.1.2"
@@ -13994,6 +14366,15 @@ __metadata:
languageName: node
linkType: hard
+"fast-wrap-ansi@npm:^0.2.0":
+ version: 0.2.0
+ resolution: "fast-wrap-ansi@npm:0.2.0"
+ dependencies:
+ fast-string-width: "npm:^3.0.2"
+ checksum: 10c0/c0eb6debee565c5dbb9132dddff5c4d4aba5eb02185ae4dab285acd6186018cffca04264e92f373cbf592a9bcd1c33d65dba036030a8f3baeff1169969a1b59b
+ languageName: node
+ linkType: hard
+
"fast-xml-builder@npm:^1.1.5":
version: 1.2.0
resolution: "fast-xml-builder@npm:1.2.0"
@@ -14380,6 +14761,13 @@ __metadata:
languageName: node
linkType: hard
+"get-east-asian-width@npm:^1.5.0":
+ version: 1.6.0
+ resolution: "get-east-asian-width@npm:1.6.0"
+ checksum: 10c0/7e72e9550fd49ca5b246f9af6bb2afc129c96412845ff6556b3274fd44817a381702ca17028efe9866b261a3d44254cbf21e6c90cf05b4b61675630af776d431
+ languageName: node
+ linkType: hard
+
"get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.2.7, get-intrinsic@npm:^1.3.0":
version: 1.3.0
resolution: "get-intrinsic@npm:1.3.0"
@@ -15086,7 +15474,7 @@ __metadata:
languageName: node
linkType: hard
-"iconv-lite@npm:^0.7.0, iconv-lite@npm:~0.7.0":
+"iconv-lite@npm:^0.7.0, iconv-lite@npm:^0.7.2, iconv-lite@npm:~0.7.0":
version: 0.7.2
resolution: "iconv-lite@npm:0.7.2"
dependencies:
@@ -15195,6 +15583,19 @@ __metadata:
languageName: node
linkType: hard
+"inquirer-select-pro@npm:^1.0.0-alpha.9":
+ version: 1.0.0-alpha.9
+ resolution: "inquirer-select-pro@npm:1.0.0-alpha.9"
+ dependencies:
+ "@inquirer/core": "npm:^8.1.0"
+ "@inquirer/figures": "npm:^1.0.1"
+ "@inquirer/type": "npm:^1.3.1"
+ ansi-escapes: "npm:^7.0.0"
+ chalk: "npm:^5.3.0"
+ checksum: 10c0/5362b6260f067d9a2309c669f6b89660eec7e61150a84ba640bb757e397e58fe856099ce9b4813fa6d96201dc2d716dc9846e9fdd68af3f4cfe0a79e569d8805
+ languageName: node
+ linkType: hard
+
"internal-slot@npm:^1.1.0":
version: 1.1.0
resolution: "internal-slot@npm:1.1.0"
@@ -15608,7 +16009,7 @@ __metadata:
languageName: node
linkType: hard
-"is-unicode-supported@npm:^2.0.0":
+"is-unicode-supported@npm:^2.0.0, is-unicode-supported@npm:^2.1.0":
version: 2.1.0
resolution: "is-unicode-supported@npm:2.1.0"
checksum: 10c0/a0f53e9a7c1fdbcf2d2ef6e40d4736fdffff1c9f8944c75e15425118ff3610172c87bf7bc6c34d3903b04be59790bb2212ddbe21ee65b5a97030fc50370545a5
@@ -16373,7 +16774,7 @@ __metadata:
languageName: node
linkType: hard
-"log-symbols@npm:^7.0.0":
+"log-symbols@npm:^7.0.0, log-symbols@npm:^7.0.1":
version: 7.0.1
resolution: "log-symbols@npm:7.0.1"
dependencies:
@@ -17471,6 +17872,20 @@ __metadata:
languageName: node
linkType: hard
+"mute-stream@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "mute-stream@npm:1.0.0"
+ checksum: 10c0/dce2a9ccda171ec979a3b4f869a102b1343dee35e920146776780de182f16eae459644d187e38d59a3d37adf85685e1c17c38cf7bfda7e39a9880f7a1d10a74c
+ languageName: node
+ linkType: hard
+
+"mute-stream@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "mute-stream@npm:3.0.0"
+ checksum: 10c0/12cdb36a101694c7a6b296632e6d93a30b74401873cf7507c88861441a090c71c77a58f213acadad03bc0c8fa186639dec99d68a14497773a8744320c136e701
+ languageName: node
+ linkType: hard
+
"mz@npm:^2.7.0":
version: 2.7.0
resolution: "mz@npm:2.7.0"
@@ -18129,6 +18544,22 @@ __metadata:
languageName: node
linkType: hard
+"ora@npm:^9.4.0":
+ version: 9.4.0
+ resolution: "ora@npm:9.4.0"
+ dependencies:
+ chalk: "npm:^5.6.2"
+ cli-cursor: "npm:^5.0.0"
+ cli-spinners: "npm:^3.2.0"
+ is-interactive: "npm:^2.0.0"
+ is-unicode-supported: "npm:^2.1.0"
+ log-symbols: "npm:^7.0.1"
+ stdin-discarder: "npm:^0.3.2"
+ string-width: "npm:^8.1.0"
+ checksum: 10c0/8f2d7a8869cd68607797ac0ffe9f5cdceeb6009437672510d9920aea794cdd055164e3fe804248624c4940a71b22f94f1ffd94ce8fecf0746baef97a5c121a91
+ languageName: node
+ linkType: hard
+
"own-keys@npm:^1.0.1":
version: 1.0.1
resolution: "own-keys@npm:1.0.1"
@@ -18505,7 +18936,7 @@ __metadata:
languageName: node
linkType: hard
-"picocolors@npm:1.1.1, picocolors@npm:^1.0.0, picocolors@npm:^1.1.1":
+"picocolors@npm:1.1.1, picocolors@npm:^1.0.0, picocolors@npm:^1.0.1, picocolors@npm:^1.1.1":
version: 1.1.1
resolution: "picocolors@npm:1.1.1"
checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58
@@ -20426,6 +20857,23 @@ __metadata:
languageName: node
linkType: hard
+"setup-sourcebot@workspace:packages/setupWizard":
+ version: 0.0.0-use.local
+ resolution: "setup-sourcebot@workspace:packages/setupWizard"
+ dependencies:
+ "@inquirer/prompts": "npm:^8.4.3"
+ "@sourcebot/schemas": "workspace:^"
+ "@types/node": "npm:^22.7.5"
+ chalk: "npm:^5.6.2"
+ inquirer-select-pro: "npm:^1.0.0-alpha.9"
+ ora: "npm:^9.4.0"
+ tsx: "npm:^4.21.0"
+ typescript: "npm:^5.6.2"
+ bin:
+ setup-sourcebot: ./dist/index.js
+ languageName: unknown
+ linkType: soft
+
"sharp@npm:^0.33.5":
version: 0.33.5
resolution: "sharp@npm:0.33.5"
@@ -21026,6 +21474,13 @@ __metadata:
languageName: node
linkType: hard
+"stdin-discarder@npm:^0.3.2":
+ version: 0.3.2
+ resolution: "stdin-discarder@npm:0.3.2"
+ checksum: 10c0/5dbaba9efbcb447a4450d5ae19794641ea9166abe96dc4b5547a109db1bb6e8bdb17bbe1029e02ca8d9d8ee996b7c7cbcce12b12c18c121871cd4f574292381a
+ languageName: node
+ linkType: hard
+
"steno@npm:^4.0.2":
version: 4.0.2
resolution: "steno@npm:4.0.2"
@@ -21108,6 +21563,16 @@ __metadata:
languageName: node
linkType: hard
+"string-width@npm:^8.1.0":
+ version: 8.2.1
+ resolution: "string-width@npm:8.2.1"
+ dependencies:
+ get-east-asian-width: "npm:^1.5.0"
+ strip-ansi: "npm:^7.1.2"
+ checksum: 10c0/d467b4eaf4c40a01bb438a2620e77badd2456ffd5131c9973abe4f3acf7c802d5b21f3b6a00a5e33a7fc28ca8f9c103226e01bac61e9f259659c6f46d78e353a
+ languageName: node
+ linkType: hard
+
"string.prototype.includes@npm:^2.0.1":
version: 2.0.1
resolution: "string.prototype.includes@npm:2.0.1"
@@ -21246,6 +21711,15 @@ __metadata:
languageName: node
linkType: hard
+"strip-ansi@npm:^7.1.2":
+ version: 7.2.0
+ resolution: "strip-ansi@npm:7.2.0"
+ dependencies:
+ ansi-regex: "npm:^6.2.2"
+ checksum: 10c0/544d13b7582f8254811ea97db202f519e189e59d35740c46095897e254e4f1aa9fe1524a83ad6bc5ad67d4dd6c0281d2e0219ed62b880a6238a16a17d375f221
+ languageName: node
+ linkType: hard
+
"strip-bom@npm:^3.0.0":
version: 3.0.0
resolution: "strip-bom@npm:3.0.0"
@@ -21901,6 +22375,13 @@ __metadata:
languageName: node
linkType: hard
+"type-fest@npm:^0.21.3":
+ version: 0.21.3
+ resolution: "type-fest@npm:0.21.3"
+ checksum: 10c0/902bd57bfa30d51d4779b641c2bc403cdf1371fb9c91d3c058b0133694fcfdb817aef07a47f40faf79039eecbaa39ee9d3c532deff244f3a19ce68cea71a61e8
+ languageName: node
+ linkType: hard
+
"type-fest@npm:^0.7.1":
version: 0.7.1
resolution: "type-fest@npm:0.7.1"
@@ -22848,6 +23329,17 @@ __metadata:
languageName: node
linkType: hard
+"wrap-ansi@npm:^6.2.0":
+ version: 6.2.0
+ resolution: "wrap-ansi@npm:6.2.0"
+ dependencies:
+ ansi-styles: "npm:^4.0.0"
+ string-width: "npm:^4.1.0"
+ strip-ansi: "npm:^6.0.0"
+ checksum: 10c0/baad244e6e33335ea24e86e51868fe6823626e3a3c88d9a6674642afff1d34d9a154c917e74af8d845fd25d170c4ea9cf69a47133c3f3656e1252b3d462d9f6c
+ languageName: node
+ linkType: hard
+
"wrap-ansi@npm:^8.1.0":
version: 8.1.0
resolution: "wrap-ansi@npm:8.1.0"