truecanvas 0.4.1 → 0.6.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.
@@ -0,0 +1,1005 @@
1
+ import {
2
+ EditError,
3
+ addArgs,
4
+ detectPm,
5
+ nodeBin,
6
+ shellSafe,
7
+ toolEnv
8
+ } from "./chunk-D565QDII.js";
9
+
10
+ // src/cli/setup.ts
11
+ import { spawn } from "child_process";
12
+ import { createHash } from "crypto";
13
+ import fs from "fs";
14
+ import os from "os";
15
+ import path from "path";
16
+ function addDevArgs(pm, spec) {
17
+ return addArgs(pm, [spec], true);
18
+ }
19
+ function runScript(pm, script) {
20
+ return pm === "npm" ? `npm run ${script}` : `${pm} ${script}`;
21
+ }
22
+ var execInherit = (cmd, args, cwd) => new Promise((resolve, reject) => {
23
+ const safe = shellSafe(cmd, args);
24
+ const p = spawn(safe.cmd, safe.args, { cwd, stdio: "inherit", env: toolEnv(), shell: safe.shell });
25
+ p.on("error", reject);
26
+ p.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`${cmd} ${args.join(" ")} exited with ${code}`)));
27
+ });
28
+ function packageDir(cliFile) {
29
+ return path.resolve(path.dirname(cliFile), "..");
30
+ }
31
+ function packageVersion(pkgDir) {
32
+ try {
33
+ return JSON.parse(fs.readFileSync(path.join(pkgDir, "package.json"), "utf8")).version;
34
+ } catch {
35
+ return "0.0.0";
36
+ }
37
+ }
38
+ function isSourceCheckout(pkgDir) {
39
+ return !pkgDir.split(path.sep).includes("node_modules") && fs.existsSync(path.join(pkgDir, "src", "cli"));
40
+ }
41
+ async function packageSpec(pkgDir, exec) {
42
+ if (process.env.TRUECANVAS_PACKAGE) return process.env.TRUECANVAS_PACKAGE;
43
+ const version = packageVersion(pkgDir);
44
+ if (!isSourceCheckout(pkgDir)) return `truecanvas@${version}`;
45
+ const cacheDir = path.join(os.homedir(), ".cache", "truecanvas");
46
+ fs.mkdirSync(cacheDir, { recursive: true });
47
+ const file = path.join(cacheDir, `truecanvas-${version}.tgz`);
48
+ await exec("npm", ["pack", "--silent", "--pack-destination", cacheDir], pkgDir);
49
+ const hash = createHash("sha256").update(fs.readFileSync(file)).digest("hex").slice(0, 10);
50
+ const named = path.join(cacheDir, `truecanvas-${version}-${hash}.tgz`);
51
+ fs.renameSync(file, named);
52
+ return named;
53
+ }
54
+ function installedIn(root) {
55
+ let declared = false;
56
+ try {
57
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
58
+ declared = "truecanvas" in { ...pkg.dependencies, ...pkg.devDependencies };
59
+ } catch {
60
+ }
61
+ const file = path.join(root, "node_modules", "truecanvas", "package.json");
62
+ const installed = fs.existsSync(file);
63
+ return { declared, installed, version: installed ? packageVersion(path.dirname(file)) : null };
64
+ }
65
+ function readPackage(root) {
66
+ try {
67
+ return JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+ function depVersion(root, name) {
73
+ const file = path.join(root, "node_modules", name, "package.json");
74
+ if (fs.existsSync(file)) return packageVersion(path.dirname(file));
75
+ const pkg = readPackage(root);
76
+ return pkg ? { ...pkg.dependencies, ...pkg.devDependencies }[name] ?? null : null;
77
+ }
78
+ function mergeJson(file, update) {
79
+ let data = {};
80
+ if (fs.existsSync(file)) {
81
+ try {
82
+ data = JSON.parse(fs.readFileSync(file, "utf8"));
83
+ } catch {
84
+ throw new Error(`${file} isn't valid JSON; fix it or remove it, then run connect again.`);
85
+ }
86
+ }
87
+ const before = JSON.stringify(data);
88
+ update(data);
89
+ if (JSON.stringify(data) === before) return "unchanged";
90
+ fs.mkdirSync(path.dirname(file), { recursive: true });
91
+ fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}
92
+ `);
93
+ return "written";
94
+ }
95
+ function connectAgents(root, port, opts = {}) {
96
+ const url = `http://localhost:${port}/mcp`;
97
+ const out = { written: [], unchanged: [] };
98
+ const add = (rel, update) => out[mergeJson(path.join(root, rel), update)].push(rel);
99
+ const servers = (d, key) => d[key] ??= {};
100
+ add(".mcp.json", (d) => void (servers(d, "mcpServers").truecanvas = { type: "http", url }));
101
+ if (opts.cursor || fs.existsSync(path.join(root, ".cursor"))) add(".cursor/mcp.json", (d) => void (servers(d, "mcpServers").truecanvas = { url }));
102
+ if (opts.vscode || fs.existsSync(path.join(root, ".vscode"))) add(".vscode/mcp.json", (d) => void (servers(d, "servers").truecanvas = { type: "http", url }));
103
+ return out;
104
+ }
105
+ function hasAgentConfig(root, port) {
106
+ try {
107
+ const d = JSON.parse(fs.readFileSync(path.join(root, ".mcp.json"), "utf8"));
108
+ const url = d?.mcpServers?.truecanvas?.url;
109
+ return typeof url === "string" && (port === void 0 || url.includes(`:${port}/`));
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+
115
+ // src/hub/locate.ts
116
+ import fs2 from "fs";
117
+ import os2 from "os";
118
+ import path2 from "path";
119
+ var HUB_PORT = 4800;
120
+ function stateDir() {
121
+ if (process.platform === "win32") return path2.join(process.env.LOCALAPPDATA ?? path2.join(os2.homedir(), "AppData", "Local"), "truecanvas");
122
+ if (process.platform === "darwin") return path2.join(os2.homedir(), "Library", "Application Support", "truecanvas");
123
+ return path2.join(process.env.XDG_STATE_HOME ?? path2.join(os2.homedir(), ".local", "state"), "truecanvas");
124
+ }
125
+ var hubFile = () => path2.join(stateDir(), "hub.json");
126
+ function writeHubFile(port) {
127
+ try {
128
+ fs2.mkdirSync(stateDir(), { recursive: true });
129
+ fs2.writeFileSync(hubFile(), JSON.stringify({ port, pid: process.pid }));
130
+ } catch {
131
+ }
132
+ }
133
+ function removeHubFile() {
134
+ try {
135
+ const saved = JSON.parse(fs2.readFileSync(hubFile(), "utf8"));
136
+ if (saved.pid === process.pid) fs2.rmSync(hubFile());
137
+ } catch {
138
+ }
139
+ }
140
+ function hubPort() {
141
+ try {
142
+ const saved = JSON.parse(fs2.readFileSync(hubFile(), "utf8"));
143
+ if (saved.port && saved.pid) {
144
+ process.kill(saved.pid, 0);
145
+ return saved.port;
146
+ }
147
+ } catch {
148
+ }
149
+ return HUB_PORT;
150
+ }
151
+
152
+ // src/hub/server.ts
153
+ import http from "http";
154
+ import fs5 from "fs";
155
+ import os5 from "os";
156
+ import path5 from "path";
157
+ import { randomUUID } from "crypto";
158
+ import { fileURLToPath } from "url";
159
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
160
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
161
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
162
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
163
+ import { CallToolRequestSchema, ListToolsRequestSchema, isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
164
+
165
+ // src/hub/projects.ts
166
+ import fs3 from "fs";
167
+ import os3 from "os";
168
+ import path3 from "path";
169
+ var CONFIG_DIR = path3.join(process.env.XDG_CONFIG_HOME ?? path3.join(os3.homedir(), ".config"), "truecanvas");
170
+ var FILE = path3.join(CONFIG_DIR, "projects.json");
171
+ var ProjectStore = class {
172
+ list = [];
173
+ constructor() {
174
+ try {
175
+ this.list = JSON.parse(fs3.readFileSync(FILE, "utf8"));
176
+ } catch {
177
+ this.list = [];
178
+ }
179
+ }
180
+ save() {
181
+ fs3.mkdirSync(CONFIG_DIR, { recursive: true });
182
+ fs3.writeFileSync(FILE, JSON.stringify(this.list, null, 2));
183
+ }
184
+ all() {
185
+ return [...this.list].sort((a, b) => b.lastOpened - a.lastOpened).map((p) => ({ ...p, ...inspect(p.path) }));
186
+ }
187
+ touch(dir) {
188
+ const abs = path3.resolve(dir);
189
+ const existing = this.list.find((p) => p.path === abs);
190
+ if (existing) existing.lastOpened = Date.now();
191
+ else this.list.push({ path: abs, name: projectName(abs), lastOpened: Date.now() });
192
+ this.save();
193
+ }
194
+ forget(dir) {
195
+ this.list = this.list.filter((p) => p.path !== path3.resolve(dir));
196
+ this.save();
197
+ }
198
+ };
199
+ function readPkg(dir) {
200
+ try {
201
+ return JSON.parse(fs3.readFileSync(path3.join(dir, "package.json"), "utf8"));
202
+ } catch {
203
+ return null;
204
+ }
205
+ }
206
+ function projectName(dir) {
207
+ const pkg = readPkg(dir);
208
+ const name = typeof pkg?.name === "string" ? pkg.name : "";
209
+ return name && !name.startsWith("@") ? name : path3.basename(dir);
210
+ }
211
+ function inspect(dir) {
212
+ const pkg = readPkg(dir);
213
+ if (!pkg) return { next: false, ready: false, canvases: 0, exists: fs3.existsSync(dir) };
214
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
215
+ let canvases = 0;
216
+ for (const d of ["canvas", "src/canvas"]) {
217
+ try {
218
+ canvases += fs3.readdirSync(path3.join(dir, d)).filter((f) => f.endsWith(".canvas.tsx")).length;
219
+ } catch {
220
+ }
221
+ }
222
+ return { next: "next" in deps || "vite" in deps && "react" in deps, ready: "truecanvas" in deps, canvases, exists: true };
223
+ }
224
+ function discover() {
225
+ const home = os3.homedir();
226
+ const roots = ["Github", "GitHub", "github", "Projects", "projects", "code", "Code", "dev", "src", "Developer", "Documents", "work"].map((d) => path3.join(home, d)).filter((d) => fs3.existsSync(d));
227
+ const found = [];
228
+ let visited = 0;
229
+ const walk = (dir, depth) => {
230
+ if (depth > 3 || visited > 3e3 || found.length > 60) return;
231
+ visited++;
232
+ let entries;
233
+ try {
234
+ entries = fs3.readdirSync(dir, { withFileTypes: true });
235
+ } catch {
236
+ return;
237
+ }
238
+ if (entries.some((e) => e.name === "package.json")) {
239
+ const info = inspect(dir);
240
+ if (info.next) found.push({ path: dir, name: projectName(dir), ready: info.ready });
241
+ }
242
+ for (const e of entries) {
243
+ if (!e.isDirectory() || e.name.startsWith(".") || e.name === "node_modules" || e.name === "dist" || e.name === "build") continue;
244
+ walk(path3.join(dir, e.name), depth + 1);
245
+ }
246
+ };
247
+ for (const r of roots) walk(r, 0);
248
+ return found;
249
+ }
250
+ function listDirs(dir) {
251
+ const abs = path3.resolve(dir.replace(/^~(?=$|[\\/])/, os3.homedir()));
252
+ const dirs = fs3.readdirSync(abs, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => ({ name: e.name, next: inspect(path3.join(abs, e.name)).next })).sort((a, b) => Number(b.next) - Number(a.next) || a.name.localeCompare(b.name));
253
+ return { path: abs, dirs };
254
+ }
255
+
256
+ // src/hub/runner.ts
257
+ import { execFile, spawn as spawn2 } from "child_process";
258
+ import fs4 from "fs";
259
+ import net from "net";
260
+ import os4 from "os";
261
+ import path4 from "path";
262
+ var NO_NODE = "Node.js wasn't found. Install it (nodejs.org, or your package manager), then restart Truecanvas: your projects run on your own Node.";
263
+ function node() {
264
+ const bin = nodeBin();
265
+ if (!bin) throw new Error(NO_NODE);
266
+ return bin;
267
+ }
268
+ function projectCli(dir, fallback) {
269
+ const local = path4.join(dir, "node_modules", "truecanvas", "dist", "cli.js");
270
+ return fs4.existsSync(local) ? local : fallback;
271
+ }
272
+ function portFree(port) {
273
+ return new Promise((resolve) => {
274
+ const srv = net.createServer();
275
+ srv.once("error", () => resolve(false));
276
+ srv.listen(port, "127.0.0.1", () => srv.close(() => resolve(true)));
277
+ });
278
+ }
279
+ async function freePort(from, taken) {
280
+ for (let p = from; p < from + 200; p++) if (!taken.has(p) && await portFree(p)) return p;
281
+ throw new Error("No free port");
282
+ }
283
+ var Runner = class {
284
+ constructor(cli, onChange, hubUrl) {
285
+ this.cli = cli;
286
+ this.onChange = onChange;
287
+ this.hubUrl = hubUrl;
288
+ }
289
+ cli;
290
+ onChange;
291
+ hubUrl;
292
+ running = /* @__PURE__ */ new Map();
293
+ opening = /* @__PURE__ */ new Map();
294
+ ports = Promise.resolve();
295
+ /** Opens a project; concurrent calls for the same folder share one start. */
296
+ open(dir) {
297
+ const existing = this.running.get(dir);
298
+ if (existing && existing.status !== "failed") return Promise.resolve(existing);
299
+ const pending = this.opening.get(dir);
300
+ if (pending) return pending;
301
+ const job = this.start(dir).finally(() => this.opening.delete(dir));
302
+ this.opening.set(dir, job);
303
+ return job;
304
+ }
305
+ /** Two ports, picked one project at a time so parallel opens never collide. */
306
+ allocatePorts() {
307
+ const next = this.ports.then(async () => {
308
+ const taken = new Set([...this.running.values()].flatMap((r) => [r.editorPort, r.appPort]));
309
+ const editorPort = await freePort(4810, taken);
310
+ taken.add(editorPort);
311
+ return [editorPort, await freePort(3010, taken)];
312
+ });
313
+ this.ports = next.catch(() => {
314
+ });
315
+ return next;
316
+ }
317
+ async start(dir) {
318
+ if (!fs4.existsSync(path4.join(dir, "package.json"))) throw new Error(`${dir} isn't there anymore (or has no package.json).`);
319
+ const bin = node();
320
+ const [editorPort, appPort] = await this.allocatePorts();
321
+ const proc = spawn2(bin, [projectCli(dir, this.cli), "dev", "--no-open", "--port", String(editorPort), "--app", `http://localhost:${appPort}`], {
322
+ cwd: dir,
323
+ env: toolEnv({ FORCE_COLOR: "0", TRUECANVAS_HUB: "1", ...this.hubUrl ? { TRUECANVAS_HUB_URL: this.hubUrl } : {} }),
324
+ stdio: ["ignore", "pipe", "pipe"],
325
+ // its own process group: closing the project also stops the next dev it started
326
+ detached: process.platform !== "win32"
327
+ });
328
+ const run2 = { path: dir, editorPort, appPort, proc, status: "starting", log: [], startedAt: Date.now() };
329
+ this.running.set(dir, run2);
330
+ const capture2 = (chunk) => {
331
+ for (const line of chunk.toString().split("\n")) if (line.trim()) run2.log.push(line.replace(/\x1b\[[0-9;]*m/g, ""));
332
+ if (run2.log.length > 200) run2.log.splice(0, run2.log.length - 200);
333
+ };
334
+ proc.stdout.on("data", capture2);
335
+ proc.stderr.on("data", capture2);
336
+ proc.on("error", (err) => {
337
+ run2.status = "failed";
338
+ run2.error = err.message;
339
+ this.onChange();
340
+ });
341
+ proc.on("exit", (code) => {
342
+ if (this.running.get(dir) !== run2) return;
343
+ if (run2.status === "starting" || code) {
344
+ run2.status = "failed";
345
+ run2.error = run2.log.slice(-6).join("\n") || `exited with ${code}`;
346
+ } else this.running.delete(dir);
347
+ this.onChange();
348
+ });
349
+ this.onChange();
350
+ const until = Date.now() + 6e4;
351
+ while (Date.now() < until && run2.status === "starting") {
352
+ try {
353
+ const res = await fetch(`http://127.0.0.1:${editorPort}/api/state`, { signal: AbortSignal.timeout(1e3) });
354
+ if (res.ok) {
355
+ run2.status = "ready";
356
+ break;
357
+ }
358
+ } catch {
359
+ }
360
+ await new Promise((r) => setTimeout(r, 400));
361
+ }
362
+ if (run2.status === "starting") {
363
+ run2.status = "failed";
364
+ run2.error = "Truecanvas didn't start within a minute.";
365
+ killTree(run2.proc, "SIGTERM");
366
+ }
367
+ this.onChange();
368
+ return run2;
369
+ }
370
+ /** Stops a project and everything it started; resolves once it has exited (or after 5s). */
371
+ close(dir) {
372
+ const run2 = this.running.get(dir);
373
+ if (!run2) return Promise.resolve();
374
+ this.running.delete(dir);
375
+ this.onChange();
376
+ if (run2.proc.exitCode !== null || run2.proc.signalCode !== null) return Promise.resolve();
377
+ return new Promise((resolve) => {
378
+ const force = setTimeout(() => killTree(run2.proc, "SIGKILL"), 4e3);
379
+ const done = setTimeout(resolve, 5e3);
380
+ run2.proc.once("exit", () => {
381
+ clearTimeout(force);
382
+ clearTimeout(done);
383
+ resolve();
384
+ });
385
+ killTree(run2.proc, "SIGTERM");
386
+ });
387
+ }
388
+ closeAll() {
389
+ return Promise.all([...this.running.keys()].map((dir) => this.close(dir))).then(() => {
390
+ });
391
+ }
392
+ /** Resident memory (MB) of each project's process tree. */
393
+ memory() {
394
+ return new Promise((resolve) => {
395
+ execFile("ps", ["-eo", "pid=,ppid=,rss="], (err, out) => {
396
+ if (err) return resolve({});
397
+ const rows = out.trim().split("\n").map((l) => l.trim().split(/\s+/).map(Number));
398
+ const kids = /* @__PURE__ */ new Map();
399
+ const rss = /* @__PURE__ */ new Map();
400
+ for (const [pid, ppid, kb] of rows) {
401
+ rss.set(pid, kb);
402
+ kids.set(ppid, [...kids.get(ppid) ?? [], pid]);
403
+ }
404
+ const total = (pid) => (rss.get(pid) ?? 0) + (kids.get(pid) ?? []).reduce((s, k) => s + total(k), 0);
405
+ const outMap = {};
406
+ for (const [dir, run2] of this.running) if (run2.proc.pid) outMap[dir] = Math.round(total(run2.proc.pid) / 1024);
407
+ resolve(outMap);
408
+ });
409
+ });
410
+ }
411
+ };
412
+ function killTree(proc, signal) {
413
+ try {
414
+ if (process.platform === "win32" && proc.pid) spawn2("taskkill", ["/pid", String(proc.pid), "/T", "/F"], { stdio: "ignore" }).on("error", () => proc.kill());
415
+ else if (proc.pid) process.kill(-proc.pid, signal);
416
+ else proc.kill(signal);
417
+ } catch {
418
+ proc.kill(signal);
419
+ }
420
+ }
421
+ var jobs = /* @__PURE__ */ new Map();
422
+ var jobSeq = 0;
423
+ function getJob(id) {
424
+ return jobs.get(id);
425
+ }
426
+ async function install(job, cmd, args, cwd) {
427
+ try {
428
+ await run(job, cmd, args, cwd);
429
+ } catch (err) {
430
+ const ignoredBuilds = job.log.some((l) => l.includes("ERR_PNPM_IGNORED_BUILDS"));
431
+ const installed = fs4.existsSync(path4.join(cwd, "node_modules", "truecanvas", "package.json"));
432
+ if (!(ignoredBuilds && installed)) throw err;
433
+ job.log.push("(pnpm only complained about other packages' build scripts; Truecanvas is installed, continuing)");
434
+ }
435
+ }
436
+ function run(job, cmd, args, cwd) {
437
+ job.log.push(`$ ${cmd} ${args.join(" ")}`);
438
+ return new Promise((resolve, reject) => {
439
+ const safe = shellSafe(cmd, args);
440
+ const p = spawn2(safe.cmd, safe.args, { cwd, env: toolEnv({ CI: "1", FORCE_COLOR: "0" }), stdio: ["ignore", "pipe", "pipe"], shell: safe.shell });
441
+ const capture2 = (c) => {
442
+ for (const line of c.toString().split(/\r?\n/)) if (line.trim()) job.log.push(line.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, ""));
443
+ if (job.log.length > 300) job.log.splice(0, job.log.length - 300);
444
+ };
445
+ p.stdout.on("data", capture2);
446
+ p.stderr.on("data", capture2);
447
+ p.on("error", reject);
448
+ p.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`${cmd} exited with ${code}`)));
449
+ });
450
+ }
451
+ function startJob(title, work) {
452
+ const job = { id: String(++jobSeq), title, status: "running", log: [] };
453
+ jobs.set(job.id, job);
454
+ work(job).then((result) => {
455
+ job.status = "done";
456
+ if (result) job.result = result;
457
+ }).catch((err) => {
458
+ job.status = "failed";
459
+ job.log.push(`\u2717 ${err.message}`);
460
+ });
461
+ return job;
462
+ }
463
+ function packageManager(dir) {
464
+ const pm = detectPm(dir);
465
+ return { cmd: pm, add: addArgs(pm, [], true) };
466
+ }
467
+ async function preferredPm() {
468
+ const { ok } = await capture("pnpm", ["--version"], os4.homedir());
469
+ return ok ? "pnpm" : "npm";
470
+ }
471
+ function setupJob(dir, pkgDir, cli) {
472
+ return startJob(`Set up ${path4.basename(dir)}`, async (job) => {
473
+ const pm = packageManager(dir);
474
+ const source = await packageSpec(pkgDir, (cmd, args, cwd) => run(job, cmd, args, cwd));
475
+ await install(job, pm.cmd, [...pm.add, source], dir);
476
+ await run(job, node(), [projectCli(dir, cli), "init"], dir);
477
+ return dir;
478
+ });
479
+ }
480
+ function createJob(name, parent, pkgDir, cli) {
481
+ const safe = name.trim().toLowerCase().replace(/[^a-z0-9-_]+/g, "-").replace(/^-+|-+$/g, "");
482
+ const parentDir = path4.resolve(parent.replace(/^~(?=$|[\\/])/, os4.homedir()));
483
+ const dir = path4.join(parentDir, safe);
484
+ return startJob(`Create ${safe}`, async (job) => {
485
+ if (!safe) throw new Error("Pick a project name.");
486
+ if (fs4.existsSync(dir)) throw new Error(`${dir} already exists.`);
487
+ fs4.mkdirSync(parentDir, { recursive: true });
488
+ const pm = await preferredPm();
489
+ await run(job, "npx", ["--yes", "create-next-app@latest", safe, "--ts", "--tailwind", "--app", "--eslint", "--no-src-dir", "--import-alias", "@/*", `--use-${pm}`, "--turbopack", "--yes"], parentDir);
490
+ const source = await packageSpec(pkgDir, (cmd, args, cwd) => run(job, cmd, args, cwd));
491
+ await install(job, pm, addArgs(pm, [source], true), dir);
492
+ await run(job, node(), [projectCli(dir, cli), "init"], dir);
493
+ return dir;
494
+ });
495
+ }
496
+ function capture(cmd, args, cwd) {
497
+ return new Promise((resolve) => {
498
+ const safe = shellSafe(cmd, args);
499
+ execFile(
500
+ safe.cmd,
501
+ safe.args,
502
+ { shell: safe.shell, cwd, timeout: 6e4, maxBuffer: 8 * 1024 * 1024, env: toolEnv({ GH_PROMPT_DISABLED: "1", GIT_TERMINAL_PROMPT: "0" }) },
503
+ (err, stdout, stderr) => resolve({ ok: !err, out: err ? (stderr || err.message).trim() : stdout })
504
+ );
505
+ });
506
+ }
507
+ function cloneJob(repo, parent, pkgDir, cli) {
508
+ const parentDir = path4.resolve(parent.replace(/^~(?=$|[\\/])/, os4.homedir()));
509
+ const name = repo.replace(/\.git$/, "").split(/[/:]/).pop() ?? "";
510
+ const dir = path4.join(parentDir, name);
511
+ return startJob(`Clone ${name}`, async (job) => {
512
+ if (!name || !/^[\w.-]+$/.test(name)) throw new Error("Pick a repository (owner/name or a git URL).");
513
+ if (fs4.existsSync(dir)) throw new Error(`${dir} already exists. Open it with Open folder instead.`);
514
+ fs4.mkdirSync(parentDir, { recursive: true });
515
+ const url = /^(https?:|git@|ssh:|file:)/.test(repo);
516
+ if (url) await run(job, "git", ["clone", repo, dir], parentDir);
517
+ else await run(job, "gh", ["repo", "clone", repo, dir], parentDir);
518
+ if (!fs4.existsSync(path4.join(dir, "package.json"))) throw new Error("Cloned, but there's no package.json at the root: Truecanvas needs a Next.js or Vite app.");
519
+ const pm = packageManager(dir);
520
+ await install(job, pm.cmd, ["install"], dir).catch(async (err) => {
521
+ if (!fs4.existsSync(path4.join(dir, "node_modules"))) throw err;
522
+ });
523
+ const pkg = JSON.parse(fs4.readFileSync(path4.join(dir, "package.json"), "utf8"));
524
+ if (!("truecanvas" in { ...pkg.dependencies, ...pkg.devDependencies })) {
525
+ const source = await packageSpec(pkgDir, (cmd, args, cwd) => run(job, cmd, args, cwd));
526
+ await install(job, pm.cmd, [...pm.add, source], dir);
527
+ await run(job, node(), [projectCli(dir, cli), "init"], dir);
528
+ } else job.log.push("Truecanvas is already part of this project.");
529
+ return dir;
530
+ });
531
+ }
532
+
533
+ // src/server/body.ts
534
+ async function readJson(req, limit = 8 * 1024 * 1024) {
535
+ const chunks = [];
536
+ let size = 0;
537
+ for await (const chunk of req) {
538
+ size += chunk.length;
539
+ if (size > limit) throw new EditError("Request too large.");
540
+ chunks.push(chunk);
541
+ }
542
+ const raw = Buffer.concat(chunks).toString("utf8");
543
+ if (!raw) return {};
544
+ try {
545
+ return JSON.parse(raw);
546
+ } catch {
547
+ throw new EditError("The request body isn't valid JSON.");
548
+ }
549
+ }
550
+
551
+ // src/hub/server.ts
552
+ var MIME = {
553
+ ".html": "text/html; charset=utf-8",
554
+ ".js": "text/javascript; charset=utf-8",
555
+ ".css": "text/css; charset=utf-8",
556
+ ".svg": "image/svg+xml",
557
+ ".woff2": "font/woff2",
558
+ ".png": "image/png"
559
+ };
560
+ var HUB_INSTRUCTIONS = `Truecanvas hub: several projects, each a Next.js or Vite + React app with canvases of real React components.
561
+ Your tools act on the current project (the active tab in the Truecanvas window, unless you pick one with open_project).
562
+ Use list_projects to see them. All other tools are the regular Truecanvas canvas tools for that project.`;
563
+ async function startHub(opts) {
564
+ const store = new ProjectStore();
565
+ const editorDir = fileURLToPath(new URL("./editor/", import.meta.url));
566
+ const pkgDir = path5.resolve(path5.dirname(opts.cli), "..");
567
+ const origin = `http://localhost:${opts.port}`;
568
+ const allowedHosts = /* @__PURE__ */ new Set([`localhost:${opts.port}`, `127.0.0.1:${opts.port}`]);
569
+ const clients = /* @__PURE__ */ new Set();
570
+ let active = null;
571
+ const broadcast = () => {
572
+ for (const res of clients) res.write(`data: {"type":"state"}
573
+
574
+ `);
575
+ for (const fn of toolListeners) fn();
576
+ };
577
+ const runner = new Runner(opts.cli, broadcast, origin);
578
+ const idleMs = (opts.idleMinutes ?? Number(process.env.TRUECANVAS_IDLE_MINUTES ?? 30)) * 6e4;
579
+ const lastUsed = /* @__PURE__ */ new Map();
580
+ const used = (dir) => {
581
+ if (dir) lastUsed.set(dir, Date.now());
582
+ };
583
+ if (idleMs > 0)
584
+ setInterval(() => {
585
+ if (clients.size) used(active);
586
+ for (const [dir, run2] of runner.running) {
587
+ if (dir === active && clients.size || run2.status === "starting") continue;
588
+ if (Date.now() - (lastUsed.get(dir) ?? run2.startedAt) < idleMs) continue;
589
+ lastUsed.delete(dir);
590
+ void runner.close(dir);
591
+ }
592
+ }, Math.min(6e4, idleMs)).unref();
593
+ setInterval(() => {
594
+ for (const res of clients) res.write(": ping\n\n");
595
+ }, 2e4).unref();
596
+ let focus = null;
597
+ async function sessionsOf(dir) {
598
+ const r = runner.running.get(dir);
599
+ if (!r || r.status !== "ready") return [];
600
+ try {
601
+ const res = await fetch(`http://localhost:${r.editorPort}/api/session`, { signal: AbortSignal.timeout(800) });
602
+ return res.ok ? (await res.json()).sessions.filter((s) => s.status !== "stopped") : [];
603
+ } catch {
604
+ return [];
605
+ }
606
+ }
607
+ async function sessionAction(action, dir = active, canvas) {
608
+ if (!dir) throw new Error("Open a project first.");
609
+ const r = runner.running.get(dir);
610
+ if (!r || r.status !== "ready") throw new Error("Open the project first: its Truecanvas runs the session.");
611
+ const live = await sessionsOf(dir);
612
+ const start = action === "start" || action === "toggle" && !live.length;
613
+ const res = await fetch(`http://localhost:${r.editorPort}/api/session/${start ? "start" : "stop"}`, {
614
+ method: "POST",
615
+ headers: { "content-type": "application/json" },
616
+ body: JSON.stringify(canvas ? { canvas } : {}),
617
+ signal: AbortSignal.timeout(3e4)
618
+ });
619
+ const data = await res.json().catch(() => ({}));
620
+ if (!res.ok) throw new Error(data.error ?? `${res.status} ${res.statusText}`);
621
+ used(dir);
622
+ broadcast();
623
+ return sessionsOf(dir);
624
+ }
625
+ async function state() {
626
+ const memory = await runner.memory();
627
+ const running = [...runner.running.keys()];
628
+ const live = await Promise.all(running.map(async (dir) => [dir, await sessionsOf(dir)]));
629
+ return {
630
+ origin,
631
+ active,
632
+ focus,
633
+ notice: opts.notice ?? null,
634
+ liveSession: { available: true, sessions: Object.fromEntries(live.filter(([, s]) => s.length)) },
635
+ projects: store.all().map((p) => {
636
+ const r = runner.running.get(p.path);
637
+ return { ...p, running: r ? { status: r.status, editor: `http://localhost:${r.editorPort}`, app: `http://localhost:${r.appPort}`, error: r.error, memory: memory[p.path] ?? null } : null };
638
+ })
639
+ };
640
+ }
641
+ const toolListeners = /* @__PURE__ */ new Set();
642
+ const transports = /* @__PURE__ */ new Map();
643
+ const lastUse = /* @__PURE__ */ new Map();
644
+ setInterval(() => {
645
+ for (const [id, at] of lastUse) {
646
+ if (Date.now() - at < 60 * 6e4) continue;
647
+ lastUse.delete(id);
648
+ void transports.get(id)?.close();
649
+ }
650
+ }, 5 * 6e4).unref();
651
+ function createProxy() {
652
+ const server2 = new Server({ name: "truecanvas", version: "0.1.0" }, { capabilities: { tools: { listChanged: true } }, instructions: HUB_INSTRUCTIONS });
653
+ let current = null;
654
+ const upstreams = /* @__PURE__ */ new Map();
655
+ const target = () => current ?? active;
656
+ const drop = (dir) => {
657
+ const hit = upstreams.get(dir);
658
+ upstreams.delete(dir);
659
+ void hit?.client.then((c) => c.close()).catch(() => {
660
+ });
661
+ };
662
+ const upstream = (dir) => {
663
+ const run2 = runner.running.get(dir);
664
+ if (!run2 || run2.status !== "ready") return null;
665
+ const hit = upstreams.get(dir);
666
+ if (hit && hit.pid === run2.proc.pid) return hit.client;
667
+ if (hit) drop(dir);
668
+ const info = server2.getClientVersion();
669
+ const client = new Client({ name: info?.name ?? "agent", version: info?.version ?? "0.0.0" });
670
+ const c = client.connect(new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${run2.editorPort}/mcp`))).then(() => client);
671
+ c.catch(() => upstreams.delete(dir));
672
+ upstreams.set(dir, { pid: run2.proc.pid, client: c });
673
+ return c;
674
+ };
675
+ const withUpstream = async (dir, fn) => {
676
+ used(dir);
677
+ for (let attempt = 0; attempt < 2; attempt++) {
678
+ const up = upstream(dir);
679
+ if (!up) return null;
680
+ try {
681
+ return await fn(await up);
682
+ } catch (err) {
683
+ drop(dir);
684
+ const lost = /session|not connected|ECONNREFUSED|ECONNRESET|fetch failed|404/i.test(err.message ?? "");
685
+ if (attempt === 1 || !lost) throw err;
686
+ }
687
+ }
688
+ return null;
689
+ };
690
+ const notify = () => void server2.sendToolListChanged().catch(() => {
691
+ });
692
+ toolListeners.add(notify);
693
+ server2.onclose = () => {
694
+ toolListeners.delete(notify);
695
+ for (const dir of [...upstreams.keys()]) drop(dir);
696
+ };
697
+ const hubTools = [
698
+ { name: "list_projects", description: "Projects known to the Truecanvas hub, which are open, and which one your tools act on.", inputSchema: { type: "object", properties: {} } },
699
+ {
700
+ name: "open_project",
701
+ description: "Open a project (starts its app if needed) and make it the target of your tools.",
702
+ inputSchema: { type: "object", properties: { project: { type: "string", description: "Project name or path." } }, required: ["project"] }
703
+ }
704
+ ];
705
+ server2.setRequestHandler(ListToolsRequestSchema, async () => {
706
+ const dir = target();
707
+ const projectTools = dir ? (await withUpstream(dir, (c) => c.listTools()).catch(() => null))?.tools ?? [] : [];
708
+ return { tools: [...hubTools, ...projectTools] };
709
+ });
710
+ server2.setRequestHandler(CallToolRequestSchema, async (req) => {
711
+ const text = (t, isError = false) => ({ content: [{ type: "text", text: t }], isError });
712
+ if (req.params.name === "list_projects") {
713
+ const s = await state();
714
+ const lines = s.projects.map((p) => `${p.path === target() ? "\u2192 " : " "}${p.name} (${p.path})${p.running ? ` [${p.running.status}]` : ""}${p.ready ? "" : " (Truecanvas not set up)"}`);
715
+ return text(lines.join("\n") || "No projects yet. The user can add some from the Dashboard.");
716
+ }
717
+ if (req.params.name === "open_project") {
718
+ const q = String(req.params.arguments?.project ?? "").toLowerCase();
719
+ const p = store.all().find((x) => x.name.toLowerCase() === q || x.path.toLowerCase() === q || path5.basename(x.path).toLowerCase() === q);
720
+ if (!p) return text(`No project "${q}". Use list_projects.`, true);
721
+ if (!p.ready) return text(`${p.name} isn't set up for Truecanvas yet. Ask the user to click "Set up" on the Dashboard.`, true);
722
+ store.touch(p.path);
723
+ const run2 = await runner.open(p.path);
724
+ if (run2.status !== "ready") return text(`Couldn't start ${p.name}: ${run2.error}`, true);
725
+ current = p.path;
726
+ notify();
727
+ return text(`\u2713 ${p.name} is open. Your tools now act on it.`);
728
+ }
729
+ const dir = target();
730
+ const out = dir ? await withUpstream(dir, (c) => c.callTool(req.params, void 0, { timeout: 18e4 })) : null;
731
+ if (!out) return text("No project is open. Use list_projects and open_project, or open one in the Truecanvas window.", true);
732
+ return out;
733
+ });
734
+ return server2;
735
+ }
736
+ async function handleMcp(req, res) {
737
+ const sessionId = req.headers["mcp-session-id"];
738
+ const body = req.method === "POST" ? await readJson(req) : void 0;
739
+ let transport = sessionId ? transports.get(sessionId) : void 0;
740
+ if (sessionId && transport) lastUse.set(sessionId, Date.now());
741
+ if (!transport) {
742
+ if (req.method !== "POST" || !isInitializeRequest(body)) return json(res, 400, { jsonrpc: "2.0", error: { code: -32e3, message: "No valid MCP session." }, id: null });
743
+ const t = new StreamableHTTPServerTransport({
744
+ sessionIdGenerator: () => randomUUID(),
745
+ onsessioninitialized: (id) => {
746
+ transports.set(id, t);
747
+ lastUse.set(id, Date.now());
748
+ }
749
+ });
750
+ t.onclose = () => {
751
+ if (t.sessionId) {
752
+ transports.delete(t.sessionId);
753
+ lastUse.delete(t.sessionId);
754
+ }
755
+ };
756
+ await createProxy().connect(t);
757
+ transport = t;
758
+ }
759
+ await transport.handleRequest(req, res, body);
760
+ }
761
+ async function api(req, res, url) {
762
+ const route = `${req.method} ${url.pathname}`;
763
+ const body = req.method === "POST" ? await readJson(req) : {};
764
+ switch (route) {
765
+ case "GET /api/hub/state":
766
+ return json(res, 200, await state());
767
+ case "GET /api/hub/discover": {
768
+ const known = new Set(store.all().map((p) => p.path));
769
+ return json(res, 200, { found: discover().filter((f) => !known.has(f.path)) });
770
+ }
771
+ case "GET /api/hub/icon": {
772
+ const icon = findIcon(url.searchParams.get("path") ?? "");
773
+ if (!icon) return json(res, 404, { error: "No icon" });
774
+ res.writeHead(200, { "content-type": ICON_MIME[path5.extname(icon)] ?? "application/octet-stream", "cache-control": "private, max-age=300" });
775
+ return fs5.createReadStream(icon).on("error", () => res.destroy()).pipe(res);
776
+ }
777
+ case "GET /api/hub/ls":
778
+ return json(res, 200, listDirs(url.searchParams.get("path") || "~"));
779
+ case "GET /api/hub/events":
780
+ res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
781
+ res.write(": connected\n\n");
782
+ clients.add(res);
783
+ req.on("close", () => clients.delete(res));
784
+ return;
785
+ case "POST /api/hub/add": {
786
+ const dir = path5.resolve(body.path.replace(/^~(?=$|[\\/])/, os5.homedir()));
787
+ if (!fs5.existsSync(path5.join(dir, "package.json"))) return json(res, 400, { error: "No package.json in that folder. Pick the root of a Next.js or Vite app." });
788
+ store.touch(dir);
789
+ broadcast();
790
+ return json(res, 200, { path: dir, ready: store.all().find((p) => p.path === dir)?.ready ?? false });
791
+ }
792
+ case "POST /api/hub/open": {
793
+ store.touch(body.path);
794
+ active = body.path;
795
+ used(body.path);
796
+ if (body.focus) focus = { path: body.path, at: Date.now() };
797
+ let run2;
798
+ try {
799
+ run2 = await runner.open(body.path);
800
+ } catch (err) {
801
+ broadcast();
802
+ return json(res, 400, { error: err.message });
803
+ }
804
+ broadcast();
805
+ return json(res, 200, { status: run2.status, editor: `http://localhost:${run2.editorPort}`, error: run2.error });
806
+ }
807
+ case "POST /api/hub/close":
808
+ runner.close(body.path);
809
+ if (active === body.path) active = null;
810
+ broadcast();
811
+ return json(res, 200, { ok: true });
812
+ case "POST /api/hub/active":
813
+ used(active);
814
+ active = body.path || null;
815
+ used(active);
816
+ broadcast();
817
+ return json(res, 200, { ok: true });
818
+ case "POST /api/hub/forget":
819
+ runner.close(body.path);
820
+ store.forget(body.path);
821
+ broadcast();
822
+ return json(res, 200, { ok: true });
823
+ case "POST /api/hub/setup":
824
+ return json(res, 200, setupJob(body.path, pkgDir, opts.cli));
825
+ case "POST /api/hub/create":
826
+ return json(res, 200, createJob(body.name, body.parent, pkgDir, opts.cli));
827
+ // ---------- GitHub ----------
828
+ case "GET /api/hub/github/repos": {
829
+ const r = await capture("gh", ["repo", "list", "--limit", "100", "--json", "nameWithOwner,description,updatedAt,isPrivate"], os5.homedir());
830
+ if (!r.ok) return json(res, 200, { gh: false, error: r.out.split("\n")[0], repos: [] });
831
+ const repos = JSON.parse(r.out).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
832
+ return json(res, 200, { gh: true, repos });
833
+ }
834
+ case "POST /api/hub/clone":
835
+ return json(res, 200, cloneJob(body.repo, body.parent || "~", pkgDir, opts.cli));
836
+ case "GET /api/hub/prs": {
837
+ const dir = url.searchParams.get("path") ?? "";
838
+ const r = await capture("gh", ["pr", "list", "--limit", "30", "--json", "number,title,headRefName,author,updatedAt,isDraft,url"], dir);
839
+ if (!r.ok) {
840
+ const msg = /no git remotes|not a git repository|none of the git remotes/i.test(r.out) ? "This project isn't on GitHub yet. Publish it from the Git panel (branch pill) first." : /gh auth login|not logged/i.test(r.out) ? "Sign in to GitHub first: run gh auth login in a terminal." : r.out.split("\n")[0];
841
+ return json(res, 200, { error: msg, prs: [] });
842
+ }
843
+ return json(res, 200, { prs: JSON.parse(r.out) });
844
+ }
845
+ case "POST /api/hub/pr-checkout": {
846
+ const r = await capture("gh", ["pr", "checkout", String(Number(body.number))], body.path);
847
+ if (!r.ok) {
848
+ const dirty = /local changes|would be overwritten|uncommitted/i.test(r.out);
849
+ return json(res, 400, { error: dirty ? "This project has uncommitted changes. Commit them (Git panel) before opening another pull request." : r.out.split("\n").slice(-2).join(" ") });
850
+ }
851
+ store.touch(body.path);
852
+ active = body.path;
853
+ await runner.open(body.path);
854
+ broadcast();
855
+ return json(res, 200, { ok: true });
856
+ }
857
+ case "POST /api/hub/notify": {
858
+ const p = store.all().find((x) => x.path === body.path);
859
+ if (!p || !body.name || !body.canvas) return json(res, 400, { error: "Unknown project or comment" });
860
+ opts.onNotice?.({ path: p.path, project: p.name, canvas: String(body.canvas), name: String(body.name).slice(0, 80), text: String(body.text ?? "").slice(0, 300) });
861
+ return json(res, 200, { ok: true });
862
+ }
863
+ case "POST /api/hub/session": {
864
+ const action = body.action === "start" || body.action === "stop" ? body.action : "toggle";
865
+ try {
866
+ const sessions = await sessionAction(action, body.path ? String(body.path) : active, body.canvas ? String(body.canvas) : void 0);
867
+ return json(res, 200, { sessions });
868
+ } catch (err) {
869
+ return json(res, 400, { error: err.message });
870
+ }
871
+ }
872
+ case "POST /api/hub/quit":
873
+ json(res, 200, { ok: true });
874
+ if (opts.onQuit) return opts.onQuit();
875
+ void shutdown().finally(() => process.exit(0));
876
+ return;
877
+ }
878
+ const job = /^GET \/api\/hub\/jobs\/(\d+)$/.exec(route);
879
+ if (job) {
880
+ const j = getJob(job[1]);
881
+ if (j?.status === "done" && j.result) {
882
+ store.touch(j.result);
883
+ broadcast();
884
+ }
885
+ return j ? json(res, 200, j) : json(res, 404, { error: "No such job" });
886
+ }
887
+ return json(res, 404, { error: `No route ${route}` });
888
+ }
889
+ function serveStatic(res, pathname) {
890
+ let file = path5.join(editorDir, pathname === "/" ? "hub.html" : pathname);
891
+ if (!inside(editorDir, file) || !fs5.existsSync(file) || fs5.statSync(file).isDirectory()) file = path5.join(editorDir, "hub.html");
892
+ if (!fs5.existsSync(file)) {
893
+ res.writeHead(500, { "content-type": "text/plain" });
894
+ return res.end("Hub bundle missing. Run `pnpm build`.");
895
+ }
896
+ res.writeHead(200, { "content-type": MIME[path5.extname(file)] ?? "application/octet-stream", "cache-control": pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache" });
897
+ fs5.createReadStream(file).on("error", () => res.destroy()).pipe(res);
898
+ }
899
+ const server = http.createServer(async (req, res) => {
900
+ try {
901
+ if (!allowedHosts.has(req.headers.host ?? "")) return json(res, 403, { error: "Forbidden host" });
902
+ const reqOrigin = req.headers.origin;
903
+ if (reqOrigin && reqOrigin !== origin && reqOrigin !== `http://127.0.0.1:${opts.port}`) return json(res, 403, { error: "Forbidden origin" });
904
+ const url = new URL(req.url ?? "/", origin);
905
+ if (url.pathname === "/mcp") return await handleMcp(req, res);
906
+ if (url.pathname.startsWith("/api/")) return await api(req, res, url);
907
+ if (req.method === "GET") return serveStatic(res, url.pathname);
908
+ json(res, 405, { error: "Method not allowed" });
909
+ } catch (err) {
910
+ if (!res.headersSent) json(res, err instanceof EditError ? 400 : 500, { error: err.message });
911
+ else res.end();
912
+ }
913
+ });
914
+ await new Promise((resolve, reject) => {
915
+ server.once("error", reject);
916
+ server.listen(opts.port, "127.0.0.1", resolve);
917
+ });
918
+ writeHubFile(opts.port);
919
+ const shutdown = () => {
920
+ server.close();
921
+ removeHubFile();
922
+ return runner.closeAll();
923
+ };
924
+ if (opts.signals !== false) {
925
+ const exit = () => void shutdown().finally(() => process.exit(0));
926
+ process.once("SIGINT", exit);
927
+ process.once("SIGTERM", exit);
928
+ }
929
+ return {
930
+ origin,
931
+ shutdown,
932
+ state,
933
+ /** shows a project's tab in the window (and a canvas, when given) */
934
+ focus(dir, canvas) {
935
+ active = dir;
936
+ used(dir);
937
+ focus = { path: dir, at: Date.now(), ...canvas ? { canvas } : {} };
938
+ broadcast();
939
+ },
940
+ /** live sessions: clients follow a project's open canvas live on its share link */
941
+ liveSession: {
942
+ available: true,
943
+ /** the project's running sessions (the active project by default) */
944
+ sessions: (dir = active) => dir ? sessionsOf(dir) : Promise.resolve([]),
945
+ /** stops the project's session if one runs, else starts one on the canvas open in its editor */
946
+ async toggle(dir = active, canvas) {
947
+ return sessionAction("toggle", dir, canvas);
948
+ }
949
+ }
950
+ };
951
+ }
952
+ var ICON_MIME = { ".ico": "image/x-icon", ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp" };
953
+ function findIcon(dir) {
954
+ if (!dir || !fs5.existsSync(path5.join(dir, "package.json"))) return null;
955
+ const names = ["icon.svg", "icon.png", "favicon.svg", "favicon.ico", "icon.ico", "apple-icon.png", "favicon.png", "logo.svg", "icon.jpg"];
956
+ const roots = ["app", "src/app", "public", "static", "assets"];
957
+ for (const root of roots) {
958
+ for (const name of names) {
959
+ const file = path5.join(dir, root, name);
960
+ if (fs5.existsSync(file) && fs5.statSync(file).size < 2e6) return file;
961
+ }
962
+ }
963
+ for (const root of ["app", "src/app"]) {
964
+ let entries = [];
965
+ try {
966
+ entries = fs5.readdirSync(path5.join(dir, root), { withFileTypes: true });
967
+ } catch {
968
+ continue;
969
+ }
970
+ for (const e of entries) {
971
+ if (!e.isDirectory()) continue;
972
+ for (const name of names.slice(0, 4)) {
973
+ const file = path5.join(dir, root, e.name, name);
974
+ if (fs5.existsSync(file)) return file;
975
+ }
976
+ }
977
+ }
978
+ return null;
979
+ }
980
+ function json(res, status, body) {
981
+ res.writeHead(status, { "content-type": "application/json" });
982
+ res.end(JSON.stringify(body));
983
+ }
984
+ function inside(dir, file) {
985
+ const rel = path5.relative(dir, file);
986
+ return !!rel && !rel.startsWith("..") && !path5.isAbsolute(rel);
987
+ }
988
+
989
+ export {
990
+ readJson,
991
+ addDevArgs,
992
+ runScript,
993
+ execInherit,
994
+ packageDir,
995
+ packageVersion,
996
+ packageSpec,
997
+ installedIn,
998
+ readPackage,
999
+ depVersion,
1000
+ connectAgents,
1001
+ hasAgentConfig,
1002
+ HUB_PORT,
1003
+ hubPort,
1004
+ startHub
1005
+ };