hurler 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 +73 -0
- package/dist/client/assets/index-DvpFIZnB.css +1 -0
- package/dist/client/assets/index-MRBoxMW3.js +67 -0
- package/dist/client/index.html +14 -0
- package/dist/server/cli.js +19 -0
- package/dist/server/dev.js +8 -0
- package/dist/server/index.js +248 -0
- package/package.json +56 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>hurler-temp</title>
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-MRBoxMW3.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DvpFIZnB.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="root"></div>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createApp } from "./index.js";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import open from "open";
|
|
6
|
+
const { values } = parseArgs({
|
|
7
|
+
options: {
|
|
8
|
+
port: { type: "string", short: "p" },
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
const dataDir = path.join(process.cwd(), ".hurl");
|
|
12
|
+
const port = parseInt(values.port ?? process.env.PORT ?? "4000", 10);
|
|
13
|
+
const app = createApp(dataDir);
|
|
14
|
+
app.listen(port, () => {
|
|
15
|
+
const url = `http://localhost:${port}`;
|
|
16
|
+
console.log(`Hurler running at ${url}`);
|
|
17
|
+
console.log(`Collections: ${dataDir}/collections/`);
|
|
18
|
+
open(url);
|
|
19
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createApp } from "./index.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
const dataDir = path.join(os.homedir(), ".hurler");
|
|
5
|
+
const app = createApp(dataDir);
|
|
6
|
+
app.listen(3001, () => {
|
|
7
|
+
console.log("Hurler API server running on http://localhost:3001");
|
|
8
|
+
});
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
const DEFAULT_METADATA = { sections: [], fileGroups: {} };
|
|
8
|
+
export function createApp(dataDir) {
|
|
9
|
+
const app = express();
|
|
10
|
+
app.use(express.json());
|
|
11
|
+
const COLLECTIONS_DIR = path.join(dataDir, "collections");
|
|
12
|
+
const ENVIRONMENTS_DIR = path.join(dataDir, "environments");
|
|
13
|
+
const METADATA_PATH = path.join(dataDir, "metadata.json");
|
|
14
|
+
async function ensureDirs() {
|
|
15
|
+
await fs.mkdir(COLLECTIONS_DIR, { recursive: true });
|
|
16
|
+
await fs.mkdir(ENVIRONMENTS_DIR, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
async function readMetadata() {
|
|
19
|
+
try {
|
|
20
|
+
const raw = await fs.readFile(METADATA_PATH, "utf-8");
|
|
21
|
+
return JSON.parse(raw);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return { ...DEFAULT_METADATA, sections: [], fileGroups: {} };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function writeMetadata(metadata) {
|
|
28
|
+
await fs.writeFile(METADATA_PATH, JSON.stringify(metadata, null, 2), "utf-8");
|
|
29
|
+
}
|
|
30
|
+
// --- File endpoints ---
|
|
31
|
+
app.get("/api/files", async (_req, res) => {
|
|
32
|
+
await ensureDirs();
|
|
33
|
+
const files = await fs.readdir(COLLECTIONS_DIR);
|
|
34
|
+
const hurlFiles = files.filter((f) => f.endsWith(".hurl"));
|
|
35
|
+
res.json(hurlFiles.map((f) => f.replace(/\.hurl$/, "")));
|
|
36
|
+
});
|
|
37
|
+
app.post("/api/files", async (req, res) => {
|
|
38
|
+
await ensureDirs();
|
|
39
|
+
const { name, content } = req.body;
|
|
40
|
+
if (!name || typeof name !== "string") {
|
|
41
|
+
res.status(400).json({ error: "name is required" });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
45
|
+
const filePath = path.join(COLLECTIONS_DIR, `${safeName}.hurl`);
|
|
46
|
+
try {
|
|
47
|
+
await fs.access(filePath);
|
|
48
|
+
res.status(409).json({ error: "File already exists" });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// File doesn't exist, good
|
|
53
|
+
}
|
|
54
|
+
await fs.writeFile(filePath, content ?? "", "utf-8");
|
|
55
|
+
res.status(201).json({ name: safeName });
|
|
56
|
+
});
|
|
57
|
+
app.get("/api/files/:name", async (req, res) => {
|
|
58
|
+
await ensureDirs();
|
|
59
|
+
const filePath = path.join(COLLECTIONS_DIR, `${req.params.name}.hurl`);
|
|
60
|
+
try {
|
|
61
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
62
|
+
res.json({ name: req.params.name, content });
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
res.status(404).json({ error: "File not found" });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
app.put("/api/files/:name", async (req, res) => {
|
|
69
|
+
await ensureDirs();
|
|
70
|
+
const filePath = path.join(COLLECTIONS_DIR, `${req.params.name}.hurl`);
|
|
71
|
+
const { content } = req.body;
|
|
72
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
73
|
+
res.json({ name: req.params.name });
|
|
74
|
+
});
|
|
75
|
+
app.delete("/api/files/:name", async (req, res) => {
|
|
76
|
+
await ensureDirs();
|
|
77
|
+
const filePath = path.join(COLLECTIONS_DIR, `${req.params.name}.hurl`);
|
|
78
|
+
try {
|
|
79
|
+
await fs.unlink(filePath);
|
|
80
|
+
res.json({ ok: true });
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
res.status(404).json({ error: "File not found" });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
// --- Metadata endpoints ---
|
|
87
|
+
app.get("/api/metadata", async (_req, res) => {
|
|
88
|
+
const metadata = await readMetadata();
|
|
89
|
+
res.json(metadata);
|
|
90
|
+
});
|
|
91
|
+
app.put("/api/metadata", async (req, res) => {
|
|
92
|
+
const metadata = req.body;
|
|
93
|
+
await writeMetadata(metadata);
|
|
94
|
+
res.json(metadata);
|
|
95
|
+
});
|
|
96
|
+
// --- Environment endpoints ---
|
|
97
|
+
app.get("/api/environments", async (_req, res) => {
|
|
98
|
+
await ensureDirs();
|
|
99
|
+
const files = await fs.readdir(ENVIRONMENTS_DIR);
|
|
100
|
+
const envFiles = files.filter((f) => f.endsWith(".env"));
|
|
101
|
+
res.json(envFiles.map((f) => f.replace(/\.env$/, "")));
|
|
102
|
+
});
|
|
103
|
+
app.post("/api/environments", async (req, res) => {
|
|
104
|
+
await ensureDirs();
|
|
105
|
+
const { name, variables } = req.body;
|
|
106
|
+
if (!name || typeof name !== "string") {
|
|
107
|
+
res.status(400).json({ error: "name is required" });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
111
|
+
const filePath = path.join(ENVIRONMENTS_DIR, `${safeName}.env`);
|
|
112
|
+
try {
|
|
113
|
+
await fs.access(filePath);
|
|
114
|
+
res.status(409).json({ error: "Environment already exists" });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Doesn't exist, good
|
|
119
|
+
}
|
|
120
|
+
const content = variables
|
|
121
|
+
? Object.entries(variables)
|
|
122
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
123
|
+
.join("\n")
|
|
124
|
+
: "";
|
|
125
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
126
|
+
res.status(201).json({ name: safeName });
|
|
127
|
+
});
|
|
128
|
+
app.get("/api/environments/:name", async (req, res) => {
|
|
129
|
+
await ensureDirs();
|
|
130
|
+
const filePath = path.join(ENVIRONMENTS_DIR, `${req.params.name}.env`);
|
|
131
|
+
try {
|
|
132
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
133
|
+
const variables = {};
|
|
134
|
+
for (const line of content.split("\n")) {
|
|
135
|
+
const trimmed = line.trim();
|
|
136
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
137
|
+
continue;
|
|
138
|
+
const eqIdx = trimmed.indexOf("=");
|
|
139
|
+
if (eqIdx > 0) {
|
|
140
|
+
variables[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
res.json({ name: req.params.name, variables });
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
res.status(404).json({ error: "Environment not found" });
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
app.put("/api/environments/:name", async (req, res) => {
|
|
150
|
+
await ensureDirs();
|
|
151
|
+
const filePath = path.join(ENVIRONMENTS_DIR, `${req.params.name}.env`);
|
|
152
|
+
const { variables } = req.body;
|
|
153
|
+
const content = Object.entries(variables)
|
|
154
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
155
|
+
.join("\n");
|
|
156
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
157
|
+
res.json({ name: req.params.name });
|
|
158
|
+
});
|
|
159
|
+
app.delete("/api/environments/:name", async (req, res) => {
|
|
160
|
+
await ensureDirs();
|
|
161
|
+
const filePath = path.join(ENVIRONMENTS_DIR, `${req.params.name}.env`);
|
|
162
|
+
try {
|
|
163
|
+
await fs.unlink(filePath);
|
|
164
|
+
res.json({ ok: true });
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
res.status(404).json({ error: "Environment not found" });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
// --- Run endpoint ---
|
|
171
|
+
app.post("/api/run", async (req, res) => {
|
|
172
|
+
await ensureDirs();
|
|
173
|
+
const { file, environment } = req.body;
|
|
174
|
+
if (!file) {
|
|
175
|
+
res.status(400).json({ error: "file is required" });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const hurlPath = path.join(COLLECTIONS_DIR, `${file}.hurl`);
|
|
179
|
+
try {
|
|
180
|
+
await fs.access(hurlPath);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
res.status(404).json({ error: "Hurl file not found" });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const args = ["--json", "--very-verbose", hurlPath];
|
|
187
|
+
if (environment) {
|
|
188
|
+
const envPath = path.join(ENVIRONMENTS_DIR, `${environment}.env`);
|
|
189
|
+
try {
|
|
190
|
+
await fs.access(envPath);
|
|
191
|
+
args.push("--variables-file", envPath);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
res.status(404).json({ error: "Environment file not found" });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const startTime = Date.now();
|
|
200
|
+
const { stdout, stderr } = await execFileAsync("hurl", args, {
|
|
201
|
+
timeout: 30000,
|
|
202
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
203
|
+
});
|
|
204
|
+
const duration = Date.now() - startTime;
|
|
205
|
+
let jsonOutput = null;
|
|
206
|
+
try {
|
|
207
|
+
jsonOutput = JSON.parse(stdout);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// stdout may not be valid JSON
|
|
211
|
+
}
|
|
212
|
+
res.json({
|
|
213
|
+
success: true,
|
|
214
|
+
duration,
|
|
215
|
+
json: jsonOutput,
|
|
216
|
+
stdout,
|
|
217
|
+
stderr,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
const duration = 0;
|
|
222
|
+
const error = err;
|
|
223
|
+
let jsonOutput = null;
|
|
224
|
+
if (error.stdout) {
|
|
225
|
+
try {
|
|
226
|
+
jsonOutput = JSON.parse(error.stdout);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// not valid JSON
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
res.json({
|
|
233
|
+
success: false,
|
|
234
|
+
duration,
|
|
235
|
+
json: jsonOutput,
|
|
236
|
+
stdout: error.stdout ?? "",
|
|
237
|
+
stderr: error.stderr ?? error.message ?? "Unknown error",
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
// --- Serve frontend ---
|
|
242
|
+
const clientDir = path.join(import.meta.dirname, "../client");
|
|
243
|
+
app.use(express.static(clientDir));
|
|
244
|
+
app.get("/{*path}", (_req, res) => {
|
|
245
|
+
res.sendFile(path.join(clientDir, "index.html"));
|
|
246
|
+
});
|
|
247
|
+
return app;
|
|
248
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hurler",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"hurler": "./dist/server/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist/"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"dev": "concurrently -n fe,be -c cyan,green \"vite\" \"npm run dev --prefix server\"",
|
|
13
|
+
"dev:fe": "vite",
|
|
14
|
+
"dev:be": "npm run dev --prefix server",
|
|
15
|
+
"build": "rm -rf dist && tsc -b && vite build && tsc -p server/tsconfig.json",
|
|
16
|
+
"lint": "eslint .",
|
|
17
|
+
"preview": "vite preview",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@codemirror/lang-javascript": "^6.2.4",
|
|
22
|
+
"@codemirror/state": "^6.5.4",
|
|
23
|
+
"@codemirror/theme-one-dark": "^6.1.3",
|
|
24
|
+
"@codemirror/view": "^6.39.13",
|
|
25
|
+
"class-variance-authority": "^0.7.1",
|
|
26
|
+
"clsx": "^2.1.1",
|
|
27
|
+
"codemirror": "^6.0.2",
|
|
28
|
+
"express": "^5.1.0",
|
|
29
|
+
"lucide-react": "^0.563.0",
|
|
30
|
+
"open": "^10.1.0",
|
|
31
|
+
"radix-ui": "^1.4.3",
|
|
32
|
+
"react": "^19.2.0",
|
|
33
|
+
"react-dom": "^19.2.0",
|
|
34
|
+
"react-resizable-panels": "^4.6.2",
|
|
35
|
+
"tailwind-merge": "^3.4.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@eslint/js": "^9.39.1",
|
|
39
|
+
"@tailwindcss/vite": "^4.1.18",
|
|
40
|
+
"@types/node": "^24.10.1",
|
|
41
|
+
"@types/react": "^19.2.7",
|
|
42
|
+
"@types/react-dom": "^19.2.3",
|
|
43
|
+
"@vitejs/plugin-react": "^5.1.1",
|
|
44
|
+
"concurrently": "^9.2.1",
|
|
45
|
+
"eslint": "^9.39.1",
|
|
46
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
47
|
+
"eslint-plugin-react-refresh": "^0.4.24",
|
|
48
|
+
"globals": "^16.5.0",
|
|
49
|
+
"shadcn": "^3.8.4",
|
|
50
|
+
"tailwindcss": "^4.1.18",
|
|
51
|
+
"tw-animate-css": "^1.4.0",
|
|
52
|
+
"typescript": "~5.9.3",
|
|
53
|
+
"typescript-eslint": "^8.48.0",
|
|
54
|
+
"vite": "^7.3.1"
|
|
55
|
+
}
|
|
56
|
+
}
|