From b4861770660529c0ecec14bbae9ffeaf5555984d Mon Sep 17 00:00:00 2001 From: Iouri Goussev Date: Sat, 21 Mar 2026 13:48:32 -0400 Subject: [PATCH] refactor: split shared/mod.ts into pure and TUI-dependent barrels (#1807) --- .../extensions/ask-user-questions.ts | 2 +- .../extensions/get-secrets-from-user.ts | 5 ++- .../extensions/gsd/auto-dashboard.ts | 3 +- .../extensions/gsd/commands/context.ts | 2 +- .../extensions/gsd/guided-flow-queue.ts | 2 +- src/resources/extensions/gsd/guided-flow.ts | 4 +- src/resources/extensions/gsd/init-wizard.ts | 2 +- .../extensions/gsd/migrate/command.ts | 2 +- .../extensions/gsd/queue-reorder-ui.ts | 3 +- .../gsd/tests/lazy-pi-tui-import.test.ts | 41 +++---------------- src/resources/extensions/gsd/triage-ui.ts | 2 +- .../extensions/gsd/worktree-command.ts | 2 +- src/resources/extensions/shared/mod.ts | 5 --- src/resources/extensions/shared/tui.ts | 11 +++++ src/resources/extensions/shared/ui.ts | 19 +-------- .../slash-commands/create-extension.ts | 2 +- .../slash-commands/create-slash-command.ts | 2 +- 17 files changed, 35 insertions(+), 74 deletions(-) create mode 100644 src/resources/extensions/shared/tui.ts diff --git a/src/resources/extensions/ask-user-questions.ts b/src/resources/extensions/ask-user-questions.ts index b14697667..c227c1ad4 100644 --- a/src/resources/extensions/ask-user-questions.ts +++ b/src/resources/extensions/ask-user-questions.ts @@ -18,7 +18,7 @@ import { type Question, type QuestionOption, type RoundResult, -} from "./shared/mod.js"; +} from "./shared/tui.js"; // ─── Types ──────────────────────────────────────────────────────────────────── diff --git a/src/resources/extensions/get-secrets-from-user.ts b/src/resources/extensions/get-secrets-from-user.ts index e80c0c0db..9ff6cbb03 100644 --- a/src/resources/extensions/get-secrets-from-user.ts +++ b/src/resources/extensions/get-secrets-from-user.ts @@ -13,7 +13,8 @@ import { resolve } from "node:path"; import type { ExtensionAPI, Theme } from "@gsd/pi-coding-agent"; import { Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth, wrapTextWithAnsi } from "@gsd/pi-tui"; import { Type } from "@sinclair/typebox"; -import { makeUI, maskEditorLine, type ProgressStatus } from "./shared/mod.js"; +import { makeUI } from "./shared/tui.js"; +import { maskEditorLine, type ProgressStatus } from "./shared/mod.js"; import { parseSecretsManifest, formatSecretsManifest } from "./gsd/files.js"; import { resolveMilestoneFile } from "./gsd/paths.js"; import type { SecretsManifestEntry } from "./gsd/types.js"; @@ -234,7 +235,7 @@ export async function showSecretsSummary( const existingSet = new Set(existingKeys); - await ctx.ui.custom((tui: any, theme: Theme, _kb: any, done: (r: null) => void) => { + await ctx.ui.custom((_tui: any, theme: Theme, _kb: any, done: (r: null) => void) => { let cachedLines: string[] | undefined; function handleInput(_data: string) { diff --git a/src/resources/extensions/gsd/auto-dashboard.ts b/src/resources/extensions/gsd/auto-dashboard.ts index 85f06ca44..65146c3f7 100644 --- a/src/resources/extensions/gsd/auto-dashboard.ts +++ b/src/resources/extensions/gsd/auto-dashboard.ts @@ -19,7 +19,8 @@ import { parseRoadmap, parsePlan } from "./files.js"; import { readFileSync, writeFileSync, existsSync } from "node:fs"; import { execFileSync } from "node:child_process"; import { truncateToWidth, visibleWidth } from "@gsd/pi-tui"; -import { makeUI, GLYPH, INDENT } from "../shared/mod.js"; +import { makeUI } from "../shared/tui.js"; +import { GLYPH, INDENT } from "../shared/mod.js"; import { computeProgressScore } from "./progress-score.js"; import { getActiveWorktreeName } from "./worktree-command.js"; import { loadEffectiveGSDPreferences, getGlobalGSDPreferencesPath } from "./preferences.js"; diff --git a/src/resources/extensions/gsd/commands/context.ts b/src/resources/extensions/gsd/commands/context.ts index b8c95e608..c098b285d 100644 --- a/src/resources/extensions/gsd/commands/context.ts +++ b/src/resources/extensions/gsd/commands/context.ts @@ -3,7 +3,7 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent import { checkRemoteAutoSession, isAutoActive, isAutoPaused, stopAutoRemote } from "../auto.js"; import { assertSafeDirectory } from "../validate-directory.js"; import { resolveProjectRoot } from "../worktree.js"; -import { showNextAction } from "../../shared/mod.js"; +import { showNextAction } from "../../shared/tui.js"; import { handleStatus } from "./handlers/core.js"; export interface GsdDispatchContext { diff --git a/src/resources/extensions/gsd/guided-flow-queue.ts b/src/resources/extensions/gsd/guided-flow-queue.ts index 619690a83..929a74428 100644 --- a/src/resources/extensions/gsd/guided-flow-queue.ts +++ b/src/resources/extensions/gsd/guided-flow-queue.ts @@ -7,7 +7,7 @@ */ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; -import { showNextAction } from "../shared/mod.js"; +import { showNextAction } from "../shared/tui.js"; import { setQueuePhaseActive } from "./index.js"; import { loadFile } from "./files.js"; import { loadPrompt, inlineTemplate } from "./prompt-loader.js"; diff --git a/src/resources/extensions/gsd/guided-flow.ts b/src/resources/extensions/gsd/guided-flow.ts index 473b0e669..62b32e12d 100644 --- a/src/resources/extensions/gsd/guided-flow.ts +++ b/src/resources/extensions/gsd/guided-flow.ts @@ -7,7 +7,7 @@ */ import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@gsd/pi-coding-agent"; -import { showNextAction } from "../shared/mod.js"; +import { showNextAction } from "../shared/tui.js"; import { loadFile, parseRoadmap } from "./files.js"; import { loadPrompt, inlineTemplate } from "./prompt-loader.js"; import { buildSkillActivationBlock } from "./auto-prompts.js"; @@ -31,7 +31,7 @@ import { loadEffectiveGSDPreferences } from "./preferences.js"; import { detectProjectState } from "./detection.js"; import { showProjectInit, offerMigration } from "./init-wizard.js"; import { validateDirectory } from "./validate-directory.js"; -import { showConfirm } from "../shared/mod.js"; +import { showConfirm } from "../shared/tui.js"; import { debugLog } from "./debug-logger.js"; import { findMilestoneIds, nextMilestoneId, reserveMilestoneId, getReservedMilestoneIds } from "./milestone-ids.js"; import { parkMilestone, discardMilestone } from "./milestone-actions.js"; diff --git a/src/resources/extensions/gsd/init-wizard.ts b/src/resources/extensions/gsd/init-wizard.ts index 555139b81..c83cda4a6 100644 --- a/src/resources/extensions/gsd/init-wizard.ts +++ b/src/resources/extensions/gsd/init-wizard.ts @@ -9,7 +9,7 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs"; import { join } from "node:path"; -import { showNextAction } from "../shared/mod.js"; +import { showNextAction } from "../shared/tui.js"; import { nativeIsRepo, nativeInit } from "./native-git-bridge.js"; import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js"; import { gsdRoot } from "./paths.js"; diff --git a/src/resources/extensions/gsd/migrate/command.ts b/src/resources/extensions/gsd/migrate/command.ts index 233ab61f3..f2567c640 100644 --- a/src/resources/extensions/gsd/migrate/command.ts +++ b/src/resources/extensions/gsd/migrate/command.ts @@ -14,7 +14,7 @@ import { existsSync, readFileSync } from "node:fs"; import { resolve, join, dirname } from "node:path"; import { gsdRoot } from "../paths.js"; import { fileURLToPath } from "node:url"; -import { showNextAction } from "../../shared/mod.js"; +import { showNextAction } from "../../shared/tui.js"; import { validatePlanningDirectory, parsePlanningDirectory, diff --git a/src/resources/extensions/gsd/queue-reorder-ui.ts b/src/resources/extensions/gsd/queue-reorder-ui.ts index e578b20fe..37ff600a1 100644 --- a/src/resources/extensions/gsd/queue-reorder-ui.ts +++ b/src/resources/extensions/gsd/queue-reorder-ui.ts @@ -11,7 +11,8 @@ import type { ExtensionContext } from "@gsd/pi-coding-agent"; import { type Theme } from "@gsd/pi-coding-agent"; import { Key, matchesKey, truncateToWidth, type TUI } from "@gsd/pi-tui"; -import { makeUI, GLYPH } from "../shared/mod.js"; +import { makeUI } from "../shared/tui.js"; +import { GLYPH } from "../shared/mod.js"; import { validateQueueOrder, type DependencyValidation } from "./queue-order.js"; export interface ReorderItem { diff --git a/src/resources/extensions/gsd/tests/lazy-pi-tui-import.test.ts b/src/resources/extensions/gsd/tests/lazy-pi-tui-import.test.ts index c95e26b91..3e03cddda 100644 --- a/src/resources/extensions/gsd/tests/lazy-pi-tui-import.test.ts +++ b/src/resources/extensions/gsd/tests/lazy-pi-tui-import.test.ts @@ -1,8 +1,5 @@ -// Verifies that shared/ui.ts does NOT eagerly import @gsd/pi-tui at the -// module level. An eager top-level import causes /exit (and any other -// command that transitively loads shared/mod → shared/ui) to blow up when -// @gsd/pi-tui cannot be resolved — e.g. extensions copied to -// ~/.gsd/agent/extensions/ where no node_modules tree exists. +// Structural contract: shared/mod.ts must never import @gsd/pi-tui. +// TUI-dependent exports live in shared/tui.ts instead. import test from "node:test"; import assert from "node:assert/strict"; @@ -11,36 +8,8 @@ import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); -const uiSrc = readFileSync(join(__dirname, "../../shared/ui.ts"), "utf-8"); -test("shared/ui.ts has no top-level import from @gsd/pi-tui", () => { - // Match lines like: import { ... } from "@gsd/pi-tui"; - // But ignore type-only imports (import type / import("@gsd/pi-tui").X) - // and comments. - const lines = uiSrc.split("\n"); - for (const line of lines) { - const trimmed = line.trim(); - // Skip comments and type-only references - if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) continue; - // Skip type-only import statements - if (trimmed.startsWith("import type ")) continue; - // Skip inline import() type annotations (erased at runtime) - if (/import\(["']@gsd\/pi-tui["']\)/.test(trimmed) && !trimmed.startsWith("import ")) continue; - - // Flag any eager import statement pulling runtime values from @gsd/pi-tui - if (/^\s*import\s+\{/.test(line) && line.includes("@gsd/pi-tui")) { - assert.fail( - `Found eager top-level import from @gsd/pi-tui — this must be lazy.\n` + - `Line: ${trimmed}`, - ); - } - } -}); - -test("shared/ui.ts lazily resolves @gsd/pi-tui inside makeUI", () => { - // The lazy accessor pattern: require("@gsd/pi-tui") inside a function body - assert.ok( - uiSrc.includes('require("@gsd/pi-tui")'), - "Expected a lazy require(\"@gsd/pi-tui\") call inside a function body", - ); +test("shared/mod.ts has no import from @gsd/pi-tui", () => { + const src = readFileSync(join(__dirname, "../../shared/mod.ts"), "utf-8"); + assert.ok(!src.includes("@gsd/pi-tui"), "mod.ts must not import @gsd/pi-tui"); }); diff --git a/src/resources/extensions/gsd/triage-ui.ts b/src/resources/extensions/gsd/triage-ui.ts index ebf73bc26..2f5db0c64 100644 --- a/src/resources/extensions/gsd/triage-ui.ts +++ b/src/resources/extensions/gsd/triage-ui.ts @@ -10,7 +10,7 @@ */ import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; -import { showNextAction } from "../shared/mod.js"; +import { showNextAction } from "../shared/tui.js"; import type { CaptureEntry, Classification, TriageResult } from "./captures.js"; import { markCaptureResolved } from "./captures.js"; diff --git a/src/resources/extensions/gsd/worktree-command.ts b/src/resources/extensions/gsd/worktree-command.ts index 1be5a016b..672fd8a65 100644 --- a/src/resources/extensions/gsd/worktree-command.ts +++ b/src/resources/extensions/gsd/worktree-command.ts @@ -14,7 +14,7 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent import { loadPrompt } from "./prompt-loader.js"; import { autoCommitCurrentBranch, getMainBranch, resolveGitHeadPath, nudgeGitBranchCache } from "./worktree.js"; import { runWorktreePostCreateHook } from "./auto-worktree.js"; -import { showConfirm } from "../shared/mod.js"; +import { showConfirm } from "../shared/tui.js"; import { gsdRoot, milestonesDir } from "./paths.js"; import { createWorktree, diff --git a/src/resources/extensions/shared/mod.ts b/src/resources/extensions/shared/mod.ts index 44a2706ae..d34bbb5e5 100644 --- a/src/resources/extensions/shared/mod.ts +++ b/src/resources/extensions/shared/mod.ts @@ -1,7 +1,6 @@ // Barrel file — re-exports consumed by external modules export { - makeUI, GLYPH, INDENT, STATUS_GLYPH, @@ -27,10 +26,6 @@ export { export { shortcutDesc } from "./terminal.js"; export { toPosixPath } from "./path-display.js"; -export { showInterviewRound } from "./interview-ui.js"; -export type { Question, QuestionOption, RoundResult } from "./interview-ui.js"; -export { showNextAction } from "./next-action-ui.js"; -export { showConfirm } from "./confirm-ui.js"; export { sanitizeError, maskEditorLine } from "./sanitize.js"; export { formatDateShort, truncateWithEllipsis } from "./format-utils.js"; export { splitFrontmatter, parseFrontmatterMap } from "./frontmatter.js"; diff --git a/src/resources/extensions/shared/tui.ts b/src/resources/extensions/shared/tui.ts new file mode 100644 index 000000000..33977a9d6 --- /dev/null +++ b/src/resources/extensions/shared/tui.ts @@ -0,0 +1,11 @@ +// Barrel — TUI-dependent exports. +// Import from here when your code needs makeUI, showInterviewRound, +// showNextAction, or showConfirm. These all have a transitive dependency +// on @gsd/pi-tui and must not be imported from shared/mod. + +export { makeUI } from "./ui.js"; +export type { UI } from "./ui.js"; +export { showInterviewRound } from "./interview-ui.js"; +export type { Question, QuestionOption, RoundResult } from "./interview-ui.js"; +export { showNextAction } from "./next-action-ui.js"; +export { showConfirm } from "./confirm-ui.js"; diff --git a/src/resources/extensions/shared/ui.ts b/src/resources/extensions/shared/ui.ts index c0050a558..17588a360 100644 --- a/src/resources/extensions/shared/ui.ts +++ b/src/resources/extensions/shared/ui.ts @@ -29,23 +29,7 @@ */ import { type Theme } from "@gsd/pi-coding-agent"; - -// ─── Lazy @gsd/pi-tui resolution ───────────────────────────────────────────── -// Deferred to first makeUI() call so that importing this module (via the -// shared/mod barrel) does not blow up when @gsd/pi-tui cannot be resolved — -// e.g. for commands like /exit that never render TUI components. - -import { createRequire } from "node:module"; - -type PiTuiFns = typeof import("@gsd/pi-tui"); -let _piTui: PiTuiFns | undefined; -function piTui(): PiTuiFns { - if (!_piTui) { - const _require = createRequire(import.meta.url); - _piTui = _require("@gsd/pi-tui") as PiTuiFns; - } - return _piTui; -} +import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@gsd/pi-tui"; // ─── Glyphs ─────────────────────────────────────────────────────────────────── // Change these to restyle every cursor, checkbox, and indicator at once. @@ -217,7 +201,6 @@ export interface UI { export function makeUI(theme: Theme, width: number): UI { // ── Internal helpers ─────────────────────────────────────────────────────── - const { truncateToWidth, visibleWidth, wrapTextWithAnsi } = piTui(); const add = (s: string): string => truncateToWidth(s, width); const wrap = (s: string): string[] => wrapTextWithAnsi(s, width); diff --git a/src/resources/extensions/slash-commands/create-extension.ts b/src/resources/extensions/slash-commands/create-extension.ts index 86a2cff3a..35f916e2e 100644 --- a/src/resources/extensions/slash-commands/create-extension.ts +++ b/src/resources/extensions/slash-commands/create-extension.ts @@ -1,5 +1,5 @@ import type { ExtensionAPI } from "@gsd/pi-coding-agent"; -import { showInterviewRound, type Question, type RoundResult } from "../shared/mod.js"; +import { showInterviewRound, type Question, type RoundResult } from "../shared/tui.js"; export default function createExtension(pi: ExtensionAPI) { pi.registerCommand("create-extension", { diff --git a/src/resources/extensions/slash-commands/create-slash-command.ts b/src/resources/extensions/slash-commands/create-slash-command.ts index ef76dad44..ce6dab4aa 100644 --- a/src/resources/extensions/slash-commands/create-slash-command.ts +++ b/src/resources/extensions/slash-commands/create-slash-command.ts @@ -1,5 +1,5 @@ import type { ExtensionAPI } from "@gsd/pi-coding-agent"; -import { showInterviewRound, type Question, type RoundResult } from "../shared/mod.js"; +import { showInterviewRound, type Question, type RoundResult } from "../shared/tui.js"; export default function createSlashCommand(pi: ExtensionAPI) { pi.registerCommand("create-slash-command", {