Flatten the repo: move everything out of app/ to the root
Now that the CLI and the Next.js app are one package, nesting it inside app/ served no purpose - the repo root itself becomes the published npm package. Merges app/.gitignore and app/README.md into the root versions, drops the now-duplicate app/LICENSE, and updates path references (README, docs/ARCHITECTURE.md, docs/CONFIG_REFERENCE.md, package.json's repository.directory) that assumed the app/ nesting. Also fixes a real bug this surfaced: the in-app docs viewer resolved docs/ relative to process.cwd(), which only worked by accident when the CLI happened to be invoked from app/'s parent directory. A first attempt at fixing it with import.meta.dirname broke instead, for the same cross-module-graph reason config-path resolution already documented - Next compiles Route Handlers through a separate module graph that doesn't preserve source-relative import.meta paths. Fixed by exposing the app root via TRIGGERSHELL_APP_ROOT (set once in server.ts, where import.meta *does* resolve correctly), the same pattern already used for TRIGGERSHELL_CONFIG_PATH. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
|
||||
export interface XtermViewHandle {
|
||||
write: (data: string) => void;
|
||||
}
|
||||
|
||||
const theme = {
|
||||
background: "#09090b",
|
||||
foreground: "#f4f4f5",
|
||||
cursor: "#f4f4f5",
|
||||
selectionBackground: "#3f3f46",
|
||||
black: "#18181b",
|
||||
red: "#f87171",
|
||||
green: "#4ade80",
|
||||
yellow: "#facc15",
|
||||
blue: "#60a5fa",
|
||||
magenta: "#c084fc",
|
||||
cyan: "#22d3ee",
|
||||
white: "#e4e4e7",
|
||||
brightBlack: "#3f3f46",
|
||||
brightRed: "#fca5a5",
|
||||
brightGreen: "#86efac",
|
||||
brightYellow: "#fde047",
|
||||
brightBlue: "#93c5fd",
|
||||
brightMagenta: "#d8b4fe",
|
||||
brightCyan: "#67e8f9",
|
||||
brightWhite: "#f4f4f5",
|
||||
};
|
||||
|
||||
interface XtermViewProps {
|
||||
/** Written into the terminal as soon as it's created. Only read once, on mount. */
|
||||
initialData?: string;
|
||||
}
|
||||
|
||||
/** A read-only xterm.js instance: interprets ANSI colors/control codes from streamed script
|
||||
* output, unlike a plain <pre> which would just show raw escape characters.
|
||||
*
|
||||
* `initialData` is written inside the same effect that creates the Terminal - not via a
|
||||
* ref call from the parent after the fact - specifically because React's Strict Mode (dev
|
||||
* only) mounts, cleans up, and re-mounts every effect once: a fresh Terminal is created on
|
||||
* each real mount, so a "write once" flag in the parent would write to the first (disposed)
|
||||
* instance and silently skip the surviving one, leaving the terminal blank until the next
|
||||
* live write arrives.
|
||||
*/
|
||||
export const XtermView = forwardRef<XtermViewHandle, XtermViewProps>(
|
||||
function XtermView({ initialData }, ref) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const termRef = useRef<Terminal | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const term = new Terminal({
|
||||
convertEol: true,
|
||||
disableStdin: true,
|
||||
cursorBlink: false,
|
||||
fontSize: 12,
|
||||
lineHeight: 1.2,
|
||||
fontFamily: "ui-monospace, Menlo, monospace",
|
||||
theme,
|
||||
});
|
||||
const fitAddon = new FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
term.open(containerRef.current);
|
||||
fitAddon.fit();
|
||||
if (initialData) term.write(initialData);
|
||||
termRef.current = term;
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
try {
|
||||
fitAddon.fit();
|
||||
} catch {
|
||||
// container has zero size (e.g. mid-layout-shift) - next resize will retry
|
||||
}
|
||||
});
|
||||
resizeObserver.observe(containerRef.current);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
term.dispose();
|
||||
termRef.current = null;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- initialData is intentionally only used on mount
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
write: (data: string) => termRef.current?.write(data),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="h-[60vh] overflow-hidden rounded-lg bg-zinc-950 p-2"
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user