Skip to content
Draft
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
120 changes: 118 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,15 @@ iloom supports multiple issue tracking providers to fit your team's workflow.
| **Linear** | `il init` | Requires API token. Supports full read/write on Linear issues. |
| **Jira** | Configure in `.iloom/settings.json` | Atlassian Cloud. Requires API token. See [Jira Setup](#jira-setup) below. |

### Version Control Providers

Choose which platform hosts your pull requests and code reviews.

| **Provider** | **Setup** | **Notes** |
|--------------|-----------|-----------|
| **GitHub** | `gh auth login` | Default. Integrated with GitHub Issues. |
| **BitBucket** | Configure in `.iloom/settings.json` | Atlassian Cloud. Requires API token. See [BitBucket Setup](#bitbucket-setup) below. |

### Jira Setup

To use Jira as your issue tracker, add this configuration:
Expand Down Expand Up @@ -478,14 +487,122 @@ To use Jira as your issue tracker, add this configuration:
- `doneStatuses`: (Optional) Status names to exclude from `il issues` lists (default: `["Done"]`). Set to match your Jira workflow, e.g., `["Done", "Closed", "Verified"]`
- `transitionMappings`: (Optional) Map iloom states to your Jira workflow transition names

### BitBucket Setup

To use BitBucket for pull requests, add this configuration:

**.iloom/settings.json (Committed)**
```json
{
"versionControl": {
"provider": "bitbucket",
"bitbucket": {
"username": "your-bitbucket-username",
"workspace": "your-workspace",
"repoSlug": "your-repo"
}
},
"mergeBehavior": {
"mode": "bitbucket-pr"
}
}
```

**.iloom/settings.local.json (Gitignored - Never commit this file)**
```json
{
"versionControl": {
"bitbucket": {
"apiToken": "your-bitbucket-api-token"
}
}
}
```

**Generate a BitBucket API Token:**
1. Visit https://bitbucket.org/account/settings/app-passwords/
2. Click "Create API token" (Note: App passwords were deprecated September 2025)
3. Grant permissions: `repository:read`, `repository:write`, `pullrequest:read`, `pullrequest:write`
4. Copy the token to `.iloom/settings.local.json`

**Configuration Options:**
- `username`: Your BitBucket username
- `apiToken`: API token (store in settings.local.json only!)
- `workspace`: (Optional) BitBucket workspace, auto-detected from git remote if not provided
- `repoSlug`: (Optional) Repository slug, auto-detected from git remote if not provided
- `reviewers`: (Optional) Array of BitBucket usernames to automatically add as PR reviewers. Usernames are resolved to BitBucket account IDs at PR creation time. Unresolved usernames are logged as warnings but don't block PR creation.

**Example with Reviewers:**
```json
{
"versionControl": {
"provider": "bitbucket",
"bitbucket": {
"username": "your-bitbucket-username",
"reviewers": [
"alice.jones",
"bob.smith"
]
}
},
"mergeBehavior": {
"mode": "bitbucket-pr"
}
}
```

### Jira + BitBucket Together

Use Jira for issues and BitBucket for pull requests:

**.iloom/settings.json**
```json
{
"issueManagement": {
"provider": "jira",
"jira": {
"host": "https://yourcompany.atlassian.net",
"username": "your.email@company.com",
"projectKey": "PROJ"
}
},
"versionControl": {
"provider": "bitbucket",
"bitbucket": {
"username": "your-bitbucket-username"
}
},
"mergeBehavior": {
"mode": "bitbucket-pr"
}
}
```

**.iloom/settings.local.json**
```json
{
"issueManagement": {
"jira": {
"apiToken": "your-jira-api-token"
}
},
"versionControl": {
"bitbucket": {
"apiToken": "your-bitbucket-api-token"
}
}
}
```


### IDE Support
iloom creates isolated workspace settings for your editor. Color synchronization (visual context) only works best VS Code-based editors.

* **Supported:** VS Code, Cursor, Windsurf, Antigravity, WebStorm, IntelliJ, Sublime Text.

* **Config:** Set your preference via `il init` or `il start --set ide.type=cursor`.


### Git Operation Settings

Configure git operation timeouts for projects with long-running pre-commit hooks.
Expand All @@ -505,7 +622,6 @@ Configure git operation timeouts for projects with long-running pre-commit hooks

**When to increase:** If you see timeout errors during `il commit` or `il finish`, your pre-commit hooks are taking longer than the default 60 seconds. Set a higher value based on your typical hook duration.


Advanced Features
-----------------

Expand Down
88 changes: 88 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,7 @@ program
.option('-n, --dry-run', 'Preview actions without executing')
.option('--pr <number>', 'Treat input as PR number', parseFloat)
.option('--skip-build', 'Skip post-merge build verification')
.addOption(new Option('--skip-to-pr').hideHelp())
.option('--no-browser', 'Skip opening PR in browser (github-pr and github-draft-pr modes)')
.option('--cleanup', 'Clean up worktree after finishing (default in local mode)')
.option('--no-cleanup', 'Keep worktree after finishing')
Expand Down Expand Up @@ -2308,6 +2309,93 @@ program
process.exit(0)
})

