* fix: apply pi manifest opt-out to extension-discovery.ts (#1537 follow-up) The cmux fix in #1537 patched resolveExtensionEntries() in packages/pi-coding-agent/src/core/extensions/loader.ts to honor "pi": {} as an opt-out from auto-discovery. However, there is a second copy of resolveExtensionEntries() in src/extension-discovery.ts that was not updated. This is the version actually used at startup by loader.js via discoverExtensionEntryPaths(). As a result, cmux/index.js is still discovered and loaded as an extension on startup, producing: Extension does not export a valid factory function: .../cmux/index.js Fix: Apply the same authoritative-manifest logic to the extension-discovery.ts copy. When a package.json has a "pi" field, treat it as authoritative and return early — either with declared extension paths or an empty array for library opt-out. Tests: 7 new tests covering resolveExtensionEntries and discoverExtensionEntryPaths behavior for opt-out, declared extensions, and fallback discovery. * fix: apply pi manifest opt-out to package-manager.ts (third copy) There are THREE copies of resolveExtensionEntries(): 1. packages/pi-coding-agent/src/core/extensions/loader.ts (fixed in #1537) 2. src/extension-discovery.ts (fixed in previous commit) 3. packages/pi-coding-agent/src/core/package-manager.ts (THIS commit) Copy #3 is used by collectAutoExtensionEntries() which is called from addAutoDiscoveredResources() during DefaultPackageManager.resolve(). This is the actual code path that discovers ~/.gsd/agent/extensions/cmux and passes it to loadExtensions(), producing the factory function error. * fix: rewrite pi.extensions .ts paths to .js during resource copy copy-resources.cjs compiles .ts → .js via tsc but copies package.json files verbatim. Extensions with pi.extensions: ["./index.ts"] end up in dist/ pointing to a .ts file that doesn't exist (only .js does). This causes resolveExtensionEntries() to find no valid entry points, silently skipping the extension. Affected: gsd, browser-tools, context7, google-search, universal-config — all extensions with pi manifests. Fix: When copying package.json files, rewrite .ts/.tsx extensions in pi.extensions arrays to .js so they match the compiled output. * fix: add missing commands to /gsd description and rate sub-completions - Add 9 missing commands to the description string: widget, rate, park, unpark, init, setup, logs, inspect, extensions - Add sub-completions for /gsd rate (over/ok/under) * feat: grid layout for parallel cmux splits and completion trailing-space fix CmuxClient.createGridLayout(count) pre-creates a tiled grid of surfaces before launching parallel agents, instead of the previous approach of creating splits per-agent with alternating right/down directions. Grid layout strategy: 1 agent: [gsd | A] 2 agents: [gsd | A] (A split down) [ | B] 3 agents: [gsd | A] (2x2 grid) [ C | B] 4 agents: [gsd | A] (additional splits from bottom-right) [ C | B] [ | D] Changes: - Add CmuxClient.createSplitFrom(sourceSurfaceId, direction) to split from a specific surface rather than always the gsd surface - Add CmuxClient.createGridLayout(count) that builds the grid and returns surface IDs in order - Update runSingleAgentInCmuxSplit to accept a pre-created surface ID (string) or a direction for backward compatibility - Parallel dispatch pre-creates grid, assigns each agent a surface - Fix getArgumentCompletions trailing-space handling so sub-completions work (e.g., /gsd cmux <tab> now shows status/on/off/etc.) - 5 new tests for grid layout logic
80 lines
2.7 KiB
TypeScript
80 lines
2.7 KiB
TypeScript
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
import { join, resolve } from 'node:path'
|
|
|
|
function isExtensionFile(name: string): boolean {
|
|
return name.endsWith('.ts') || name.endsWith('.js')
|
|
}
|
|
|
|
/**
|
|
* Resolves the entry-point file(s) for a single extension directory.
|
|
*
|
|
* 1. If the directory contains a package.json with a `pi` manifest object,
|
|
* the manifest is authoritative:
|
|
* - `pi.extensions` array → resolve each entry relative to the directory.
|
|
* - `pi: {}` (no extensions) → return empty (library opt-out, e.g. cmux).
|
|
* 2. Only when no `pi` manifest exists does it fall back to `index.ts` → `index.js`.
|
|
*/
|
|
export function resolveExtensionEntries(dir: string): string[] {
|
|
const packageJsonPath = join(dir, 'package.json')
|
|
if (existsSync(packageJsonPath)) {
|
|
try {
|
|
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
|
|
if (pkg?.pi && typeof pkg.pi === 'object') {
|
|
// When a pi manifest exists, it is authoritative — don't fall through
|
|
// to index.ts/index.js auto-detection. This allows library directories
|
|
// (like cmux) to opt out by declaring "pi": {} with no extensions.
|
|
const declared = pkg.pi.extensions
|
|
if (!Array.isArray(declared) || declared.length === 0) {
|
|
return []
|
|
}
|
|
return declared
|
|
.filter((entry: unknown): entry is string => typeof entry === 'string')
|
|
.map((entry: string) => resolve(dir, entry))
|
|
.filter((entry: string) => existsSync(entry))
|
|
}
|
|
} catch {
|
|
// Ignore malformed manifests and fall back to index.ts/index.js discovery.
|
|
}
|
|
}
|
|
|
|
const indexTs = join(dir, 'index.ts')
|
|
if (existsSync(indexTs)) {
|
|
return [indexTs]
|
|
}
|
|
|
|
const indexJs = join(dir, 'index.js')
|
|
if (existsSync(indexJs)) {
|
|
return [indexJs]
|
|
}
|
|
|
|
return []
|
|
}
|
|
|
|
/**
|
|
* Discovers all extension entry-point paths under an extensions directory.
|
|
*
|
|
* - Top-level .ts/.js files are treated as standalone extension entry points.
|
|
* - Subdirectories are resolved via `resolveExtensionEntries()` (package.json →
|
|
* pi.extensions, then index.ts/index.js fallback).
|
|
*/
|
|
export function discoverExtensionEntryPaths(extensionsDir: string): string[] {
|
|
if (!existsSync(extensionsDir)) {
|
|
return []
|
|
}
|
|
|
|
const discovered: string[] = []
|
|
for (const entry of readdirSync(extensionsDir, { withFileTypes: true })) {
|
|
const entryPath = join(extensionsDir, entry.name)
|
|
|
|
if ((entry.isFile() || entry.isSymbolicLink()) && isExtensionFile(entry.name)) {
|
|
discovered.push(entryPath)
|
|
continue
|
|
}
|
|
|
|
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
discovered.push(...resolveExtensionEntries(entryPath))
|
|
}
|
|
}
|
|
|
|
return discovered
|
|
}
|