From 66a0ca85fabed2a49e79fd5e5bdf717338899812 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 01:17:17 +0000 Subject: [PATCH 1/8] Initial plan From ce24f4ca92ce869ead03d02c554e9ff56f6357e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 01:21:27 +0000 Subject: [PATCH 2/8] Implement CLI bundling with automatic detection Co-authored-by: colindembovsky <1932561+colindembovsky@users.noreply.github.com> --- README.md | 3 +- src/screens/home.tsx | 11 ++- src/services/copilot.ts | 23 ++++- src/utils/cli-locator.test.ts | 28 ++++++ src/utils/cli-locator.ts | 156 ++++++++++++++++++++++++++++++++++ 5 files changed, 218 insertions(+), 3 deletions(-) create mode 100644 src/utils/cli-locator.test.ts create mode 100644 src/utils/cli-locator.ts diff --git a/README.md b/README.md index 9987d3b..b88cc8d 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,6 @@ AI-powered work breakdown and parallel execution TUI. Describe what you want to ## Prerequisites - **Node.js 22+** -- **GitHub Copilot CLI** installed and authenticated (`npm install -g @github/copilot && copilot auth`) ## Quick Start @@ -23,6 +22,8 @@ npm run build npm start # Launch the TUI (home screen) ``` +**Note:** Planeteer bundles the GitHub Copilot CLI automatically. You'll need to authenticate with GitHub Copilot on first use (via GitHub authentication in your browser). + ## Usage ```bash diff --git a/src/screens/home.tsx b/src/screens/home.tsx index 68e2d50..eb8d969 100644 --- a/src/screens/home.tsx +++ b/src/screens/home.tsx @@ -3,7 +3,7 @@ import { Box, Text, useInput } from 'ink'; import SelectInput from 'ink-select-input'; import type { Plan } from '../models/plan.js'; import { listPlans, loadPlan, summarizePlan } from '../services/persistence.js'; -import { fetchModels, getModel, setModel, getModelLabel, type ModelEntry } from '../services/copilot.js'; +import { fetchModels, getModel, setModel, getModelLabel, getCliInfo, type ModelEntry } from '../services/copilot.js'; import StatusBar from '../components/status-bar.js'; interface HomeScreenProps { @@ -23,12 +23,18 @@ export default function HomeScreen({ onNewPlan, onLoadPlan, onExecutePlan, onVal const [commandMode, setCommandMode] = useState(false); const [selectedPlanId, setSelectedPlanId] = useState(null); const [summarized, setSummarized] = useState(''); + const [cliInfo, setCliInfo] = useState<{ version: string; source: 'bundled' | 'system' } | null>(null); React.useEffect(() => { listPlans().then((plans) => { setSavedPlans(plans); setLoaded(true); }); + // Load CLI info asynchronously + const info = getCliInfo(); + if (info) { + setCliInfo(info); + } }, []); const items = [ @@ -113,6 +119,9 @@ export default function HomeScreen({ onNewPlan, onLoadPlan, onExecutePlan, onVal 🌍 Planeteer — AI-powered work breakdown + {cliInfo && ( + [{cliInfo.source} CLI v{cliInfo.version}] + )} {showModelPicker ? ( diff --git a/src/services/copilot.ts b/src/services/copilot.ts index b691e58..aabe584 100644 --- a/src/services/copilot.ts +++ b/src/services/copilot.ts @@ -3,6 +3,7 @@ import { readFile, writeFile, mkdir } from 'node:fs/promises'; import { join } from 'node:path'; import { existsSync } from 'node:fs'; import type { ChatMessage } from '../models/plan.js'; +import { locateCopilotCli } from '../utils/cli-locator.js'; const SETTINGS_PATH = join(process.cwd(), '.planeteer', 'settings.json'); @@ -78,13 +79,33 @@ export function getModelLabel(): string { let client: CopilotClient | null = null; let clientPromise: Promise | null = null; +let cliLocation: { path: string; version: string; source: 'bundled' | 'system' } | null = null; + +/** Get information about the CLI being used. */ +export function getCliInfo(): { path: string; version: string; source: 'bundled' | 'system' } | null { + return cliLocation; +} export async function getClient(): Promise { if (client) return client; if (clientPromise) return clientPromise; clientPromise = (async () => { - const c = new CopilotClient(); + // Locate the Copilot CLI binary (bundled or system) + const location = locateCopilotCli(); + if (!location) { + throw new Error( + 'GitHub Copilot CLI not found. Please install it with: npm install -g @github/copilot' + ); + } + + cliLocation = location; + + // Create client with the located CLI path + const c = new CopilotClient({ + cliPath: location.path, + }); + await c.start(); client = c; return c; diff --git a/src/utils/cli-locator.test.ts b/src/utils/cli-locator.test.ts new file mode 100644 index 0000000..530f0cb --- /dev/null +++ b/src/utils/cli-locator.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from 'vitest'; +import { locateCopilotCli } from './cli-locator.js'; + +describe('cli-locator', () => { + it('should locate a Copilot CLI binary', () => { + const location = locateCopilotCli(); + + // Should find either bundled or system CLI + expect(location).toBeTruthy(); + + if (location) { + expect(location.path).toBeTruthy(); + expect(['bundled', 'system']).toContain(location.source); + expect(location.version).toBeTruthy(); + } + }); + + it('should prefer bundled CLI over system CLI', () => { + const location = locateCopilotCli(); + + if (location) { + // If bundled CLI exists, it should be used first + // We can't guarantee this in all test environments, + // but at least verify the location has valid properties + expect(location.source).toMatch(/^(bundled|system)$/); + } + }); +}); diff --git a/src/utils/cli-locator.ts b/src/utils/cli-locator.ts new file mode 100644 index 0000000..69742e0 --- /dev/null +++ b/src/utils/cli-locator.ts @@ -0,0 +1,156 @@ +import { existsSync } from 'node:fs'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { execSync } from 'node:child_process'; + +export interface CliLocation { + path: string; + version: string; + source: 'bundled' | 'system'; +} + +/** + * Locate the bundled Copilot CLI binary. + * Returns the path if found, otherwise null. + */ +function findBundledCli(): string | null { + try { + // Try to resolve the platform-specific package + const platform = process.platform; + const arch = process.arch; + const packageName = `@github/copilot-${platform}-${arch}`; + + // Attempt to resolve via import.meta.resolve + try { + const resolved = import.meta.resolve(packageName); + const packagePath = fileURLToPath(resolved); + const binaryDir = dirname(packagePath); + const binaryName = platform === 'win32' ? 'copilot.exe' : 'copilot'; + const binaryPath = join(binaryDir, binaryName); + + if (existsSync(binaryPath)) { + return binaryPath; + } + } catch { + // If import.meta.resolve fails, try manual path construction + } + + // Fallback: Construct path from this file's location + // Assuming this file is in src/utils/ and node_modules is at repo root + const currentFile = fileURLToPath(import.meta.url); + const repoRoot = join(dirname(currentFile), '..', '..'); + + // Try node_modules location + const nodeModulesPath = join( + repoRoot, + 'node_modules', + '@github', + `copilot-${platform}-${arch}`, + platform === 'win32' ? 'copilot.exe' : 'copilot' + ); + + if (existsSync(nodeModulesPath)) { + return nodeModulesPath; + } + + // Try prebuilds location (legacy structure) + const prebuildsPath = join( + repoRoot, + 'node_modules', + '@github', + 'copilot', + 'prebuilds', + `${platform}-${arch}`, + platform === 'win32' ? 'copilot.exe' : 'copilot' + ); + + if (existsSync(prebuildsPath)) { + return prebuildsPath; + } + } catch { + // Ignore errors + } + + return null; +} + +/** + * Find the system-installed Copilot CLI. + * Returns the path if found, otherwise null. + */ +function findSystemCli(): string | null { + try { + const command = process.platform === 'win32' ? 'where copilot' : 'which copilot'; + const result = execSync(command, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }); + const path = result.trim().split('\n')[0]; + + if (path && existsSync(path)) { + return path; + } + } catch { + // Ignore errors - CLI not in PATH + } + + return null; +} + +/** + * Get the version of a CLI binary. + */ +function getCliVersion(cliPath: string): string { + try { + const result = execSync(`"${cliPath}" --version`, { + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'ignore'], + timeout: 5000, + }); + + // Parse version from output (e.g., "GitHub Copilot CLI 0.0.403.") + const match = result.match(/(\d+\.\d+\.\d+)/); + return match ? match[1] : 'unknown'; + } catch { + return 'unknown'; + } +} + +/** + * Locate the Copilot CLI binary, checking bundled first, then system. + * Returns null if no CLI is found. + */ +export function locateCopilotCli(): CliLocation | null { + // Try bundled CLI first + const bundledPath = findBundledCli(); + if (bundledPath) { + const version = getCliVersion(bundledPath); + return { path: bundledPath, version, source: 'bundled' }; + } + + // Fallback to system CLI + const systemPath = findSystemCli(); + if (systemPath) { + const version = getCliVersion(systemPath); + return { path: systemPath, version, source: 'system' }; + } + + return null; +} + +/** + * Check if the CLI is authenticated. + * Returns true if authenticated, false otherwise. + */ +export function checkCliAuthentication(cliPath: string): boolean { + try { + // Run a simple command that requires authentication + execSync(`"${cliPath}" --help`, { + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'], + timeout: 5000, + }); + // If --help works, the binary is at least executable + // Authentication check would need SDK connection attempt + return true; + } catch { + return false; + } +} From 060f493852d183092a92eb157d080d4fc0af8591 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 01:23:00 +0000 Subject: [PATCH 3/8] Add CLI info display and improved error handling Co-authored-by: colindembovsky <1932561+colindembovsky@users.noreply.github.com> --- README.md | 37 +++++++++++++++++++++++++++++++++++++ src/services/copilot.ts | 39 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b88cc8d..09fe29d 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,16 @@ npm start # Launch the TUI (home screen) **Note:** Planeteer bundles the GitHub Copilot CLI automatically. You'll need to authenticate with GitHub Copilot on first use (via GitHub authentication in your browser). +## Features + +- **Zero-config CLI**: The GitHub Copilot CLI is bundled with Planeteer — no separate installation needed +- **Automatic fallback**: Uses bundled CLI by default, falls back to system-installed CLI if available +- **Version detection**: Displays CLI version and source (bundled vs. system) on the home screen +- **Multi-turn clarification**: Copilot asks questions to understand your project requirements +- **Smart work breakdown**: Generates tasks with acceptance criteria and dependency graphs +- **Parallel execution**: Runs independent tasks simultaneously via Copilot agents +- **Progress tracking**: Real-time updates on task execution with event logs + ## Usage ```bash @@ -158,6 +168,32 @@ Plans are saved to `.planeteer/` in the current working directory: - `.json` — Machine-readable plan (used by the app) - `.md` — Human-readable Markdown export +## Troubleshooting + +### CLI Not Found + +If you see "GitHub Copilot CLI not found" errors: + +1. **Reinstall dependencies**: `npm install` (ensures bundled CLI is installed) +2. **Check platform support**: The bundled CLI supports Linux (x64, arm64), macOS (x64, arm64), and Windows (x64, arm64) +3. **Manual installation**: As a fallback, install globally with `npm install -g @github/copilot` + +### Authentication Issues + +The Copilot CLI requires GitHub authentication: + +1. On first use, you'll be prompted to authenticate via your browser +2. You need an active GitHub Copilot subscription +3. If authentication fails, try running `copilot auth` in your terminal + +### CLI Version Mismatch + +If you experience issues with the bundled CLI: + +- Check the CLI version on the home screen (displays as `[bundled CLI vX.X.X]`) +- The bundled version is tied to `@github/copilot-sdk` version in package.json +- To use a newer CLI, install it globally: `npm install -g @github/copilot` + ## Project Structure ``` @@ -181,6 +217,7 @@ src/ ├── models/ │ └── plan.ts # Types: Plan, Task, ChatMessage └── utils/ + ├── cli-locator.ts # Locate bundled/system Copilot CLI ├── dependency-graph.ts # Topological sort & cycle detection └── markdown.ts # Plan → Markdown renderer ``` diff --git a/src/services/copilot.ts b/src/services/copilot.ts index aabe584..37c9d97 100644 --- a/src/services/copilot.ts +++ b/src/services/copilot.ts @@ -81,8 +81,22 @@ let client: CopilotClient | null = null; let clientPromise: Promise | null = null; let cliLocation: { path: string; version: string; source: 'bundled' | 'system' } | null = null; +/** Initialize CLI location info early (doesn't start the client). */ +export function initCliInfo(): void { + if (!cliLocation) { + const location = locateCopilotCli(); + if (location) { + cliLocation = location; + } + } +} + /** Get information about the CLI being used. */ export function getCliInfo(): { path: string; version: string; source: 'bundled' | 'system' } | null { + // Initialize on first access if not already done + if (!cliLocation) { + initCliInfo(); + } return cliLocation; } @@ -95,7 +109,12 @@ export async function getClient(): Promise { const location = locateCopilotCli(); if (!location) { throw new Error( - 'GitHub Copilot CLI not found. Please install it with: npm install -g @github/copilot' + 'GitHub Copilot CLI not found.\n\n' + + 'The bundled CLI should be automatically available, but it appears to be missing.\n' + + 'Please try:\n' + + ' 1. Reinstalling dependencies: npm install\n' + + ' 2. Installing the CLI globally: npm install -g @github/copilot\n\n' + + 'If the problem persists, please report this issue.' ); } @@ -106,7 +125,23 @@ export async function getClient(): Promise { cliPath: location.path, }); - await c.start(); + try { + await c.start(); + } catch (err) { + const message = (err as Error).message || 'Unknown error'; + throw new Error( + `Failed to start GitHub Copilot CLI.\n\n` + + `Error: ${message}\n\n` + + `The CLI was found at: ${location.path}\n` + + `Version: ${location.version}\n` + + `Source: ${location.source}\n\n` + + `Please ensure you have:\n` + + ` 1. Authenticated with GitHub Copilot (the CLI will prompt you)\n` + + ` 2. Active GitHub Copilot subscription\n` + + ` 3. Proper permissions to execute the CLI binary` + ); + } + client = c; return c; })(); From e19a226e7e698ccdc6f3ad72308dc6e526e89907 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 01:24:41 +0000 Subject: [PATCH 4/8] Address code review feedback: rename function, extract CliInfo type, fix comment Co-authored-by: colindembovsky <1932561+colindembovsky@users.noreply.github.com> --- src/screens/home.tsx | 3 ++- src/services/copilot.ts | 6 +++--- src/utils/cli-locator.ts | 17 ++++++++--------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/screens/home.tsx b/src/screens/home.tsx index eb8d969..41dcee4 100644 --- a/src/screens/home.tsx +++ b/src/screens/home.tsx @@ -4,6 +4,7 @@ import SelectInput from 'ink-select-input'; import type { Plan } from '../models/plan.js'; import { listPlans, loadPlan, summarizePlan } from '../services/persistence.js'; import { fetchModels, getModel, setModel, getModelLabel, getCliInfo, type ModelEntry } from '../services/copilot.js'; +import type { CliInfo } from '../utils/cli-locator.js'; import StatusBar from '../components/status-bar.js'; interface HomeScreenProps { @@ -23,7 +24,7 @@ export default function HomeScreen({ onNewPlan, onLoadPlan, onExecutePlan, onVal const [commandMode, setCommandMode] = useState(false); const [selectedPlanId, setSelectedPlanId] = useState(null); const [summarized, setSummarized] = useState(''); - const [cliInfo, setCliInfo] = useState<{ version: string; source: 'bundled' | 'system' } | null>(null); + const [cliInfo, setCliInfo] = useState(null); React.useEffect(() => { listPlans().then((plans) => { diff --git a/src/services/copilot.ts b/src/services/copilot.ts index 37c9d97..7dbcb90 100644 --- a/src/services/copilot.ts +++ b/src/services/copilot.ts @@ -3,7 +3,7 @@ import { readFile, writeFile, mkdir } from 'node:fs/promises'; import { join } from 'node:path'; import { existsSync } from 'node:fs'; import type { ChatMessage } from '../models/plan.js'; -import { locateCopilotCli } from '../utils/cli-locator.js'; +import { locateCopilotCli, type CliInfo } from '../utils/cli-locator.js'; const SETTINGS_PATH = join(process.cwd(), '.planeteer', 'settings.json'); @@ -79,7 +79,7 @@ export function getModelLabel(): string { let client: CopilotClient | null = null; let clientPromise: Promise | null = null; -let cliLocation: { path: string; version: string; source: 'bundled' | 'system' } | null = null; +let cliLocation: CliInfo | null = null; /** Initialize CLI location info early (doesn't start the client). */ export function initCliInfo(): void { @@ -92,7 +92,7 @@ export function initCliInfo(): void { } /** Get information about the CLI being used. */ -export function getCliInfo(): { path: string; version: string; source: 'bundled' | 'system' } | null { +export function getCliInfo(): CliInfo | null { // Initialize on first access if not already done if (!cliLocation) { initCliInfo(); diff --git a/src/utils/cli-locator.ts b/src/utils/cli-locator.ts index 69742e0..698c98f 100644 --- a/src/utils/cli-locator.ts +++ b/src/utils/cli-locator.ts @@ -3,7 +3,7 @@ import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { execSync } from 'node:child_process'; -export interface CliLocation { +export interface CliInfo { path: string; version: string; source: 'bundled' | 'system'; @@ -105,7 +105,7 @@ function getCliVersion(cliPath: string): string { timeout: 5000, }); - // Parse version from output (e.g., "GitHub Copilot CLI 0.0.403.") + // Parse version from output (e.g., "GitHub Copilot CLI 0.0.403") const match = result.match(/(\d+\.\d+\.\d+)/); return match ? match[1] : 'unknown'; } catch { @@ -117,7 +117,7 @@ function getCliVersion(cliPath: string): string { * Locate the Copilot CLI binary, checking bundled first, then system. * Returns null if no CLI is found. */ -export function locateCopilotCli(): CliLocation | null { +export function locateCopilotCli(): CliInfo | null { // Try bundled CLI first const bundledPath = findBundledCli(); if (bundledPath) { @@ -136,19 +136,18 @@ export function locateCopilotCli(): CliLocation | null { } /** - * Check if the CLI is authenticated. - * Returns true if authenticated, false otherwise. + * Check if the CLI binary is executable. + * Returns true if the binary can be executed, false otherwise. + * Note: This does NOT verify authentication status. */ -export function checkCliAuthentication(cliPath: string): boolean { +export function checkCliExecutable(cliPath: string): boolean { try { - // Run a simple command that requires authentication + // Run a simple command to verify the binary is executable execSync(`"${cliPath}" --help`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000, }); - // If --help works, the binary is at least executable - // Authentication check would need SDK connection attempt return true; } catch { return false; From 68215bfe08879c0b0d4b79ab9b8214ae17827f23 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 01:26:43 +0000 Subject: [PATCH 5/8] Fix command injection vulnerability by using execFileSync Co-authored-by: colindembovsky <1932561+colindembovsky@users.noreply.github.com> --- src/utils/cli-locator.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/utils/cli-locator.ts b/src/utils/cli-locator.ts index 698c98f..749e651 100644 --- a/src/utils/cli-locator.ts +++ b/src/utils/cli-locator.ts @@ -1,7 +1,7 @@ import { existsSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { execSync } from 'node:child_process'; +import { execFileSync, execSync } from 'node:child_process'; export interface CliInfo { path: string; @@ -99,9 +99,8 @@ function findSystemCli(): string | null { */ function getCliVersion(cliPath: string): string { try { - const result = execSync(`"${cliPath}" --version`, { + const result = execFileSync(cliPath, ['--version'], { encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'ignore'], timeout: 5000, }); @@ -143,9 +142,8 @@ export function locateCopilotCli(): CliInfo | null { export function checkCliExecutable(cliPath: string): boolean { try { // Run a simple command to verify the binary is executable - execSync(`"${cliPath}" --help`, { + execFileSync(cliPath, ['--help'], { encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000, }); return true; From 2b2f4d5aaded2ed94cabafc3f1cf25cf06ff104f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 01:27:54 +0000 Subject: [PATCH 6/8] Fix remaining command injection vulnerability and improve test clarity Co-authored-by: colindembovsky <1932561+colindembovsky@users.noreply.github.com> --- src/utils/cli-locator.test.ts | 8 ++++---- src/utils/cli-locator.ts | 8 +++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/utils/cli-locator.test.ts b/src/utils/cli-locator.test.ts index 530f0cb..7d1cac0 100644 --- a/src/utils/cli-locator.test.ts +++ b/src/utils/cli-locator.test.ts @@ -15,14 +15,14 @@ describe('cli-locator', () => { } }); - it('should prefer bundled CLI over system CLI', () => { + it('should return valid CLI info when found', () => { const location = locateCopilotCli(); if (location) { - // If bundled CLI exists, it should be used first - // We can't guarantee this in all test environments, - // but at least verify the location has valid properties + // Verify the location has valid properties expect(location.source).toMatch(/^(bundled|system)$/); + expect(location.path).toBeTruthy(); + expect(location.version).toBeTruthy(); } }); }); diff --git a/src/utils/cli-locator.ts b/src/utils/cli-locator.ts index 749e651..802cc56 100644 --- a/src/utils/cli-locator.ts +++ b/src/utils/cli-locator.ts @@ -1,7 +1,7 @@ import { existsSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { execFileSync, execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; export interface CliInfo { path: string; @@ -80,8 +80,10 @@ function findBundledCli(): string | null { */ function findSystemCli(): string | null { try { - const command = process.platform === 'win32' ? 'where copilot' : 'which copilot'; - const result = execSync(command, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }); + const executable = process.platform === 'win32' ? 'where' : 'which'; + const result = execFileSync(executable, ['copilot'], { + encoding: 'utf-8', + }); const path = result.trim().split('\n')[0]; if (path && existsSync(path)) { From 128db986593d395ae558fe262b32275f06cb36fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 01:29:12 +0000 Subject: [PATCH 7/8] Optimize CLI location caching to avoid redundant file system operations Co-authored-by: colindembovsky <1932561+colindembovsky@users.noreply.github.com> --- src/screens/home.tsx | 5 +---- src/services/copilot.ts | 18 +++++++++--------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/screens/home.tsx b/src/screens/home.tsx index 41dcee4..7dab0c3 100644 --- a/src/screens/home.tsx +++ b/src/screens/home.tsx @@ -32,10 +32,7 @@ export default function HomeScreen({ onNewPlan, onLoadPlan, onExecutePlan, onVal setLoaded(true); }); // Load CLI info asynchronously - const info = getCliInfo(); - if (info) { - setCliInfo(info); - } + setCliInfo(getCliInfo()); }, []); const items = [ diff --git a/src/services/copilot.ts b/src/services/copilot.ts index 7dbcb90..4e55250 100644 --- a/src/services/copilot.ts +++ b/src/services/copilot.ts @@ -105,9 +105,11 @@ export async function getClient(): Promise { if (clientPromise) return clientPromise; clientPromise = (async () => { - // Locate the Copilot CLI binary (bundled or system) - const location = locateCopilotCli(); - if (!location) { + // Initialize CLI location if not already done + initCliInfo(); + + // Use the cached location + if (!cliLocation) { throw new Error( 'GitHub Copilot CLI not found.\n\n' + 'The bundled CLI should be automatically available, but it appears to be missing.\n' + @@ -118,11 +120,9 @@ export async function getClient(): Promise { ); } - cliLocation = location; - // Create client with the located CLI path const c = new CopilotClient({ - cliPath: location.path, + cliPath: cliLocation.path, }); try { @@ -132,9 +132,9 @@ export async function getClient(): Promise { throw new Error( `Failed to start GitHub Copilot CLI.\n\n` + `Error: ${message}\n\n` + - `The CLI was found at: ${location.path}\n` + - `Version: ${location.version}\n` + - `Source: ${location.source}\n\n` + + `The CLI was found at: ${cliLocation.path}\n` + + `Version: ${cliLocation.version}\n` + + `Source: ${cliLocation.source}\n\n` + `Please ensure you have:\n` + ` 1. Authenticated with GitHub Copilot (the CLI will prompt you)\n` + ` 2. Active GitHub Copilot subscription\n` + From eaa681b74cec87a801044de7f64eabfad0b1b010 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 20:48:25 +0000 Subject: [PATCH 8/8] Address PR review feedback: preserve error stack, add timeout, fix tests, defer CLI lookup Co-authored-by: colindembovsky <1932561+colindembovsky@users.noreply.github.com> --- src/screens/home.tsx | 6 ++++-- src/services/copilot.ts | 6 ++++-- src/utils/cli-locator.test.ts | 18 ++++++++++-------- src/utils/cli-locator.ts | 7 +++++-- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/screens/home.tsx b/src/screens/home.tsx index 7dab0c3..84001ae 100644 --- a/src/screens/home.tsx +++ b/src/screens/home.tsx @@ -31,8 +31,10 @@ export default function HomeScreen({ onNewPlan, onLoadPlan, onExecutePlan, onVal setSavedPlans(plans); setLoaded(true); }); - // Load CLI info asynchronously - setCliInfo(getCliInfo()); + // Load CLI info asynchronously (off the initial render path) + Promise.resolve().then(() => { + setCliInfo(getCliInfo()); + }); }, []); const items = [ diff --git a/src/services/copilot.ts b/src/services/copilot.ts index 4e55250..791c6df 100644 --- a/src/services/copilot.ts +++ b/src/services/copilot.ts @@ -129,7 +129,7 @@ export async function getClient(): Promise { await c.start(); } catch (err) { const message = (err as Error).message || 'Unknown error'; - throw new Error( + const enhancedError = new Error( `Failed to start GitHub Copilot CLI.\n\n` + `Error: ${message}\n\n` + `The CLI was found at: ${cliLocation.path}\n` + @@ -138,8 +138,10 @@ export async function getClient(): Promise { `Please ensure you have:\n` + ` 1. Authenticated with GitHub Copilot (the CLI will prompt you)\n` + ` 2. Active GitHub Copilot subscription\n` + - ` 3. Proper permissions to execute the CLI binary` + ` 3. Proper permissions to execute the CLI binary`, + { cause: err } ); + throw enhancedError; } client = c; diff --git a/src/utils/cli-locator.test.ts b/src/utils/cli-locator.test.ts index 7d1cac0..0f3ae4a 100644 --- a/src/utils/cli-locator.test.ts +++ b/src/utils/cli-locator.test.ts @@ -2,27 +2,29 @@ import { describe, it, expect } from 'vitest'; import { locateCopilotCli } from './cli-locator.js'; describe('cli-locator', () => { - it('should locate a Copilot CLI binary', () => { + it('should return null or valid CLI info', () => { const location = locateCopilotCli(); - // Should find either bundled or system CLI - expect(location).toBeTruthy(); - + // Location may be null if CLI is not available (e.g., CI with --omit=optional) if (location) { expect(location.path).toBeTruthy(); expect(['bundled', 'system']).toContain(location.source); expect(location.version).toBeTruthy(); + } else { + // If no CLI is found, location should be null + expect(location).toBeNull(); } }); - it('should return valid CLI info when found', () => { + it('should return valid structure when CLI is found', () => { const location = locateCopilotCli(); + // Only validate structure if a CLI was found if (location) { - // Verify the location has valid properties + expect(location).toHaveProperty('path'); + expect(location).toHaveProperty('version'); + expect(location).toHaveProperty('source'); expect(location.source).toMatch(/^(bundled|system)$/); - expect(location.path).toBeTruthy(); - expect(location.version).toBeTruthy(); } }); }); diff --git a/src/utils/cli-locator.ts b/src/utils/cli-locator.ts index 802cc56..f3c2ecb 100644 --- a/src/utils/cli-locator.ts +++ b/src/utils/cli-locator.ts @@ -83,14 +83,17 @@ function findSystemCli(): string | null { const executable = process.platform === 'win32' ? 'where' : 'which'; const result = execFileSync(executable, ['copilot'], { encoding: 'utf-8', + // Prevent PATH lookups from hanging indefinitely + timeout: 2000, }); const path = result.trim().split('\n')[0]; if (path && existsSync(path)) { return path; } - } catch { - // Ignore errors - CLI not in PATH + } catch (error) { + // On timeout or lookup errors, treat as "CLI not in PATH" + return null; } return null;