refactor: split shared/mod.ts into pure and TUI-dependent barrels (#1807)

This commit is contained in:
Iouri Goussev 2026-03-21 13:48:32 -04:00 committed by GitHub
parent 28a3387e2b
commit b486177066
17 changed files with 35 additions and 74 deletions

View file

@ -18,7 +18,7 @@ import {
type Question,
type QuestionOption,
type RoundResult,
} from "./shared/mod.js";
} from "./shared/tui.js";
// ─── Types ────────────────────────────────────────────────────────────────────

View file

@ -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) {

View file

@ -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";

View file

@ -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 {

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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,

View file

@ -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 {

View file

@ -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");
});

View file

@ -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";

View file

@ -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,

View file

@ -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";

View file

@ -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";

View file

@ -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);

View file

@ -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", {

View file

@ -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", {