Reduce Huginn to Eve DSL projection

This commit is contained in:
Meta Vens 2026-06-05 12:42:30 +01:00
commit 0d58398b4b
26 changed files with 104 additions and 8034 deletions

View file

@ -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
View file

@ -0,0 +1,3 @@
#!/usr/bin/env node
export {};
//# sourceMappingURL=cli.d.ts.map

1
dist/cli.d.ts.map vendored Normal file
View file

@ -0,0 +1 @@
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}

21
dist/cli.js vendored Normal file
View 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
View file

@ -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
View file

@ -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
View file

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

View file

@ -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"]
}
}

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,4 +1,5 @@
export { buildHuginnEveDsl } from "./huginn-eve-dsl";
export { inspectCultCacheBytes } from "cultcache-ts/inspection";
export type {
CultCacheInspection,
InspectedCatalogEntry,

View file

@ -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"
]
}

View file

@ -17,7 +17,8 @@
},
"include": [
"src/index.ts",
"src/huginn-eve-dsl.ts"
"src/huginn-eve-dsl.ts",
"src/cli.ts"
],
"exclude": [
"test/**/*.ts"

View file

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