headless-query.ts imported extension modules with .js extensions, but those files only exist as .ts (never compiled). Other code paths work because they go through the extension loader's jiti setup, but headless-query bypasses that as a performance optimization. Fix: use createJiti() to dynamically import the 4 extension modules, matching the pattern used by the extension loader. The modules are loaded lazily in handleQuery() so the jiti overhead only applies when the query command is actually used. Fixes #1137
113 lines
4.1 KiB
TypeScript
113 lines
4.1 KiB
TypeScript
/**
|
|
* Headless Query — `gsd headless query`
|
|
*
|
|
* Single read-only command that returns the full project snapshot as JSON
|
|
* to stdout, without spawning an LLM session. Instant (~50ms).
|
|
*
|
|
* Output: { state, next, cost }
|
|
* state — deriveState() output (phase, milestones, progress, blockers)
|
|
* next — dry-run dispatch preview (what auto-mode would do next)
|
|
* cost — aggregated parallel worker costs
|
|
*
|
|
* Note: Extension modules are .ts files loaded via jiti (not compiled to .js).
|
|
* We use createJiti() here because this module is imported directly from cli.ts,
|
|
* bypassing the extension loader's jiti setup (#1137).
|
|
*/
|
|
|
|
import { createJiti } from '@mariozechner/jiti'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { dirname, join } from 'node:path'
|
|
import type { GSDState } from './resources/extensions/gsd/types.js'
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
const jiti = createJiti(fileURLToPath(import.meta.url), { interopDefault: true, debug: false })
|
|
|
|
async function loadExtensionModules() {
|
|
const stateModule = await jiti.import(join(__dirname, 'resources/extensions/gsd/state.ts'), {}) as any
|
|
const dispatchModule = await jiti.import(join(__dirname, 'resources/extensions/gsd/auto-dispatch.ts'), {}) as any
|
|
const sessionModule = await jiti.import(join(__dirname, 'resources/extensions/gsd/session-status-io.ts'), {}) as any
|
|
const prefsModule = await jiti.import(join(__dirname, 'resources/extensions/gsd/preferences.ts'), {}) as any
|
|
return {
|
|
deriveState: stateModule.deriveState as (basePath: string) => Promise<GSDState>,
|
|
resolveDispatch: dispatchModule.resolveDispatch as (opts: any) => Promise<any>,
|
|
readAllSessionStatuses: sessionModule.readAllSessionStatuses as (basePath: string) => any[],
|
|
loadEffectiveGSDPreferences: prefsModule.loadEffectiveGSDPreferences as () => any,
|
|
}
|
|
}
|
|
|
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
|
|
export interface QuerySnapshot {
|
|
state: GSDState
|
|
next: {
|
|
action: 'dispatch' | 'stop' | 'skip'
|
|
unitType?: string
|
|
unitId?: string
|
|
reason?: string
|
|
}
|
|
cost: {
|
|
workers: Array<{
|
|
milestoneId: string
|
|
pid: number
|
|
state: string
|
|
cost: number
|
|
lastHeartbeat: number
|
|
}>
|
|
total: number
|
|
}
|
|
}
|
|
|
|
export interface QueryResult {
|
|
exitCode: number
|
|
data?: QuerySnapshot
|
|
}
|
|
|
|
// ─── Implementation ─────────────────────────────────────────────────────────
|
|
|
|
export async function handleQuery(basePath: string): Promise<QueryResult> {
|
|
const { deriveState, resolveDispatch, readAllSessionStatuses, loadEffectiveGSDPreferences } = await loadExtensionModules()
|
|
const state = await deriveState(basePath)
|
|
|
|
// Derive next dispatch action
|
|
let next: QuerySnapshot['next']
|
|
if (!state.activeMilestone) {
|
|
next = {
|
|
action: 'stop',
|
|
reason: state.phase === 'complete' ? 'All milestones complete.' : state.nextAction,
|
|
}
|
|
} else {
|
|
const loaded = loadEffectiveGSDPreferences()
|
|
const dispatch = await resolveDispatch({
|
|
basePath,
|
|
mid: state.activeMilestone.id,
|
|
midTitle: state.activeMilestone.title,
|
|
state,
|
|
prefs: loaded?.preferences,
|
|
})
|
|
next = {
|
|
action: dispatch.action,
|
|
unitType: dispatch.action === 'dispatch' ? dispatch.unitType : undefined,
|
|
unitId: dispatch.action === 'dispatch' ? dispatch.unitId : undefined,
|
|
reason: dispatch.action === 'stop' ? dispatch.reason : undefined,
|
|
}
|
|
}
|
|
|
|
// Aggregate parallel worker costs
|
|
const statuses = readAllSessionStatuses(basePath)
|
|
const workers = statuses.map((s) => ({
|
|
milestoneId: s.milestoneId,
|
|
pid: s.pid,
|
|
state: s.state,
|
|
cost: s.cost,
|
|
lastHeartbeat: s.lastHeartbeat,
|
|
}))
|
|
|
|
const snapshot: QuerySnapshot = {
|
|
state,
|
|
next,
|
|
cost: { workers, total: workers.reduce((sum, w) => sum + w.cost, 0) },
|
|
}
|
|
|
|
process.stdout.write(JSON.stringify(snapshot) + '\n')
|
|
return { exitCode: 0, data: snapshot }
|
|
}
|