Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/commands/cleanup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
import { CleanupCommand } from '../../src/commands/cleanup.js'
import { GitWorktreeManager } from '../../src/lib/GitWorktreeManager.js'
import { ResourceCleanup } from '../../src/lib/ResourceCleanup.js'
import { SettingsManager } from '../../src/lib/SettingsManager.js'
import { logger } from '../../src/utils/logger.js'
import { promptConfirmation } from '../../src/utils/prompt.js'
import type { CleanupResult, SafetyCheck } from '../../src/types/cleanup.js'
Expand All @@ -10,6 +11,7 @@ import type { CleanupResult, SafetyCheck } from '../../src/types/cleanup.js'
vi.mock('../../src/lib/GitWorktreeManager.js')
vi.mock('../../src/lib/ResourceCleanup.js')
vi.mock('../../src/utils/prompt.js')
vi.mock('../../src/lib/SettingsManager.js')
vi.mock('../../src/utils/logger.js', () => ({
logger: {
info: vi.fn(),
Expand All @@ -33,6 +35,24 @@ describe('CleanupCommand', () => {

beforeEach(() => {
vi.clearAllMocks()

// Mock SettingsManager to prevent reading real config files
vi.mocked(SettingsManager).mockImplementation(() => ({
loadSettings: vi.fn().mockResolvedValue({
capabilities: {
database: {
databaseUrlEnvVarName: 'DATABASE_URL'
}
},
mergeBehavior: {
mode: 'local'
}
}),
getProtectedBranches: vi.fn(),
getSpinModel: vi.fn(),
getSummaryModel: vi.fn(),
}) as Partial<SettingsManager> as SettingsManager)

mockGitWorktreeManager = new GitWorktreeManager() as vi.Mocked<GitWorktreeManager>
// Mock listWorktrees by default to prevent executeIssueCleanup from failing
mockGitWorktreeManager.listWorktrees = vi.fn().mockResolvedValue([])
Expand Down
1 change: 1 addition & 0 deletions src/commands/start.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ vi.mock('../utils/git.js', async () => {
// Mock remote utilities
vi.mock('../utils/remote.js', () => ({
hasMultipleRemotes: vi.fn().mockResolvedValue(false),
hasNoRemotes: vi.fn().mockResolvedValue(false),
getConfiguredRepoFromSettings: vi.fn().mockResolvedValue('owner/repo'),
parseGitRemotes: vi.fn().mockResolvedValue([]),
validateConfiguredRemote: vi.fn().mockResolvedValue(undefined),
Expand Down
9 changes: 8 additions & 1 deletion src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { findMainWorktreePathWithSettings } from '../utils/git.js'
import { loadEnvIntoProcess } from '../utils/env.js'
import { extractSettingsOverrides } from '../utils/cli-overrides.js'
import { createNeonProviderFromSettings } from '../utils/neon-helpers.js'
import { getConfiguredRepoFromSettings, hasMultipleRemotes } from '../utils/remote.js'
import { getConfiguredRepoFromSettings, hasMultipleRemotes, hasNoRemotes } from '../utils/remote.js'
import { capitalizeFirstLetter } from '../utils/text.js'
import type { StartOptions, StartResult } from '../types/index.js'
import { launchFirstRunSetup, needsFirstRunSetup } from '../utils/first-run-setup.js'
Expand Down Expand Up @@ -127,6 +127,13 @@ export class StartCommand {
}
}

// Check for missing remotes and trigger init flow if needed
if (!isJsonMode && (await hasNoRemotes())) {
getLogger().warn('No git remotes detected. iloom requires a GitHub remote to function.')
await launchFirstRunSetup('Help me configure a GitHub remote for this repository. There are no git remotes configured.')
// Continue after setup - remotes should now be configured
}

let repo: string | undefined

// Only get repo if we have multiple remotes (prehook already validated config)
Expand Down
8 changes: 4 additions & 4 deletions src/utils/first-run-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,9 @@ async function hasNonEmptySettings(filePath: string): Promise<boolean> {

/**
* Launch interactive first-run setup via InitCommand
* @param customMessage Optional custom message to pass to Claude instead of the default
*/
export async function launchFirstRunSetup(): Promise<void> {
export async function launchFirstRunSetup(customMessage?: string): Promise<void> {
logger.info('First-time project setup detected.')
logger.info(
'iloom will now launch an interactive configuration session with Claude.'
Expand All @@ -89,9 +90,8 @@ export async function launchFirstRunSetup(): Promise<void> {
await waitForKeypress('Press any key to start configuration...')

const initCommand = new InitCommand()
await initCommand.execute(
'Help me configure iloom settings for this project. This is my first time using iloom here. Note: Your iloom command will execute once we are done with configuration changes.'
)
const defaultMessage = 'Help me configure iloom settings for this project. This is my first time using iloom here. Note: Your iloom command will execute once we are done with configuration changes.'
await initCommand.execute(customMessage ?? defaultMessage)

// Mark project as configured to prevent wizard from re-triggering
// Use the same project root resolution as needsFirstRunSetup() for consistency
Expand Down
31 changes: 31 additions & 0 deletions src/utils/remote.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { ExecaReturnValue } from 'execa'
import {
parseGitRemotes,
hasMultipleRemotes,
hasNoRemotes,
getConfiguredRepoFromSettings,
validateConfiguredRemote,
} from './remote.js'
Expand Down Expand Up @@ -160,6 +161,36 @@ describe('remote utils', () => {
})
})

describe('hasNoRemotes', () => {
it('should return true when no remotes exist', async () => {
vi.mocked(execa).mockResolvedValue({
stdout: '',
} as Partial<ExecaReturnValue> as ExecaReturnValue)

const result = await hasNoRemotes()

expect(result).toBe(true)
})

it('should return false when remotes exist', async () => {
vi.mocked(execa).mockResolvedValue({
stdout: 'origin\[email protected]:user/repo.git (fetch)\norigin\[email protected]:user/repo.git (push)',
} as Partial<ExecaReturnValue> as ExecaReturnValue)

const result = await hasNoRemotes()

expect(result).toBe(false)
})

it('should return false when git command fails', async () => {
vi.mocked(execa).mockRejectedValue(new Error('fatal: not a git repository'))

const result = await hasNoRemotes()

expect(result).toBe(false)
})
})

describe('getConfiguredRepoFromSettings', () => {
it('should return repo string from configured remote', async () => {
const settings: IloomSettings = {
Expand Down
19 changes: 19 additions & 0 deletions src/utils/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,25 @@ export async function hasMultipleRemotes(cwd?: string): Promise<boolean> {
}
}

/**
* Check if repository has no remotes
*/
export async function hasNoRemotes(cwd?: string): Promise<boolean> {
try {
const remotes = await parseGitRemotes(cwd)
return remotes.length === 0
} catch (error) {
// Same error handling pattern as hasMultipleRemotes
const errMsg = error instanceof Error ? error.message : String(error)
if (/not a git repository/i.test(errMsg)) {
logger.debug('Skipping git remote check: not a git repository')
} else {
logger.warn(`Unable to check git remotes: ${errMsg}`)
}
return false
}
}

/**
* Get configured repository string from settings
* Returns "owner/repo" format for use with gh CLI --repo flag
Expand Down
26 changes: 25 additions & 1 deletion templates/prompts/init-prompt.txt
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,31 @@ REMOTES_INFO
{{/IF SINGLE_REMOTE}}

{{#IF NO_REMOTES}}
**Warning:** No git remotes detected. The user will need to configure a remote before using iloom's GitHub features.
**CRITICAL: No git remotes detected.** This repository has no Git remotes configured. iloom requires a GitHub remote to function properly.

**Before proceeding with other configuration, you MUST help the user set up a remote:**

1. **Ask for the GitHub repository URL:**
- Question: "Please provide your GitHub repository URL (e.g., https://github.com/owner/repo or [email protected]:owner/repo)"
- Validate the URL matches GitHub format (HTTPS or SSH)

2. **Add the remote using git:**
```bash
git remote add origin <user-provided-url>
```

3. **Verify the remote was added:**
```bash
git remote -v
```

4. **Inform the user:** "Remote 'origin' has been configured. You can now proceed with iloom configuration."

**Important:** If the user doesn't have a GitHub repository yet, guide them to:
- Create one at https://github.com/new
- Then return to provide the URL

**After configuring the remote, proceed to Phase 1 (Local Development Settings).**
{{/IF NO_REMOTES}}

**If Linear was selected:**
Expand Down