@sprig-and-prose/sprig 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ui.js ADDED
@@ -0,0 +1,293 @@
1
+ import { createServer } from "node:http";
2
+ import { readFileSync, existsSync, statSync } from "node:fs";
3
+ import { join, extname, resolve, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import chokidar from "chokidar";
6
+ import { loadProseFiles } from "./prose.js";
7
+ import { compileUniverse } from "./compiler.js";
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ const mimeTypes = {
11
+ ".html": "text/html",
12
+ ".js": "application/javascript",
13
+ ".json": "application/json",
14
+ ".css": "text/css",
15
+ ".png": "image/png",
16
+ ".jpg": "image/jpeg",
17
+ ".jpeg": "image/jpeg",
18
+ ".gif": "image/gif",
19
+ ".svg": "image/svg+xml",
20
+ ".woff": "font/woff",
21
+ ".woff2": "font/woff2",
22
+ ".ttf": "font/ttf",
23
+ ".eot": "application/vnd.ms-fontobject",
24
+ };
25
+ /**
26
+ * Resolves the dist directory path for sprig-ui-csr
27
+ */
28
+ function resolveUIDist() {
29
+ // Try to resolve from node_modules
30
+ try {
31
+ // First, try to import the package and check for an export
32
+ // If that doesn't work, resolve from node_modules
33
+ const packagePath = resolve(__dirname, "../node_modules/@sprig-and-prose/sprig-ui-csr");
34
+ const distPath = join(packagePath, "dist");
35
+ if (existsSync(distPath)) {
36
+ return distPath;
37
+ }
38
+ }
39
+ catch {
40
+ // Fall through to error
41
+ }
42
+ // Fallback: try relative to current package
43
+ const fallbackPath = resolve(__dirname, "../../sprig-ui-csr/dist");
44
+ if (existsSync(fallbackPath)) {
45
+ return fallbackPath;
46
+ }
47
+ throw new Error("Could not locate sprig-ui-csr dist directory");
48
+ }
49
+ let revisionCounter = 0;
50
+ const sseClients = new Set();
51
+ /**
52
+ * Broadcasts a manifest changed event to all connected clients
53
+ */
54
+ function broadcastManifestChanged() {
55
+ revisionCounter += 1;
56
+ const message = `id: ${revisionCounter}\nevent: manifest\ndata: ${JSON.stringify({ revision: revisionCounter })}\n\n`;
57
+ console.log(`Broadcasting manifest change (revision ${revisionCounter}) to ${sseClients.size} client(s)`);
58
+ for (const res of sseClients) {
59
+ try {
60
+ res.write(message);
61
+ }
62
+ catch (error) {
63
+ // Client disconnected, remove it
64
+ console.log(`Client disconnected, removing from set`);
65
+ sseClients.delete(res);
66
+ }
67
+ }
68
+ }
69
+ /**
70
+ * Creates the HTTP server
71
+ */
72
+ function createServerHandler(uiDistDir, manifestPath) {
73
+ return (req, res) => {
74
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
75
+ const pathname = url.pathname;
76
+ // Handle /_ui/* routes - serve static files from dist/
77
+ if (pathname.startsWith("/_ui/")) {
78
+ const filePath = pathname.slice(5); // Remove '/_ui/'
79
+ const fullPath = join(uiDistDir, filePath);
80
+ // Security: ensure file is within dist directory
81
+ const resolvedPath = resolve(fullPath);
82
+ if (!resolvedPath.startsWith(resolve(uiDistDir))) {
83
+ res.writeHead(403, { "Content-Type": "text/plain" });
84
+ res.end("Forbidden");
85
+ return;
86
+ }
87
+ if (!existsSync(resolvedPath) || !statSync(resolvedPath).isFile()) {
88
+ res.writeHead(404, { "Content-Type": "text/plain" });
89
+ res.end("Not Found");
90
+ return;
91
+ }
92
+ try {
93
+ const content = readFileSync(resolvedPath);
94
+ const ext = extname(resolvedPath);
95
+ const contentType = mimeTypes[ext] || "application/octet-stream";
96
+ res.writeHead(200, { "Content-Type": contentType });
97
+ res.end(content);
98
+ }
99
+ catch {
100
+ res.writeHead(500, { "Content-Type": "text/plain" });
101
+ res.end("Internal Server Error");
102
+ }
103
+ return;
104
+ }
105
+ // Handle /api/manifest route
106
+ if (pathname === "/api/manifest") {
107
+ if (!existsSync(manifestPath) || !statSync(manifestPath).isFile()) {
108
+ res.writeHead(404, { "Content-Type": "application/json" });
109
+ res.end(JSON.stringify({ error: "Manifest not found" }));
110
+ return;
111
+ }
112
+ try {
113
+ const content = readFileSync(manifestPath, "utf-8");
114
+ const headers = {
115
+ "Content-Type": "application/json",
116
+ };
117
+ res.writeHead(200, headers);
118
+ res.end(content);
119
+ }
120
+ catch {
121
+ res.writeHead(500, { "Content-Type": "application/json" });
122
+ res.end(JSON.stringify({ error: "Error reading manifest" }));
123
+ }
124
+ return;
125
+ }
126
+ // Handle /api/events route - Server-Sent Events
127
+ if (pathname === "/api/events") {
128
+ res.writeHead(200, {
129
+ "Content-Type": "text/event-stream",
130
+ "Cache-Control": "no-cache",
131
+ Connection: "keep-alive",
132
+ "X-Accel-Buffering": "no",
133
+ });
134
+ // Send initial connection comment
135
+ res.write(": connected\n\n");
136
+ sseClients.add(res);
137
+ console.log(`SSE client connected (total: ${sseClients.size})`);
138
+ // Handle client disconnect
139
+ const cleanup = () => {
140
+ sseClients.delete(res);
141
+ console.log(`SSE client disconnected (remaining: ${sseClients.size})`);
142
+ };
143
+ req.on("close", cleanup);
144
+ res.on("close", cleanup);
145
+ return;
146
+ }
147
+ // All other routes - serve index.html for client-side routing
148
+ const indexPath = join(uiDistDir, "index.html");
149
+ if (!existsSync(indexPath)) {
150
+ res.writeHead(404, { "Content-Type": "text/plain" });
151
+ res.end("Not Found - dist/index.html does not exist.");
152
+ return;
153
+ }
154
+ try {
155
+ const content = readFileSync(indexPath, "utf-8");
156
+ res.writeHead(200, { "Content-Type": "text/html" });
157
+ res.end(content);
158
+ }
159
+ catch {
160
+ res.writeHead(500, { "Content-Type": "text/plain" });
161
+ res.end("Internal Server Error");
162
+ }
163
+ };
164
+ }
165
+ /**
166
+ * Rebuilds the manifest and broadcasts SSE event
167
+ * Only broadcasts after successful compile and atomic manifest write
168
+ */
169
+ async function rebuildManifest(universeRoot, manifestPath) {
170
+ try {
171
+ const files = await loadProseFiles(universeRoot);
172
+ const result = await compileUniverse(universeRoot, files, true);
173
+ // Only broadcast if compilation succeeded (manifest was atomically written)
174
+ if (result.success) {
175
+ broadcastManifestChanged();
176
+ return true;
177
+ }
178
+ return false;
179
+ }
180
+ catch {
181
+ return false;
182
+ }
183
+ }
184
+ /**
185
+ * Starts the UI server with watch mode
186
+ */
187
+ export async function startUIServer(universeRoot, port = 6336) {
188
+ const uiDistDir = resolveUIDist();
189
+ const manifestPath = join(universeRoot, ".sprig", "manifest.json");
190
+ // Compile once on startup
191
+ console.log("Compiling universe...");
192
+ const files = await loadProseFiles(universeRoot);
193
+ const result = await compileUniverse(universeRoot, files, true);
194
+ if (!result.success) {
195
+ const errors = result.diagnostics.filter((d) => d.severity === "error");
196
+ if (errors.length > 0) {
197
+ for (const error of errors) {
198
+ console.error(`Error: ${error.message}`);
199
+ }
200
+ }
201
+ process.exit(1);
202
+ }
203
+ console.log("✓ Universe compiled");
204
+ // Create HTTP server
205
+ const server = createServer(createServerHandler(uiDistDir, manifestPath));
206
+ server.on("error", (error) => {
207
+ if (error.code === "EADDRINUSE") {
208
+ console.error("");
209
+ console.error("Couldn't start the UI server.");
210
+ console.error("");
211
+ console.error(`Port ${port} is already in use.`);
212
+ process.exit(1);
213
+ }
214
+ else {
215
+ throw error;
216
+ }
217
+ });
218
+ server.listen(port, () => {
219
+ console.log(`Server running at http://localhost:${port}`);
220
+ });
221
+ // Set up heartbeat for SSE connections (every 25 seconds)
222
+ const heartbeatInterval = setInterval(() => {
223
+ for (const res of sseClients) {
224
+ try {
225
+ res.write(": ping\n\n");
226
+ }
227
+ catch {
228
+ // Client disconnected, remove it
229
+ sseClients.delete(res);
230
+ }
231
+ }
232
+ }, 25000);
233
+ // Set up file watching with debounce
234
+ let rebuildTimeout = null;
235
+ const DEBOUNCE_MS = 200;
236
+ const watcher = chokidar.watch(join(universeRoot, "**/*.prose"), {
237
+ ignored: [
238
+ join(universeRoot, ".sprig/**"),
239
+ join(universeRoot, "dist/**"),
240
+ join(universeRoot, "node_modules/**"),
241
+ join(universeRoot, ".git/**"),
242
+ ],
243
+ persistent: true,
244
+ });
245
+ watcher.on("change", () => {
246
+ if (rebuildTimeout) {
247
+ clearTimeout(rebuildTimeout);
248
+ }
249
+ rebuildTimeout = setTimeout(async () => {
250
+ const success = await rebuildManifest(universeRoot, manifestPath);
251
+ if (success) {
252
+ // One-line status per rebuild (calm output)
253
+ process.stdout.write("\r✓ Rebuilt\n");
254
+ }
255
+ else {
256
+ process.stdout.write("\r✗ Rebuild failed\n");
257
+ }
258
+ }, DEBOUNCE_MS);
259
+ });
260
+ // Handle graceful shutdown
261
+ function shutdown() {
262
+ // Clear heartbeat interval
263
+ clearInterval(heartbeatInterval);
264
+ // Clear any pending rebuild timeout
265
+ if (rebuildTimeout) {
266
+ clearTimeout(rebuildTimeout);
267
+ }
268
+ // Close all SSE connections
269
+ for (const res of sseClients) {
270
+ try {
271
+ res.end();
272
+ }
273
+ catch {
274
+ // Ignore errors on already closed connections
275
+ }
276
+ }
277
+ sseClients.clear();
278
+ // Close file watcher
279
+ watcher.close();
280
+ // Close server
281
+ server.close(() => {
282
+ process.exit(0);
283
+ });
284
+ // Force exit after 2 seconds if server doesn't close cleanly
285
+ setTimeout(() => {
286
+ console.error("Forcing exit...");
287
+ process.exit(1);
288
+ }, 2000);
289
+ }
290
+ process.on("SIGINT", shutdown);
291
+ process.on("SIGTERM", shutdown);
292
+ }
293
+ //# sourceMappingURL=ui.js.map
package/dist/ui.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui.js","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@sprig-and-prose/sprig",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Sprig CLI tool for compiling and serving universes",
6
+ "main": "dist/cli.js",
7
+ "bin": {
8
+ "sprig": "./dist/cli.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "prepare": "npm run build",
13
+ "test": "node --test",
14
+ "format": "biome format . --write",
15
+ "lint": "biome lint .",
16
+ "typecheck": "tsc --noEmit"
17
+ },
18
+ "keywords": [],
19
+ "author": "",
20
+ "license": "ISC",
21
+ "dependencies": {
22
+ "@sprig-and-prose/sprig-universe": "^0.1.1",
23
+ "@sprig-and-prose/sprig-ui-csr": "^0.1.1",
24
+ "chokidar": "^3.6.0",
25
+ "fast-glob": "^3.3.2"
26
+ },
27
+ "devDependencies": {
28
+ "@biomejs/biome": "^1.9.4",
29
+ "@types/node": "^22.0.0",
30
+ "typescript": "^5.7.2"
31
+ }
32
+ }
33
+
package/src/cli.ts ADDED
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { resolve } from "node:path";
4
+ import { discoverUniverseRoot } from "./root.js";
5
+ import { loadProseFiles } from "./prose.js";
6
+ import { compileUniverse } from "./compiler.js";
7
+ import { startUIServer } from "./ui.js";
8
+
9
+ const commands = ["compile", "validate", "ui"] as const;
10
+ type Command = (typeof commands)[number];
11
+
12
+ function printHelp() {
13
+ console.log("Usage: sprig <command> [path?] [options]");
14
+ console.log("");
15
+ console.log("Commands:");
16
+ console.log(" compile Compile universe files to manifest");
17
+ console.log(" validate Validate universe files (no output)");
18
+ console.log(" ui Start UI server with watch mode");
19
+ console.log("");
20
+ console.log("Options (for ui command):");
21
+ console.log(" -p, --port <number> Port to listen on (default: 6336)");
22
+ console.log("");
23
+ console.log("Root discovery:");
24
+ console.log(" - Walks upward from provided path (or current directory)");
25
+ console.log(" - Finds directory containing universe.prose");
26
+ }
27
+
28
+ function parseArgs(): {
29
+ command: Command | null;
30
+ path: string;
31
+ port?: number;
32
+ } {
33
+ const args = process.argv.slice(2);
34
+
35
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
36
+ return { command: null, path: process.cwd() };
37
+ }
38
+
39
+ const command = args[0] as Command;
40
+ if (!commands.includes(command)) {
41
+ console.error(`Error: Unknown command "${command}"`);
42
+ console.error("Run 'sprig --help' for usage information");
43
+ process.exit(1);
44
+ }
45
+
46
+ // Parse path and options
47
+ let path = process.cwd();
48
+ let port: number | undefined;
49
+
50
+ for (let i = 1; i < args.length; i++) {
51
+ const arg = args[i];
52
+ if (arg === "--port" || arg === "-p") {
53
+ const portValue = args[i + 1];
54
+ if (!portValue) {
55
+ console.error("Error: --port requires a value");
56
+ process.exit(1);
57
+ }
58
+ const parsedPort = parseInt(portValue, 10);
59
+ if (isNaN(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
60
+ console.error("Error: --port must be a number between 1 and 65535");
61
+ process.exit(1);
62
+ }
63
+ port = parsedPort;
64
+ i++; // Skip the port value
65
+ } else if (!arg.startsWith("-")) {
66
+ // First non-option argument is the path
67
+ path = resolve(arg);
68
+ } else {
69
+ console.error(`Error: Unknown option "${arg}"`);
70
+ console.error("Run 'sprig --help' for usage information");
71
+ process.exit(1);
72
+ }
73
+ }
74
+
75
+ return { command, path, port };
76
+ }
77
+
78
+ async function handleCompile(path: string): Promise<void> {
79
+ const root = discoverUniverseRoot(path);
80
+ if (!root) {
81
+ console.error("Couldn't find universe.prose");
82
+ console.error("");
83
+ console.error("Make sure you're inside a universe repository.");
84
+ process.exit(1);
85
+ }
86
+
87
+ const files = await loadProseFiles(root);
88
+ const result = await compileUniverse(root, files, true);
89
+
90
+ if (!result.success) {
91
+ const errors = result.diagnostics.filter((d) => d.severity === "error");
92
+ if (errors.length > 0) {
93
+ for (const error of errors) {
94
+ console.error(`Error: ${error.message}`);
95
+ }
96
+ }
97
+ process.exit(1);
98
+ }
99
+
100
+ const warnings = result.diagnostics.filter((d) => d.severity === "warning");
101
+ if (warnings.length > 0) {
102
+ for (const warning of warnings) {
103
+ const source = warning.source
104
+ ? // @ts-expect-error - source structure may vary
105
+ `${warning.source.file}:${warning.source.start?.line}:${warning.source.start?.col}`
106
+ : "unknown location";
107
+ console.warn(`Warning: ${source}: ${warning.message}`);
108
+ }
109
+ }
110
+ }
111
+
112
+ async function handleValidate(path: string): Promise<void> {
113
+ const root = discoverUniverseRoot(path);
114
+ if (!root) {
115
+ console.error("Couldn't find universe.prose");
116
+ console.error("");
117
+ console.error("Make sure you're inside a universe repository.");
118
+ process.exit(1);
119
+ }
120
+
121
+ const files = await loadProseFiles(root);
122
+ const result = await compileUniverse(root, files, false);
123
+
124
+ if (!result.success) {
125
+ const errors = result.diagnostics.filter((d) => d.severity === "error");
126
+ if (errors.length > 0) {
127
+ for (const error of errors) {
128
+ console.error(`Error: ${error.message}`);
129
+ }
130
+ }
131
+ process.exit(1);
132
+ }
133
+
134
+ const warnings = result.diagnostics.filter((d) => d.severity === "warning");
135
+ if (warnings.length > 0) {
136
+ for (const warning of warnings) {
137
+ const source = warning.source
138
+ ? // @ts-expect-error - source structure may vary
139
+ `${warning.source.file}:${warning.source.start?.line}:${warning.source.start?.col}`
140
+ : "unknown location";
141
+ console.warn(`Warning: ${source}: ${warning.message}`);
142
+ }
143
+ }
144
+ }
145
+
146
+ async function handleUI(path: string, port?: number): Promise<void> {
147
+ const root = discoverUniverseRoot(path);
148
+ if (!root) {
149
+ console.error("Couldn't find universe.prose");
150
+ console.error("");
151
+ console.error("Make sure you're inside a universe repository.");
152
+ process.exit(1);
153
+ }
154
+
155
+ await startUIServer(root, port);
156
+ }
157
+
158
+ async function main() {
159
+ const { command, path, port } = parseArgs();
160
+
161
+ if (!command) {
162
+ printHelp();
163
+ process.exit(0);
164
+ }
165
+
166
+ try {
167
+ switch (command) {
168
+ case "compile":
169
+ await handleCompile(path);
170
+ break;
171
+ case "validate":
172
+ await handleValidate(path);
173
+ break;
174
+ case "ui":
175
+ await handleUI(path, port);
176
+ break;
177
+ }
178
+ } catch (error) {
179
+ const message = error instanceof Error ? error.message : String(error);
180
+ console.error(`Error: ${message}`);
181
+ process.exit(1);
182
+ }
183
+ }
184
+
185
+ main();
186
+
@@ -0,0 +1,142 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ // @ts-expect-error - sprig-universe doesn't have TypeScript types
4
+ import { parseFiles } from "@sprig-and-prose/sprig-universe";
5
+
6
+ export interface CompileResult {
7
+ success: boolean;
8
+ manifest?: Record<string, unknown> & { repositories?: Record<string, unknown>; generatedAt: string };
9
+ diagnostics: Array<{ severity: string; message: string; source?: unknown }>;
10
+ }
11
+
12
+ /**
13
+ * Validates that exactly one universe declaration exists in the parsed graph
14
+ */
15
+ function validateUniverseCount(graph: Record<string, unknown>): {
16
+ valid: boolean;
17
+ universeName?: string;
18
+ error?: string;
19
+ } {
20
+ const universes = graph.universes as Record<string, unknown> | undefined;
21
+ const universeNames = Object.keys(universes || {});
22
+
23
+ if (universeNames.length === 0) {
24
+ return {
25
+ valid: false,
26
+ error: "No universe declaration found. At least one universe declaration is required.",
27
+ };
28
+ }
29
+
30
+ if (universeNames.length > 1) {
31
+ const nodes = graph.nodes as Record<string, { source?: { file?: string } }> | undefined;
32
+ const fileList = Array.from(universeNames)
33
+ .map((name) => {
34
+ const universe = universes?.[name] as { root?: string } | undefined;
35
+ const rootNode = universe?.root && nodes ? nodes[universe.root] : null;
36
+ return rootNode?.source?.file || "unknown";
37
+ })
38
+ .filter((file, index, arr) => arr.indexOf(file) === index) // unique files
39
+ .sort()
40
+ .join(", ");
41
+ return {
42
+ valid: false,
43
+ error: `Multiple distinct universes found: ${universeNames.join(", ")}. Files: ${fileList}. Exactly one universe declaration is required.`,
44
+ };
45
+ }
46
+
47
+ return {
48
+ valid: true,
49
+ universeName: universeNames[0],
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Compiles prose files into a manifest
55
+ * @param universeRoot - Universe root directory
56
+ * @param files - Array of prose file paths
57
+ * @param writeManifest - If true, write manifest to disk
58
+ * @returns Compile result with success status, manifest, and diagnostics
59
+ */
60
+ export async function compileUniverse(
61
+ universeRoot: string,
62
+ files: string[],
63
+ writeManifest = true,
64
+ ): Promise<CompileResult> {
65
+ const manifestPath = join(universeRoot, ".sprig", "manifest.json");
66
+
67
+ // Read and parse files
68
+ const fileContents = files.map((file) => ({
69
+ file,
70
+ text: readFileSync(file, "utf-8"),
71
+ }));
72
+
73
+ try {
74
+ const graph = parseFiles(fileContents);
75
+
76
+ // Validate exactly one universe
77
+ const validation = validateUniverseCount(graph);
78
+ if (!validation.valid) {
79
+ return {
80
+ success: false,
81
+ diagnostics: [
82
+ {
83
+ severity: "error",
84
+ message: validation.error || "Unknown validation error",
85
+ },
86
+ ],
87
+ };
88
+ }
89
+
90
+ // Add repositories and metadata to manifest
91
+ const manifest = {
92
+ ...graph,
93
+ repositories: graph.repositories || {},
94
+ generatedAt: new Date().toISOString(),
95
+ };
96
+
97
+ // Check for parsing errors
98
+ const hasErrors = graph.diagnostics.some((d: { severity: string }) => d.severity === "error");
99
+ if (hasErrors) {
100
+ return {
101
+ success: false,
102
+ manifest,
103
+ diagnostics: graph.diagnostics,
104
+ };
105
+ }
106
+
107
+ // Write manifest if requested
108
+ if (writeManifest) {
109
+ // Ensure output directory exists
110
+ const manifestDir = dirname(manifestPath);
111
+ try {
112
+ mkdirSync(manifestDir, { recursive: true });
113
+ } catch (error) {
114
+ // Directory might already exist, ignore
115
+ }
116
+
117
+ // Write atomically (temp file + rename)
118
+ const tempPath = `${manifestPath}.tmp`;
119
+ const json = JSON.stringify(manifest, null, 2);
120
+ writeFileSync(tempPath, json, "utf-8");
121
+ renameSync(tempPath, manifestPath);
122
+ }
123
+
124
+ return {
125
+ success: true,
126
+ manifest,
127
+ diagnostics: graph.diagnostics,
128
+ };
129
+ } catch (error) {
130
+ const message = error instanceof Error ? error.message : String(error);
131
+ return {
132
+ success: false,
133
+ diagnostics: [
134
+ {
135
+ severity: "error",
136
+ message: `Compilation error: ${message}`,
137
+ },
138
+ ],
139
+ };
140
+ }
141
+ }
142
+
package/src/prose.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { join } from "node:path";
2
+ import fastGlob from "fast-glob";
3
+
4
+ const DEFAULT_EXCLUDES = [".sprig/**", "dist/**", "node_modules/**", ".git/**"];
5
+
6
+ /**
7
+ * Loads all .prose files under root with default excludes
8
+ * @param root - Universe root directory
9
+ * @returns Array of prose file paths, sorted deterministically
10
+ */
11
+ export async function loadProseFiles(root: string): Promise<string[]> {
12
+ const pattern = join(root, "**/*.prose");
13
+ const allFiles = await fastGlob(pattern, {
14
+ absolute: true,
15
+ ignore: DEFAULT_EXCLUDES.map((exclude) => join(root, exclude)),
16
+ });
17
+
18
+ // Sort for deterministic ordering
19
+ return allFiles.sort();
20
+ }
21
+