@drewpayment/mink 0.2.0 → 0.2.2
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/cli.js +231 -91
- package/package.json +3 -2
- package/src/commands/dashboard.ts +1 -1
- package/src/core/daemon.ts +2 -1
- package/src/core/dashboard-server.ts +47 -48
- package/src/core/runtime.ts +214 -0
package/dist/cli.js
CHANGED
|
@@ -2594,7 +2594,8 @@ function startDaemon(cwd) {
|
|
|
2594
2594
|
if (existing) {
|
|
2595
2595
|
removePidFile();
|
|
2596
2596
|
}
|
|
2597
|
-
const
|
|
2597
|
+
const __dir = dirname4(new URL(import.meta.url).pathname);
|
|
2598
|
+
const cliPath = resolve2(__dir, "../cli.ts");
|
|
2598
2599
|
const logPath = schedulerLogPath();
|
|
2599
2600
|
mkdirSync6(dirname4(logPath), { recursive: true });
|
|
2600
2601
|
const logFd = openSync(logPath, "a");
|
|
@@ -4582,6 +4583,146 @@ var init_project_registry = __esm(() => {
|
|
|
4582
4583
|
init_fs_utils();
|
|
4583
4584
|
});
|
|
4584
4585
|
|
|
4586
|
+
// src/core/runtime.ts
|
|
4587
|
+
import { readFile as readFile2, stat } from "fs/promises";
|
|
4588
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
4589
|
+
function runtimeFile(path) {
|
|
4590
|
+
if (isBun) {
|
|
4591
|
+
const f = Bun.file(path);
|
|
4592
|
+
return {
|
|
4593
|
+
exists: () => f.exists(),
|
|
4594
|
+
bytes: () => f.arrayBuffer().then((ab) => new Uint8Array(ab))
|
|
4595
|
+
};
|
|
4596
|
+
}
|
|
4597
|
+
return {
|
|
4598
|
+
async exists() {
|
|
4599
|
+
try {
|
|
4600
|
+
await stat(path);
|
|
4601
|
+
return true;
|
|
4602
|
+
} catch {
|
|
4603
|
+
return false;
|
|
4604
|
+
}
|
|
4605
|
+
},
|
|
4606
|
+
async bytes() {
|
|
4607
|
+
return readFile2(path);
|
|
4608
|
+
}
|
|
4609
|
+
};
|
|
4610
|
+
}
|
|
4611
|
+
function runtimeSpawn(cmd, opts = {}) {
|
|
4612
|
+
if (isBun) {
|
|
4613
|
+
const proc2 = Bun.spawn(cmd, {
|
|
4614
|
+
cwd: opts.cwd,
|
|
4615
|
+
env: opts.env,
|
|
4616
|
+
stdout: opts.stdout ?? "ignore",
|
|
4617
|
+
stderr: opts.stderr ?? "ignore",
|
|
4618
|
+
stdin: opts.stdin ?? "ignore"
|
|
4619
|
+
});
|
|
4620
|
+
return { unref: () => proc2.unref() };
|
|
4621
|
+
}
|
|
4622
|
+
const [bin, ...args] = cmd;
|
|
4623
|
+
const proc = nodeSpawn(bin, args, {
|
|
4624
|
+
cwd: opts.cwd,
|
|
4625
|
+
env: opts.env,
|
|
4626
|
+
stdio: [
|
|
4627
|
+
opts.stdin ?? "ignore",
|
|
4628
|
+
opts.stdout ?? "ignore",
|
|
4629
|
+
opts.stderr ?? "ignore"
|
|
4630
|
+
],
|
|
4631
|
+
detached: true
|
|
4632
|
+
});
|
|
4633
|
+
proc.unref();
|
|
4634
|
+
return { unref: () => {} };
|
|
4635
|
+
}
|
|
4636
|
+
async function runtimeServe(opts) {
|
|
4637
|
+
if (isBun) {
|
|
4638
|
+
const server = Bun.serve({
|
|
4639
|
+
port: opts.port,
|
|
4640
|
+
hostname: opts.hostname,
|
|
4641
|
+
idleTimeout: opts.idleTimeout ?? 0,
|
|
4642
|
+
fetch: opts.fetch
|
|
4643
|
+
});
|
|
4644
|
+
return {
|
|
4645
|
+
port: server.port,
|
|
4646
|
+
stop: (close) => server.stop(close)
|
|
4647
|
+
};
|
|
4648
|
+
}
|
|
4649
|
+
const { createServer } = await import("node:http");
|
|
4650
|
+
const { Readable } = await import("node:stream");
|
|
4651
|
+
return new Promise((resolve3) => {
|
|
4652
|
+
const httpServer = createServer(async (req, res) => {
|
|
4653
|
+
const url = `http://${opts.hostname}:${opts.port}${req.url ?? "/"}`;
|
|
4654
|
+
const headers = new Headers;
|
|
4655
|
+
for (const [key, val] of Object.entries(req.headers)) {
|
|
4656
|
+
if (val)
|
|
4657
|
+
headers.set(key, Array.isArray(val) ? val.join(", ") : val);
|
|
4658
|
+
}
|
|
4659
|
+
let body = null;
|
|
4660
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
4661
|
+
const chunks = [];
|
|
4662
|
+
for await (const chunk of req) {
|
|
4663
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
4664
|
+
}
|
|
4665
|
+
body = Buffer.concat(chunks);
|
|
4666
|
+
}
|
|
4667
|
+
const request = new Request(url, {
|
|
4668
|
+
method: req.method,
|
|
4669
|
+
headers,
|
|
4670
|
+
body,
|
|
4671
|
+
duplex: body ? "half" : undefined
|
|
4672
|
+
});
|
|
4673
|
+
try {
|
|
4674
|
+
const response = await opts.fetch(request);
|
|
4675
|
+
res.writeHead(response.status, Object.fromEntries(response.headers));
|
|
4676
|
+
if (!response.body) {
|
|
4677
|
+
res.end();
|
|
4678
|
+
return;
|
|
4679
|
+
}
|
|
4680
|
+
const reader = response.body.getReader();
|
|
4681
|
+
const nodeStream = new Readable({
|
|
4682
|
+
async read() {
|
|
4683
|
+
try {
|
|
4684
|
+
const { done, value } = await reader.read();
|
|
4685
|
+
if (done) {
|
|
4686
|
+
this.push(null);
|
|
4687
|
+
} else {
|
|
4688
|
+
this.push(Buffer.from(value));
|
|
4689
|
+
}
|
|
4690
|
+
} catch {
|
|
4691
|
+
this.push(null);
|
|
4692
|
+
}
|
|
4693
|
+
}
|
|
4694
|
+
});
|
|
4695
|
+
res.on("close", () => {
|
|
4696
|
+
reader.cancel().catch(() => {});
|
|
4697
|
+
nodeStream.destroy();
|
|
4698
|
+
});
|
|
4699
|
+
nodeStream.pipe(res);
|
|
4700
|
+
} catch (err) {
|
|
4701
|
+
if (!res.headersSent) {
|
|
4702
|
+
res.writeHead(500);
|
|
4703
|
+
res.end(String(err));
|
|
4704
|
+
}
|
|
4705
|
+
}
|
|
4706
|
+
});
|
|
4707
|
+
httpServer.listen(opts.port, opts.hostname, () => {
|
|
4708
|
+
const addr = httpServer.address();
|
|
4709
|
+
const boundPort = typeof addr === "object" && addr ? addr.port : opts.port;
|
|
4710
|
+
resolve3({
|
|
4711
|
+
port: boundPort,
|
|
4712
|
+
stop: (close) => {
|
|
4713
|
+
if (close)
|
|
4714
|
+
httpServer.closeAllConnections();
|
|
4715
|
+
httpServer.close();
|
|
4716
|
+
}
|
|
4717
|
+
});
|
|
4718
|
+
});
|
|
4719
|
+
});
|
|
4720
|
+
}
|
|
4721
|
+
var isBun;
|
|
4722
|
+
var init_runtime = __esm(() => {
|
|
4723
|
+
isBun = typeof globalThis.Bun !== "undefined";
|
|
4724
|
+
});
|
|
4725
|
+
|
|
4585
4726
|
// src/core/dashboard-server.ts
|
|
4586
4727
|
var exports_dashboard_server = {};
|
|
4587
4728
|
__export(exports_dashboard_server, {
|
|
@@ -4589,7 +4730,7 @@ __export(exports_dashboard_server, {
|
|
|
4589
4730
|
});
|
|
4590
4731
|
import { watch } from "fs";
|
|
4591
4732
|
import { existsSync as existsSync13 } from "fs";
|
|
4592
|
-
import { basename as basename6, join as join14, extname as extname2 } from "path";
|
|
4733
|
+
import { basename as basename6, dirname as dirname5, join as join14, extname as extname2 } from "path";
|
|
4593
4734
|
|
|
4594
4735
|
class SSEManager {
|
|
4595
4736
|
clients = new Map;
|
|
@@ -4740,7 +4881,7 @@ function extractPathParam(pathname, prefix) {
|
|
|
4740
4881
|
return rest || null;
|
|
4741
4882
|
return rest.slice(0, slashIdx) || null;
|
|
4742
4883
|
}
|
|
4743
|
-
function startDashboardServer(cwd, options = {}) {
|
|
4884
|
+
async function startDashboardServer(cwd, options = {}) {
|
|
4744
4885
|
const port = options.port ?? 4040;
|
|
4745
4886
|
const hostname = options.hostname ?? "127.0.0.1";
|
|
4746
4887
|
const sseManager = new SSEManager;
|
|
@@ -4762,13 +4903,29 @@ function startDashboardServer(cwd, options = {}) {
|
|
|
4762
4903
|
timestamp: new Date().toISOString()
|
|
4763
4904
|
});
|
|
4764
4905
|
});
|
|
4765
|
-
const
|
|
4906
|
+
const __dir = dirname5(new URL(import.meta.url).pathname);
|
|
4907
|
+
let pkgRoot = __dir;
|
|
4908
|
+
while (pkgRoot !== dirname5(pkgRoot)) {
|
|
4909
|
+
if (existsSync13(join14(pkgRoot, "package.json")))
|
|
4910
|
+
break;
|
|
4911
|
+
pkgRoot = dirname5(pkgRoot);
|
|
4912
|
+
}
|
|
4913
|
+
const dashboardOutDir = join14(pkgRoot, "dashboard", "out");
|
|
4766
4914
|
const dashboardBuilt = existsSync13(join14(dashboardOutDir, "index.html"));
|
|
4767
4915
|
let clientIdCounter = 0;
|
|
4768
4916
|
if (!dashboardBuilt) {
|
|
4769
4917
|
console.warn("[mink] dashboard not built. Run: cd dashboard && bun run build");
|
|
4770
4918
|
}
|
|
4771
|
-
|
|
4919
|
+
async function serveFile(filePath, contentType) {
|
|
4920
|
+
const file = runtimeFile(filePath);
|
|
4921
|
+
if (await file.exists()) {
|
|
4922
|
+
return new Response(await file.bytes(), {
|
|
4923
|
+
headers: { "Content-Type": contentType }
|
|
4924
|
+
});
|
|
4925
|
+
}
|
|
4926
|
+
return null;
|
|
4927
|
+
}
|
|
4928
|
+
const server = await runtimeServe({
|
|
4772
4929
|
port,
|
|
4773
4930
|
hostname,
|
|
4774
4931
|
idleTimeout: 0,
|
|
@@ -4791,26 +4948,17 @@ function startDashboardServer(cwd, options = {}) {
|
|
|
4791
4948
|
if (!filePath.startsWith(dashboardOutDir)) {
|
|
4792
4949
|
return jsonResponse({ error: "Forbidden" }, 403);
|
|
4793
4950
|
}
|
|
4794
|
-
const
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
return
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
const
|
|
4803
|
-
if (
|
|
4804
|
-
return
|
|
4805
|
-
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
4806
|
-
});
|
|
4807
|
-
}
|
|
4808
|
-
const indexFile = Bun.file(join14(dashboardOutDir, "index.html"));
|
|
4809
|
-
if (await indexFile.exists()) {
|
|
4810
|
-
return new Response(indexFile, {
|
|
4811
|
-
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
4812
|
-
});
|
|
4813
|
-
}
|
|
4951
|
+
const ext = extname2(filePath);
|
|
4952
|
+
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
4953
|
+
const served = await serveFile(filePath, contentType);
|
|
4954
|
+
if (served)
|
|
4955
|
+
return served;
|
|
4956
|
+
const htmlServed = await serveFile(filePath + ".html", "text/html; charset=utf-8");
|
|
4957
|
+
if (htmlServed)
|
|
4958
|
+
return htmlServed;
|
|
4959
|
+
const indexServed = await serveFile(join14(dashboardOutDir, "index.html"), "text/html; charset=utf-8");
|
|
4960
|
+
if (indexServed)
|
|
4961
|
+
return indexServed;
|
|
4814
4962
|
}
|
|
4815
4963
|
}
|
|
4816
4964
|
if (method === "GET" && pathname === "/api/events") {
|
|
@@ -4869,15 +5017,11 @@ retry: 3000
|
|
|
4869
5017
|
return jsonResponse({ error: "Invalid filename" }, 400);
|
|
4870
5018
|
}
|
|
4871
5019
|
const imgPath = join14(designCapturesDir(resolvedCwd), filename);
|
|
4872
|
-
const
|
|
4873
|
-
if (
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
"Cache-Control": "public, max-age=60",
|
|
4878
|
-
"Access-Control-Allow-Origin": "*"
|
|
4879
|
-
}
|
|
4880
|
-
});
|
|
5020
|
+
const served = await serveFile(imgPath, "image/jpeg");
|
|
5021
|
+
if (served) {
|
|
5022
|
+
served.headers.set("Cache-Control", "public, max-age=60");
|
|
5023
|
+
served.headers.set("Access-Control-Allow-Origin", "*");
|
|
5024
|
+
return served;
|
|
4881
5025
|
}
|
|
4882
5026
|
return jsonResponse({ error: "Image not found" }, 404);
|
|
4883
5027
|
}
|
|
@@ -4951,12 +5095,7 @@ retry: 3000
|
|
|
4951
5095
|
try {
|
|
4952
5096
|
const platform = process.platform;
|
|
4953
5097
|
const cmd = platform === "darwin" ? ["open", serverUrl] : platform === "win32" ? ["cmd", "/c", "start", serverUrl] : ["xdg-open", serverUrl];
|
|
4954
|
-
|
|
4955
|
-
stdout: "ignore",
|
|
4956
|
-
stderr: "ignore",
|
|
4957
|
-
stdin: "ignore"
|
|
4958
|
-
});
|
|
4959
|
-
proc.unref();
|
|
5098
|
+
runtimeSpawn(cmd).unref();
|
|
4960
5099
|
} catch {}
|
|
4961
5100
|
}
|
|
4962
5101
|
return {
|
|
@@ -4974,6 +5113,7 @@ var init_dashboard_server = __esm(() => {
|
|
|
4974
5113
|
init_dashboard_api();
|
|
4975
5114
|
init_project_registry();
|
|
4976
5115
|
init_project_id();
|
|
5116
|
+
init_runtime();
|
|
4977
5117
|
MIME_TYPES = {
|
|
4978
5118
|
".html": "text/html; charset=utf-8",
|
|
4979
5119
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -5018,7 +5158,7 @@ async function dashboard(cwd, args) {
|
|
|
5018
5158
|
const port = portArg ? parseInt(portArg.split("=")[1], 10) : 4040;
|
|
5019
5159
|
const noOpen = args.includes("--no-open");
|
|
5020
5160
|
const { startDashboardServer: startDashboardServer2 } = await Promise.resolve().then(() => (init_dashboard_server(), exports_dashboard_server));
|
|
5021
|
-
const { url } = startDashboardServer2(cwd, { port, open: !noOpen });
|
|
5161
|
+
const { url } = await startDashboardServer2(cwd, { port, open: !noOpen });
|
|
5022
5162
|
console.log(`[mink] dashboard running at ${url}`);
|
|
5023
5163
|
console.log("[mink] press Ctrl+C to stop");
|
|
5024
5164
|
await new Promise(() => {});
|
|
@@ -5166,7 +5306,7 @@ var init_config2 = __esm(() => {
|
|
|
5166
5306
|
// src/commands/init.ts
|
|
5167
5307
|
import { execSync as execSync3 } from "child_process";
|
|
5168
5308
|
import { mkdirSync as mkdirSync7, existsSync as existsSync16 } from "fs";
|
|
5169
|
-
import { resolve as resolve3, dirname as
|
|
5309
|
+
import { resolve as resolve3, dirname as dirname6, basename as basename7, join as join15 } from "path";
|
|
5170
5310
|
function detectRuntime2() {
|
|
5171
5311
|
try {
|
|
5172
5312
|
execSync3("bun --version", { stdio: "ignore" });
|
|
@@ -5207,7 +5347,7 @@ function isMinkHook2(entry) {
|
|
|
5207
5347
|
return false;
|
|
5208
5348
|
}
|
|
5209
5349
|
function mergeHooksIntoSettings2(settingsPath, newHooks) {
|
|
5210
|
-
mkdirSync7(
|
|
5350
|
+
mkdirSync7(dirname6(settingsPath), { recursive: true });
|
|
5211
5351
|
const existing = safeReadJson(settingsPath) ?? {};
|
|
5212
5352
|
const existingHooks = existing.hooks ?? {};
|
|
5213
5353
|
for (const [event, entries] of Object.entries(newHooks)) {
|
|
@@ -5230,7 +5370,7 @@ var exports_update = {};
|
|
|
5230
5370
|
__export(exports_update, {
|
|
5231
5371
|
update: () => update
|
|
5232
5372
|
});
|
|
5233
|
-
import { resolve as resolve4, dirname as
|
|
5373
|
+
import { resolve as resolve4, dirname as dirname7 } from "path";
|
|
5234
5374
|
function parseArgs(args) {
|
|
5235
5375
|
let dryRun = false;
|
|
5236
5376
|
let project = null;
|
|
@@ -5278,7 +5418,7 @@ async function update(cwd, args) {
|
|
|
5278
5418
|
return;
|
|
5279
5419
|
}
|
|
5280
5420
|
const runtime = detectRuntime2();
|
|
5281
|
-
const cliPath = resolve4(
|
|
5421
|
+
const cliPath = resolve4(dirname7(new URL(import.meta.url).pathname), "../cli.ts");
|
|
5282
5422
|
const newHooks = buildHooksConfig2(runtime, cliPath);
|
|
5283
5423
|
for (const target of targets) {
|
|
5284
5424
|
console.log(`[mink] updating: ${target.name} (${target.id})`);
|
|
@@ -5495,8 +5635,8 @@ function findFiles(dir, pattern) {
|
|
|
5495
5635
|
continue;
|
|
5496
5636
|
const full = join17(current, entry);
|
|
5497
5637
|
try {
|
|
5498
|
-
const
|
|
5499
|
-
if (
|
|
5638
|
+
const stat2 = statSync8(full);
|
|
5639
|
+
if (stat2.isDirectory()) {
|
|
5500
5640
|
walk(full);
|
|
5501
5641
|
} else if (pattern.test(entry)) {
|
|
5502
5642
|
results.push(full);
|
|
@@ -35737,18 +35877,18 @@ var __runInitializers10 = function(thisArg, initializers, value) {
|
|
|
35737
35877
|
if (target)
|
|
35738
35878
|
Object.defineProperty(target, contextIn.name, descriptor);
|
|
35739
35879
|
done = true;
|
|
35740
|
-
},
|
|
35880
|
+
}, Request2;
|
|
35741
35881
|
var init_Request = __esm(() => {
|
|
35742
35882
|
init_Errors();
|
|
35743
35883
|
init_EventEmitter();
|
|
35744
35884
|
init_decorators();
|
|
35745
35885
|
init_disposable();
|
|
35746
|
-
|
|
35886
|
+
Request2 = (() => {
|
|
35747
35887
|
var _a2;
|
|
35748
35888
|
let _classSuper = EventEmitter;
|
|
35749
35889
|
let _instanceExtraInitializers = [];
|
|
35750
35890
|
let _dispose_decorators;
|
|
35751
|
-
return class
|
|
35891
|
+
return class Request3 extends _classSuper {
|
|
35752
35892
|
static {
|
|
35753
35893
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : undefined;
|
|
35754
35894
|
__esDecorate10(this, null, _dispose_decorators, { kind: "method", name: "dispose", static: false, private: false, access: { has: (obj) => ("dispose" in obj), get: (obj) => obj.dispose }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
@@ -35756,7 +35896,7 @@ var init_Request = __esm(() => {
|
|
|
35756
35896
|
Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
35757
35897
|
}
|
|
35758
35898
|
static from(browsingContext, event) {
|
|
35759
|
-
const request = new
|
|
35899
|
+
const request = new Request3(browsingContext, event);
|
|
35760
35900
|
request.#initialize();
|
|
35761
35901
|
return request;
|
|
35762
35902
|
}
|
|
@@ -35795,7 +35935,7 @@ var init_Request = __esm(() => {
|
|
|
35795
35935
|
if (event.redirectCount !== this.#event.redirectCount + 1 && !isAfterAuth) {
|
|
35796
35936
|
return;
|
|
35797
35937
|
}
|
|
35798
|
-
this.#redirect =
|
|
35938
|
+
this.#redirect = Request3.from(this.#browsingContext, event);
|
|
35799
35939
|
this.emit("redirect", this.#redirect);
|
|
35800
35940
|
this.dispose();
|
|
35801
35941
|
});
|
|
@@ -36369,7 +36509,7 @@ var init_BrowsingContext = __esm(() => {
|
|
|
36369
36509
|
if (this.#requests.has(event.request.request)) {
|
|
36370
36510
|
return;
|
|
36371
36511
|
}
|
|
36372
|
-
const request =
|
|
36512
|
+
const request = Request2.from(this, event);
|
|
36373
36513
|
this.#requests.set(request.id, request);
|
|
36374
36514
|
this.emit("request", { request });
|
|
36375
36515
|
});
|
|
@@ -51207,8 +51347,8 @@ var require_file = __commonJS((exports) => {
|
|
|
51207
51347
|
debug2("Normalized pathname: %o", filepath);
|
|
51208
51348
|
const fdHandle = await fs_1.promises.open(filepath, flags, mode);
|
|
51209
51349
|
const fd = fdHandle.fd;
|
|
51210
|
-
const
|
|
51211
|
-
if (cache && cache.stat &&
|
|
51350
|
+
const stat2 = await fdHandle.stat();
|
|
51351
|
+
if (cache && cache.stat && stat2 && isNotModified(cache.stat, stat2)) {
|
|
51212
51352
|
await fdHandle.close();
|
|
51213
51353
|
throw new notmodified_1.default;
|
|
51214
51354
|
}
|
|
@@ -51217,7 +51357,7 @@ var require_file = __commonJS((exports) => {
|
|
|
51217
51357
|
...opts,
|
|
51218
51358
|
fd
|
|
51219
51359
|
});
|
|
51220
|
-
rs.stat =
|
|
51360
|
+
rs.stat = stat2;
|
|
51221
51361
|
return rs;
|
|
51222
51362
|
} catch (err) {
|
|
51223
51363
|
if (err.code === "ENOENT") {
|
|
@@ -77280,7 +77420,7 @@ var require_tar_fs = __commonJS((exports) => {
|
|
|
77280
77420
|
pack2.entry(header, onnextentry);
|
|
77281
77421
|
});
|
|
77282
77422
|
}
|
|
77283
|
-
function onstat(err, filename,
|
|
77423
|
+
function onstat(err, filename, stat2) {
|
|
77284
77424
|
if (pack2.destroyed)
|
|
77285
77425
|
return;
|
|
77286
77426
|
if (err)
|
|
@@ -77290,31 +77430,31 @@ var require_tar_fs = __commonJS((exports) => {
|
|
|
77290
77430
|
pack2.finalize();
|
|
77291
77431
|
return finish(pack2);
|
|
77292
77432
|
}
|
|
77293
|
-
if (
|
|
77433
|
+
if (stat2.isSocket())
|
|
77294
77434
|
return onnextentry();
|
|
77295
77435
|
let header = {
|
|
77296
77436
|
name: normalize(filename),
|
|
77297
|
-
mode: (
|
|
77298
|
-
mtime:
|
|
77299
|
-
size:
|
|
77437
|
+
mode: (stat2.mode | (stat2.isDirectory() ? dmode : fmode)) & umask,
|
|
77438
|
+
mtime: stat2.mtime,
|
|
77439
|
+
size: stat2.size,
|
|
77300
77440
|
type: "file",
|
|
77301
|
-
uid:
|
|
77302
|
-
gid:
|
|
77441
|
+
uid: stat2.uid,
|
|
77442
|
+
gid: stat2.gid
|
|
77303
77443
|
};
|
|
77304
|
-
if (
|
|
77444
|
+
if (stat2.isDirectory()) {
|
|
77305
77445
|
header.size = 0;
|
|
77306
77446
|
header.type = "directory";
|
|
77307
77447
|
header = map2(header) || header;
|
|
77308
77448
|
return pack2.entry(header, onnextentry);
|
|
77309
77449
|
}
|
|
77310
|
-
if (
|
|
77450
|
+
if (stat2.isSymbolicLink()) {
|
|
77311
77451
|
header.size = 0;
|
|
77312
77452
|
header.type = "symlink";
|
|
77313
77453
|
header = map2(header) || header;
|
|
77314
77454
|
return onsymlink(filename, header);
|
|
77315
77455
|
}
|
|
77316
77456
|
header = map2(header) || header;
|
|
77317
|
-
if (!
|
|
77457
|
+
if (!stat2.isFile()) {
|
|
77318
77458
|
if (strict)
|
|
77319
77459
|
return pack2.destroy(new Error("unsupported type for " + filename));
|
|
77320
77460
|
return onnextentry();
|
|
@@ -77397,7 +77537,7 @@ var require_tar_fs = __commonJS((exports) => {
|
|
|
77397
77537
|
uid: header.uid,
|
|
77398
77538
|
gid: header.gid,
|
|
77399
77539
|
mode: header.mode
|
|
77400
|
-
},
|
|
77540
|
+
}, stat2);
|
|
77401
77541
|
}
|
|
77402
77542
|
mkdirfix(dir, {
|
|
77403
77543
|
fs: xfs,
|
|
@@ -77422,7 +77562,7 @@ var require_tar_fs = __commonJS((exports) => {
|
|
|
77422
77562
|
next();
|
|
77423
77563
|
});
|
|
77424
77564
|
});
|
|
77425
|
-
function
|
|
77565
|
+
function stat2(err) {
|
|
77426
77566
|
if (err)
|
|
77427
77567
|
return next(err);
|
|
77428
77568
|
utimes(name, header, function(err2) {
|
|
@@ -77445,7 +77585,7 @@ var require_tar_fs = __commonJS((exports) => {
|
|
|
77445
77585
|
return next(err);
|
|
77446
77586
|
if (!valid && validateSymLinks)
|
|
77447
77587
|
return next(new Error(name + " is not a valid symlink"));
|
|
77448
|
-
xfs.symlink(header.linkname, name,
|
|
77588
|
+
xfs.symlink(header.linkname, name, stat2);
|
|
77449
77589
|
});
|
|
77450
77590
|
});
|
|
77451
77591
|
}
|
|
@@ -77462,7 +77602,7 @@ var require_tar_fs = __commonJS((exports) => {
|
|
|
77462
77602
|
stream = xfs.createReadStream(dst);
|
|
77463
77603
|
return onfile();
|
|
77464
77604
|
}
|
|
77465
|
-
|
|
77605
|
+
stat2(err2);
|
|
77466
77606
|
});
|
|
77467
77607
|
});
|
|
77468
77608
|
});
|
|
@@ -77479,7 +77619,7 @@ var require_tar_fs = __commonJS((exports) => {
|
|
|
77479
77619
|
pump(rs, ws, function(err) {
|
|
77480
77620
|
if (err)
|
|
77481
77621
|
return next(err);
|
|
77482
|
-
ws.on("close",
|
|
77622
|
+
ws.on("close", stat2);
|
|
77483
77623
|
});
|
|
77484
77624
|
}
|
|
77485
77625
|
}
|
|
@@ -77568,7 +77708,7 @@ var require_tar_fs = __commonJS((exports) => {
|
|
|
77568
77708
|
function normalize(name) {
|
|
77569
77709
|
return win32 ? name.replace(/\\/g, "/").replace(/[:?<>|]/g, "_") : name;
|
|
77570
77710
|
}
|
|
77571
|
-
function statAll(fs4,
|
|
77711
|
+
function statAll(fs4, stat2, cwd, ignore, entries, sort) {
|
|
77572
77712
|
if (!entries)
|
|
77573
77713
|
entries = ["."];
|
|
77574
77714
|
const queue = entries.slice(0);
|
|
@@ -77577,11 +77717,11 @@ var require_tar_fs = __commonJS((exports) => {
|
|
|
77577
77717
|
return callback(null);
|
|
77578
77718
|
const next = queue.shift();
|
|
77579
77719
|
const nextAbs = path7.join(cwd, next);
|
|
77580
|
-
|
|
77720
|
+
stat2.call(fs4, nextAbs, function(err, stat3) {
|
|
77581
77721
|
if (err)
|
|
77582
77722
|
return callback(entries.indexOf(next) === -1 && err.code === "ENOENT" ? null : err);
|
|
77583
|
-
if (!
|
|
77584
|
-
return callback(null, next,
|
|
77723
|
+
if (!stat3.isDirectory())
|
|
77724
|
+
return callback(null, next, stat3);
|
|
77585
77725
|
fs4.readdir(nextAbs, function(err2, files) {
|
|
77586
77726
|
if (err2)
|
|
77587
77727
|
return callback(err2);
|
|
@@ -77591,7 +77731,7 @@ var require_tar_fs = __commonJS((exports) => {
|
|
|
77591
77731
|
if (!ignore(path7.join(cwd, next, files[i])))
|
|
77592
77732
|
queue.push(path7.join(next, files[i]));
|
|
77593
77733
|
}
|
|
77594
|
-
callback(null, next,
|
|
77734
|
+
callback(null, next, stat3);
|
|
77595
77735
|
});
|
|
77596
77736
|
});
|
|
77597
77737
|
};
|
|
@@ -78323,19 +78463,19 @@ var init_cliui = __esm(() => {
|
|
|
78323
78463
|
});
|
|
78324
78464
|
|
|
78325
78465
|
// node_modules/escalade/sync/index.mjs
|
|
78326
|
-
import { dirname as
|
|
78466
|
+
import { dirname as dirname8, resolve as resolve6 } from "path";
|
|
78327
78467
|
import { readdirSync as readdirSync6, statSync as statSync9 } from "fs";
|
|
78328
78468
|
function sync_default(start, callback) {
|
|
78329
78469
|
let dir = resolve6(".", start);
|
|
78330
78470
|
let tmp, stats = statSync9(dir);
|
|
78331
78471
|
if (!stats.isDirectory()) {
|
|
78332
|
-
dir =
|
|
78472
|
+
dir = dirname8(dir);
|
|
78333
78473
|
}
|
|
78334
78474
|
while (true) {
|
|
78335
78475
|
tmp = callback(dir, readdirSync6(dir));
|
|
78336
78476
|
if (tmp)
|
|
78337
78477
|
return resolve6(dir, tmp);
|
|
78338
|
-
dir =
|
|
78478
|
+
dir = dirname8(tmp = dir);
|
|
78339
78479
|
if (tmp === dir)
|
|
78340
78480
|
break;
|
|
78341
78481
|
}
|
|
@@ -79549,7 +79689,7 @@ import { notStrictEqual, strictEqual } from "assert";
|
|
|
79549
79689
|
import { inspect } from "util";
|
|
79550
79690
|
import { readFileSync as readFileSync20 } from "fs";
|
|
79551
79691
|
import { fileURLToPath } from "url";
|
|
79552
|
-
import { basename as basename9, dirname as
|
|
79692
|
+
import { basename as basename9, dirname as dirname9, extname as extname3, relative as relative7, resolve as resolve9 } from "path";
|
|
79553
79693
|
var REQUIRE_ERROR = "require is not supported by ESM", REQUIRE_DIRECTORY_ERROR = "loading a directory of commands is not supported yet for ESM", __dirname2, mainFilename, esm_default;
|
|
79554
79694
|
var init_esm = __esm(() => {
|
|
79555
79695
|
init_cliui();
|
|
@@ -79582,7 +79722,7 @@ var init_esm = __esm(() => {
|
|
|
79582
79722
|
Parser: lib_default,
|
|
79583
79723
|
path: {
|
|
79584
79724
|
basename: basename9,
|
|
79585
|
-
dirname:
|
|
79725
|
+
dirname: dirname9,
|
|
79586
79726
|
extname: extname3,
|
|
79587
79727
|
relative: relative7,
|
|
79588
79728
|
resolve: resolve9
|
|
@@ -84279,7 +84419,7 @@ var init_PuppeteerNode = __esm(() => {
|
|
|
84279
84419
|
import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
|
|
84280
84420
|
import fs5 from "node:fs";
|
|
84281
84421
|
import os8 from "node:os";
|
|
84282
|
-
import { dirname as
|
|
84422
|
+
import { dirname as dirname10 } from "node:path";
|
|
84283
84423
|
import { PassThrough } from "node:stream";
|
|
84284
84424
|
var import_debug6, __runInitializers22 = function(thisArg, initializers, value) {
|
|
84285
84425
|
var useValue = arguments.length > 2;
|
|
@@ -84403,7 +84543,7 @@ var init_ScreenRecorder = __esm(() => {
|
|
|
84403
84543
|
filters.push(formatArgs.splice(vf, 2).at(-1) ?? "");
|
|
84404
84544
|
}
|
|
84405
84545
|
if (path11) {
|
|
84406
|
-
fs5.mkdirSync(
|
|
84546
|
+
fs5.mkdirSync(dirname10(path11), { recursive: overwrite });
|
|
84407
84547
|
}
|
|
84408
84548
|
this.#process = spawn2(ffmpegPath, [
|
|
84409
84549
|
["-loglevel", "error"],
|
|
@@ -84589,8 +84729,8 @@ function findChromeInDir(dir) {
|
|
|
84589
84729
|
for (const entry of entries) {
|
|
84590
84730
|
const full = join20(dir, entry);
|
|
84591
84731
|
try {
|
|
84592
|
-
const
|
|
84593
|
-
if (
|
|
84732
|
+
const stat2 = statSync12(full);
|
|
84733
|
+
if (stat2.isDirectory()) {
|
|
84594
84734
|
const found = findChromeInDir(full);
|
|
84595
84735
|
if (found)
|
|
84596
84736
|
return found;
|
|
@@ -86320,10 +86460,10 @@ function collectMarkdownFiles(dirPath, rootPath) {
|
|
|
86320
86460
|
if (entry.isDirectory()) {
|
|
86321
86461
|
files.push(...collectMarkdownFiles(fullPath, rootPath));
|
|
86322
86462
|
} else if (entry.name.endsWith(".md") && !entry.name.startsWith("_")) {
|
|
86323
|
-
const
|
|
86463
|
+
const stat2 = statSync12(fullPath);
|
|
86324
86464
|
const title = entry.name.replace(/\.md$/, "");
|
|
86325
86465
|
const relativePath = fullPath.slice(rootPath.length + 1);
|
|
86326
|
-
files.push({ title, relativePath, mtime:
|
|
86466
|
+
files.push({ title, relativePath, mtime: stat2.mtimeMs });
|
|
86327
86467
|
}
|
|
86328
86468
|
}
|
|
86329
86469
|
} catch {}
|
|
@@ -86880,7 +87020,7 @@ var exports_skill = {};
|
|
|
86880
87020
|
__export(exports_skill, {
|
|
86881
87021
|
skill: () => skill
|
|
86882
87022
|
});
|
|
86883
|
-
import { join as join24, resolve as resolve12, dirname as
|
|
87023
|
+
import { join as join24, resolve as resolve12, dirname as dirname11 } from "path";
|
|
86884
87024
|
import { homedir as homedir5 } from "os";
|
|
86885
87025
|
import {
|
|
86886
87026
|
existsSync as existsSync26,
|
|
@@ -86893,7 +87033,7 @@ import {
|
|
|
86893
87033
|
lstatSync
|
|
86894
87034
|
} from "fs";
|
|
86895
87035
|
function getSkillsSourceDir() {
|
|
86896
|
-
return resolve12(
|
|
87036
|
+
return resolve12(dirname11(new URL(import.meta.url).pathname), "../../skills");
|
|
86897
87037
|
}
|
|
86898
87038
|
function getAvailableSkills() {
|
|
86899
87039
|
const dir = getSkillsSourceDir();
|
|
@@ -87407,8 +87547,8 @@ switch (command2) {
|
|
|
87407
87547
|
case "version":
|
|
87408
87548
|
case "--version":
|
|
87409
87549
|
case "-v": {
|
|
87410
|
-
const { resolve: resolve13, dirname:
|
|
87411
|
-
const cliPath = resolve13(
|
|
87550
|
+
const { resolve: resolve13, dirname: dirname12 } = await import("path");
|
|
87551
|
+
const cliPath = resolve13(dirname12(new URL(import.meta.url).pathname));
|
|
87412
87552
|
const { readFileSync: readFileSync25 } = await import("fs");
|
|
87413
87553
|
try {
|
|
87414
87554
|
const pkg = JSON.parse(readFileSync25(resolve13(cliPath, "../package.json"), "utf-8"));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drewpayment/mink",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "A hidden presence that moves alongside the developer — token efficiency and cross-project wiki for AI coding assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
"src/**/*.ts",
|
|
20
20
|
"dist/cli.js",
|
|
21
|
-
"skills/**/*"
|
|
21
|
+
"skills/**/*",
|
|
22
|
+
"dashboard/out"
|
|
22
23
|
],
|
|
23
24
|
"publishConfig": {
|
|
24
25
|
"access": "public"
|
|
@@ -12,7 +12,7 @@ export async function dashboard(cwd: string, args: string[]): Promise<void> {
|
|
|
12
12
|
const noOpen = args.includes("--no-open");
|
|
13
13
|
|
|
14
14
|
const { startDashboardServer } = await import("../core/dashboard-server");
|
|
15
|
-
const { url } = startDashboardServer(cwd, { port, open: !noOpen });
|
|
15
|
+
const { url } = await startDashboardServer(cwd, { port, open: !noOpen });
|
|
16
16
|
|
|
17
17
|
console.log(`[mink] dashboard running at ${url}`);
|
|
18
18
|
console.log("[mink] press Ctrl+C to stop");
|
package/src/core/daemon.ts
CHANGED
|
@@ -64,7 +64,8 @@ export function startDaemon(cwd: string): void {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// Resolve the CLI entry point
|
|
67
|
-
const
|
|
67
|
+
const __dir = dirname(new URL(import.meta.url).pathname);
|
|
68
|
+
const cliPath = resolve(__dir, "../cli.ts");
|
|
68
69
|
|
|
69
70
|
// Ensure log directory exists
|
|
70
71
|
const logPath = schedulerLogPath();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { watch, type FSWatcher } from "fs";
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
|
-
import { basename, join, extname } from "path";
|
|
3
|
+
import { basename, dirname, join, extname } from "path";
|
|
4
4
|
import { projectDir, designCapturesDir } from "./paths";
|
|
5
5
|
import {
|
|
6
6
|
loadOverview,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "./dashboard-api";
|
|
18
18
|
import { listRegisteredProjects, getProjectMeta } from "./project-registry";
|
|
19
19
|
import { generateProjectId } from "./project-id";
|
|
20
|
+
import { runtimeFile, runtimeServe, runtimeSpawn } from "./runtime";
|
|
20
21
|
import type { StateFileId, StateChangeEvent } from "../types/dashboard";
|
|
21
22
|
import type { RegisteredProject } from "./project-registry";
|
|
22
23
|
|
|
@@ -262,10 +263,10 @@ export interface DashboardServer {
|
|
|
262
263
|
close(): void;
|
|
263
264
|
}
|
|
264
265
|
|
|
265
|
-
export function startDashboardServer(
|
|
266
|
+
export async function startDashboardServer(
|
|
266
267
|
cwd: string,
|
|
267
268
|
options: { port?: number; hostname?: string; open?: boolean } = {}
|
|
268
|
-
): DashboardServer {
|
|
269
|
+
): Promise<DashboardServer> {
|
|
269
270
|
const port = options.port ?? 4040;
|
|
270
271
|
const hostname = options.hostname ?? "127.0.0.1";
|
|
271
272
|
|
|
@@ -295,13 +296,15 @@ export function startDashboardServer(
|
|
|
295
296
|
});
|
|
296
297
|
|
|
297
298
|
// Resolve the Next.js static build directory
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
"
|
|
304
|
-
|
|
299
|
+
// Walk up from import.meta.url to find the package root (where package.json lives).
|
|
300
|
+
// From source: src/core/ → ../../ From compiled bundle: dist/ → ../
|
|
301
|
+
const __dir = dirname(new URL(import.meta.url).pathname);
|
|
302
|
+
let pkgRoot = __dir;
|
|
303
|
+
while (pkgRoot !== dirname(pkgRoot)) {
|
|
304
|
+
if (existsSync(join(pkgRoot, "package.json"))) break;
|
|
305
|
+
pkgRoot = dirname(pkgRoot);
|
|
306
|
+
}
|
|
307
|
+
const dashboardOutDir = join(pkgRoot, "dashboard", "out");
|
|
305
308
|
const dashboardBuilt = existsSync(join(dashboardOutDir, "index.html"));
|
|
306
309
|
let clientIdCounter = 0;
|
|
307
310
|
|
|
@@ -311,7 +314,20 @@ export function startDashboardServer(
|
|
|
311
314
|
);
|
|
312
315
|
}
|
|
313
316
|
|
|
314
|
-
|
|
317
|
+
async function serveFile(
|
|
318
|
+
filePath: string,
|
|
319
|
+
contentType: string
|
|
320
|
+
): Promise<Response | null> {
|
|
321
|
+
const file = runtimeFile(filePath);
|
|
322
|
+
if (await file.exists()) {
|
|
323
|
+
return new Response(await file.bytes() as unknown as BodyInit, {
|
|
324
|
+
headers: { "Content-Type": contentType },
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const server = await runtimeServe({
|
|
315
331
|
port,
|
|
316
332
|
hostname,
|
|
317
333
|
idleTimeout: 0, // Disable idle timeout — SSE connections are long-lived
|
|
@@ -343,30 +359,24 @@ export function startDashboardServer(
|
|
|
343
359
|
return jsonResponse({ error: "Forbidden" }, 403);
|
|
344
360
|
}
|
|
345
361
|
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
return new Response(file, {
|
|
351
|
-
headers: { "Content-Type": contentType },
|
|
352
|
-
});
|
|
353
|
-
}
|
|
362
|
+
const ext = extname(filePath);
|
|
363
|
+
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
364
|
+
const served = await serveFile(filePath, contentType);
|
|
365
|
+
if (served) return served;
|
|
354
366
|
|
|
355
367
|
// Client-side routing fallback: try {pathname}.html then index.html
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
368
|
+
const htmlServed = await serveFile(
|
|
369
|
+
filePath + ".html",
|
|
370
|
+
"text/html; charset=utf-8"
|
|
371
|
+
);
|
|
372
|
+
if (htmlServed) return htmlServed;
|
|
362
373
|
|
|
363
374
|
// SPA fallback — serve index.html for unmatched routes
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
375
|
+
const indexServed = await serveFile(
|
|
376
|
+
join(dashboardOutDir, "index.html"),
|
|
377
|
+
"text/html; charset=utf-8"
|
|
378
|
+
);
|
|
379
|
+
if (indexServed) return indexServed;
|
|
370
380
|
}
|
|
371
381
|
}
|
|
372
382
|
|
|
@@ -376,8 +386,6 @@ export function startDashboardServer(
|
|
|
376
386
|
const stream = new ReadableStream<Uint8Array>({
|
|
377
387
|
start(controller) {
|
|
378
388
|
sseManager.addClient(clientId, controller);
|
|
379
|
-
// Send initial comment immediately to establish the stream
|
|
380
|
-
// and prevent Bun from treating it as idle/complete
|
|
381
389
|
controller.enqueue(encoder.encode(": connected\nretry: 3000\n\n"));
|
|
382
390
|
},
|
|
383
391
|
cancel() {
|
|
@@ -435,15 +443,11 @@ export function startDashboardServer(
|
|
|
435
443
|
return jsonResponse({ error: "Invalid filename" }, 400);
|
|
436
444
|
}
|
|
437
445
|
const imgPath = join(designCapturesDir(resolvedCwd), filename);
|
|
438
|
-
const
|
|
439
|
-
if (
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
"Cache-Control": "public, max-age=60",
|
|
444
|
-
"Access-Control-Allow-Origin": "*",
|
|
445
|
-
},
|
|
446
|
-
});
|
|
446
|
+
const served = await serveFile(imgPath, "image/jpeg");
|
|
447
|
+
if (served) {
|
|
448
|
+
served.headers.set("Cache-Control", "public, max-age=60");
|
|
449
|
+
served.headers.set("Access-Control-Allow-Origin", "*");
|
|
450
|
+
return served;
|
|
447
451
|
}
|
|
448
452
|
return jsonResponse({ error: "Image not found" }, 404);
|
|
449
453
|
}
|
|
@@ -558,12 +562,7 @@ export function startDashboardServer(
|
|
|
558
562
|
: platform === "win32"
|
|
559
563
|
? ["cmd", "/c", "start", serverUrl]
|
|
560
564
|
: ["xdg-open", serverUrl];
|
|
561
|
-
|
|
562
|
-
stdout: "ignore",
|
|
563
|
-
stderr: "ignore",
|
|
564
|
-
stdin: "ignore",
|
|
565
|
-
});
|
|
566
|
-
proc.unref();
|
|
565
|
+
runtimeSpawn(cmd).unref();
|
|
567
566
|
} catch {
|
|
568
567
|
// Browser open is best-effort
|
|
569
568
|
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-runtime utilities — Bun-first with Node.js fallbacks.
|
|
3
|
+
*
|
|
4
|
+
* Detects the runtime once at import time and exports helpers that
|
|
5
|
+
* abstract over Bun.serve / node:http, Bun.file / fs, and Bun.spawn / child_process.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFile, stat } from "fs/promises";
|
|
9
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
10
|
+
|
|
11
|
+
export const isBun = typeof globalThis.Bun !== "undefined";
|
|
12
|
+
|
|
13
|
+
// ── File helpers ──────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export interface RuntimeFile {
|
|
16
|
+
exists(): Promise<boolean>;
|
|
17
|
+
bytes(): Promise<Uint8Array>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Returns a lightweight file handle with `.exists()` and `.bytes()`.
|
|
22
|
+
* Uses Bun.file when available, otherwise falls back to fs.
|
|
23
|
+
*/
|
|
24
|
+
export function runtimeFile(path: string): RuntimeFile {
|
|
25
|
+
if (isBun) {
|
|
26
|
+
const f = Bun.file(path);
|
|
27
|
+
return {
|
|
28
|
+
exists: () => f.exists(),
|
|
29
|
+
bytes: () => f.arrayBuffer().then((ab) => new Uint8Array(ab)),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
async exists() {
|
|
34
|
+
try {
|
|
35
|
+
await stat(path);
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
async bytes() {
|
|
42
|
+
return readFile(path);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Spawn helper ──────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
export interface SpawnOptions {
|
|
50
|
+
cwd?: string;
|
|
51
|
+
env?: Record<string, string | undefined>;
|
|
52
|
+
stdout?: "ignore" | "pipe";
|
|
53
|
+
stderr?: "ignore" | "pipe";
|
|
54
|
+
stdin?: "ignore";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SpawnedProcess {
|
|
58
|
+
unref(): void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Fire-and-forget process spawning. Uses Bun.spawn when available,
|
|
63
|
+
* otherwise child_process.spawn with detached + unref.
|
|
64
|
+
*/
|
|
65
|
+
export function runtimeSpawn(
|
|
66
|
+
cmd: string[],
|
|
67
|
+
opts: SpawnOptions = {}
|
|
68
|
+
): SpawnedProcess {
|
|
69
|
+
if (isBun) {
|
|
70
|
+
const proc = Bun.spawn(cmd, {
|
|
71
|
+
cwd: opts.cwd,
|
|
72
|
+
env: opts.env,
|
|
73
|
+
stdout: opts.stdout ?? "ignore",
|
|
74
|
+
stderr: opts.stderr ?? "ignore",
|
|
75
|
+
stdin: opts.stdin ?? "ignore",
|
|
76
|
+
});
|
|
77
|
+
return { unref: () => proc.unref() };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const [bin, ...args] = cmd;
|
|
81
|
+
const proc = nodeSpawn(bin, args, {
|
|
82
|
+
cwd: opts.cwd,
|
|
83
|
+
env: opts.env as NodeJS.ProcessEnv,
|
|
84
|
+
stdio: [
|
|
85
|
+
opts.stdin ?? "ignore",
|
|
86
|
+
opts.stdout ?? "ignore",
|
|
87
|
+
opts.stderr ?? "ignore",
|
|
88
|
+
],
|
|
89
|
+
detached: true,
|
|
90
|
+
});
|
|
91
|
+
proc.unref();
|
|
92
|
+
return { unref: () => {} };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── HTTP Server ───────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
export interface RuntimeServer {
|
|
98
|
+
port: number;
|
|
99
|
+
stop(closeConnections?: boolean): void;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
type FetchHandler = (req: Request) => Response | Promise<Response>;
|
|
103
|
+
|
|
104
|
+
interface ServeOptions {
|
|
105
|
+
port: number;
|
|
106
|
+
hostname: string;
|
|
107
|
+
idleTimeout?: number;
|
|
108
|
+
fetch: FetchHandler;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Start an HTTP server using Bun.serve or node:http.
|
|
113
|
+
* The fetch handler uses standard Web API Request/Response in both runtimes.
|
|
114
|
+
*/
|
|
115
|
+
export async function runtimeServe(opts: ServeOptions): Promise<RuntimeServer> {
|
|
116
|
+
if (isBun) {
|
|
117
|
+
const server = Bun.serve({
|
|
118
|
+
port: opts.port,
|
|
119
|
+
hostname: opts.hostname,
|
|
120
|
+
idleTimeout: opts.idleTimeout ?? 0,
|
|
121
|
+
fetch: opts.fetch,
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
port: server.port as number,
|
|
125
|
+
stop: (close) => server.stop(close),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Node.js fallback using node:http
|
|
130
|
+
const { createServer } = await import("node:http");
|
|
131
|
+
const { Readable } = await import("node:stream");
|
|
132
|
+
|
|
133
|
+
return new Promise<RuntimeServer>((resolve) => {
|
|
134
|
+
const httpServer = createServer(async (req, res) => {
|
|
135
|
+
// Build a Web API Request from the Node IncomingMessage
|
|
136
|
+
const url = `http://${opts.hostname}:${opts.port}${req.url ?? "/"}`;
|
|
137
|
+
const headers = new Headers();
|
|
138
|
+
for (const [key, val] of Object.entries(req.headers)) {
|
|
139
|
+
if (val) headers.set(key, Array.isArray(val) ? val.join(", ") : val);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let body: BodyInit | null = null;
|
|
143
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
144
|
+
const chunks: Buffer[] = [];
|
|
145
|
+
for await (const chunk of req) {
|
|
146
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
147
|
+
}
|
|
148
|
+
body = Buffer.concat(chunks);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const request = new Request(url, {
|
|
152
|
+
method: req.method,
|
|
153
|
+
headers,
|
|
154
|
+
body,
|
|
155
|
+
// @ts-expect-error -- Node 18+ supports duplex on Request
|
|
156
|
+
duplex: body ? "half" : undefined,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const response = await opts.fetch(request);
|
|
161
|
+
|
|
162
|
+
res.writeHead(response.status, Object.fromEntries(response.headers));
|
|
163
|
+
|
|
164
|
+
if (!response.body) {
|
|
165
|
+
res.end();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Stream the response body
|
|
170
|
+
const reader = response.body.getReader();
|
|
171
|
+
const nodeStream = new Readable({
|
|
172
|
+
async read() {
|
|
173
|
+
try {
|
|
174
|
+
const { done, value } = await reader.read();
|
|
175
|
+
if (done) {
|
|
176
|
+
this.push(null);
|
|
177
|
+
} else {
|
|
178
|
+
this.push(Buffer.from(value));
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
this.push(null);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Clean up SSE streams when client disconnects
|
|
187
|
+
res.on("close", () => {
|
|
188
|
+
reader.cancel().catch(() => {});
|
|
189
|
+
nodeStream.destroy();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
nodeStream.pipe(res);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
if (!res.headersSent) {
|
|
195
|
+
res.writeHead(500);
|
|
196
|
+
res.end(String(err));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
httpServer.listen(opts.port, opts.hostname, () => {
|
|
202
|
+
const addr = httpServer.address();
|
|
203
|
+
const boundPort =
|
|
204
|
+
typeof addr === "object" && addr ? addr.port : opts.port;
|
|
205
|
+
resolve({
|
|
206
|
+
port: boundPort,
|
|
207
|
+
stop: (close) => {
|
|
208
|
+
if (close) httpServer.closeAllConnections();
|
|
209
|
+
httpServer.close();
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|