// Debug commands - only registered when debug mode is enabled
if (process.env.ILOOM_DEBUG === 'true') {
const debugCommand = program
.command('debug')
.description('Debug tools (only available in debug mode)')

const bitbucketDebugCommand = debugCommand
.command('bitbucket')
.description('BitBucket debug tools')

bitbucketDebugCommand
.command('resolve-reviewer-ids')
.description('Resolve configured reviewer usernames to BitBucket account IDs')
.action(async () => {
try {
const settingsManager = new SettingsManager()
const settings = await settingsManager.loadSettings()

const bitbucketConfig = settings.versionControl?.bitbucket
if (!bitbucketConfig) {
logger.error('BitBucket configuration not found in settings')
logger.info('Configure versionControl.bitbucket in .iloom/settings.json')
process.exit(1)
}

if (!bitbucketConfig.username) {
logger.error('BitBucket username not configured')
logger.info('Configure versionControl.bitbucket.username in .iloom/settings.json')
process.exit(1)
}

if (!bitbucketConfig.apiToken) {
logger.error('BitBucket API token not configured')
logger.info('Configure versionControl.bitbucket.apiToken in .iloom/settings.local.json')
process.exit(1)
}

const reviewers = bitbucketConfig.reviewers ?? []
if (reviewers.length === 0) {
logger.warn('No reviewers configured in settings')
logger.info('Configure versionControl.bitbucket.reviewers in .iloom/settings.json')
console.log(JSON.stringify({}, null, 2))
process.exit(0)
}

// Get workspace from config or auto-detect from git remote
let workspace = bitbucketConfig.workspace
if (!workspace) {
const { parseGitRemotes } = await import('./utils/remote.js')
const remotes = await parseGitRemotes()
const bitbucketRemote = remotes.find(r => r.url.includes('bitbucket.org'))
if (!bitbucketRemote) {
logger.error('Could not auto-detect BitBucket workspace from git remote')
logger.info('Configure versionControl.bitbucket.workspace in .iloom/settings.json')
process.exit(1)
}
workspace = bitbucketRemote.owner
}

// At this point workspace is guaranteed to be a string (either from config or auto-detected)
const resolvedWorkspace = workspace

// Create BitBucket API client and resolve reviewer IDs
const { BitBucketApiClient } = await import('./lib/providers/bitbucket/BitBucketApiClient.js')
const apiClient = new BitBucketApiClient({
username: bitbucketConfig.username,
apiToken: bitbucketConfig.apiToken,
workspace: resolvedWorkspace,
})

const resolvedMap = await apiClient.findUsersByUsername(resolvedWorkspace, reviewers)

// Convert Map to plain object for JSON output
const result: Record<string, string> = {}
for (const [username, accountId] of resolvedMap) {
result[username] = accountId
}

console.log(JSON.stringify(result, null, 2))
process.exit(0)
} catch (error) {
logger.error(`Failed to resolve reviewer IDs: ${error instanceof Error ? error.message : 'Unknown error'}`)
process.exit(1)
}
})
}

// Parse CLI arguments (only when run directly, not when imported for testing)
// Resolve symlinks to handle npm link and global installs
const isRunDirectly = process.argv[1] && ((): boolean => {
Expand Down
22 changes: 11 additions & 11 deletions src/commands/finish.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3576,10 +3576,10 @@ describe('FinishCommand', () => {
},
})

// Mock the executeGitHubPRWorkflow method to verify it's called
// Mock the executeVCSPRWorkflow method to verify it's called
// (Issue #464: Linear + github-pr should work since PRs go through GitHub CLI)
const executeGitHubPRWorkflowSpy = vi
.spyOn(command as unknown as { executeGitHubPRWorkflow: () => Promise<void> }, 'executeGitHubPRWorkflow')
const executeVCSPRWorkflowSpy = vi
.spyOn(command as unknown as { executeVCSPRWorkflow: () => Promise<void> }, 'executeVCSPRWorkflow')
.mockResolvedValue()

await command.execute({
Expand All @@ -3589,8 +3589,8 @@ describe('FinishCommand', () => {

// Rebase runs before PR workflow
expect(mockMergeManager.rebaseOnMain).toHaveBeenCalled()
// The github-pr workflow should be executed (not the local merge)
expect(executeGitHubPRWorkflowSpy).toHaveBeenCalled()
// The unified VCS PR workflow should be executed (not the local merge)
expect(executeVCSPRWorkflowSpy).toHaveBeenCalled()
// Local merge should NOT be performed (PR workflow handles merging)
expect(mockMergeManager.performFastForwardMerge).not.toHaveBeenCalled()
})
Expand All @@ -3605,11 +3605,11 @@ describe('FinishCommand', () => {
},
})

// Mock the executeGitHubPRWorkflow as fallback handler
// When no draftPrNumber in metadata, github-draft-pr falls back to github-pr workflow
// Mock the executeVCSPRWorkflow as fallback handler
// When no draftPrNumber in metadata, github-draft-pr falls back to the unified VCS PR workflow
// (Issue #464: Linear + github-draft-pr should work since PRs go through GitHub CLI)
const executeGitHubPRWorkflowSpy = vi
.spyOn(command as unknown as { executeGitHubPRWorkflow: () => Promise<void> }, 'executeGitHubPRWorkflow')
const executeVCSPRWorkflowSpy = vi
.spyOn(command as unknown as { executeVCSPRWorkflow: () => Promise<void> }, 'executeVCSPRWorkflow')
.mockResolvedValue()

await command.execute({
Expand All @@ -3619,8 +3619,8 @@ describe('FinishCommand', () => {

// Rebase runs before PR workflow
expect(mockMergeManager.rebaseOnMain).toHaveBeenCalled()
// For github-draft-pr without existing draft PR, it falls back to executeGitHubPRWorkflow
expect(executeGitHubPRWorkflowSpy).toHaveBeenCalled()
// For github-draft-pr without existing draft PR, it falls back to executeVCSPRWorkflow
expect(executeVCSPRWorkflowSpy).toHaveBeenCalled()
// Local merge should NOT be performed (PR workflow handles merging)
expect(mockMergeManager.performFastForwardMerge).not.toHaveBeenCalled()
})
Expand Down
Loading