Skip to content
Merged
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ Each loom is a fully isolated container for your work:

* **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.

When inside a loom shell (`il shell`), the following environment variables are automatically set:

| Variable | Description | Example |
|----------|-------------|---------|
| `ILOOM_LOOM` | Loom identifier for PS1 customization | `issue-87` |
| `ILOOM_COLOR_HEX` | Hex color assigned to this loom (if available) | `#dcebff` |

`ILOOM_COLOR_HEX` is useful for downstream tools that want to visually distinguish looms. For example, a Vite app can read it via `import.meta.env.VITE_ILOOM_COLOR_HEX` to tint the UI. See [Vite Integration Guide](docs/vite-iloom-color.md) for details.

* **Unique Runtime:**

* **Web Apps:** Runs on a deterministic port (e.g., base port 3000 + issue #25 = 3025).
Expand Down
101 changes: 101 additions & 0 deletions docs/vite-iloom-color.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Using ILOOM_COLOR_HEX in a Vite App

When you run a dev server inside a loom shell (`il shell`), the `ILOOM_COLOR_HEX` environment variable is automatically set to the hex color assigned to that loom (e.g., `#dcebff`). This lets your app visually distinguish which loom it's running in.

## Setup

### 1. Prefix the variable for Vite

Vite only exposes env vars that start with `VITE_`. Add a `.env.local` (or use your existing one) in the project root:

```bash
# .env.local
VITE_ILOOM_COLOR_HEX=$ILOOM_COLOR_HEX
```

Or, if you use `il shell` and then start the dev server manually, you can set it inline:

```bash
VITE_ILOOM_COLOR_HEX=$ILOOM_COLOR_HEX pnpm dev
```

### 2. Access it in your app

```ts
// src/main.ts (or any client-side file)
const loomColor = import.meta.env.VITE_ILOOM_COLOR_HEX

if (loomColor) {
document.documentElement.style.setProperty('--loom-color', loomColor)
}
```

### 3. Use the CSS variable

```css
/* src/styles.css */
:root {
--loom-color: transparent; /* fallback when not in a loom */
}

body {
border-top: 4px solid var(--loom-color);
}
```

## Alternative: Use `define` in vite.config.ts

If you don't want an extra `.env.local` entry, you can inject the value at build time:

```ts
// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
define: {
__ILOOM_COLOR_HEX__: JSON.stringify(process.env.ILOOM_COLOR_HEX ?? ''),
},
})
```

Then in your app:

```ts
declare const __ILOOM_COLOR_HEX__: string

if (__ILOOM_COLOR_HEX__) {
document.documentElement.style.setProperty('--loom-color', __ILOOM_COLOR_HEX__)
}
```

## React Example

```tsx
// src/components/LoomIndicator.tsx
function LoomIndicator() {
const color = import.meta.env.VITE_ILOOM_COLOR_HEX
if (!color) return null

return (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
height: 4,
backgroundColor: color,
zIndex: 9999,
}}
/>
)
}
```

## How It Works

1. `il shell <issue>` reads the loom's metadata (stored in `~/.config/iloom-ai/looms/`) and exports `ILOOM_COLOR_HEX` into the shell environment.
2. Any process spawned from that shell inherits the variable.
3. Vite picks it up (via `VITE_` prefix or `define`) and makes it available to client code.

When you're not inside a loom shell, the variable is simply absent and your fallback styles apply.
93 changes: 88 additions & 5 deletions src/commands/dev-server.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { DevServerCommand } from './dev-server.js'
import { GitWorktreeManager } from '../lib/GitWorktreeManager.js'
import { MetadataManager } from '../lib/MetadataManager.js'
import { ProjectCapabilityDetector } from '../lib/ProjectCapabilityDetector.js'
import { DevServerManager } from '../lib/DevServerManager.js'
import { SettingsManager } from '../lib/SettingsManager.js'
Expand All @@ -12,6 +13,7 @@ import fs from 'fs-extra'

// Mock dependencies
vi.mock('../lib/GitWorktreeManager.js')
vi.mock('../lib/MetadataManager.js')
vi.mock('../lib/ProjectCapabilityDetector.js')
vi.mock('../lib/DevServerManager.js')
vi.mock('../utils/IdentifierParser.js')
Expand Down Expand Up @@ -40,6 +42,7 @@ vi.mock('../utils/logger.js', () => ({
describe('DevServerCommand', () => {
let command: DevServerCommand
let mockGitWorktreeManager: GitWorktreeManager
let mockMetadataManager: MetadataManager
let mockCapabilityDetector: ProjectCapabilityDetector
let mockDevServerManager: DevServerManager
let mockIdentifierParser: IdentifierParser
Expand All @@ -54,11 +57,17 @@ describe('DevServerCommand', () => {

beforeEach(() => {
mockGitWorktreeManager = new GitWorktreeManager()
mockMetadataManager = new MetadataManager()
mockCapabilityDetector = new ProjectCapabilityDetector()
mockDevServerManager = new DevServerManager()
mockIdentifierParser = new IdentifierParser(mockGitWorktreeManager)
mockSettingsManager = new SettingsManager()

// Mock MetadataManager - default to returning metadata with color
vi.mocked(mockMetadataManager.readMetadata).mockResolvedValue({
colorHex: '#dcebff',
})

// Mock DevServerManager methods
vi.mocked(mockDevServerManager.isServerRunning).mockResolvedValue(false)
vi.mocked(mockDevServerManager.runServerForeground).mockImplementation(
Expand All @@ -83,7 +92,8 @@ describe('DevServerCommand', () => {
mockCapabilityDetector,
mockIdentifierParser,
mockDevServerManager,
mockSettingsManager
mockSettingsManager,
mockMetadataManager
)
})

Expand Down Expand Up @@ -458,7 +468,7 @@ describe('DevServerCommand', () => {
3087,
false,
expect.any(Function),
{ DATABASE_URL: 'postgres://test', API_KEY: 'secret' }
expect.objectContaining({ DATABASE_URL: 'postgres://test', API_KEY: 'secret', ILOOM_LOOM: '87' })
)
})

Expand All @@ -475,7 +485,7 @@ describe('DevServerCommand', () => {
3087,
false,
expect.any(Function),
{}
expect.objectContaining({ ILOOM_LOOM: '87' })
)
})

Expand All @@ -490,7 +500,7 @@ describe('DevServerCommand', () => {
3087,
false,
expect.any(Function),
{}
expect.objectContaining({ ILOOM_LOOM: '87' })
)
})

Expand All @@ -513,7 +523,7 @@ describe('DevServerCommand', () => {
3087,
false,
expect.any(Function),
{}
expect.objectContaining({ ILOOM_LOOM: '87' })
)
})

Expand All @@ -533,4 +543,77 @@ describe('DevServerCommand', () => {
expect(mockDevServerManager.runServerForeground).toHaveBeenCalled()
})
})

describe('loom environment variables', () => {
beforeEach(() => {
vi.mocked(mockIdentifierParser.parseForPatternDetection).mockResolvedValue({
type: 'issue',
number: 87,
originalInput: '87',
})

vi.mocked(mockGitWorktreeManager.findWorktreeForIssue).mockResolvedValue(mockWorktree)

const mockCapabilities: ProjectCapabilities = {
capabilities: ['web'],
binEntries: {},
}
vi.mocked(mockCapabilityDetector.detectCapabilities).mockResolvedValue(mockCapabilities)

vi.mocked(fs.pathExists).mockResolvedValue(true)
vi.mocked(fs.readFile).mockResolvedValue('PORT=3087\n')
})

it('should set ILOOM_LOOM env var to original input', async () => {
await command.execute({ identifier: '87' })

const envArg = vi.mocked(mockDevServerManager.runServerForeground).mock.calls[0]?.[4]
expect(envArg).toHaveProperty('ILOOM_LOOM', '87')
})

it('should set ILOOM_LOOM for PR identifier', async () => {
vi.mocked(mockIdentifierParser.parseForPatternDetection).mockResolvedValue({
type: 'pr',
number: 42,
originalInput: '42',
})
vi.mocked(mockGitWorktreeManager.findWorktreeForPR).mockResolvedValue(mockWorktree)

await command.execute({ identifier: '42' })

const envArg = vi.mocked(mockDevServerManager.runServerForeground).mock.calls[0]?.[4]
expect(envArg).toHaveProperty('ILOOM_LOOM', '42')
})

it('should set ILOOM_COLOR_HEX from metadata', async () => {
vi.mocked(mockMetadataManager.readMetadata).mockResolvedValue({
colorHex: '#dcebff',
})

await command.execute({ identifier: '87' })

const envArg = vi.mocked(mockDevServerManager.runServerForeground).mock.calls[0]?.[4]
expect(envArg).toHaveProperty('ILOOM_COLOR_HEX', '#dcebff')
})

it('should not set ILOOM_COLOR_HEX when metadata has no colorHex', async () => {
vi.mocked(mockMetadataManager.readMetadata).mockResolvedValue({
colorHex: null,
})

await command.execute({ identifier: '87' })

const envArg = vi.mocked(mockDevServerManager.runServerForeground).mock.calls[0]?.[4]
expect(envArg).not.toHaveProperty('ILOOM_COLOR_HEX')
})

it('should not set ILOOM_COLOR_HEX when metadata is null', async () => {
vi.mocked(mockMetadataManager.readMetadata).mockResolvedValue(null)

await command.execute({ identifier: '87' })

const envArg = vi.mocked(mockDevServerManager.runServerForeground).mock.calls[0]?.[4]
expect(envArg).not.toHaveProperty('ILOOM_COLOR_HEX')
})
})
})
20 changes: 19 additions & 1 deletion src/commands/dev-server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import path from 'path'
import { GitWorktreeManager } from '../lib/GitWorktreeManager.js'
import { MetadataManager } from '../lib/MetadataManager.js'
import { ProjectCapabilityDetector } from '../lib/ProjectCapabilityDetector.js'
import { DevServerManager } from '../lib/DevServerManager.js'
import { SettingsManager } from '../lib/SettingsManager.js'
Expand Down Expand Up @@ -42,7 +43,8 @@ export class DevServerCommand {
private capabilityDetector = new ProjectCapabilityDetector(),
private identifierParser = new IdentifierParser(new GitWorktreeManager()),
private devServerManager = new DevServerManager(),
private settingsManager = new SettingsManager()
private settingsManager = new SettingsManager(),
private metadataManager = new MetadataManager()
) {}

/**
Expand Down Expand Up @@ -82,6 +84,15 @@ export class DevServerCommand {
}
}

// 3b. Set ILOOM_LOOM for loom identification
envOverrides.ILOOM_LOOM = this.formatLoomIdentifier(parsed)

// 3c. Set ILOOM_COLOR_HEX from loom metadata if available
const metadata = await this.metadataManager.readMetadata(worktree.path)
if (metadata?.colorHex) {
envOverrides.ILOOM_COLOR_HEX = metadata.colorHex
}

// 4. Detect project capabilities
const { capabilities } =
await this.capabilityDetector.detectCapabilities(worktree.path)
Expand Down Expand Up @@ -312,4 +323,11 @@ export class DevServerCommand {
}
return `branch "${parsed.branchName}"${autoLabel}`
}

/**
* Format loom identifier for ILOOM_LOOM env var
*/
private formatLoomIdentifier(parsed: ParsedDevServerInput): string {
return parsed.originalInput
}
}
Loading