Skip to content
Draft
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Each loom is a fully isolated container for your work:

* **Git Worktree:** A separate filesystem at ~/project-looms/issue-25/. No stashing, no branch switching overhead.

* **Database Branch:** (Neon support) Schema changes in this loom are isolated—they won't break your main environment or your other active looms.
* **Database Branch:** (Neon and Supabase support) Schema changes in this loom are isolated—they won't break your main environment or your other active looms.

* **Environment Variables:** Each loom has its own environment files (`.env`, `.env.local`, `.env.development`, `.env.development.local`). Uses `development` by default, override with `DOTENV_FLOW_NODE_ENV`. See [Secret Storage Limitations](#multi-language-project-support) for frameworks with encrypted credentials.

Expand Down Expand Up @@ -153,7 +153,7 @@ Configuration

### 1. Interactive Setup (Recommended)

The easiest way to configure iloom is the interactive wizard. It guides you through setting up your environment (GitHub/Linear, Neon, IDE).
The easiest way to configure iloom is the interactive wizard. It guides you through setting up your environment (GitHub/Linear, Neon/Supabase, IDE).

You can even use natural language to jump-start the process:

Expand Down Expand Up @@ -206,6 +206,21 @@ This example shows how to configure a project-wide default (e.g., GitHub remote)
}
```

Or, if using Supabase (requires a paid plan):

```json
{
"databaseProviders": {
"supabase": {
"projectRef": "abcdefghijklmnop",
"parentBranch": "main"
}
}
}
```

> Only one database provider can be active at a time. See [Database Branching](docs/iloom-commands.md#database-branching) for full details.

**.iloom/settings.local.json (Gitignored)**

```json
Expand Down
60 changes: 59 additions & 1 deletion docs/iloom-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -2107,7 +2107,7 @@ il init "configure neon database with project ID abc-123"

**Configuration Areas:**
- Issue tracker (GitHub/Linear/Jira)
- Database provider (Neon)
- Database provider (Neon, Supabase)
- IDE preference (VS Code, Cursor, Windsurf, etc.)
- Merge behavior (local, pr, draft-pr)
- Permission modes
Expand Down Expand Up @@ -2184,6 +2184,64 @@ iloom supports multiple version control providers for PR operations. By default,

---

**Database Provider Settings:**

iloom supports database branching to create isolated database copies per workspace. Configure one provider under `databaseProviders` in `.iloom/settings.json`. Only one provider may be active at a time.

**Neon:**

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `databaseProviders.neon.projectId` | string | (required) | Neon project ID from your project URL (e.g., `"fantastic-fox-3566354"`) |
| `databaseProviders.neon.parentBranch` | string | (required) | Branch from which new database branches are created |

**Supabase:**

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `databaseProviders.supabase.projectRef` | string | (required) | Supabase project reference ID (e.g., `"abcdefghijklmnop"`) |
| `databaseProviders.supabase.withData` | boolean | `true` | Whether to include data when creating a new branch |

**Prerequisites (Supabase):**

- Supabase CLI installed and authenticated (`supabase login`)
- Supabase project with database branching enabled

> **Paid plan required:** Supabase database branching requires a paid Supabase plan (Pro or higher). Free-tier projects do not support branching.

**`withData` option:** When `withData` is `true` (the default), new branches include a copy of the parent branch's data. Set to `false` to create branches with schema only (no data), which is faster for large databases.

**Example Configuration (Neon):**

`.iloom/settings.json`:
```json
{
"databaseProviders": {
"neon": {
"projectId": "fantastic-fox-3566354",
"parentBranch": "main"
}
}
}
```

**Example Configuration (Supabase):**

`.iloom/settings.json`:
```json
{
"databaseProviders": {
"supabase": {
"projectRef": "abcdefghijklmnop"
}
}
}
```

**Note:** Configuring both `neon` and `supabase` simultaneously will cause an error at startup.

---

### il update

Update iloom CLI to the latest version.
Expand Down
58 changes: 30 additions & 28 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2171,58 +2171,60 @@ testJiraCommand
}
})

