@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/src/root.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { resolve, dirname, join } from "node:path";
3
+
4
+ /**
5
+ * Walks upward from startPath to find universe.prose marker
6
+ * @param startPath - Starting directory path
7
+ * @returns Path to directory containing universe.prose, or null if not found
8
+ */
9
+ export function discoverUniverseRoot(startPath: string): string | null {
10
+ let current = resolve(startPath);
11
+ const root = resolve("/");
12
+
13
+ while (current !== root) {
14
+ const markerPath = join(current, "universe.prose");
15
+ if (existsSync(markerPath) && statSync(markerPath).isFile()) {
16
+ return current;
17
+ }
18
+ current = dirname(current);
19
+ }
20
+
21
+ // Check root directory as well
22
+ const rootMarkerPath = join(root, "universe.prose");
23
+ if (existsSync(rootMarkerPath) && statSync(rootMarkerPath).isFile()) {
24
+ return root;
25
+ }
26
+
27
+ return null;
28
+ }
29
+
package/src/ui.ts ADDED
@@ -0,0 +1,336 @@
1
+ import { createServer, type Server } 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 { discoverUniverseRoot } from "./root.js";
7
+ import { loadProseFiles } from "./prose.js";
8
+ import { compileUniverse } from "./compiler.js";
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+
13
+ const mimeTypes: Record<string, string> = {
14
+ ".html": "text/html",
15
+ ".js": "application/javascript",
16
+ ".json": "application/json",
17
+ ".css": "text/css",
18
+ ".png": "image/png",
19
+ ".jpg": "image/jpeg",
20
+ ".jpeg": "image/jpeg",
21
+ ".gif": "image/gif",
22
+ ".svg": "image/svg+xml",
23
+ ".woff": "font/woff",
24
+ ".woff2": "font/woff2",
25
+ ".ttf": "font/ttf",
26
+ ".eot": "application/vnd.ms-fontobject",
27
+ };
28
+
29
+ /**
30
+ * Resolves the dist directory path for sprig-ui-csr
31
+ */
32
+ function resolveUIDist(): string {
33
+ // Try to resolve from node_modules
34
+ try {
35
+ // First, try to import the package and check for an export
36
+ // If that doesn't work, resolve from node_modules
37
+ const packagePath = resolve(__dirname, "../node_modules/@sprig-and-prose/sprig-ui-csr");
38
+ const distPath = join(packagePath, "dist");
39
+ if (existsSync(distPath)) {
40
+ return distPath;
41
+ }
42
+ } catch {
43
+ // Fall through to error
44
+ }
45
+
46
+ // Fallback: try relative to current package
47
+ const fallbackPath = resolve(__dirname, "../../sprig-ui-csr/dist");
48
+ if (existsSync(fallbackPath)) {
49
+ return fallbackPath;
50
+ }
51
+
52
+ throw new Error("Could not locate sprig-ui-csr dist directory");
53
+ }
54
+
55
+ let revisionCounter = 0;
56
+ const sseClients = new Set<import("node:http").ServerResponse>();
57
+
58
+ /**
59
+ * Broadcasts a manifest changed event to all connected clients
60
+ */
61
+ function broadcastManifestChanged(): void {
62
+ revisionCounter += 1;
63
+ const message = `id: ${revisionCounter}\nevent: manifest\ndata: ${JSON.stringify({ revision: revisionCounter })}\n\n`;
64
+ console.log(`Broadcasting manifest change (revision ${revisionCounter}) to ${sseClients.size} client(s)`);
65
+ for (const res of sseClients) {
66
+ try {
67
+ res.write(message);
68
+ } catch (error) {
69
+ // Client disconnected, remove it
70
+ console.log(`Client disconnected, removing from set`);
71
+ sseClients.delete(res);
72
+ }
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Creates the HTTP server
78
+ */
79
+ function createServerHandler(
80
+ uiDistDir: string,
81
+ manifestPath: string,
82
+ ): (req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse) => void {
83
+ return (req, res) => {
84
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
85
+ const pathname = url.pathname;
86
+
87
+ // Handle /_ui/* routes - serve static files from dist/
88
+ if (pathname.startsWith("/_ui/")) {
89
+ const filePath = pathname.slice(5); // Remove '/_ui/'
90
+ const fullPath = join(uiDistDir, filePath);
91
+
92
+ // Security: ensure file is within dist directory
93
+ const resolvedPath = resolve(fullPath);
94
+ if (!resolvedPath.startsWith(resolve(uiDistDir))) {
95
+ res.writeHead(403, { "Content-Type": "text/plain" });
96
+ res.end("Forbidden");
97
+ return;
98
+ }
99
+
100
+ if (!existsSync(resolvedPath) || !statSync(resolvedPath).isFile()) {
101
+ res.writeHead(404, { "Content-Type": "text/plain" });
102
+ res.end("Not Found");
103
+ return;
104
+ }
105
+
106
+ try {
107
+ const content = readFileSync(resolvedPath);
108
+ const ext = extname(resolvedPath);
109
+ const contentType = mimeTypes[ext] || "application/octet-stream";
110
+
111
+ res.writeHead(200, { "Content-Type": contentType });
112
+ res.end(content);
113
+ } catch {
114
+ res.writeHead(500, { "Content-Type": "text/plain" });
115
+ res.end("Internal Server Error");
116
+ }
117
+ return;
118
+ }
119
+
120
+ // Handle /api/manifest route
121
+ if (pathname === "/api/manifest") {
122
+ if (!existsSync(manifestPath) || !statSync(manifestPath).isFile()) {
123
+ res.writeHead(404, { "Content-Type": "application/json" });
124
+ res.end(JSON.stringify({ error: "Manifest not found" }));
125
+ return;
126
+ }
127
+
128
+ try {
129
+ const content = readFileSync(manifestPath, "utf-8");
130
+ const headers = {
131
+ "Content-Type": "application/json",
132
+ };
133
+ res.writeHead(200, headers);
134
+ res.end(content);
135
+ } catch {
136
+ res.writeHead(500, { "Content-Type": "application/json" });
137
+ res.end(JSON.stringify({ error: "Error reading manifest" }));
138
+ }
139
+ return;
140
+ }
141
+
142
+ // Handle /api/events route - Server-Sent Events
143
+ if (pathname === "/api/events") {
144
+ res.writeHead(200, {
145
+ "Content-Type": "text/event-stream",
146
+ "Cache-Control": "no-cache",
147
+ Connection: "keep-alive",
148
+ "X-Accel-Buffering": "no",
149
+ });
150
+
151
+ // Send initial connection comment
152
+ res.write(": connected\n\n");
153
+
154
+ sseClients.add(res);
155
+ console.log(`SSE client connected (total: ${sseClients.size})`);
156
+
157
+ // Handle client disconnect
158
+ const cleanup = () => {
159
+ sseClients.delete(res);
160
+ console.log(`SSE client disconnected (remaining: ${sseClients.size})`);
161
+ };
162
+ req.on("close", cleanup);
163
+ res.on("close", cleanup);
164
+
165
+ return;
166
+ }
167
+
168
+ // All other routes - serve index.html for client-side routing
169
+ const indexPath = join(uiDistDir, "index.html");
170
+ if (!existsSync(indexPath)) {
171
+ res.writeHead(404, { "Content-Type": "text/plain" });
172
+ res.end("Not Found - dist/index.html does not exist.");
173
+ return;
174
+ }
175
+
176
+ try {
177
+ const content = readFileSync(indexPath, "utf-8");
178
+ res.writeHead(200, { "Content-Type": "text/html" });
179
+ res.end(content);
180
+ } catch {
181
+ res.writeHead(500, { "Content-Type": "text/plain" });
182
+ res.end("Internal Server Error");
183
+ }
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Rebuilds the manifest and broadcasts SSE event
189
+ * Only broadcasts after successful compile and atomic manifest write
190
+ */
191
+ async function rebuildManifest(
192
+ universeRoot: string,
193
+ manifestPath: string,
194
+ ): Promise<boolean> {
195
+ try {
196
+ const files = await loadProseFiles(universeRoot);
197
+ const result = await compileUniverse(universeRoot, files, true);
198
+
199
+ // Only broadcast if compilation succeeded (manifest was atomically written)
200
+ if (result.success) {
201
+ broadcastManifestChanged();
202
+ return true;
203
+ }
204
+ return false;
205
+ } catch {
206
+ return false;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Starts the UI server with watch mode
212
+ */
213
+ export async function startUIServer(
214
+ universeRoot: string,
215
+ port: number = 6336,
216
+ ): Promise<void> {
217
+ const uiDistDir = resolveUIDist();
218
+ const manifestPath = join(universeRoot, ".sprig", "manifest.json");
219
+
220
+ // Compile once on startup
221
+ console.log("Compiling universe...");
222
+ const files = await loadProseFiles(universeRoot);
223
+ const result = await compileUniverse(universeRoot, files, true);
224
+
225
+ if (!result.success) {
226
+ const errors = result.diagnostics.filter((d) => d.severity === "error");
227
+ if (errors.length > 0) {
228
+ for (const error of errors) {
229
+ console.error(`Error: ${error.message}`);
230
+ }
231
+ }
232
+ process.exit(1);
233
+ }
234
+
235
+ console.log("✓ Universe compiled");
236
+
237
+ // Create HTTP server
238
+ const server = createServer(createServerHandler(uiDistDir, manifestPath));
239
+
240
+ server.on("error", (error: NodeJS.ErrnoException) => {
241
+ if (error.code === "EADDRINUSE") {
242
+ console.error("");
243
+ console.error("Couldn't start the UI server.");
244
+ console.error("");
245
+ console.error(`Port ${port} is already in use.`);
246
+ process.exit(1);
247
+ } else {
248
+ throw error;
249
+ }
250
+ });
251
+
252
+ server.listen(port, () => {
253
+ console.log(`Server running at http://localhost:${port}`);
254
+ });
255
+
256
+ // Set up heartbeat for SSE connections (every 25 seconds)
257
+ const heartbeatInterval = setInterval(() => {
258
+ for (const res of sseClients) {
259
+ try {
260
+ res.write(": ping\n\n");
261
+ } catch {
262
+ // Client disconnected, remove it
263
+ sseClients.delete(res);
264
+ }
265
+ }
266
+ }, 25000);
267
+
268
+ // Set up file watching with debounce
269
+ let rebuildTimeout: NodeJS.Timeout | null = null;
270
+ const DEBOUNCE_MS = 200;
271
+
272
+ const watcher = chokidar.watch(join(universeRoot, "**/*.prose"), {
273
+ ignored: [
274
+ join(universeRoot, ".sprig/**"),
275
+ join(universeRoot, "dist/**"),
276
+ join(universeRoot, "node_modules/**"),
277
+ join(universeRoot, ".git/**"),
278
+ ],
279
+ persistent: true,
280
+ });
281
+
282
+ watcher.on("change", () => {
283
+ if (rebuildTimeout) {
284
+ clearTimeout(rebuildTimeout);
285
+ }
286
+
287
+ rebuildTimeout = setTimeout(async () => {
288
+ const success = await rebuildManifest(universeRoot, manifestPath);
289
+ if (success) {
290
+ // One-line status per rebuild (calm output)
291
+ process.stdout.write("\r✓ Rebuilt\n");
292
+ } else {
293
+ process.stdout.write("\r✗ Rebuild failed\n");
294
+ }
295
+ }, DEBOUNCE_MS);
296
+ });
297
+
298
+ // Handle graceful shutdown
299
+ function shutdown() {
300
+ // Clear heartbeat interval
301
+ clearInterval(heartbeatInterval);
302
+
303
+ // Clear any pending rebuild timeout
304
+ if (rebuildTimeout) {
305
+ clearTimeout(rebuildTimeout);
306
+ }
307
+
308
+ // Close all SSE connections
309
+ for (const res of sseClients) {
310
+ try {
311
+ res.end();
312
+ } catch {
313
+ // Ignore errors on already closed connections
314
+ }
315
+ }
316
+ sseClients.clear();
317
+
318
+ // Close file watcher
319
+ watcher.close();
320
+
321
+ // Close server
322
+ server.close(() => {
323
+ process.exit(0);
324
+ });
325
+
326
+ // Force exit after 2 seconds if server doesn't close cleanly
327
+ setTimeout(() => {
328
+ console.error("Forcing exit...");
329
+ process.exit(1);
330
+ }, 2000);
331
+ }
332
+
333
+ process.on("SIGINT", shutdown);
334
+ process.on("SIGTERM", shutdown);
335
+ }
336
+
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=compile.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile.test.d.ts","sourceRoot":"","sources":["compile.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,64 @@
1
+ import { test } from "node:test";
2
+ import { strict as assert } from "node:assert";
3
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs";
4
+ import { join, tmpdir } from "node:path";
5
+ import { compileUniverse } from "../src/compiler.js";
6
+ test("compileUniverse writes .sprig/manifest.json", async () => {
7
+ const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
8
+ mkdirSync(testDir, { recursive: true });
9
+ // Create universe.prose marker
10
+ writeFileSync(join(testDir, "universe.prose"), `universe TestUniverse {
11
+ describe {
12
+ A test universe.
13
+ }
14
+ }`);
15
+ // Create a simple prose file
16
+ const proseFile = join(testDir, "test.prose");
17
+ writeFileSync(proseFile, `series TestSeries {
18
+ describe {
19
+ A test series.
20
+ }
21
+ }`);
22
+ try {
23
+ const result = await compileUniverse(testDir, [proseFile], true);
24
+ assert.strictEqual(result.success, true);
25
+ assert.ok(result.manifest);
26
+ // Check that manifest.json was written
27
+ const manifestPath = join(testDir, ".sprig", "manifest.json");
28
+ assert.ok(existsSync(manifestPath), "manifest.json should exist");
29
+ // Check manifest content
30
+ const manifestContent = JSON.parse(readFileSync(manifestPath, "utf-8"));
31
+ assert.strictEqual(manifestContent.universes?.TestUniverse?.name, "TestUniverse");
32
+ assert.ok(manifestContent.generatedAt);
33
+ }
34
+ finally {
35
+ rmSync(testDir, { recursive: true, force: true });
36
+ }
37
+ });
38
+ test("compileUniverse does not write manifest when writeManifest is false", async () => {
39
+ const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
40
+ mkdirSync(testDir, { recursive: true });
41
+ writeFileSync(join(testDir, "universe.prose"), `universe TestUniverse {
42
+ describe {
43
+ A test universe.
44
+ }
45
+ }`);
46
+ const proseFile = join(testDir, "test.prose");
47
+ writeFileSync(proseFile, `series TestSeries {
48
+ describe {
49
+ A test series.
50
+ }
51
+ }`);
52
+ try {
53
+ const result = await compileUniverse(testDir, [proseFile], false);
54
+ assert.strictEqual(result.success, true);
55
+ assert.ok(result.manifest);
56
+ // Check that manifest.json was NOT written
57
+ const manifestPath = join(testDir, ".sprig", "manifest.json");
58
+ assert.ok(!existsSync(manifestPath), "manifest.json should not exist when writeManifest is false");
59
+ }
60
+ finally {
61
+ rmSync(testDir, { recursive: true, force: true });
62
+ }
63
+ });
64
+ //# sourceMappingURL=compile.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile.test.js","sourceRoot":"","sources":["compile.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,87 @@
1
+ import { test } from "node:test";
2
+ import { strict as assert } from "node:assert";
3
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs";
4
+ import { join, tmpdir } from "node:path";
5
+ import { compileUniverse } from "../src/compiler.js";
6
+
7
+ test("compileUniverse writes .sprig/manifest.json", async () => {
8
+ const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
9
+ mkdirSync(testDir, { recursive: true });
10
+
11
+ // Create universe.prose marker
12
+ writeFileSync(
13
+ join(testDir, "universe.prose"),
14
+ `universe TestUniverse {
15
+ describe {
16
+ A test universe.
17
+ }
18
+ }`,
19
+ );
20
+
21
+ // Create a simple prose file
22
+ const proseFile = join(testDir, "test.prose");
23
+ writeFileSync(
24
+ proseFile,
25
+ `series TestSeries {
26
+ describe {
27
+ A test series.
28
+ }
29
+ }`,
30
+ );
31
+
32
+ try {
33
+ const result = await compileUniverse(testDir, [proseFile], true);
34
+
35
+ assert.strictEqual(result.success, true);
36
+ assert.ok(result.manifest);
37
+
38
+ // Check that manifest.json was written
39
+ const manifestPath = join(testDir, ".sprig", "manifest.json");
40
+ assert.ok(existsSync(manifestPath), "manifest.json should exist");
41
+
42
+ // Check manifest content
43
+ const manifestContent = JSON.parse(readFileSync(manifestPath, "utf-8"));
44
+ assert.strictEqual(manifestContent.universes?.TestUniverse?.name, "TestUniverse");
45
+ assert.ok(manifestContent.generatedAt);
46
+ } finally {
47
+ rmSync(testDir, { recursive: true, force: true });
48
+ }
49
+ });
50
+
51
+ test("compileUniverse does not write manifest when writeManifest is false", async () => {
52
+ const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
53
+ mkdirSync(testDir, { recursive: true });
54
+
55
+ writeFileSync(
56
+ join(testDir, "universe.prose"),
57
+ `universe TestUniverse {
58
+ describe {
59
+ A test universe.
60
+ }
61
+ }`,
62
+ );
63
+
64
+ const proseFile = join(testDir, "test.prose");
65
+ writeFileSync(
66
+ proseFile,
67
+ `series TestSeries {
68
+ describe {
69
+ A test series.
70
+ }
71
+ }`,
72
+ );
73
+
74
+ try {
75
+ const result = await compileUniverse(testDir, [proseFile], false);
76
+
77
+ assert.strictEqual(result.success, true);
78
+ assert.ok(result.manifest);
79
+
80
+ // Check that manifest.json was NOT written
81
+ const manifestPath = join(testDir, ".sprig", "manifest.json");
82
+ assert.ok(!existsSync(manifestPath), "manifest.json should not exist when writeManifest is false");
83
+ } finally {
84
+ rmSync(testDir, { recursive: true, force: true });
85
+ }
86
+ });
87
+
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=root.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root.test.d.ts","sourceRoot":"","sources":["root.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,43 @@
1
+ import { test } from "node:test";
2
+ import { strict as assert } from "node:assert";
3
+ import { mkdirSync, writeFileSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { discoverUniverseRoot } from "../src/root.js";
7
+ test("discoverUniverseRoot finds universe.prose in current directory", () => {
8
+ const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
9
+ mkdirSync(testDir, { recursive: true });
10
+ writeFileSync(join(testDir, "universe.prose"), "universe Test {}");
11
+ try {
12
+ const root = discoverUniverseRoot(testDir);
13
+ assert.strictEqual(root, testDir);
14
+ }
15
+ finally {
16
+ rmSync(testDir, { recursive: true, force: true });
17
+ }
18
+ });
19
+ test("discoverUniverseRoot finds universe.prose in parent directory", () => {
20
+ const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
21
+ const subDir = join(testDir, "sub", "nested");
22
+ mkdirSync(subDir, { recursive: true });
23
+ writeFileSync(join(testDir, "universe.prose"), "universe Test {}");
24
+ try {
25
+ const root = discoverUniverseRoot(subDir);
26
+ assert.strictEqual(root, testDir);
27
+ }
28
+ finally {
29
+ rmSync(testDir, { recursive: true, force: true });
30
+ }
31
+ });
32
+ test("discoverUniverseRoot returns null when not found", () => {
33
+ const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
34
+ mkdirSync(testDir, { recursive: true });
35
+ try {
36
+ const root = discoverUniverseRoot(testDir);
37
+ assert.strictEqual(root, null);
38
+ }
39
+ finally {
40
+ rmSync(testDir, { recursive: true, force: true });
41
+ }
42
+ });
43
+ //# sourceMappingURL=root.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root.test.js","sourceRoot":"","sources":["root.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,MAAM,IAAI,MAAM,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC3D,OAAO,EAAE,IAAI,EAAW,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAEtD,IAAI,CAAC,gEAAgE,EAAE,GAAG,EAAE;IAC1E,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,cAAc,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAC3D,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACxC,aAAa,CAAC,IAAI,CAAC,OAAO,EAAE,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,CAAC;IAEnE,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAC;QAC3C,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACpC,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,+DAA+D,EAAE,GAAG,EAAE;IACzE,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,cAAc,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAC3D,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;IAC9C,SAAS,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACvC,aAAa,CAAC,IAAI,CAAC,OAAO,EAAE,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,CAAC;IAEnE,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC;QAC1C,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACpC,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,kDAAkD,EAAE,GAAG,EAAE;IAC5D,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,cAAc,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAC3D,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAExC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAC;QAC3C,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACjC,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,CAAC;AACH,CAAC,CAAC,CAAC"}
@@ -0,0 +1,46 @@
1
+ import { test } from "node:test";
2
+ import { strict as assert } from "node:assert";
3
+ import { mkdirSync, writeFileSync, rmSync } from "node:fs";
4
+ import { join, resolve } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { discoverUniverseRoot } from "../src/root.js";
7
+
8
+ test("discoverUniverseRoot finds universe.prose in current directory", () => {
9
+ const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
10
+ mkdirSync(testDir, { recursive: true });
11
+ writeFileSync(join(testDir, "universe.prose"), "universe Test {}");
12
+
13
+ try {
14
+ const root = discoverUniverseRoot(testDir);
15
+ assert.strictEqual(root, testDir);
16
+ } finally {
17
+ rmSync(testDir, { recursive: true, force: true });
18
+ }
19
+ });
20
+
21
+ test("discoverUniverseRoot finds universe.prose in parent directory", () => {
22
+ const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
23
+ const subDir = join(testDir, "sub", "nested");
24
+ mkdirSync(subDir, { recursive: true });
25
+ writeFileSync(join(testDir, "universe.prose"), "universe Test {}");
26
+
27
+ try {
28
+ const root = discoverUniverseRoot(subDir);
29
+ assert.strictEqual(root, testDir);
30
+ } finally {
31
+ rmSync(testDir, { recursive: true, force: true });
32
+ }
33
+ });
34
+
35
+ test("discoverUniverseRoot returns null when not found", () => {
36
+ const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
37
+ mkdirSync(testDir, { recursive: true });
38
+
39
+ try {
40
+ const root = discoverUniverseRoot(testDir);
41
+ assert.strictEqual(root, null);
42
+ } finally {
43
+ rmSync(testDir, { recursive: true, force: true });
44
+ }
45
+ });
46
+
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "declaration": true,
15
+ "declarationMap": true,
16
+ "sourceMap": true
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist", "tests"]
20
+ }
21
+