jevstrudel.git / nix / strudel.nix
1# jevstrudel, built from this repo: the Astro site (Strudel's REPL with the
2# song browser), the static site deployed from it, and the stdio MCP proxy
3# Claude Code starts from .mcp.json.
4{
5  stdenv,
6  nodejs,
7  pnpm_10,
8  fetchPnpmDeps,
9  pnpmConfigHook,
10  importNpmLock,
11  runCommand,
12  writeShellApplication,
13  src,
14}:
15let
16  pname = "jevstrudel";
17  version = "0-unstable-${src.lastModifiedDate or "0"}";
18
19  pnpmDeps = fetchPnpmDeps {
20    inherit pname version src;
21    pnpm = pnpm_10;
22    fetcherVersion = 4;
23    hash = "sha256-xdDD3pJNjPjNAHLnGncq8ofay+HMaKGoAuPVEvFzG3g=";
24  };
25  nativeBuildInputs = [
26    nodejs
27    pnpm_10
28    pnpmConfigHook
29  ];
30
31  site = stdenv.mkDerivation {
32    inherit
33      pname
34      version
35      src
36      pnpmDeps
37      nativeBuildInputs
38      ;
39
40    buildPhase = ''
41      runHook preBuild
42      pnpm run build
43      runHook postBuild
44    '';
45
46    installPhase = ''
47      runHook preInstall
48      share=$out/share/jevstrudel
49      mkdir -p $share/website
50      cp -r website/dist $share/website/dist
51      # Upstream's CNAME names strudel.cc; a host that honours it (GitHub
52      # Pages) would try to claim Strudel's domain for this build.
53      rm $share/website/dist/CNAME
54      runHook postInstall
55    '';
56  };
57
58  # jevstrudel's tests (`nix flake check`): Jev's core, the Worker (the
59  # relay's rules and log, the votes, the D1 migrations), the agreement
60  # report, the deploy's storage step, the telemetry reader, Jev's decision
61  # history, and every song evaluated and played to its end against a stand-in Jev
62  # (website/src/jev/allSongs.test.mjs). Upstream's own suites are
63  # Strudel's to run.
64  tests = stdenv.mkDerivation {
65    pname = "${pname}-tests";
66    inherit
67      version
68      src
69      pnpmDeps
70      nativeBuildInputs
71      ;
72    dontBuild = true;
73    doCheck = true;
74    checkPhase = ''
75      runHook preCheck
76      pnpm exec vitest run website/src/jev worker tools/agreement tools/deploy tools/events tools/jev-history
77      runHook postCheck
78    '';
79    installPhase = "touch $out";
80  };
81  # The Worker as the deploy uploads it and the type check reads it:
82  # worker/ with its own node_modules, holding exactly its dependencies'
83  # closure (the WebAuthn library), taken from the installed workspace by
84  # worker-closure.mjs. So a copy of it anywhere bundles and type-checks
85  # without the rest of the repo.
86  worker = stdenv.mkDerivation {
87    pname = "${pname}-worker";
88    inherit
89      version
90      src
91      pnpmDeps
92      nativeBuildInputs
93      ;
94    dontBuild = true;
95    installPhase = ''
96      runHook preInstall
97      node nix/worker-closure.mjs worker $out
98      runHook postInstall
99    '';
100    dontFixup = true;
101  };
102
103  # tools/mcp/ with its node_modules, beside the two files it reads from
104  # worker/ (the dev port, and the dev tools for when the Worker is down), at
105  # the same relative paths as in the repo.
106  mcpNodeModules = importNpmLock.buildNodeModules {
107    npmRoot = "${src}/tools/mcp";
108    inherit nodejs;
109  };
110  mcpTree = runCommand "jevstrudel-mcp-tree" { } ''
111    mkdir -p $out/tools/mcp $out/worker/src
112    cp ${src}/tools/mcp/*.mjs $out/tools/mcp/
113    ln -s ${mcpNodeModules}/node_modules $out/tools/mcp/node_modules
114    cp ${src}/worker/wrangler.json $out/worker/
115    cp ${src}/worker/src/tools.json ${src}/worker/src/shared-tools.json $out/worker/src/
116  '';
117in
118{
119  inherit site tests worker;
120
121  # The static site alone, ready for any static host: the REPL and song
122  # browser at /, the sample pack under /jev-samples/ and /_astro/. No MCP:
123  # the production Worker has no hub, so the page's probe finds none.
124  static = runCommand "jevstrudel-static" { } "ln -s ${site}/share/jevstrudel/website/dist $out";
125
126  # The stdio MCP server in .mcp.json: a proxy with no state, forwarding
127  # each message to the MCP hub in the jevstrudel Worker under `wrangler dev`
128  # or, switched with its use_environment tool, to the public site's hosted
129  # MCP as an OAuth client (tools/mcp/), so neither side has to restart for
130  # the other. It needs only Node and the MCP SDK (tools/mcp/package-lock.json,
131  # each package fetched by its integrity hash, apart from the site's pnpm
132  # closure), not the built site.
133  mcp = writeShellApplication {
134    name = "strudel-mcp";
135    runtimeInputs = [ nodejs ];
136    text = ''
137      exec node ${mcpTree}/tools/mcp/strudel-mcp.mjs
138    '';
139  };
140
141  # The proxy's own tests (`nix flake check`): the environment switch and
142  # forwarding, against fakes (tools/mcp/proxy.test.mjs).
143  mcpTests = runCommand "jevstrudel-mcp-tests" { nativeBuildInputs = [ nodejs ]; } ''
144    cd ${mcpTree}
145    node --test tools/mcp/*.test.mjs
146    touch $out
147  '';
148}