// Test command for Neon integration
// Test command for database provider integration
program
.command('test-neon')
.description('Test Neon integration and debug configuration')
.command('test-db')
.description('Test database provider integration and debug configuration')
.action(async () => {
try {
const { SettingsManager } = await import('./lib/SettingsManager.js')
const { createNeonProviderFromSettings } = await import('./utils/neon-helpers.js')
const { createDatabaseProviderFromSettings } = await import('./utils/database-helpers.js')

logger.info('Testing Neon Integration\n')
logger.info('Testing Database Provider Integration\n')

// Test 1: Settings Configuration
logger.info('1. Settings Configuration:')
const settingsManager = new SettingsManager()
const settings = await settingsManager.loadSettings()
const neonConfig = settings.databaseProviders?.neon
logger.info(` projectId: ${neonConfig?.projectId ?? '(not configured)'}`)
logger.info(` parentBranch: ${neonConfig?.parentBranch ?? '(not configured)'}`)

// Test 2: Create provider and test initialization
logger.info('\n2. Creating NeonProvider...')
logger.info('\n2. Creating database provider...')
try {
const neonProvider = createNeonProviderFromSettings(settings)
logger.success(' NeonProvider created successfully')
const provider = createDatabaseProviderFromSettings(settings)
logger.info(` Provider: ${provider.displayName}`)
const isConfigured = provider.isConfigured()
if (isConfigured) {
logger.success(` ${provider.displayName} is configured`)
} else {
logger.warn(` ${provider.displayName} is not configured`)
}

// Test 3: CLI availability
logger.info('\n3. Testing Neon CLI availability...')
const isAvailable = await neonProvider.isCliAvailable()
logger.info(`\n3. Testing ${provider.displayName} CLI availability...`)
const isAvailable = await provider.isCliAvailable()
if (isAvailable) {
logger.success(' Neon CLI is available')
logger.success(` ${provider.displayName} CLI is available`)
} else {
logger.error(' Neon CLI not found')
logger.info(' Install with: npm install -g @neon/cli')
logger.error(` ${provider.displayName} CLI not found`)
logger.info(` Install with: ${provider.installHint}`)
return
}

// Test 4: Authentication
logger.info('\n4. Testing Neon CLI authentication...')
const isAuthenticated = await neonProvider.isAuthenticated()
logger.info(`\n4. Testing ${provider.displayName} CLI authentication...`)
const isAuthenticated = await provider.isAuthenticated()
if (isAuthenticated) {
logger.success(' Neon CLI is authenticated')
logger.success(` ${provider.displayName} CLI is authenticated`)
} else {
logger.error(' Neon CLI not authenticated')
logger.info(' Run: neon auth')
logger.error(` ${provider.displayName} CLI not authenticated`)
return
}

// Test 5: List branches (if config is valid)
if (neonConfig?.projectId) {
if (isConfigured) {
logger.info('\n5. Testing branch listing...')
try {
const branches = await neonProvider.listBranches()
const branches = await provider.listBranches()
logger.success(` Found ${branches.length} branches:`)
for (const branch of branches.slice(0, 5)) { // Show first 5
logger.info(` - ${branch}`)
Expand All @@ -2234,19 +2236,19 @@ program
logger.error(` Failed to list branches: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
} else {
logger.warn('\n5. Skipping branch listing (Neon not configured in settings)')
logger.warn(`\n5. Skipping branch listing (${provider.displayName} not configured in settings)`)
}

} catch (error) {
logger.error(` Failed to create NeonProvider: ${error instanceof Error ? error.message : 'Unknown error'}`)
logger.error(` Failed to create database provider: ${error instanceof Error ? error.message : 'Unknown error'}`)
if (error instanceof Error && error.message.includes('not configured')) {
logger.info('\n This is expected if Neon is not configured.')
logger.info(' Configure databaseProviders.neon in .iloom/settings.json to test fully.')
logger.info('\n This is expected if no database provider is configured.')
logger.info(' Configure databaseProviders in .iloom/settings.json to test fully.')
}
}

logger.info('\n' + '='.repeat(50))
logger.success('Neon integration test complete!')
logger.success('Database provider integration test complete!')

} catch (error) {
logger.error(`Test failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
Expand Down
6 changes: 3 additions & 3 deletions src/commands/cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { SettingsManager } from '../lib/SettingsManager.js'
import { promptConfirmation } from '../utils/prompt.js'
import { IdentifierParser } from '../utils/IdentifierParser.js'
import { loadEnvIntoProcess } from '../utils/env.js'
import { createNeonProviderFromSettings } from '../utils/neon-helpers.js'
import { createDatabaseProviderFromSettings } from '../utils/database-helpers.js'
import { LoomManager } from '../lib/LoomManager.js'
import { TelemetryService } from '../lib/TelemetryService.js'
import { MetadataManager } from '../lib/MetadataManager.js'
Expand Down Expand Up @@ -104,8 +104,8 @@ export class CleanupCommand {
const databaseUrlEnvVarName = settings.capabilities?.database?.databaseUrlEnvVarName ?? 'DATABASE_URL'

const environmentManager = new EnvironmentManager()
const neonProvider = createNeonProviderFromSettings(settings)
const databaseManager = new DatabaseManager(neonProvider, environmentManager, databaseUrlEnvVarName)
const databaseProvider = createDatabaseProviderFromSettings(settings)
const databaseManager = new DatabaseManager(databaseProvider, environmentManager, databaseUrlEnvVarName)
const cliIsolationManager = new CLIIsolationManager()

this.resourceCleanup ??= new ResourceCleanup(
Expand Down
6 changes: 3 additions & 3 deletions src/commands/finish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { SessionSummaryService } from '../lib/SessionSummaryService.js'
import { findMainWorktreePathWithSettings, pushBranchToRemote, extractIssueNumber, getMergeTargetBranch, isPlaceholderCommit, findPlaceholderCommitSha, removePlaceholderCommitFromHead, removePlaceholderCommitFromHistory, executeGitCommand } from '../utils/git.js'
import { loadEnvIntoProcess } from '../utils/env.js'
import { installDependencies } from '../utils/package-manager.js'
import { createNeonProviderFromSettings } from '../utils/neon-helpers.js'
import { createDatabaseProviderFromSettings } from '../utils/database-helpers.js'
import { getConfiguredRepoFromSettings, hasMultipleRemotes } from '../utils/remote.js'
import { promptConfirmation } from '../utils/prompt.js'
import { UserAbortedCommitError, type FinishResult } from '../types/index.js'
Expand Down Expand Up @@ -116,8 +116,8 @@ export class FinishCommand {
const databaseUrlEnvVarName = settings.capabilities?.database?.databaseUrlEnvVarName ?? 'DATABASE_URL'

const environmentManager = new EnvironmentManager()
const neonProvider = createNeonProviderFromSettings(settings)
const databaseManager = new DatabaseManager(neonProvider, environmentManager, databaseUrlEnvVarName)
const databaseProvider = createDatabaseProviderFromSettings(settings)
const databaseManager = new DatabaseManager(databaseProvider, environmentManager, databaseUrlEnvVarName)
const cliIsolationManager = new CLIIsolationManager()

// Initialize LoomManager if not provided
Expand Down
75 changes: 75 additions & 0 deletions src/commands/ignite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3190,6 +3190,7 @@ describe('IgniteCommand', () => {
const mockTrack = TelemetryService.getInstance().track
expect(mockTrack).toHaveBeenCalledWith('session.started', {
has_neon: false,
has_supabase: false,
language: 'typescript',
})
} finally {
Expand Down Expand Up @@ -3226,6 +3227,7 @@ describe('IgniteCommand', () => {
const mockTrack = TelemetryService.getInstance().track
expect(mockTrack).toHaveBeenCalledWith('session.started', {
has_neon: true,
has_supabase: false,
language: 'typescript',
})
} finally {
Expand Down Expand Up @@ -3260,6 +3262,79 @@ describe('IgniteCommand', () => {
const mockTrack = TelemetryService.getInstance().track
expect(mockTrack).toHaveBeenCalledWith('session.started', {
has_neon: false,
has_supabase: false,
language: 'typescript',
})
} finally {
process.cwd = originalCwd
launchClaudeSpy.mockRestore()
}
})

it('has_supabase is true when supabase settings are configured', async () => {
const launchClaudeSpy = vi.spyOn(claudeUtils, 'launchClaude').mockResolvedValue(undefined)

const mockSettingsManager = {
loadSettings: vi.fn().mockResolvedValue({
databaseProviders: {
supabase: { projectRef: 'abcdefghijklmnop', parentBranch: 'main' },
},
}),
getSpinModel: vi.fn().mockReturnValue('opus'),
}

const originalCwd = process.cwd
process.cwd = vi.fn().mockReturnValue('/path/to/feat/issue-53__supabase-test')

const commandWithSupabase = new IgniteCommand(
mockTemplateManager,
mockGitWorktreeManager,
undefined,
mockSettingsManager as never,
)

try {
await commandWithSupabase.execute()

const mockTrack = TelemetryService.getInstance().track
expect(mockTrack).toHaveBeenCalledWith('session.started', {
has_neon: false,
has_supabase: true,
language: 'typescript',
})
} finally {
process.cwd = originalCwd
launchClaudeSpy.mockRestore()
}
})

it('has_supabase is false when supabase settings are absent', async () => {
const launchClaudeSpy = vi.spyOn(claudeUtils, 'launchClaude').mockResolvedValue(undefined)

const mockSettingsManager = {
loadSettings: vi.fn().mockResolvedValue({
databaseProviders: {},
}),
getSpinModel: vi.fn().mockReturnValue('opus'),
}

const originalCwd = process.cwd
process.cwd = vi.fn().mockReturnValue('/path/to/feat/issue-54__no-supabase')

const commandWithoutSupabase = new IgniteCommand(
mockTemplateManager,
mockGitWorktreeManager,
undefined,
mockSettingsManager as never,
)

try {
await commandWithoutSupabase.execute()

const mockTrack = TelemetryService.getInstance().track
expect(mockTrack).toHaveBeenCalledWith('session.started', {
has_neon: false,
has_supabase: false,
language: 'typescript',
})
} finally {
Expand Down
2 changes: 2 additions & 0 deletions src/commands/ignite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,11 @@ export class IgniteCommand {
// Step 2.0.5.1: Track session.started telemetry
try {
const hasNeon = !!this.settings?.databaseProviders?.neon
const hasSupabase = !!this.settings?.databaseProviders?.supabase
const language = await detectProjectLanguage(context.workspacePath)
TelemetryService.getInstance().track('session.started', {
has_neon: hasNeon,
has_supabase: hasSupabase,
language,
})
} catch (error) {
Expand Down
6 changes: 3 additions & 3 deletions src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { findMainWorktreePathWithSettings } from '../utils/git.js'
import { matchIssueIdentifier } from '../utils/IdentifierParser.js'
import { loadEnvIntoProcess } from '../utils/env.js'
import { extractSettingsOverrides } from '../utils/cli-overrides.js'
import { createNeonProviderFromSettings } from '../utils/neon-helpers.js'
import { createDatabaseProviderFromSettings } from '../utils/database-helpers.js'
import { getConfiguredRepoFromSettings, hasMultipleRemotes } from '../utils/remote.js'
import { capitalizeFirstLetter } from '../utils/text.js'
import type { StartOptions, StartResult } from '../types/index.js'
Expand Down Expand Up @@ -100,10 +100,10 @@ export class StartCommand {

// Create DatabaseManager with NeonProvider and EnvironmentManager
const environmentManager = new EnvironmentManager()
const neonProvider = createNeonProviderFromSettings(settings)
const databaseProvider = createDatabaseProviderFromSettings(settings)
const databaseUrlEnvVarName = settings.capabilities?.database?.databaseUrlEnvVarName ?? 'DATABASE_URL'

const databaseManager = new DatabaseManager(neonProvider, environmentManager, databaseUrlEnvVarName)
const databaseManager = new DatabaseManager(databaseProvider, environmentManager, databaseUrlEnvVarName)

// Create BranchNamingService (defaults to Claude-based strategy)
const branchNaming = new DefaultBranchNamingService({ useClaude: true })
Expand Down
Loading