Reduce Huginn to Eve DSL projection
This commit is contained in:
parent
49970186dd
commit
0d58398b4b
26 changed files with 104 additions and 8034 deletions
54
README.md
54
README.md
|
|
@ -1,46 +1,40 @@
|
|||
# Huginn
|
||||
|
||||
Huginn is the `.cc` inspection and projection surface for CultCache state.
|
||||
Huginn reads CultCache `.cc` state and emits Eve DSL.
|
||||
|
||||
CultLib owns the CultCache implementation: document registration, MessagePack
|
||||
payloads, schema catalogs, backing stores, and the canonical
|
||||
`cultcache.store.v1` snapshot parser/writer. Huginn consumes that package and
|
||||
turns `.cc`, `.msgpack`, or `.mpack` bytes into read-only operator surfaces.
|
||||
`cultcache.store.v1` snapshot parser/writer. Huginn consumes CultLib's
|
||||
`cultcache-ts/inspection` surface and projects inspection results into UI
|
||||
documents that Eve-capable runtimes can lower.
|
||||
|
||||
## Current Shape
|
||||
## Authority
|
||||
|
||||
- `cultcache-ts` is loaded from `../CultLib/packages/cultcache-ts`
|
||||
- `src/huginn-eve-dsl.ts` lowers a CultCache inspection into Eve DSL
|
||||
- `inspector/` contains the local React/Norn visual inspector
|
||||
- `electron/` packages the inspector as a desktop Huginn build
|
||||
- Huginn does not write `.cc` files or decide CultCache persistence behavior
|
||||
- Owner: Huginn owns `.cc` inspection projection, not `.cc` persistence.
|
||||
- Input: `.cc`, `.msgpack`, or `.mpack` bytes readable by CultLib.
|
||||
- Output: Eve DSL for `cultcache.huginn.inspector`.
|
||||
- Renderers: browser, native, overlay, TUI, or future rooms lower the emitted
|
||||
DSL without becoming state owners.
|
||||
|
||||
## Development
|
||||
There is no Electron app, Vite dashboard, React renderer, or Norn-owned
|
||||
presentation path in this repo. If a runtime wants to display Huginn, it should
|
||||
consume the emitted Eve DSL.
|
||||
|
||||
## CLI
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm run build
|
||||
npm run dev:inspector
|
||||
npx huginn path/to/state.cc > huginn.eve
|
||||
```
|
||||
|
||||
Build the Vite inspector bundle:
|
||||
The CLI writes Eve DSL to stdout and errors to stderr.
|
||||
|
||||
```sh
|
||||
npm run build:inspector
|
||||
## API
|
||||
|
||||
```ts
|
||||
import { inspectCultCacheBytes, buildHuginnEveDsl } from "@gamecult/huginn";
|
||||
|
||||
const inspection = inspectCultCacheBytes(filePath, bytes);
|
||||
const eveDsl = buildHuginnEveDsl(inspection);
|
||||
```
|
||||
|
||||
Build the desktop package:
|
||||
|
||||
```sh
|
||||
npm run dist:inspector
|
||||
```
|
||||
|
||||
Release builds must carry a new semantic version before any deployable artifact
|
||||
is produced. While the package is pre-1.0, breaking public behavior increments
|
||||
the minor version, compatible fixes increment the patch version, and the
|
||||
generated Huginn executable must use that package version in its filename.
|
||||
|
||||
Huginn is read-only. Drop a `.cc`, `.msgpack`, or `.mpack` file onto the window
|
||||
to inspect the snapshot header, schema catalog, records, decoded MessagePack
|
||||
payload previews, and an Norn graph cloud of the file's structured payload data
|
||||
without registering application schemas.
|
||||
|
|
|
|||
3
dist/cli.d.ts
vendored
Normal file
3
dist/cli.d.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env node
|
||||
export {};
|
||||
//# sourceMappingURL=cli.d.ts.map
|
||||
1
dist/cli.d.ts.map
vendored
Normal file
1
dist/cli.d.ts.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
||||
21
dist/cli.js
vendored
Normal file
21
dist/cli.js
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const promises_1 = require("node:fs/promises");
|
||||
const inspection_1 = require("cultcache-ts/inspection");
|
||||
const huginn_eve_dsl_1 = require("./huginn-eve-dsl");
|
||||
async function main(args) {
|
||||
const filePath = args[0];
|
||||
if (!filePath || args.includes("--help") || args.includes("-h")) {
|
||||
process.stderr.write("Usage: huginn <state.cc|state.msgpack|state.mpack>\n");
|
||||
process.exitCode = filePath ? 0 : 1;
|
||||
return;
|
||||
}
|
||||
const bytes = await (0, promises_1.readFile)(filePath);
|
||||
const inspection = (0, inspection_1.inspectCultCacheBytes)(filePath, bytes);
|
||||
process.stdout.write(`${(0, huginn_eve_dsl_1.buildHuginnEveDsl)(inspection)}\n`);
|
||||
}
|
||||
void main(process.argv.slice(2)).catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
1
dist/index.d.ts
vendored
1
dist/index.d.ts
vendored
|
|
@ -1,3 +1,4 @@
|
|||
export { buildHuginnEveDsl } from "./huginn-eve-dsl";
|
||||
export { inspectCultCacheBytes } from "cultcache-ts/inspection";
|
||||
export type { CultCacheInspection, InspectedCatalogEntry, InspectedCatalogMember, InspectedRecord, } from "cultcache-ts/inspection";
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
2
dist/index.d.ts.map
vendored
2
dist/index.d.ts.map
vendored
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,YAAY,EACV,mBAAmB,EACnB,qBAAqB,EACrB,sBAAsB,EACtB,eAAe,GAChB,MAAM,yBAAyB,CAAC"}
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,YAAY,EACV,mBAAmB,EACnB,qBAAqB,EACrB,sBAAsB,EACtB,eAAe,GAChB,MAAM,yBAAyB,CAAC"}
|
||||
4
dist/index.js
vendored
4
dist/index.js
vendored
|
|
@ -1,5 +1,7 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.buildHuginnEveDsl = void 0;
|
||||
exports.inspectCultCacheBytes = exports.buildHuginnEveDsl = void 0;
|
||||
var huginn_eve_dsl_1 = require("./huginn-eve-dsl");
|
||||
Object.defineProperty(exports, "buildHuginnEveDsl", { enumerable: true, get: function () { return huginn_eve_dsl_1.buildHuginnEveDsl; } });
|
||||
var inspection_1 = require("cultcache-ts/inspection");
|
||||
Object.defineProperty(exports, "inspectCultCacheBytes", { enumerable: true, get: function () { return inspection_1.inspectCultCacheBytes; } });
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
{
|
||||
"appId": "com.gamecult.cultcache.inspector",
|
||||
"productName": "Huginn",
|
||||
"executableName": "Huginn",
|
||||
"asar": false,
|
||||
"icon": "inspector/public/hugin.ico",
|
||||
"extraMetadata": {
|
||||
"main": "electron/main.cjs"
|
||||
},
|
||||
"files": [
|
||||
"electron/main.cjs",
|
||||
"dist-inspector/**/*"
|
||||
],
|
||||
"directories": {
|
||||
"output": "release-inspector"
|
||||
},
|
||||
"win": {
|
||||
"signAndEditExecutable": false,
|
||||
"target": ["portable"],
|
||||
"artifactName": "Huginn-${version}.${ext}"
|
||||
},
|
||||
"mac": {
|
||||
"target": ["dir"]
|
||||
},
|
||||
"linux": {
|
||||
"target": ["dir"]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
const { app, BrowserWindow } = require("electron");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
|
||||
const devServer = process.env.CULTCACHE_INSPECTOR_DEV_SERVER;
|
||||
app.setName("Huginn");
|
||||
const logPath = path.join(app.getPath("userData"), "huginn-renderer.log");
|
||||
|
||||
function writeLog(message) {
|
||||
const line = `[${new Date().toISOString()}] ${message}${os.EOL}`;
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
||||
fs.appendFileSync(logPath, line);
|
||||
} catch {
|
||||
// Logging must never be the reason the inspector refuses to open.
|
||||
}
|
||||
console.log(line.trimEnd());
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
const window = new BrowserWindow({
|
||||
width: 1180,
|
||||
height: 780,
|
||||
minWidth: 820,
|
||||
minHeight: 560,
|
||||
title: "Huginn",
|
||||
icon: path.join(__dirname, "..", "dist-inspector", "hugin-64.png"),
|
||||
backgroundColor: "#05090d",
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: true,
|
||||
},
|
||||
});
|
||||
|
||||
window.webContents.on("console-message", (_event, level, message, line, sourceId) => {
|
||||
writeLog(`renderer console[${level}] ${sourceId}:${line} ${message}`);
|
||||
});
|
||||
window.webContents.on("did-fail-load", (_event, code, description, validatedUrl) => {
|
||||
writeLog(`renderer failed load ${code} ${description} ${validatedUrl}`);
|
||||
});
|
||||
window.webContents.on("render-process-gone", (_event, details) => {
|
||||
writeLog(`renderer process gone ${details.reason} exitCode=${details.exitCode}`);
|
||||
});
|
||||
window.webContents.on("did-finish-load", () => {
|
||||
writeLog(`renderer loaded ${window.webContents.getURL()}`);
|
||||
});
|
||||
|
||||
if (devServer) {
|
||||
void window.loadURL(devServer);
|
||||
} else {
|
||||
void window.loadFile(path.join(__dirname, "..", "dist-inspector", "index.html"));
|
||||
}
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Huginn</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 MiB |
File diff suppressed because it is too large
Load diff
|
|
@ -1,856 +0,0 @@
|
|||
import { NornViewer, type NornGraphEdge, type NornGraphNode, type NornGraphsState, type ViewerSelection } from "@gamecult/norn-viewer";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { Component, useMemo, useState, type DragEvent, type ReactNode } from "react";
|
||||
|
||||
import { inspectCultCacheBytes, type CultCacheInspection, type InspectedCatalogEntry, type InspectedRecord } from "cultcache-ts/inspection";
|
||||
import { HuginnFieldCanvas } from "./HuginnFieldCanvas";
|
||||
import "./styles.css";
|
||||
|
||||
type Selection = {
|
||||
record: number;
|
||||
};
|
||||
|
||||
type NodeArticle = NonNullable<Parameters<typeof NornViewer>[0]["nodeArticle"]>;
|
||||
|
||||
type RawSelection =
|
||||
| { kind: "record"; index: number }
|
||||
| { kind: "value"; recordIndex: number; path: string; value: unknown };
|
||||
|
||||
type GraphProjection = {
|
||||
state: NornGraphsState;
|
||||
recordNodeIds: string[];
|
||||
nodeSelections: Map<string, RawSelection>;
|
||||
edgeSelections: Map<string, RawSelection>;
|
||||
collapsedPayloadValues: number;
|
||||
};
|
||||
|
||||
const MAX_PAYLOAD_REGION_NODES = 160;
|
||||
const MAX_RECORD_REGION_NODES = 32;
|
||||
const MAX_PAYLOAD_REGION_DEPTH = 3;
|
||||
const MAX_PAYLOAD_REGION_CHILDREN = 10;
|
||||
const MAX_NESTED_PAYLOAD_REGION_CHILDREN = 6;
|
||||
const assetPath = (name: string) => `${import.meta.env.BASE_URL}${name}`;
|
||||
const HUGINN_ART = {
|
||||
surface: assetPath("huginn-surface.png"),
|
||||
field: assetPath("huginn-fieldpack.png"),
|
||||
};
|
||||
|
||||
function HuginnField() {
|
||||
return (
|
||||
<HuginnFieldCanvas
|
||||
imageUrl={HUGINN_ART.surface}
|
||||
fieldUrl={HUGINN_ART.field}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const app = document.querySelector<HTMLDivElement>("#app");
|
||||
if (!app) {
|
||||
throw new Error("Missing inspector app root.");
|
||||
}
|
||||
|
||||
class RenderCrashBoundary extends Component<{ children: ReactNode }, { error: string | null }> {
|
||||
state = { error: null };
|
||||
|
||||
static getDerivedStateFromError(error: unknown): { error: string } {
|
||||
return { error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
if (this.state.error) {
|
||||
return (
|
||||
<main className="workspace">
|
||||
<section className="sidebar">
|
||||
<div className="brand">
|
||||
<img src={assetPath("hugin-64.png")} alt="" width="64" height="64" />
|
||||
<div>
|
||||
<h1>Huginn</h1>
|
||||
<p>CultCache state inspection for <code>.cc</code> files.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className="content">
|
||||
<div className="error fatal-error">
|
||||
<strong>Renderer error</strong>
|
||||
<span>{this.state.error}</span>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
createRoot(app).render(
|
||||
<RenderCrashBoundary>
|
||||
<HuginnApp />
|
||||
</RenderCrashBoundary>,
|
||||
);
|
||||
|
||||
function HuginnApp() {
|
||||
const [inspection, setInspection] = useState<CultCacheInspection | undefined>();
|
||||
const [selection, setSelection] = useState<Selection>({ record: 0 });
|
||||
const [graphSelection, setGraphSelection] = useState<ViewerSelection | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [dragging, setDragging] = useState(false);
|
||||
|
||||
const graphProjection = useMemo(
|
||||
() => inspection ? buildGraphProjection(inspection) : emptyGraphProjection(),
|
||||
[inspection],
|
||||
);
|
||||
|
||||
async function inspectFile(file: File): Promise<void> {
|
||||
try {
|
||||
const bytes = new Uint8Array(await file.arrayBuffer());
|
||||
const nextInspection = inspectCultCacheBytes(file.name, bytes, file.size);
|
||||
setInspection(nextInspection);
|
||||
setSelection({ record: 0 });
|
||||
setGraphSelection({
|
||||
kind: "node",
|
||||
graphKey: "dataflow",
|
||||
nodeId: nextInspection.records[0]
|
||||
? recordNodeId(nextInspection.records[0], 0)
|
||||
: "store",
|
||||
});
|
||||
setErrorMessage("");
|
||||
} catch (error) {
|
||||
setInspection(undefined);
|
||||
setErrorMessage(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
function handleDrop(event: DragEvent<HTMLElement>): void {
|
||||
event.preventDefault();
|
||||
setDragging(false);
|
||||
const file = event.dataTransfer.files.item(0);
|
||||
if (file) {
|
||||
void inspectFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main
|
||||
className={`workspace ${dragging ? "is-dragging" : ""}`}
|
||||
onDragOver={(event) => {
|
||||
event.preventDefault();
|
||||
setDragging(true);
|
||||
}}
|
||||
onDragLeave={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
setDragging(false);
|
||||
}
|
||||
}}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{inspection
|
||||
? (
|
||||
<InspectionView
|
||||
errorMessage={errorMessage}
|
||||
inspection={inspection}
|
||||
graphProjection={graphProjection}
|
||||
graphSelection={graphSelection}
|
||||
setGraphSelection={setGraphSelection}
|
||||
selection={selection}
|
||||
setSelection={setSelection}
|
||||
onInspectFile={inspectFile}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<EmptyWorkspace
|
||||
errorMessage={errorMessage}
|
||||
onInspectFile={inspectFile}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyWorkspace({
|
||||
errorMessage,
|
||||
onInspectFile,
|
||||
}: {
|
||||
errorMessage: string;
|
||||
onInspectFile: (file: File) => Promise<void>;
|
||||
}) {
|
||||
return (
|
||||
<section className="empty-workspace">
|
||||
<HuginnField />
|
||||
<section className="floating-panel brand-panel is-primary">
|
||||
<BrandBlock />
|
||||
<FilePicker onInspectFile={onInspectFile} />
|
||||
{errorMessage ? <div className="error">{errorMessage}</div> : null}
|
||||
<EmptySummary />
|
||||
</section>
|
||||
<NoFile />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function BrandBlock() {
|
||||
return (
|
||||
<div className="brand">
|
||||
<img src={assetPath("hugin-64.png")} alt="" width="64" height="64" />
|
||||
<div>
|
||||
<h1>Huginn</h1>
|
||||
<p>CultCache state inspection for <code>.cc</code> files.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FilePicker({ onInspectFile }: { onInspectFile: (file: File) => Promise<void> }) {
|
||||
return (
|
||||
<label className="dropzone">
|
||||
<input
|
||||
type="file"
|
||||
accept=".cc,.msgpack,.mpack,application/octet-stream"
|
||||
onChange={(event) => {
|
||||
const file = event.currentTarget.files?.item(0);
|
||||
if (file) {
|
||||
void onInspectFile(file);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="drop-title">Drop CultCache file</span>
|
||||
<span className="drop-copy">or choose one from disk</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function Summary({ inspection }: { inspection: CultCacheInspection }) {
|
||||
return (
|
||||
<div className="summary">
|
||||
<Metric label="Format" value={inspection.format} />
|
||||
<Metric label="Records" value={inspection.records.length.toString()} />
|
||||
<Metric label="Schemas" value={inspection.catalog.length.toString()} />
|
||||
<Metric label="Bytes" value={inspection.fileSizeBytes.toString()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptySummary() {
|
||||
return (
|
||||
<div className="summary">
|
||||
<Metric label="Format" value=".cc" />
|
||||
<Metric label="Mode" value="Read-only" />
|
||||
<Metric label="Payload" value="MessagePack" />
|
||||
<Metric label="Runtime" value="Vite" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Metric({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="metric">
|
||||
<span>{label}</span>
|
||||
<strong>{value}</strong>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NoFile() {
|
||||
return (
|
||||
<div className="hero">
|
||||
<img src={HUGINN_ART.surface} alt="" />
|
||||
<h2>No state loaded</h2>
|
||||
<p>Drag a CultCache <code>.cc</code> store into the window to inspect the snapshot header, schema catalog, records, and decoded MessagePack payloads.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InspectionView({
|
||||
errorMessage,
|
||||
inspection,
|
||||
graphProjection,
|
||||
graphSelection,
|
||||
setGraphSelection,
|
||||
selection,
|
||||
setSelection,
|
||||
onInspectFile,
|
||||
}: {
|
||||
errorMessage: string;
|
||||
inspection: CultCacheInspection;
|
||||
graphProjection: GraphProjection;
|
||||
graphSelection: ViewerSelection | null;
|
||||
setGraphSelection: (selection: ViewerSelection | null) => void;
|
||||
selection: Selection;
|
||||
setSelection: (selection: Selection) => void;
|
||||
onInspectFile: (file: File) => Promise<void>;
|
||||
}) {
|
||||
const selectRaw = (next: Extract<RawSelection, { kind: "record" }>) => {
|
||||
setSelection({ record: next.index });
|
||||
setGraphSelection({
|
||||
kind: "node",
|
||||
graphKey: "dataflow",
|
||||
nodeId: graphProjection.recordNodeIds[next.index],
|
||||
});
|
||||
};
|
||||
const selectGraph = (next: ViewerSelection | null) => {
|
||||
setGraphSelection(next);
|
||||
if (!next) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rawSelection = next.kind === "node"
|
||||
? graphProjection.nodeSelections.get(next.nodeId)
|
||||
: graphProjection.edgeSelections.get(next.edgeId);
|
||||
if (!rawSelection) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rawSelection.kind === "record") {
|
||||
setSelection({ record: rawSelection.index });
|
||||
return;
|
||||
}
|
||||
if (rawSelection.kind === "value") {
|
||||
setSelection({ record: rawSelection.recordIndex });
|
||||
return;
|
||||
}
|
||||
};
|
||||
const nodeArticle: NodeArticle = {
|
||||
className: "huginn-expanded-node",
|
||||
ariaLabel: (node) => `${node.title} CultCache detail`,
|
||||
content: (node) => {
|
||||
const rawSelection = graphProjection.nodeSelections.get(node.id);
|
||||
const record = rawSelection?.kind === "record"
|
||||
? inspection.records[rawSelection.index]
|
||||
: rawSelection?.kind === "value"
|
||||
? inspection.records[rawSelection.recordIndex]
|
||||
: undefined;
|
||||
const expandedValue = rawSelection?.kind === "value"
|
||||
? {
|
||||
path: rawSelection.path,
|
||||
value: rawSelection.value,
|
||||
}
|
||||
: undefined;
|
||||
const catalogEntry = record
|
||||
? inspection.catalog.find((entry) => entry.schemaId === record.schemaId)
|
||||
: undefined;
|
||||
return (
|
||||
<ExpandedNodePanel
|
||||
catalogEntry={catalogEntry}
|
||||
expandedValue={expandedValue}
|
||||
inspection={inspection}
|
||||
record={record}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="inspector-stage">
|
||||
<section className="graph-panel">
|
||||
<NornViewer
|
||||
className="huginn-graph-shell"
|
||||
state={graphProjection.state}
|
||||
initialGraph="dataflow"
|
||||
selection={graphSelection}
|
||||
onSelectionChange={selectGraph}
|
||||
title="CultCache Structure"
|
||||
viewportBackdrop={<HuginnField />}
|
||||
viewportBackground="#03070a"
|
||||
layoutMode={{ architecture: "force", dataflow: "force" }}
|
||||
motion={{
|
||||
strength: 1.24,
|
||||
damping: 0.88,
|
||||
flow: 1.2,
|
||||
orbit: 0.84,
|
||||
lift: 0.9,
|
||||
pulse: 1,
|
||||
emitNodeEnvelopes: true,
|
||||
}}
|
||||
overlayPanels
|
||||
showSidebar={false}
|
||||
nodeArticle={nodeArticle}
|
||||
graphLabels={{ architecture: "File", dataflow: "Payload Cloud" }}
|
||||
style={{ minHeight: "100vh" }}
|
||||
/>
|
||||
{graphProjection.collapsedPayloadValues > 0 ? (
|
||||
<div className="graph-warning">
|
||||
Payload cloud collapsed {graphProjection.collapsedPayloadValues} leaf/deep value(s) into article detail. The graph shows records and inspectable regions, not every scalar slot.
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
<div className="overlay-layer">
|
||||
<section className="floating-panel brand-panel">
|
||||
<BrandBlock />
|
||||
<FilePicker onInspectFile={onInspectFile} />
|
||||
{errorMessage ? <div className="error">{errorMessage}</div> : null}
|
||||
<Summary inspection={inspection} />
|
||||
</section>
|
||||
<section className="floating-panel data-panel records-panel">
|
||||
<PanelHeader title="Records" count={inspection.records.length.toString()} />
|
||||
<div className="list">
|
||||
{inspection.records.length
|
||||
? inspection.records.map((entry, index) => (
|
||||
<RecordButton
|
||||
key={`${entry.schemaId}:${entry.key}:${index}`}
|
||||
record={entry}
|
||||
index={index}
|
||||
selected={index === selection.record}
|
||||
onSelect={() => selectRaw({ kind: "record", index })}
|
||||
/>
|
||||
))
|
||||
: <div className="empty">No records</div>}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PanelHeader({ title, count }: { title: string; count: string }) {
|
||||
return (
|
||||
<header>
|
||||
<h2>{title}</h2>
|
||||
<span>{count}</span>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function ExpandedNodePanel({
|
||||
catalogEntry,
|
||||
expandedValue,
|
||||
inspection,
|
||||
record,
|
||||
}: {
|
||||
catalogEntry: InspectedCatalogEntry | undefined;
|
||||
expandedValue: { path: string; value: unknown } | undefined;
|
||||
inspection: CultCacheInspection;
|
||||
record: InspectedRecord | undefined;
|
||||
}) {
|
||||
const title = expandedValue?.path ?? record?.key ?? catalogEntry?.schemaName ?? inspection.filePath;
|
||||
const payloadValue = expandedValue?.value ?? record?.payloadPreview;
|
||||
return (
|
||||
<article className="expanded-content">
|
||||
<div className="expanded-kicker">{expandedValue ? "Payload Value" : "CultCache Node"}</div>
|
||||
<header className="expanded-header">
|
||||
<h1>{title}</h1>
|
||||
<div className="expanded-chips">
|
||||
<span>{inspection.format}</span>
|
||||
{record ? <span>{record.schemaName}</span> : <span>{inspection.records.length} records</span>}
|
||||
{expandedValue
|
||||
? <span>{valueStatus(expandedValue.value)}</span>
|
||||
: record ? <span>{record.payloadBytes} bytes</span> : <span>{inspection.catalog.length} schemas</span>}
|
||||
</div>
|
||||
</header>
|
||||
<section className="expanded-summary">
|
||||
{expandedValue && record ? (
|
||||
<p>{expandedValue.path} inside {record.key}.</p>
|
||||
) : record ? (
|
||||
<>
|
||||
<p>{record.schemaName} record stored at {record.storedAt}.</p>
|
||||
{record.payloadDecodeError ? <div className="error">{record.payloadDecodeError}</div> : null}
|
||||
</>
|
||||
) : (
|
||||
<p>{inspection.filePath} contains a schema catalog and persisted MessagePack record set.</p>
|
||||
)}
|
||||
</section>
|
||||
{payloadValue !== undefined ? (
|
||||
<section className="expanded-payload">
|
||||
<h2>Payload</h2>
|
||||
<PayloadCode value={payloadValue} />
|
||||
</section>
|
||||
) : null}
|
||||
<section className="expanded-article">
|
||||
<div>
|
||||
<h2>Record Metadata</h2>
|
||||
{record ? <RecordFacts record={record} /> : <div className="empty">Select a record node to inspect payload data.</div>}
|
||||
</div>
|
||||
<div>
|
||||
<h2>Schema Entry</h2>
|
||||
{catalogEntry ? <CatalogDetail entry={catalogEntry} /> : <div className="empty">Select a schema node to inspect catalog data.</div>}
|
||||
</div>
|
||||
</section>
|
||||
<footer className="expanded-footer">
|
||||
Record {record ? inspection.records.indexOf(record) + 1 : "-"} of {inspection.records.length}
|
||||
</footer>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function RecordButton({
|
||||
record,
|
||||
index,
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
record: InspectedRecord;
|
||||
index: number;
|
||||
selected: boolean;
|
||||
onSelect: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button className="row" type="button" data-record={index} aria-selected={selected} onClick={onSelect}>
|
||||
<strong>{record.key}</strong>
|
||||
<span>{record.schemaName} - {record.payloadBytes} bytes</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function RecordFacts({ record }: { record: InspectedRecord }) {
|
||||
return (
|
||||
<Facts rows={[
|
||||
["Key", record.key],
|
||||
["Schema", record.schemaName],
|
||||
["Schema ID", record.schemaId],
|
||||
["Stored", record.storedAt],
|
||||
["Payload", `${record.payloadBytes} bytes`],
|
||||
]} />
|
||||
);
|
||||
}
|
||||
|
||||
function PayloadCode({ value }: { value: unknown }) {
|
||||
return (
|
||||
<pre className="payload-code">{JSON.stringify(value, null, 2)}</pre>
|
||||
);
|
||||
}
|
||||
|
||||
function CatalogDetail({ entry }: { entry: InspectedCatalogEntry }) {
|
||||
return (
|
||||
<>
|
||||
<Facts rows={[
|
||||
["Schema", entry.schemaName],
|
||||
["Version", entry.schemaVersion],
|
||||
["Schema ID", entry.schemaId],
|
||||
["Hash", entry.contentHash],
|
||||
["Compatible", entry.compatibleSchemaIds.join(", ")],
|
||||
]} />
|
||||
<pre>{JSON.stringify({
|
||||
members: entry.members,
|
||||
canonicalSchemaJson: parseJson(entry.canonicalSchemaJson),
|
||||
}, null, 2)}</pre>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Facts({ rows }: { rows: Array<[string, string]> }) {
|
||||
return (
|
||||
<dl>
|
||||
{rows.map(([name, value]) => (
|
||||
<div className="fact-row" key={name}>
|
||||
<dt>{name}</dt>
|
||||
<dd>{value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
|
||||
function buildGraphProjection(inspection: CultCacheInspection): GraphProjection {
|
||||
const dataflowNodes: NornGraphNode[] = [];
|
||||
const dataflowEdges: NornGraphEdge[] = [];
|
||||
const recordNodeIds: string[] = [];
|
||||
const nodeSelections = new Map<string, RawSelection>();
|
||||
const edgeSelections = new Map<string, RawSelection>();
|
||||
let collapsedPayloadValues = 0;
|
||||
const architectureNodes: NornGraphNode[] = [
|
||||
{
|
||||
id: "store",
|
||||
title: ".cc Store",
|
||||
purpose: `${inspection.format} file with ${inspection.records.length} record(s) and ${inspection.catalog.length} schema catalog entr${inspection.catalog.length === 1 ? "y" : "ies"}.`,
|
||||
mechanism: inspection.filePath,
|
||||
status: "source",
|
||||
},
|
||||
{
|
||||
id: "catalog",
|
||||
title: "Schema Catalog",
|
||||
purpose: "Maps persisted schema ids to schema names, versions, compatible ids, and slot metadata.",
|
||||
status: `${inspection.catalog.length} schemas`,
|
||||
},
|
||||
{
|
||||
id: "records",
|
||||
title: "Records",
|
||||
purpose: "Persisted document entries carrying key, schema id, timestamp, and raw MessagePack payload bytes.",
|
||||
status: `${inspection.records.length} records`,
|
||||
},
|
||||
];
|
||||
const architectureEdges: NornGraphEdge[] = [
|
||||
{
|
||||
id: "store-catalog",
|
||||
source_id: "store",
|
||||
target_id: "catalog",
|
||||
kind: "contains",
|
||||
label: "schema catalog",
|
||||
},
|
||||
{
|
||||
id: "store-records",
|
||||
source_id: "store",
|
||||
target_id: "records",
|
||||
kind: "contains",
|
||||
label: "record set",
|
||||
},
|
||||
];
|
||||
|
||||
dataflowNodes.push({
|
||||
id: "store",
|
||||
title: ".cc Store",
|
||||
purpose: `Payload regions for ${inspection.format} snapshot.`,
|
||||
mechanism: inspection.filePath,
|
||||
status: `${inspection.fileSizeBytes} bytes`,
|
||||
});
|
||||
inspection.records.forEach((record, recordIndex) => {
|
||||
const currentRecordNodeId = recordNodeId(record, recordIndex);
|
||||
const recordSelection: RawSelection = { kind: "record", index: recordIndex };
|
||||
const perRecordBudget = {
|
||||
remainingRegionNodes: Math.min(
|
||||
MAX_RECORD_REGION_NODES,
|
||||
Math.floor(MAX_PAYLOAD_REGION_NODES / Math.max(1, inspection.records.length)),
|
||||
),
|
||||
collapsedPayloadValues: 0,
|
||||
};
|
||||
recordNodeIds[recordIndex] = currentRecordNodeId;
|
||||
nodeSelections.set(currentRecordNodeId, recordSelection);
|
||||
dataflowNodes.push({
|
||||
id: currentRecordNodeId,
|
||||
title: record.key,
|
||||
purpose: valuePurpose(record.payloadPreview),
|
||||
mechanism: record.schemaName,
|
||||
status: `${record.payloadBytes} bytes`,
|
||||
});
|
||||
const recordEdgeId = edgeId("store", currentRecordNodeId, "contains");
|
||||
edgeSelections.set(recordEdgeId, recordSelection);
|
||||
dataflowEdges.push({
|
||||
id: recordEdgeId,
|
||||
source_id: "store",
|
||||
target_id: currentRecordNodeId,
|
||||
kind: "contains",
|
||||
label: "record",
|
||||
});
|
||||
appendPayloadRegions({
|
||||
nodes: dataflowNodes,
|
||||
edges: dataflowEdges,
|
||||
nodeSelections,
|
||||
edgeSelections,
|
||||
recordIndex,
|
||||
parentId: currentRecordNodeId,
|
||||
value: record.payloadPreview,
|
||||
path: "payload",
|
||||
depth: 0,
|
||||
budget: perRecordBudget,
|
||||
});
|
||||
collapsedPayloadValues += perRecordBudget.collapsedPayloadValues;
|
||||
});
|
||||
|
||||
return {
|
||||
state: {
|
||||
architecture: { nodes: architectureNodes, edges: architectureEdges },
|
||||
dataflow: { nodes: dataflowNodes, edges: dataflowEdges },
|
||||
links: [],
|
||||
},
|
||||
recordNodeIds,
|
||||
nodeSelections,
|
||||
edgeSelections,
|
||||
collapsedPayloadValues,
|
||||
};
|
||||
}
|
||||
|
||||
function appendPayloadRegions({
|
||||
nodes,
|
||||
edges,
|
||||
nodeSelections,
|
||||
edgeSelections,
|
||||
recordIndex,
|
||||
parentId,
|
||||
value,
|
||||
path,
|
||||
depth,
|
||||
budget,
|
||||
}: {
|
||||
nodes: NornGraphNode[];
|
||||
edges: NornGraphEdge[];
|
||||
nodeSelections: Map<string, RawSelection>;
|
||||
edgeSelections: Map<string, RawSelection>;
|
||||
recordIndex: number;
|
||||
parentId: string;
|
||||
value: unknown;
|
||||
path: string;
|
||||
depth: number;
|
||||
budget: { remainingRegionNodes: number; collapsedPayloadValues: number };
|
||||
}): void {
|
||||
if (depth >= MAX_PAYLOAD_REGION_DEPTH) {
|
||||
budget.collapsedPayloadValues += countPayloadDescendants(value);
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = payloadRegionEntries(value, path);
|
||||
if (!entries.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let emittedChildren = 0;
|
||||
const childLimit = depth === 0 ? MAX_PAYLOAD_REGION_CHILDREN : MAX_NESTED_PAYLOAD_REGION_CHILDREN;
|
||||
for (const entry of entries) {
|
||||
if (!isGraphworthyRegion(entry.value, depth)) {
|
||||
budget.collapsedPayloadValues += 1 + countPayloadDescendants(entry.value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (budget.remainingRegionNodes <= 0 || emittedChildren >= childLimit) {
|
||||
budget.collapsedPayloadValues += 1 + countPayloadDescendants(entry.value);
|
||||
continue;
|
||||
}
|
||||
|
||||
budget.remainingRegionNodes -= 1;
|
||||
emittedChildren += 1;
|
||||
const childId = nodeId("region", `${parentId}:${entry.path}`);
|
||||
const rawSelection: RawSelection = { kind: "value", recordIndex, path: entry.path, value: entry.value };
|
||||
nodeSelections.set(childId, rawSelection);
|
||||
nodes.push(regionNode(childId, entry.label, entry.value, entry.path));
|
||||
const edge = valueEdge(parentId, childId, entry.kind, entry.label);
|
||||
edgeSelections.set(edge.id!, rawSelection);
|
||||
edges.push(edge);
|
||||
appendPayloadRegions({
|
||||
nodes,
|
||||
edges,
|
||||
nodeSelections,
|
||||
edgeSelections,
|
||||
recordIndex,
|
||||
parentId: childId,
|
||||
value: entry.value,
|
||||
path: entry.path,
|
||||
depth: depth + 1,
|
||||
budget,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function payloadRegionEntries(
|
||||
value: unknown,
|
||||
path: string,
|
||||
): Array<{ label: string; value: unknown; path: string; kind: "field" | "slot" }> {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item, index) => ({
|
||||
label: `[${index}]`,
|
||||
value: item,
|
||||
path: `${path}[${index}]`,
|
||||
kind: "slot",
|
||||
}));
|
||||
}
|
||||
if (isPlainObject(value)) {
|
||||
return Object.entries(value).map(([key, item]) => ({
|
||||
label: key,
|
||||
value: item,
|
||||
path: `${path}.${key}`,
|
||||
kind: "field",
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function isGraphworthyRegion(value: unknown, depth: number): boolean {
|
||||
if (!Array.isArray(value) && !isPlainObject(value)) {
|
||||
return false;
|
||||
}
|
||||
const directChildren = payloadRegionEntries(value, "payload").length;
|
||||
if (directChildren >= (depth === 0 ? 2 : 4)) {
|
||||
return true;
|
||||
}
|
||||
return countPayloadDescendants(value, 64) >= 10;
|
||||
}
|
||||
|
||||
function countPayloadDescendants(value: unknown, limit = 20_000): number {
|
||||
let count = 0;
|
||||
const stack: unknown[] = [value];
|
||||
while (stack.length && count < limit) {
|
||||
const current = stack.pop();
|
||||
const children = Array.isArray(current)
|
||||
? current
|
||||
: isPlainObject(current)
|
||||
? Object.values(current)
|
||||
: [];
|
||||
for (const child of children) {
|
||||
count += 1;
|
||||
if (Array.isArray(child) || isPlainObject(child)) {
|
||||
stack.push(child);
|
||||
}
|
||||
if (count >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function regionNode(id: string, title: string, value: unknown, path: string): NornGraphNode {
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
purpose: valuePurpose(value),
|
||||
mechanism: path,
|
||||
status: `${valueStatus(value)} / ${countPayloadDescendants(value)} values`,
|
||||
};
|
||||
}
|
||||
|
||||
function recordNodeId(record: InspectedRecord, index: number): string {
|
||||
return nodeId("record", `${record.schemaId}:${record.key}:${index}`);
|
||||
}
|
||||
|
||||
function valueEdge(sourceId: string, targetId: string, kind: string, label: string): NornGraphEdge {
|
||||
return {
|
||||
id: edgeId(sourceId, targetId, kind),
|
||||
source_id: sourceId,
|
||||
target_id: targetId,
|
||||
kind,
|
||||
label,
|
||||
};
|
||||
}
|
||||
|
||||
function valuePurpose(value: unknown): string {
|
||||
if (Array.isArray(value)) {
|
||||
return `Array with ${value.length} item(s).`;
|
||||
}
|
||||
if (isPlainObject(value)) {
|
||||
return `Object with ${Object.keys(value).length} field(s).`;
|
||||
}
|
||||
if (value === null) {
|
||||
return "Null payload value.";
|
||||
}
|
||||
return JSON.stringify(value) ?? String(value);
|
||||
}
|
||||
|
||||
function valueStatus(value: unknown): string {
|
||||
if (Array.isArray(value)) {
|
||||
return "array";
|
||||
}
|
||||
if (isPlainObject(value)) {
|
||||
return "object";
|
||||
}
|
||||
return typeof value;
|
||||
}
|
||||
|
||||
function emptyGraphProjection(): GraphProjection {
|
||||
return {
|
||||
state: {
|
||||
architecture: { nodes: [], edges: [] },
|
||||
dataflow: { nodes: [], edges: [] },
|
||||
links: [],
|
||||
},
|
||||
recordNodeIds: [],
|
||||
nodeSelections: new Map(),
|
||||
edgeSelections: new Map(),
|
||||
collapsedPayloadValues: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function parseJson(value: string): unknown {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function nodeId(prefix: string, value: string): string {
|
||||
return `${prefix}:${value.replace(/[^a-zA-Z0-9:_-]/g, "_")}`;
|
||||
}
|
||||
|
||||
function edgeId(sourceId: string, targetId: string, kind: string): string {
|
||||
return `${sourceId}->${targetId}:${kind}`;
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
|
@ -1,726 +0,0 @@
|
|||
:root {
|
||||
color-scheme: dark;
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
color: #dce9ee;
|
||||
background: #05090d;
|
||||
--bg: #05090d;
|
||||
--panel: #0b131b;
|
||||
--panel-strong: #101c27;
|
||||
--ink: #dce9ee;
|
||||
--muted: #8296a3;
|
||||
--line: #1e3444;
|
||||
--line-bright: #1e7fa4;
|
||||
--cyan: #33e6f3;
|
||||
--cyan-soft: rgba(51, 230, 243, 0.16);
|
||||
--cyan-glow: rgba(51, 230, 243, 0.36);
|
||||
--brass: #d49a3d;
|
||||
--danger: #ff897d;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 760px;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "Cascadia Code", "SFMono-Regular", Consolas, monospace;
|
||||
font-size: 0.92em;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
position: relative;
|
||||
display: block;
|
||||
min-height: 100vh;
|
||||
background:
|
||||
radial-gradient(circle at 78% 18%, rgba(39, 202, 239, 0.12), transparent 30%),
|
||||
linear-gradient(135deg, #05090d 0%, #07111a 48%, #030507 100%);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
border-right: 1px solid var(--line);
|
||||
background: linear-gradient(180deg, rgba(14, 27, 38, 0.98), rgba(5, 9, 13, 0.98));
|
||||
padding: 22px;
|
||||
box-shadow: 8px 0 36px rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: grid;
|
||||
grid-template-columns: 64px minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.brand img {
|
||||
border-radius: 8px;
|
||||
image-rendering: pixelated;
|
||||
filter: drop-shadow(0 0 14px var(--cyan-glow));
|
||||
}
|
||||
|
||||
.brand h1 {
|
||||
margin: 0;
|
||||
color: #f0fbff;
|
||||
font-size: 30px;
|
||||
line-height: 1.05;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.brand p {
|
||||
margin: 7px 0 18px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
place-items: center;
|
||||
min-height: 132px;
|
||||
border: 1px dashed #276982;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(180deg, rgba(13, 38, 50, 0.72), rgba(6, 15, 22, 0.88));
|
||||
color: var(--cyan);
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
box-shadow: inset 0 0 18px rgba(51, 230, 243, 0.06);
|
||||
}
|
||||
|
||||
.is-dragging .dropzone {
|
||||
border-color: var(--cyan);
|
||||
background: rgba(51, 230, 243, 0.12);
|
||||
box-shadow: 0 0 22px var(--cyan-glow), inset 0 0 22px rgba(51, 230, 243, 0.1);
|
||||
}
|
||||
|
||||
.dropzone input {
|
||||
position: absolute;
|
||||
inline-size: 1px;
|
||||
block-size: 1px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.drop-title {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.drop-copy {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.summary {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.metric {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: rgba(12, 23, 32, 0.92);
|
||||
padding: 11px;
|
||||
}
|
||||
|
||||
.metric span {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.metric strong {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 18px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.empty-workspace,
|
||||
.inspector-stage {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
background: #03070a;
|
||||
}
|
||||
|
||||
.empty-workspace {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: clamp(18px, 4vw, 56px);
|
||||
}
|
||||
|
||||
.empty-workspace .huginn-field-canvas {
|
||||
opacity: 0.46;
|
||||
}
|
||||
|
||||
.overlay-layer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 12;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.graph-panel {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
background: #03070a;
|
||||
}
|
||||
|
||||
.graph-panel > section {
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
min-height: 100vh !important;
|
||||
}
|
||||
|
||||
.huginn-field-canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
image-rendering: auto;
|
||||
opacity: 0.96;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.norn-overlay-panel {
|
||||
background: rgba(4, 11, 16, 0.72) !important;
|
||||
border-color: rgba(51, 230, 243, 0.22) !important;
|
||||
box-shadow: 0 0 28px rgba(51, 230, 243, 0.08), 0 18px 48px rgba(0, 0, 0, 0.28) !important;
|
||||
}
|
||||
|
||||
.huginn-graph-shell {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.huginn-graph-shell svg {
|
||||
filter: drop-shadow(0 0 12px rgba(51, 230, 243, 0.28));
|
||||
}
|
||||
|
||||
.huginn-graph-shell svg path[stroke]:not([stroke="transparent"]) {
|
||||
opacity: 1;
|
||||
stroke-width: 2.4px;
|
||||
}
|
||||
|
||||
.huginn-graph-shell svg rect[filter] {
|
||||
fill-opacity: 0.94;
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
|
||||
.huginn-graph-shell svg text {
|
||||
fill-opacity: 1;
|
||||
paint-order: stroke;
|
||||
stroke: rgba(1, 6, 10, 0.72);
|
||||
stroke-width: 2px;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.huginn-graph-shell .norn-overlay-panel,
|
||||
.floating-panel {
|
||||
opacity: 0.28;
|
||||
transition:
|
||||
opacity 160ms ease,
|
||||
transform 160ms ease,
|
||||
box-shadow 160ms ease,
|
||||
background-color 160ms ease;
|
||||
}
|
||||
|
||||
.huginn-graph-shell .norn-overlay-panel:hover,
|
||||
.huginn-graph-shell .norn-overlay-panel:focus-within,
|
||||
.floating-panel:hover,
|
||||
.floating-panel:focus-within,
|
||||
.floating-panel.is-primary {
|
||||
opacity: 1;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.floating-panel {
|
||||
position: absolute;
|
||||
pointer-events: auto;
|
||||
container-type: inline-size;
|
||||
border: 1px solid rgba(51, 230, 243, 0.22);
|
||||
border-radius: 24px;
|
||||
background: rgba(4, 11, 16, 0.72);
|
||||
padding: clamp(10px, 1.4vw, 18px);
|
||||
box-shadow: 0 20px 54px rgba(0, 0, 0, 0.34), 0 0 28px rgba(51, 230, 243, 0.08);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
.brand-panel {
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
width: min(330px, calc(100vw - 32px));
|
||||
transform: translateX(-242px);
|
||||
}
|
||||
|
||||
.brand-panel:hover,
|
||||
.brand-panel:focus-within,
|
||||
.brand-panel.is-primary {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.records-panel {
|
||||
left: 16px;
|
||||
bottom: 16px;
|
||||
width: min(360px, calc(50vw - 24px));
|
||||
max-height: min(44vh, 420px);
|
||||
transform: translateY(calc(100% - 48px));
|
||||
}
|
||||
|
||||
.records-panel:hover,
|
||||
.records-panel:focus-within {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
.graph-warning {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 18px;
|
||||
z-index: 14;
|
||||
max-width: min(720px, calc(100vw - 36px));
|
||||
transform: translateX(-50%);
|
||||
border-top: 1px solid rgba(212, 154, 61, 0.3);
|
||||
border: 1px solid rgba(212, 154, 61, 0.3);
|
||||
border-radius: 18px;
|
||||
background: rgba(72, 45, 12, 0.34);
|
||||
color: #ffd38a;
|
||||
padding: 10px 12px;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
min-height: calc(100vh - 36px);
|
||||
max-width: 680px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero img {
|
||||
width: 168px;
|
||||
height: 168px;
|
||||
margin-bottom: 18px;
|
||||
image-rendering: auto;
|
||||
filter: drop-shadow(0 0 28px var(--cyan-glow));
|
||||
}
|
||||
|
||||
.hero h2 {
|
||||
margin: 0;
|
||||
color: #f0fbff;
|
||||
font-size: 42px;
|
||||
line-height: 1;
|
||||
text-shadow: 0 0 18px rgba(3, 7, 10, 0.92), 0 0 34px rgba(51, 230, 243, 0.28);
|
||||
}
|
||||
|
||||
.hero p {
|
||||
margin: 14px 0 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.55;
|
||||
text-shadow: 0 0 14px rgba(3, 7, 10, 0.96);
|
||||
}
|
||||
|
||||
.panel {
|
||||
min-width: 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: rgba(8, 16, 23, 0.94);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 18px 38px rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
.panel header,
|
||||
.floating-panel header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding: 11px 12px;
|
||||
background: rgba(16, 28, 39, 0.92);
|
||||
}
|
||||
|
||||
.panel h2,
|
||||
.floating-panel h2 {
|
||||
margin: 0;
|
||||
color: #f0fbff;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.panel header span,
|
||||
.floating-panel header span {
|
||||
overflow: hidden;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
max-height: inherit;
|
||||
overflow: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.row {
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
background: var(--panel-strong);
|
||||
color: inherit;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.row[aria-selected="true"] {
|
||||
border-color: var(--cyan);
|
||||
box-shadow: inset 3px 0 0 var(--cyan), 0 0 18px rgba(51, 230, 243, 0.12);
|
||||
}
|
||||
|
||||
.row strong,
|
||||
.row span {
|
||||
display: block;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.row span {
|
||||
margin-top: 4px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
dl {
|
||||
display: grid;
|
||||
grid-template-columns: 120px minmax(0, 1fr);
|
||||
gap: 8px 12px;
|
||||
}
|
||||
|
||||
dt {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
pre {
|
||||
max-height: min(440px, 42vh);
|
||||
overflow: auto;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #142b39;
|
||||
background: #03080c;
|
||||
color: #dffbff;
|
||||
padding: 12px;
|
||||
font-family: "Cascadia Code", "SFMono-Regular", Consolas, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.error {
|
||||
border: 1px solid rgba(255, 137, 125, 0.42);
|
||||
border-radius: 6px;
|
||||
background: rgba(94, 27, 24, 0.28);
|
||||
color: var(--danger);
|
||||
padding: 10px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.fatal-error {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
max-width: 720px;
|
||||
margin: 32px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 18px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.huginn-expanded-node {
|
||||
scrollbar-color: rgba(51, 230, 243, 0.58) rgba(3, 7, 10, 0.32);
|
||||
transition:
|
||||
border-radius 180ms ease,
|
||||
box-shadow 180ms ease,
|
||||
padding 180ms ease;
|
||||
}
|
||||
|
||||
.expanded-content {
|
||||
display: grid;
|
||||
gap: clamp(10px, 1.4vw, 18px);
|
||||
min-height: 100%;
|
||||
padding: clamp(14px, 2.2vw, 30px);
|
||||
color: #e7fbff;
|
||||
}
|
||||
|
||||
.expanded-kicker {
|
||||
color: var(--cyan);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.expanded-header {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.expanded-header h1 {
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
color: #f8fdff;
|
||||
font-size: clamp(1.45rem, 3.4vw, 3rem);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.expanded-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.expanded-chips span,
|
||||
.expanded-footer {
|
||||
border: 1px solid rgba(51, 230, 243, 0.22);
|
||||
border-radius: 999px;
|
||||
background: rgba(5, 18, 26, 0.68);
|
||||
color: #aeeef5;
|
||||
padding: 6px 9px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.expanded-summary {
|
||||
color: rgba(231, 251, 255, 0.78);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.expanded-payload {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.expanded-payload h2 {
|
||||
margin: 0;
|
||||
color: #f8fdff;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.expanded-article {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.expanded-article h2 {
|
||||
margin: 0 0 8px;
|
||||
color: #f8fdff;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.expanded-footer {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="summary"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="summary"] .expanded-content {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="summary"] .expanded-kicker,
|
||||
.huginn-expanded-node[data-node-stage="summary"] .expanded-summary,
|
||||
.huginn-expanded-node[data-node-stage="summary"] .expanded-article,
|
||||
.huginn-expanded-node[data-node-stage="summary"] .expanded-footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="summary"] .expanded-header {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="summary"] .expanded-header h1 {
|
||||
max-width: 100%;
|
||||
font-size: 1.02rem;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="summary"] .expanded-chips {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="summary"] .expanded-payload {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="preview"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="preview"] .expanded-content {
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
gap: 10px;
|
||||
padding: 18px 22px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="preview"] .expanded-header {
|
||||
justify-items: center;
|
||||
max-width: min(640px, 100%);
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="preview"] .expanded-header h1 {
|
||||
max-width: 100%;
|
||||
font-size: clamp(1.05rem, 2.1vw, 1.7rem);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="preview"] .expanded-chips {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="preview"] .expanded-summary,
|
||||
.huginn-expanded-node[data-node-stage="preview"] .expanded-article,
|
||||
.huginn-expanded-node[data-node-stage="preview"] .expanded-footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="preview"] .expanded-payload {
|
||||
width: min(680px, 100%);
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="preview"] .expanded-payload h2 {
|
||||
color: var(--cyan);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="preview"] .payload-code {
|
||||
width: 100%;
|
||||
max-height: min(280px, 44vh);
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="preview"] dl {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.huginn-expanded-node[data-node-stage="article"] {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@container (max-width: 260px) {
|
||||
.brand p,
|
||||
.drop-copy,
|
||||
.metric span,
|
||||
.row span,
|
||||
.floating-panel header span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.brand {
|
||||
grid-template-columns: 42px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.brand img {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.brand h1 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
min-height: 54px;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 180px) {
|
||||
.summary,
|
||||
.dropzone,
|
||||
.list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.floating-panel header {
|
||||
border-bottom: 0;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
body {
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
.brand-panel {
|
||||
width: min(300px, calc(100vw - 24px));
|
||||
transform: translateX(calc(-100% + 58px));
|
||||
}
|
||||
|
||||
.records-panel {
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
width: auto;
|
||||
max-height: 32vh;
|
||||
}
|
||||
|
||||
.records-panel {
|
||||
bottom: 12px;
|
||||
}
|
||||
|
||||
.expanded-article {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
4856
package-lock.json
generated
4856
package-lock.json
generated
File diff suppressed because it is too large
Load diff
27
package.json
27
package.json
|
|
@ -4,19 +4,16 @@
|
|||
"description": "Huginn .cc inspection and Eve projection surface for CultCache state.",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"bin": {
|
||||
"huginn": "dist/cli.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"electron",
|
||||
"dist-inspector"
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"build:inspector": "tsc -p tsconfig.inspector.json && vite build --config vite.inspector.config.ts",
|
||||
"dev:inspector": "concurrently -k \"vite --host 127.0.0.1 --config vite.inspector.config.ts\" \"wait-on http://127.0.0.1:5173 && cross-env CULTCACHE_INSPECTOR_DEV_SERVER=http://127.0.0.1:5173 electron electron/main.cjs\"",
|
||||
"dist:inspector": "npm run build:inspector && electron-builder --config electron-builder.json",
|
||||
"pack:inspector": "npm run build:inspector && electron-builder --config electron-builder.json --dir",
|
||||
"typecheck": "tsc -p tsconfig.json --pretty false",
|
||||
"test": "npm run build && npm run build:inspector"
|
||||
"test": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"messagepack",
|
||||
|
|
@ -32,19 +29,7 @@
|
|||
"cultcache-ts": "file:../CultLib/packages/cultcache-ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@gamecult/norn-viewer": "file:../Norn/web/norn-viewer",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"concurrently": "^9.2.1",
|
||||
"cross-env": "^10.1.0",
|
||||
"electron": "^42.1.0",
|
||||
"electron-builder": "^26.8.1",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
"typescript": "^5.9.0",
|
||||
"vite": "^8.0.13",
|
||||
"wait-on": "^9.0.10"
|
||||
"typescript": "^5.9.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
23
src/cli.ts
Normal file
23
src/cli.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env node
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { inspectCultCacheBytes } from "cultcache-ts/inspection";
|
||||
|
||||
import { buildHuginnEveDsl } from "./huginn-eve-dsl";
|
||||
|
||||
async function main(args: string[]): Promise<void> {
|
||||
const filePath = args[0];
|
||||
if (!filePath || args.includes("--help") || args.includes("-h")) {
|
||||
process.stderr.write("Usage: huginn <state.cc|state.msgpack|state.mpack>\n");
|
||||
process.exitCode = filePath ? 0 : 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const bytes = await readFile(filePath);
|
||||
const inspection = inspectCultCacheBytes(filePath, bytes);
|
||||
process.stdout.write(`${buildHuginnEveDsl(inspection)}\n`);
|
||||
}
|
||||
|
||||
void main(process.argv.slice(2)).catch((error: unknown) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
export { buildHuginnEveDsl } from "./huginn-eve-dsl";
|
||||
export { inspectCultCacheBytes } from "cultcache-ts/inspection";
|
||||
export type {
|
||||
CultCacheInspection,
|
||||
InspectedCatalogEntry,
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"jsx": "react-jsx",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"noEmit": true,
|
||||
"rootDir": ".",
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": [
|
||||
"inspector/src/**/*.ts",
|
||||
"inspector/src/**/*.tsx"
|
||||
]
|
||||
}
|
||||
|
|
@ -17,7 +17,8 @@
|
|||
},
|
||||
"include": [
|
||||
"src/index.ts",
|
||||
"src/huginn-eve-dsl.ts"
|
||||
"src/huginn-eve-dsl.ts",
|
||||
"src/cli.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"test/**/*.ts"
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = fileURLToPath(new URL(".", import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
root: "inspector",
|
||||
base: "./",
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
react: path.resolve(root, "node_modules/react"),
|
||||
"react-dom": path.resolve(root, "node_modules/react-dom"),
|
||||
"react/jsx-runtime": path.resolve(root, "node_modules/react/jsx-runtime.js"),
|
||||
},
|
||||
dedupe: ["react", "react-dom"],
|
||||
},
|
||||
build: {
|
||||
outDir: "../dist-inspector",
|
||||
emptyOutDir: true
|
||||
}
|
||||
});
|
||||
Loading…
Reference in a new issue