@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/README.md +41 -0
- package/biome.json +32 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +161 -0
- package/dist/cli.js.map +1 -0
- package/dist/compiler.d.ts +21 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +117 -0
- package/dist/compiler.js.map +1 -0
- package/dist/prose.d.ts +7 -0
- package/dist/prose.d.ts.map +1 -0
- package/dist/prose.js +18 -0
- package/dist/prose.js.map +1 -0
- package/dist/root.d.ts +7 -0
- package/dist/root.d.ts.map +1 -0
- package/dist/root.js +25 -0
- package/dist/root.js.map +1 -0
- package/dist/ui.d.ts +5 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +293 -0
- package/dist/ui.js.map +1 -0
- package/package.json +33 -0
- package/src/cli.ts +186 -0
- package/src/compiler.ts +142 -0
- package/src/prose.ts +21 -0
- package/src/root.ts +29 -0
- package/src/ui.ts +336 -0
- package/tests/compile.test.d.ts +2 -0
- package/tests/compile.test.d.ts.map +1 -0
- package/tests/compile.test.js +64 -0
- package/tests/compile.test.js.map +1 -0
- package/tests/compile.test.ts +87 -0
- package/tests/root.test.d.ts +2 -0
- package/tests/root.test.d.ts.map +1 -0
- package/tests/root.test.js +43 -0
- package/tests/root.test.js.map +1 -0
- package/tests/root.test.ts +46 -0
- package/tsconfig.json +21 -0
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
|
+
|
package/src/compiler.ts
ADDED
|
@@ -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
|
+
|