@instafy/cli 0.2.3 → 0.2.4

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.
Files changed (52) hide show
  1. package/README.md +4 -0
  2. package/dist/bind/LICENSE +661 -0
  3. package/dist/bind/bin/instafy-bind.mjs +542 -0
  4. package/dist/bind/client/bind-client.js +1219 -0
  5. package/dist/bind/src/apply.mjs +293 -0
  6. package/dist/bind/src/bound.mjs +202 -0
  7. package/dist/bind/src/budget.mjs +58 -0
  8. package/dist/bind/src/check.mjs +176 -0
  9. package/dist/bind/src/config.mjs +180 -0
  10. package/dist/bind/src/controller.mjs +244 -0
  11. package/dist/bind/src/executors/agent.mjs +336 -0
  12. package/dist/bind/src/executors/script.mjs +163 -0
  13. package/dist/bind/src/handler-kit.mjs +150 -0
  14. package/dist/bind/src/init.mjs +238 -0
  15. package/dist/bind/src/jobs.mjs +316 -0
  16. package/dist/bind/src/net.mjs +281 -0
  17. package/dist/bind/src/page-fetch.mjs +43 -0
  18. package/dist/bind/src/preview-host.mjs +114 -0
  19. package/dist/bind/src/protocol.mjs +103 -0
  20. package/dist/bind/src/records-schema.mjs +580 -0
  21. package/dist/bind/src/records.mjs +161 -0
  22. package/dist/bind/src/save-owner.mjs +492 -0
  23. package/dist/bind/src/schema-lite.mjs +212 -0
  24. package/dist/bind/src/server.mjs +908 -0
  25. package/dist/bind/src/sse.mjs +119 -0
  26. package/dist/bind/src/starters/blank/app.js +17 -0
  27. package/dist/bind/src/starters/blank/index.html +22 -0
  28. package/dist/bind/src/starters/checklist/app.js +171 -0
  29. package/dist/bind/src/starters/checklist/bound.json +29 -0
  30. package/dist/bind/src/starters/checklist/data.json +26 -0
  31. package/dist/bind/src/starters/checklist/index.html +23 -0
  32. package/dist/bind/src/starters/compare/app.js +316 -0
  33. package/dist/bind/src/starters/compare/bound.json +26 -0
  34. package/dist/bind/src/starters/compare/data.json +170 -0
  35. package/dist/bind/src/starters/compare/index.html +23 -0
  36. package/dist/bind/src/starters/handlers/read_offer.mjs +55 -0
  37. package/dist/bind/src/starters/handlers/set_check.mjs +41 -0
  38. package/dist/bind/src/starters/ranked/app.js +166 -0
  39. package/dist/bind/src/starters/ranked/bound.json +26 -0
  40. package/dist/bind/src/starters/ranked/data.json +142 -0
  41. package/dist/bind/src/starters/ranked/index.html +23 -0
  42. package/dist/bind/src/starters/site.js +888 -0
  43. package/dist/bind/src/starters/theme.css +492 -0
  44. package/dist/bind/src/starters/timeline/app.js +143 -0
  45. package/dist/bind/src/starters/timeline/data.json +41 -0
  46. package/dist/bind/src/starters/timeline/index.html +22 -0
  47. package/dist/bind/src/static.mjs +141 -0
  48. package/dist/bind/src/tokens.mjs +102 -0
  49. package/dist/bind/src/try.mjs +139 -0
  50. package/dist/bind/src/web.mjs +191 -0
  51. package/dist/cli.js +2117 -365
  52. package/package.json +3 -2
package/dist/cli.js CHANGED
@@ -1263,7 +1263,7 @@ var org_exports = {};
1263
1263
  __export(org_exports, {
1264
1264
  listOrganizations: () => listOrganizations
1265
1265
  });
1266
- import kleur17 from "kleur";
1266
+ import kleur18 from "kleur";
1267
1267
  function getTeamDisplayName2(name) {
1268
1268
  const trimmed = (name ?? "").trim();
1269
1269
  if (!trimmed || trimmed === "Personal organization" || trimmed === "Personal team" || trimmed === "Personal workspace") {
@@ -1301,11 +1301,11 @@ async function listOrganizations(params) {
1301
1301
  console.log(JSON.stringify(orgs, null, 2));
1302
1302
  } else {
1303
1303
  if (orgs.length === 0) {
1304
- console.log(kleur17.yellow("No teams found for this account."));
1304
+ console.log(kleur18.yellow("No teams found for this account."));
1305
1305
  } else {
1306
1306
  for (const org of orgs) {
1307
1307
  const role = org.role ? ` [${org.role}]` : "";
1308
- console.log(`${kleur17.green(getTeamDisplayName2(org.name))} (${org.id}${role})`);
1308
+ console.log(`${kleur18.green(getTeamDisplayName2(org.name))} (${org.id}${role})`);
1309
1309
  }
1310
1310
  }
1311
1311
  }
@@ -1335,9 +1335,9 @@ var init_hardwareProvider2 = __esm({
1335
1335
  });
1336
1336
 
1337
1337
  // ../instafy-sdk/src/hardwareNode.ts
1338
- import fs14 from "node:fs";
1339
- import os5 from "node:os";
1340
- import path15 from "node:path";
1338
+ import fs17 from "node:fs";
1339
+ import os7 from "node:os";
1340
+ import path18 from "node:path";
1341
1341
  function uniqueStrings(values) {
1342
1342
  const seen = /* @__PURE__ */ new Set();
1343
1343
  const result = [];
@@ -1376,14 +1376,14 @@ function classifySerialPort(fileName, fullPath) {
1376
1376
  return null;
1377
1377
  }
1378
1378
  function inspectPort(portPath) {
1379
- const resolvedPath = path15.resolve(portPath);
1379
+ const resolvedPath = path18.resolve(portPath);
1380
1380
  let exists = false;
1381
1381
  let readable = false;
1382
1382
  let writable = false;
1383
1383
  let isCharacterDevice = false;
1384
1384
  let error = null;
1385
1385
  try {
1386
- const stat = fs14.statSync(resolvedPath);
1386
+ const stat = fs17.statSync(resolvedPath);
1387
1387
  exists = true;
1388
1388
  isCharacterDevice = stat.isCharacterDevice();
1389
1389
  } catch (caught) {
@@ -1391,13 +1391,13 @@ function inspectPort(portPath) {
1391
1391
  }
1392
1392
  if (exists) {
1393
1393
  try {
1394
- fs14.accessSync(resolvedPath, fs14.constants.R_OK);
1394
+ fs17.accessSync(resolvedPath, fs17.constants.R_OK);
1395
1395
  readable = true;
1396
1396
  } catch {
1397
1397
  readable = false;
1398
1398
  }
1399
1399
  try {
1400
- fs14.accessSync(resolvedPath, fs14.constants.W_OK);
1400
+ fs17.accessSync(resolvedPath, fs17.constants.W_OK);
1401
1401
  writable = true;
1402
1402
  } catch {
1403
1403
  writable = false;
@@ -1419,12 +1419,12 @@ function listCandidates(scanDirs) {
1419
1419
  for (const scanDir of scanDirs) {
1420
1420
  let entries = [];
1421
1421
  try {
1422
- entries = fs14.readdirSync(scanDir);
1422
+ entries = fs17.readdirSync(scanDir);
1423
1423
  } catch {
1424
1424
  continue;
1425
1425
  }
1426
1426
  for (const entry of entries) {
1427
- const fullPath = path15.join(scanDir, entry);
1427
+ const fullPath = path18.join(scanDir, entry);
1428
1428
  const kind = classifySerialPort(entry, fullPath);
1429
1429
  if (!kind) {
1430
1430
  continue;
@@ -1432,7 +1432,7 @@ function listCandidates(scanDirs) {
1432
1432
  candidates.push({
1433
1433
  path: fullPath,
1434
1434
  kind,
1435
- displayName: path15.basename(fullPath)
1435
+ displayName: path18.basename(fullPath)
1436
1436
  });
1437
1437
  }
1438
1438
  }
@@ -1458,7 +1458,7 @@ function listHardwareSerialPorts(options = {}) {
1458
1458
  }
1459
1459
  return {
1460
1460
  providerId: SERIAL_HARDWARE_PROVIDER_ID2,
1461
- platform: os5.platform(),
1461
+ platform: os7.platform(),
1462
1462
  ports: [...byPath.values()].sort((left, right) => left.path.localeCompare(right.path))
1463
1463
  };
1464
1464
  }
@@ -1598,37 +1598,37 @@ __export(hardware_exports, {
1598
1598
  hardwareSerialList: () => hardwareSerialList,
1599
1599
  hardwareSerialProbe: () => hardwareSerialProbe
1600
1600
  });
1601
- import kleur18 from "kleur";
1601
+ import kleur19 from "kleur";
1602
1602
  function printSerialPorts(result) {
1603
1603
  if (result.ports.length === 0) {
1604
- console.log(kleur18.yellow("No local serial devices were discovered."));
1604
+ console.log(kleur19.yellow("No local serial devices were discovered."));
1605
1605
  return;
1606
1606
  }
1607
- console.log(kleur18.cyan(`Local serial devices (${result.platform}):`));
1607
+ console.log(kleur19.cyan(`Local serial devices (${result.platform}):`));
1608
1608
  for (const port of result.ports) {
1609
- const status = port.available ? kleur18.green("available") : kleur18.yellow("not ready");
1609
+ const status = port.available ? kleur19.green("available") : kleur19.yellow("not ready");
1610
1610
  console.log(`- ${port.path} (${port.kind}, ${status})`);
1611
1611
  }
1612
1612
  }
1613
1613
  function printProbe(probe) {
1614
- console.log(kleur18.cyan(`Serial device: ${probe.path}`));
1614
+ console.log(kleur19.cyan(`Serial device: ${probe.path}`));
1615
1615
  console.log(`Exists: ${probe.exists ? "yes" : "no"}`);
1616
1616
  console.log(`Readable: ${probe.readable ? "yes" : "no"}`);
1617
1617
  console.log(`Writable: ${probe.writable ? "yes" : "no"}`);
1618
1618
  console.log(`Character device: ${probe.isCharacterDevice ? "yes" : "no"}`);
1619
- console.log(`Available: ${probe.available ? kleur18.green("yes") : kleur18.yellow("no")}`);
1619
+ console.log(`Available: ${probe.available ? kleur19.green("yes") : kleur19.yellow("no")}`);
1620
1620
  if (probe.error) {
1621
1621
  console.log(`Detail: ${probe.error}`);
1622
1622
  }
1623
1623
  }
1624
1624
  function printIoOpportunities(result) {
1625
1625
  if (result.opportunities.length === 0) {
1626
- console.log(kleur18.yellow("No local IO opportunities were discovered."));
1626
+ console.log(kleur19.yellow("No local IO opportunities were discovered."));
1627
1627
  return;
1628
1628
  }
1629
- console.log(kleur18.cyan(`Local IO opportunities (${result.platform}):`));
1629
+ console.log(kleur19.cyan(`Local IO opportunities (${result.platform}):`));
1630
1630
  for (const opportunity of result.opportunities) {
1631
- const status = opportunity.available ? kleur18.green("available") : kleur18.yellow("not ready");
1631
+ const status = opportunity.available ? kleur19.green("available") : kleur19.yellow("not ready");
1632
1632
  console.log(`- ${opportunity.title} (${status})`);
1633
1633
  console.log(` Resource: ${opportunity.resource.path}`);
1634
1634
  const available = opportunity.actions.filter((action) => action.status === "available").map((action) => action.id);
@@ -1642,8 +1642,8 @@ function printIoOpportunities(result) {
1642
1642
  }
1643
1643
  }
1644
1644
  function printIoActionResult(result) {
1645
- const status = result.ok ? kleur18.green("ok") : kleur18.red("failed");
1646
- console.log(kleur18.cyan(`Local IO action: ${result.actionId} (${status})`));
1645
+ const status = result.ok ? kleur19.green("ok") : kleur19.red("failed");
1646
+ console.log(kleur19.cyan(`Local IO action: ${result.actionId} (${status})`));
1647
1647
  console.log(result.message);
1648
1648
  if (result.serialProbe) {
1649
1649
  printProbe(result.serialProbe);
@@ -1652,11 +1652,11 @@ function printIoActionResult(result) {
1652
1652
  const exit = result.process.exitCode ?? result.process.signal ?? "unknown";
1653
1653
  console.log(`Process exit: ${exit}`);
1654
1654
  if (result.process.stdout.trim()) {
1655
- console.log(kleur18.dim("stdout:"));
1655
+ console.log(kleur19.dim("stdout:"));
1656
1656
  console.log(result.process.stdout.trimEnd());
1657
1657
  }
1658
1658
  if (result.process.stderr.trim()) {
1659
- console.log(kleur18.dim("stderr:"));
1659
+ console.log(kleur19.dim("stderr:"));
1660
1660
  console.log(result.process.stderr.trimEnd());
1661
1661
  }
1662
1662
  }
@@ -1718,8 +1718,8 @@ var init_hardware = __esm({
1718
1718
  // src/index.ts
1719
1719
  import { Command, Option } from "commander";
1720
1720
  import { pathToFileURL as pathToFileURL2 } from "node:url";
1721
- import { createRequire as createRequire3 } from "node:module";
1722
- import kleur19 from "kleur";
1721
+ import { createRequire as createRequire4 } from "node:module";
1722
+ import kleur20 from "kleur";
1723
1723
 
1724
1724
  // src/runtime.ts
1725
1725
  import { spawn as spawn2, spawnSync } from "node:child_process";
@@ -3672,21 +3672,45 @@ function resolveRequestHost(request) {
3672
3672
  function splitCsv(value) {
3673
3673
  return (value ?? "").split(",").map((entry) => entry.trim().toLowerCase()).filter(Boolean);
3674
3674
  }
3675
+ var DEFAULT_HOSTED_GIT_HOST = "git.instafy.dev";
3676
+ function hostOfUrl(raw) {
3677
+ if (!raw?.trim()) return null;
3678
+ try {
3679
+ return new URL(raw.trim()).host.toLowerCase() || null;
3680
+ } catch {
3681
+ return null;
3682
+ }
3683
+ }
3684
+ function configuredGitHosts() {
3685
+ let controllerHost = null;
3686
+ try {
3687
+ controllerHost = hostOfUrl(resolveControllerUrl({ controllerUrl: null }));
3688
+ } catch {
3689
+ controllerHost = null;
3690
+ }
3691
+ return [
3692
+ DEFAULT_HOSTED_GIT_HOST,
3693
+ controllerHost,
3694
+ hostOfUrl(process.env["ORIGIN_GIT_REMOTE_URL"]),
3695
+ ...splitCsv(process.env["INSTAFY_GIT_HOSTS"])
3696
+ ].filter((host) => Boolean(host));
3697
+ }
3698
+ var LOCAL_STACK_GIT_HOST = /^(?:git-edge|git-shard(?:-[a-z0-9]+)*)$/;
3675
3699
  function isAllowedGitHost(rawHost) {
3676
3700
  if (!rawHost) return false;
3677
3701
  const normalized = normalizeHost(rawHost);
3678
3702
  if (!normalized) return false;
3679
- const allowHosts = splitCsv(process.env["INSTAFY_GIT_HOSTS"]);
3703
+ const allowHosts = configuredGitHosts();
3680
3704
  if (allowHosts.includes(normalized.host) || allowHosts.includes(normalized.hostname)) {
3681
3705
  return true;
3682
3706
  }
3683
3707
  if (normalized.hostname === "localhost" || normalized.hostname === "127.0.0.1" || normalized.hostname === "::1" || normalized.hostname === "host.docker.internal") {
3684
3708
  return true;
3685
3709
  }
3686
- if (normalized.hostname === "git-edge" || normalized.hostname.startsWith("git-shard")) {
3710
+ if (LOCAL_STACK_GIT_HOST.test(normalized.hostname)) {
3687
3711
  return true;
3688
3712
  }
3689
- return normalized.hostname.endsWith(".instafy.dev");
3713
+ return false;
3690
3714
  }
3691
3715
  function normalizeRepoName(raw) {
3692
3716
  const trimmed = raw.trim().replace(/^\/+/, "");
@@ -5028,7 +5052,7 @@ async function credentialsRevoke(options) {
5028
5052
  import fs7 from "node:fs";
5029
5053
  import os4 from "node:os";
5030
5054
  import path8 from "node:path";
5031
- import { spawn as spawn3 } from "node:child_process";
5055
+ import { execFileSync, spawn as spawn3 } from "node:child_process";
5032
5056
  import { randomUUID } from "node:crypto";
5033
5057
  import kleur7 from "kleur";
5034
5058
  init_config();
@@ -5075,7 +5099,9 @@ function buildRatholeEnvironment(source = process.env) {
5075
5099
  return environment;
5076
5100
  }
5077
5101
  function cleanUrl(raw) {
5078
- return raw.replace(/\/+$/, "");
5102
+ let end = raw.length;
5103
+ while (end > 0 && raw[end - 1] === "/") end -= 1;
5104
+ return raw.slice(0, end);
5079
5105
  }
5080
5106
  function normalizeTunnelPurpose(raw) {
5081
5107
  const trimmed = raw.trim();
@@ -5090,7 +5116,11 @@ function normalizeTunnelPurpose(raw) {
5090
5116
  normalized += "_";
5091
5117
  }
5092
5118
  }
5093
- normalized = normalized.replace(/^_+/, "").replace(/_+$/, "");
5119
+ let start = 0;
5120
+ let end = normalized.length;
5121
+ while (start < end && normalized[start] === "_") start += 1;
5122
+ while (end > start && normalized[end - 1] === "_") end -= 1;
5123
+ normalized = normalized.slice(start, end);
5094
5124
  return normalized ? normalized : null;
5095
5125
  }
5096
5126
  function readEnv(key) {
@@ -5278,6 +5308,71 @@ async function waitForProcessExit(pid, timeoutMs) {
5278
5308
  }
5279
5309
  return !isProcessAlive2(pid);
5280
5310
  }
5311
+ function readProcessCommandLine(pid) {
5312
+ if (!isProcessAlive2(pid)) return null;
5313
+ try {
5314
+ if (process.platform === "linux") {
5315
+ const raw = fs7.readFileSync(`/proc/${pid}/cmdline`, "utf8");
5316
+ return raw.split("\0").filter(Boolean).join(" ") || null;
5317
+ }
5318
+ if (process.platform === "win32") {
5319
+ const out2 = execFileSync(
5320
+ "powershell.exe",
5321
+ [
5322
+ "-NoProfile",
5323
+ "-NonInteractive",
5324
+ "-Command",
5325
+ `(Get-CimInstance Win32_Process -Filter 'ProcessId=${Math.trunc(pid)}').CommandLine`
5326
+ ],
5327
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 1e4, windowsHide: true }
5328
+ );
5329
+ return out2.trim() || null;
5330
+ }
5331
+ const out = execFileSync("ps", ["-ww", "-o", "command=", "-p", String(Math.trunc(pid))], {
5332
+ encoding: "utf8",
5333
+ stdio: ["ignore", "pipe", "ignore"],
5334
+ timeout: 5e3
5335
+ });
5336
+ return out.trim() || null;
5337
+ } catch {
5338
+ return null;
5339
+ }
5340
+ }
5341
+ function isRatholeClientFor(pid, configPath2, binary) {
5342
+ if (!configPath2) return false;
5343
+ const commandLine = readProcessCommandLine(pid);
5344
+ return commandLine !== null && commandLineIsRatholeClient(commandLine, configPath2, binary);
5345
+ }
5346
+ function endsWithProgram(program2, binary) {
5347
+ if (program2 === binary) return true;
5348
+ if (!program2.endsWith(binary)) return false;
5349
+ const before = program2[program2.length - binary.length - 1];
5350
+ return before === " " || before === "/" || before === "\\";
5351
+ }
5352
+ function commandLineIsRatholeClient(commandLine, configPath2, binary) {
5353
+ if (!configPath2) return false;
5354
+ const line = commandLine.split('"').join("").trim();
5355
+ const tail = ` -c ${configPath2}`;
5356
+ if (!line.endsWith(tail)) return false;
5357
+ const program2 = line.slice(0, line.length - tail.length).trimEnd();
5358
+ if (!program2) return false;
5359
+ if (binary && endsWithProgram(program2, binary)) return true;
5360
+ const name = program2.split(/[\\/]/).pop() ?? "";
5361
+ return name.toLowerCase().includes("rathole");
5362
+ }
5363
+ async function stopRatholeClient(pid, configPath2, binary) {
5364
+ if (!isRatholeClientFor(pid, configPath2, binary)) return;
5365
+ try {
5366
+ process.kill(pid, "SIGTERM");
5367
+ } catch {
5368
+ }
5369
+ if (await waitForProcessExit(pid, 4e3)) return;
5370
+ if (!isRatholeClientFor(pid, configPath2, binary)) return;
5371
+ try {
5372
+ process.kill(pid, "SIGKILL");
5373
+ } catch {
5374
+ }
5375
+ }
5281
5376
  function upsertTunnelState(entry) {
5282
5377
  const state = readStateFile();
5283
5378
  const next = state.tunnels.filter((tunnel) => tunnel.tunnelId !== entry.tunnelId);
@@ -5367,8 +5462,19 @@ async function revokeTunnel(controllerUrl, auth, projectId2, tunnelId) {
5367
5462
  throw new Error(`Tunnel revoke failed (${response.status} ${response.statusText}): ${text2}`);
5368
5463
  }
5369
5464
  }
5465
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
5466
+ function resolveTunnelServerAddress(server, env = process.env, inContainer = fs7.existsSync("/.dockerenv")) {
5467
+ const match = /^(\[[^\]]+\]|[^:]+):(\d+)$/.exec(server.trim());
5468
+ if (!match) return server;
5469
+ const [, host, port] = match;
5470
+ const override = env["INSTAFY_TUNNEL_SERVER_HOST"]?.trim();
5471
+ if (override) return `${override}:${port}`;
5472
+ if (inContainer && LOOPBACK_HOSTS.has(host.toLowerCase())) return `host.docker.internal:${port}`;
5473
+ return server;
5474
+ }
5370
5475
  function buildRatholeConfig(creds, port) {
5371
- const server = typeof creds["server"] === "string" ? creds["server"] : null;
5476
+ const advertised = typeof creds["server"] === "string" ? creds["server"] : null;
5477
+ const server = advertised ? resolveTunnelServerAddress(advertised) : null;
5372
5478
  const token = typeof creds["token"] === "string" ? creds["token"] : null;
5373
5479
  const service = typeof creds["service"] === "string" && creds["service"] || typeof creds["serviceName"] === "string" && creds["serviceName"] || "runtime";
5374
5480
  const protocol = typeof creds["protocol"] === "string" && creds["protocol"] || "tcp";
@@ -5471,6 +5577,40 @@ async function runTunnelCommand(opts, options) {
5471
5577
  process.once("SIGTERM", handle);
5472
5578
  });
5473
5579
  }
5580
+ async function spawnRatholeDetached(params) {
5581
+ const logFd = fs7.openSync(params.logFile, "a");
5582
+ const child = spawn3(params.rathole, ["-c", params.configPath], {
5583
+ stdio: ["ignore", logFd, logFd],
5584
+ cwd: params.workdir,
5585
+ detached: true,
5586
+ env: buildRatholeEnvironment()
5587
+ });
5588
+ fs7.closeSync(logFd);
5589
+ let exitedEarly = false;
5590
+ let exitMessage = "";
5591
+ child.on("error", (error) => {
5592
+ exitedEarly = true;
5593
+ exitMessage = error.message;
5594
+ });
5595
+ child.on("exit", (code, signal) => {
5596
+ exitedEarly = true;
5597
+ exitMessage = code !== null ? `code ${code}` : `signal ${signal}`;
5598
+ });
5599
+ await new Promise((resolve) => setTimeout(resolve, 250));
5600
+ if (exitedEarly || !isProcessAlive2(child.pid ?? -1)) {
5601
+ return { pid: -1, exit: exitMessage || "unknown", logTail: readLogTail(params.logFile) };
5602
+ }
5603
+ child.unref();
5604
+ return { pid: child.pid ?? -1, exit: null, logTail: "" };
5605
+ }
5606
+ function readLogTail(logFile, lines = 20) {
5607
+ try {
5608
+ const text2 = fs7.readFileSync(logFile, "utf8");
5609
+ return text2.split(/\r?\n/).filter(Boolean).slice(-lines).join("\n");
5610
+ } catch {
5611
+ return "";
5612
+ }
5613
+ }
5474
5614
  async function startTunnelDetached(opts) {
5475
5615
  const projectId2 = resolveProject(opts);
5476
5616
  const controllerToken = resolveControllerToken(opts);
@@ -5505,39 +5645,16 @@ async function startTunnelDetached(opts) {
5505
5645
  const configPath2 = path8.join(workdir, "rathole.toml");
5506
5646
  fs7.writeFileSync(configPath2, configBody, { encoding: "utf8", mode: 384 });
5507
5647
  const logFile = opts.logFile?.trim() ? path8.resolve(opts.logFile.trim()) : path8.join(TUNNEL_LOG_DIR, `tunnel-${tunnelIdSafe}.log`);
5508
- const logFd = fs7.openSync(logFile, "a");
5509
- const child = spawn3(rathole, ["-c", configPath2], {
5510
- stdio: ["ignore", logFd, logFd],
5511
- cwd: workdir,
5512
- detached: true,
5513
- env: buildRatholeEnvironment()
5514
- });
5515
- let exitedEarly = false;
5516
- let exitMessage = "";
5517
- child.on("exit", (code, signal) => {
5518
- exitedEarly = true;
5519
- exitMessage = code !== null ? `code ${code}` : `signal ${signal}`;
5520
- });
5521
- await new Promise((resolve) => setTimeout(resolve, 250));
5522
- if (exitedEarly || !isProcessAlive2(child.pid ?? -1)) {
5523
- const logTail = (() => {
5524
- try {
5525
- const text2 = fs7.readFileSync(logFile, "utf8");
5526
- const lines = text2.split(/\r?\n/).filter(Boolean);
5527
- return lines.slice(-20).join("\n");
5528
- } catch {
5529
- return "";
5530
- }
5531
- })();
5648
+ const started = await spawnRatholeDetached({ rathole, configPath: configPath2, workdir, logFile });
5649
+ if (started.exit !== null) {
5532
5650
  await revokeTunnel(controllerUrl, controllerToken, projectId2, grant.tunnelId).catch(() => {
5533
5651
  });
5534
- const suffix = logTail ? `
5652
+ const suffix = started.logTail ? `
5535
5653
 
5536
5654
  Last logs:
5537
- ${logTail}` : "";
5538
- throw new Error(`Tunnel process exited (${exitMessage || "unknown"}).${suffix}`);
5655
+ ${started.logTail}` : "";
5656
+ throw new Error(`Tunnel process exited (${started.exit}).${suffix}`);
5539
5657
  }
5540
- child.unref();
5541
5658
  const entry = {
5542
5659
  tunnelId: grant.tunnelId,
5543
5660
  projectId: projectId2,
@@ -5545,10 +5662,11 @@ ${logTail}` : "";
5545
5662
  url: grant.url ?? `https://${grant.hostname}`,
5546
5663
  localPort: port,
5547
5664
  controllerUrl: cleanUrl(controllerUrl),
5548
- pid: child.pid ?? -1,
5665
+ pid: started.pid,
5549
5666
  logFile,
5550
5667
  workdir,
5551
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
5668
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5669
+ binary: rathole
5552
5670
  };
5553
5671
  upsertTunnelState(entry);
5554
5672
  return entry;
@@ -5573,19 +5691,7 @@ async function stopTunnelSession(opts) {
5573
5691
  if (!entry) {
5574
5692
  throw new Error(`Tunnel not found in local state: ${tunnelId}`);
5575
5693
  }
5576
- if (isProcessAlive2(entry.pid)) {
5577
- try {
5578
- process.kill(entry.pid, "SIGTERM");
5579
- } catch {
5580
- }
5581
- const stopped = await waitForProcessExit(entry.pid, 4e3);
5582
- if (!stopped) {
5583
- try {
5584
- process.kill(entry.pid, "SIGKILL");
5585
- } catch {
5586
- }
5587
- }
5588
- }
5694
+ await stopRatholeClient(entry.pid, path8.join(entry.workdir, "rathole.toml"), entry.binary);
5589
5695
  const controllerToken = resolveControllerToken(opts, `instafy tunnel stop ${tunnelId}`);
5590
5696
  if (controllerToken.runtimeControllerUrl) {
5591
5697
  try {
@@ -5663,10 +5769,930 @@ async function tailTunnelLogs(options) {
5663
5769
  await new Promise((resolve) => child.on("exit", () => resolve()));
5664
5770
  }
5665
5771
 
5772
+ // src/preview.ts
5773
+ import fs8 from "node:fs";
5774
+ import os5 from "node:os";
5775
+ import path9 from "node:path";
5776
+ import kleur8 from "kleur";
5777
+ init_controller_fetch();
5778
+ init_errors();
5779
+
5780
+ // src/local-site.ts
5781
+ import http2 from "node:http";
5782
+ var PREVIEW_EXIT_NOT_LISTENING = 2;
5783
+ var PREVIEW_EXIT_PORT_NOT_ALLOWED = 3;
5784
+ var PreviewCommandError = class extends Error {
5785
+ constructor(message, exitCode) {
5786
+ super(message);
5787
+ this.exitCode = exitCode;
5788
+ }
5789
+ };
5790
+ var RESERVED_PORTS = [
5791
+ { env: "INSTAFY_VNC_PORT", kind: "port", port: 5900 },
5792
+ { env: "INSTAFY_PROVIDER_HOST_URL", kind: "url", port: 8789 },
5793
+ { env: "PROXY_BASE_URL", kind: "url", port: 8789 },
5794
+ { port: 9222 },
5795
+ { env: "INSTAFY_PLAYWRIGHT_CDP_PORT", kind: "port", port: 9223 },
5796
+ { env: "INSTAFY_PLAYWRIGHT_CDP_URL", kind: "url", port: 9223 },
5797
+ { port: 9225 },
5798
+ { env: "INSTAFY_BROWSER_EGRESS_PROXY_BIND", kind: "bind", port: 9226 },
5799
+ { env: "INSTAFY_PERSONAL_BROWSER_CONTROL_URL", kind: "url", port: 43127 },
5800
+ { env: "ORIGIN_BIND_PORT", kind: "port", port: 54332 }
5801
+ ];
5802
+ var PREVIEW_DENY_PORTS_ENV = "INSTAFY_PREVIEW_DENY_PORTS";
5803
+ var MIN_PREVIEW_PORT = 1024;
5804
+ var MAX_PREVIEW_PORT = 65535;
5805
+ var PROBE_TIMEOUT_MS = 5e3;
5806
+ function portNumber(raw) {
5807
+ if (!/^\d{1,5}$/.test(raw)) return null;
5808
+ const port = Number(raw);
5809
+ return port >= 1 && port <= MAX_PREVIEW_PORT ? port : null;
5810
+ }
5811
+ function portFromSetting(kind, raw) {
5812
+ const value = raw.trim();
5813
+ if (!value) return null;
5814
+ if (kind === "port") return portNumber(value);
5815
+ if (kind === "bind") return portNumber(value.slice(value.lastIndexOf(":") + 1));
5816
+ try {
5817
+ const url = new URL(value);
5818
+ return portNumber(url.port || (url.protocol === "https:" ? "443" : url.protocol === "http:" ? "80" : ""));
5819
+ } catch {
5820
+ return null;
5821
+ }
5822
+ }
5823
+ function reservedPreviewPorts(env = process.env) {
5824
+ const denied = /* @__PURE__ */ new Set();
5825
+ for (const entry of RESERVED_PORTS) {
5826
+ denied.add(entry.port);
5827
+ const raw = entry.env ? env[entry.env] : void 0;
5828
+ const port = entry.kind && raw ? portFromSetting(entry.kind, raw) : null;
5829
+ if (port !== null) denied.add(port);
5830
+ }
5831
+ for (const raw of (env[PREVIEW_DENY_PORTS_ENV] ?? "").split(",")) {
5832
+ if (!raw.trim()) continue;
5833
+ const port = portNumber(raw.trim());
5834
+ if (port === null) {
5835
+ throw new PreviewCommandError(
5836
+ `${PREVIEW_DENY_PORTS_ENV} has an entry that is not a port (${JSON.stringify(raw.trim().slice(0, 20))}), so no port is allowed until it is fixed.`,
5837
+ PREVIEW_EXIT_PORT_NOT_ALLOWED
5838
+ );
5839
+ }
5840
+ denied.add(port);
5841
+ }
5842
+ return denied;
5843
+ }
5844
+ function parsePreviewPort(raw, env = process.env) {
5845
+ const value = typeof raw === "number" ? raw : Number(String(raw ?? "").trim());
5846
+ if (!Number.isInteger(value) || value < MIN_PREVIEW_PORT || value > MAX_PREVIEW_PORT) {
5847
+ throw new PreviewCommandError(
5848
+ `Port must be a number from ${MIN_PREVIEW_PORT} to ${MAX_PREVIEW_PORT}.`,
5849
+ PREVIEW_EXIT_PORT_NOT_ALLOWED
5850
+ );
5851
+ }
5852
+ if (reservedPreviewPorts(env).has(value)) {
5853
+ throw new PreviewCommandError(
5854
+ `Port ${value} is reserved for Instafy and cannot be previewed. Serve the site on another port.`,
5855
+ PREVIEW_EXIT_PORT_NOT_ALLOWED
5856
+ );
5857
+ }
5858
+ return value;
5859
+ }
5860
+ function noAnswerMessage(port, probe, where, retry) {
5861
+ if (probe.timedOut) {
5862
+ return `A server accepted the connection on port ${port} but did not answer (${probe.reason ?? "no answer"}). It may still be starting or compiling: do not start a second server; wait for it, then ${retry}.`;
5863
+ }
5864
+ return `Nothing is answering on ${where} (${probe.reason ?? "no answer"}). Start the site first, then ${retry}.`;
5865
+ }
5866
+ var LOOPBACK_HOSTS2 = ["127.0.0.1", "::1"];
5867
+ function urlHost(host) {
5868
+ return host.includes(":") ? `[${host}]` : host;
5869
+ }
5870
+ function probeHost(host, port, sitePath, timeoutMs) {
5871
+ return new Promise((resolve) => {
5872
+ const request = http2.get({ host, port, path: sitePath, agent: false, timeout: timeoutMs }, (response) => {
5873
+ const status = response.statusCode ?? 0;
5874
+ response.resume();
5875
+ request.destroy();
5876
+ resolve({ listening: status > 0 && status < 500, status, reason: `HTTP ${status}`, host: urlHost(host) });
5877
+ });
5878
+ request.on("timeout", () => {
5879
+ request.destroy();
5880
+ resolve({ listening: false, status: null, reason: `no answer within ${timeoutMs / 1e3} s`, host: null, code: "TIMEOUT" });
5881
+ });
5882
+ request.on("error", (error) => {
5883
+ const code = error.code;
5884
+ resolve({ listening: false, status: null, reason: code ?? error.message, host: null, code });
5885
+ });
5886
+ });
5887
+ }
5888
+ async function probeLocalSite(port, sitePath, timeoutMs = PROBE_TIMEOUT_MS, hosts = LOOPBACK_HOSTS2) {
5889
+ const tried = [];
5890
+ let last = { listening: false, status: null, reason: "no answer", host: null };
5891
+ for (const host of hosts) {
5892
+ const { code, ...result } = await probeHost(host, port, sitePath, timeoutMs);
5893
+ if (result.status !== null) return result;
5894
+ tried.push(`${urlHost(host)}: ${result.reason}`);
5895
+ last = { ...result, reason: tried.join(", ") };
5896
+ if (code === "TIMEOUT") return { ...last, timedOut: true };
5897
+ }
5898
+ return last;
5899
+ }
5900
+
5901
+ // src/preview.ts
5902
+ var PREVIEW_EXIT_UNAVAILABLE = 5;
5903
+ var PREVIEWS_UNAVAILABLE_MESSAGE = "Previews are not available on this server";
5904
+ var RATHOLE_LOCAL_HOSTS = ["127.0.0.1"];
5905
+ var MAX_TITLE_LENGTH = 120;
5906
+ var MAX_PATH_LENGTH = 512;
5907
+ var STATE_LOCK_WAIT_MS = 12e4;
5908
+ var STATE_LOCK_STALE_MS = 10 * 6e4;
5909
+ function instafyDir() {
5910
+ return path9.join(os5.homedir(), ".instafy");
5911
+ }
5912
+ function previewStateFile() {
5913
+ return path9.join(instafyDir(), "cli-preview-state.json");
5914
+ }
5915
+ function previewStateLockFile() {
5916
+ return path9.join(instafyDir(), "cli-preview-state.lock");
5917
+ }
5918
+ function workdirsRoot() {
5919
+ return path9.join(instafyDir(), "cli-preview-workdirs");
5920
+ }
5921
+ function workdirFor(previewId) {
5922
+ return path9.join(workdirsRoot(), safePathSegment2(previewId));
5923
+ }
5924
+ var HOST_SUFFIX_RE = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*(?::\d{1,5})?$/;
5925
+ function parseViewerHostSuffix(raw) {
5926
+ if (typeof raw !== "string") return void 0;
5927
+ const suffix = raw.trim().toLowerCase();
5928
+ return suffix.length <= 253 && HOST_SUFFIX_RE.test(suffix) ? suffix : void 0;
5929
+ }
5930
+ function parseStudioOrigin(raw) {
5931
+ if (typeof raw !== "string" || raw.length > 300) return void 0;
5932
+ let url;
5933
+ try {
5934
+ url = new URL(raw.trim());
5935
+ } catch {
5936
+ return void 0;
5937
+ }
5938
+ if (url.protocol !== "https:" && url.protocol !== "http:") return void 0;
5939
+ if (url.username || url.password || url.search || url.hash || url.pathname !== "/" && url.pathname !== "") return void 0;
5940
+ return url.origin;
5941
+ }
5942
+ var CONTROL_CHARS_RE = /[\u0000-\u001f\u007f-\u009f]/;
5943
+ function parsePreviewTitle(raw) {
5944
+ const title = (raw ?? "").replace(/\s+/g, " ").trim();
5945
+ if (!title) {
5946
+ throw new Error('A title is required: --title "<name>".');
5947
+ }
5948
+ if ([...title].length > MAX_TITLE_LENGTH) {
5949
+ throw new Error(`Title must be at most ${MAX_TITLE_LENGTH} characters.`);
5950
+ }
5951
+ if (CONTROL_CHARS_RE.test(title)) {
5952
+ throw new Error("Title must not contain control characters.");
5953
+ }
5954
+ return title;
5955
+ }
5956
+ function parsePreviewPath(raw) {
5957
+ const value = raw?.trim() || "/";
5958
+ let printable = true;
5959
+ for (let index = 0; index < value.length; index += 1) {
5960
+ const code = value.charCodeAt(index);
5961
+ if (code < 33 || code > 126 || code === 92 || code === 35) {
5962
+ printable = false;
5963
+ break;
5964
+ }
5965
+ }
5966
+ if (!value.startsWith("/") || value.startsWith("//") || value.length > MAX_PATH_LENGTH || !printable) {
5967
+ throw new Error(
5968
+ `Path must start with / (not //) and be at most ${MAX_PATH_LENGTH} printable ASCII characters with no spaces, backslashes or #.`
5969
+ );
5970
+ }
5971
+ return value;
5972
+ }
5973
+ function readStateFile2() {
5974
+ try {
5975
+ const parsed = JSON.parse(fs8.readFileSync(previewStateFile(), "utf8"));
5976
+ if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.previews)) {
5977
+ return { version: 1, previews: [] };
5978
+ }
5979
+ return { version: 1, previews: parsed.previews.filter(Boolean) };
5980
+ } catch {
5981
+ return { version: 1, previews: [] };
5982
+ }
5983
+ }
5984
+ function writeStateFile2(state) {
5985
+ fs8.mkdirSync(instafyDir(), { recursive: true });
5986
+ fs8.writeFileSync(previewStateFile(), JSON.stringify(state, null, 2), { encoding: "utf8", mode: 384 });
5987
+ }
5988
+ function upsertPreviewState(entry) {
5989
+ const state = readStateFile2();
5990
+ const next = state.previews.filter((preview) => preview.previewId !== entry.previewId);
5991
+ next.unshift(entry);
5992
+ writeStateFile2({ version: 1, previews: next });
5993
+ }
5994
+ function removePreviewState(previewId) {
5995
+ const state = readStateFile2();
5996
+ const existing = state.previews.find((preview) => preview.previewId === previewId) ?? null;
5997
+ writeStateFile2({ version: 1, previews: state.previews.filter((preview) => preview.previewId !== previewId) });
5998
+ return existing;
5999
+ }
6000
+ function removeWorkdir(previewId) {
6001
+ fs8.rmSync(workdirFor(previewId), { recursive: true, force: true });
6002
+ }
6003
+ function forgetPreview(previewId) {
6004
+ const entry = removePreviewState(previewId);
6005
+ removeWorkdir(previewId);
6006
+ return entry;
6007
+ }
6008
+ function sleep(ms) {
6009
+ return new Promise((resolve) => setTimeout(resolve, ms));
6010
+ }
6011
+ function lockHolderIsGone(lockFile) {
6012
+ try {
6013
+ const stat = fs8.statSync(lockFile);
6014
+ if (Date.now() - stat.mtimeMs > STATE_LOCK_STALE_MS) return true;
6015
+ const pid = Number.parseInt(fs8.readFileSync(lockFile, "utf8").trim(), 10);
6016
+ if (!Number.isInteger(pid) || pid <= 0) return Date.now() - stat.mtimeMs > 5e3;
6017
+ try {
6018
+ process.kill(pid, 0);
6019
+ return false;
6020
+ } catch (error) {
6021
+ return error.code === "ESRCH";
6022
+ }
6023
+ } catch {
6024
+ return false;
6025
+ }
6026
+ }
6027
+ async function withStateLock(fn) {
6028
+ const lockFile = previewStateLockFile();
6029
+ fs8.mkdirSync(instafyDir(), { recursive: true });
6030
+ const deadline = Date.now() + STATE_LOCK_WAIT_MS;
6031
+ for (; ; ) {
6032
+ try {
6033
+ const fd = fs8.openSync(lockFile, "wx", 384);
6034
+ fs8.writeSync(fd, String(process.pid));
6035
+ fs8.closeSync(fd);
6036
+ break;
6037
+ } catch (error) {
6038
+ if (error.code !== "EEXIST") throw error;
6039
+ if (lockHolderIsGone(lockFile)) {
6040
+ fs8.rmSync(lockFile, { force: true });
6041
+ continue;
6042
+ }
6043
+ if (Date.now() > deadline) {
6044
+ throw new Error("Another instafy preview command on this machine is still running. Retry when it is done.");
6045
+ }
6046
+ await sleep(100);
6047
+ }
6048
+ }
6049
+ try {
6050
+ return await fn();
6051
+ } finally {
6052
+ fs8.rmSync(lockFile, { force: true });
6053
+ }
6054
+ }
6055
+ function configPathFor(workdir) {
6056
+ return path9.join(workdir, "rathole.toml");
6057
+ }
6058
+ async function stopClient(entry) {
6059
+ await stopRatholeClient(entry.pid, configPathFor(entry.workdir), entry.binary);
6060
+ }
6061
+ function shellQuote2(value) {
6062
+ return `'${value.split("'").join(`'\\''`)}'`;
6063
+ }
6064
+ function safePathSegment2(value) {
6065
+ return value.trim().replace(/[^a-zA-Z0-9._-]/g, "_") || "preview";
6066
+ }
6067
+ function stringField(record2, ...keys) {
6068
+ for (const key of keys) {
6069
+ const value = record2[key];
6070
+ if (typeof value === "string" && value.trim()) return value;
6071
+ }
6072
+ return null;
6073
+ }
6074
+ function numberField(record2, ...keys) {
6075
+ for (const key of keys) {
6076
+ const value = record2[key];
6077
+ if (typeof value === "number" && Number.isFinite(value)) return value;
6078
+ }
6079
+ return null;
6080
+ }
6081
+ function previewsUnavailable() {
6082
+ return new PreviewCommandError(`${PREVIEWS_UNAVAILABLE_MESSAGE}.`, PREVIEW_EXIT_UNAVAILABLE);
6083
+ }
6084
+ async function controllerRequest(params) {
6085
+ const { response } = await fetchWithControllerAuth({
6086
+ url: `${cleanUrl(params.controllerUrl)}${params.route}`,
6087
+ init: {
6088
+ method: params.method,
6089
+ headers: {
6090
+ accept: "application/json",
6091
+ ...params.body === void 0 ? {} : { "content-type": "application/json" }
6092
+ },
6093
+ body: params.body === void 0 ? void 0 : JSON.stringify(params.body)
6094
+ },
6095
+ accessToken: params.auth.token,
6096
+ tokenSource: params.auth.source,
6097
+ profile: params.auth.profile,
6098
+ cwd: process.cwd()
6099
+ });
6100
+ const text2 = await response.text().catch(() => "");
6101
+ if (!response.ok) {
6102
+ const code = errorCode(text2);
6103
+ if (response.status === 503 && code === "previews_unavailable" || response.status === 404 && params.notFoundMeansUnavailable && !isStructuredError(text2)) {
6104
+ throw previewsUnavailable();
6105
+ }
6106
+ if (response.status === 401 || response.status === 403) {
6107
+ throw formatAuthRejectedError({
6108
+ status: response.status,
6109
+ responseBody: text2,
6110
+ retryCommand: params.retryCommand
6111
+ });
6112
+ }
6113
+ if (response.status === 400 && code === "port_not_allowed") {
6114
+ throw new PreviewCommandError(
6115
+ `The server does not allow previews on this port: ${extractControllerErrorMessage(text2) ?? "port_not_allowed"}`,
6116
+ PREVIEW_EXIT_PORT_NOT_ALLOWED
6117
+ );
6118
+ }
6119
+ const message = extractControllerErrorMessage(text2) ?? response.statusText;
6120
+ throw new Error(`Preview request failed (${response.status}): ${message}`);
6121
+ }
6122
+ if (!text2.trim()) return { status: response.status, body: null };
6123
+ try {
6124
+ return { status: response.status, body: JSON.parse(text2) };
6125
+ } catch {
6126
+ throw new Error(`Preview request returned invalid JSON (${response.status}).`);
6127
+ }
6128
+ }
6129
+ function isStructuredError(body) {
6130
+ try {
6131
+ const parsed = JSON.parse(body);
6132
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
6133
+ const record2 = parsed;
6134
+ return ["message", "code", "error"].some((key) => typeof record2[key] === "string" && String(record2[key]).trim() !== "");
6135
+ } catch {
6136
+ return false;
6137
+ }
6138
+ }
6139
+ function errorCode(body) {
6140
+ try {
6141
+ const parsed = JSON.parse(body);
6142
+ for (const key of ["code", "error"]) {
6143
+ const value = parsed?.[key];
6144
+ if (typeof value === "string" && /^[a-z0-9_]{1,64}$/.test(value)) return value;
6145
+ }
6146
+ } catch {
6147
+ }
6148
+ return null;
6149
+ }
6150
+ function resolveAuth(opts, retryCommand) {
6151
+ const auth = resolveControllerToken({ controllerToken: opts.controllerToken }, retryCommand);
6152
+ const controllerUrl = resolveTunnelControllerUrl({ controllerUrl: opts.controllerUrl }, auth);
6153
+ return { auth, controllerUrl };
6154
+ }
6155
+ function previewsRoute(projectId2, previewId, action) {
6156
+ const base = `/projects/${encodeURIComponent(projectId2)}/previews`;
6157
+ if (!previewId) return base;
6158
+ return `${base}/${encodeURIComponent(previewId)}${action ? `/${action}` : ""}`;
6159
+ }
6160
+ async function closePreviewOnServer(controllerUrl, auth, projectId2, previewId) {
6161
+ await controllerRequest({
6162
+ controllerUrl,
6163
+ auth,
6164
+ method: "POST",
6165
+ route: previewsRoute(projectId2, previewId, "close"),
6166
+ body: {},
6167
+ retryCommand: `instafy preview close ${previewId}`
6168
+ });
6169
+ }
6170
+ async function resolveRathole(ratholeBin, log) {
6171
+ if (ratholeBin) {
6172
+ process.env.RATHOLE_BIN = ratholeBin;
6173
+ }
6174
+ const rathole = await resolveRatholeBinaryForCli({
6175
+ env: process.env,
6176
+ version: process.env.RATHOLE_VERSION ?? null,
6177
+ cacheDir: process.env.RATHOLE_CACHE_DIR ?? null,
6178
+ logger: (message) => log(`[rathole] ${message}`),
6179
+ warn: (message) => console.error(kleur8.yellow(message))
6180
+ });
6181
+ if (!rathole) {
6182
+ throw new Error("rathole is required to open a preview. Set RATHOLE_BIN or ensure it is on PATH.");
6183
+ }
6184
+ return rathole;
6185
+ }
6186
+ function readFileOrNull(file) {
6187
+ try {
6188
+ return fs8.readFileSync(file, "utf8");
6189
+ } catch {
6190
+ return null;
6191
+ }
6192
+ }
6193
+ function readyHealth(readyBody) {
6194
+ return readyBody["health"] && typeof readyBody["health"] === "object" && !Array.isArray(readyBody["health"]) ? readyBody["health"] : null;
6195
+ }
6196
+ function previewStatusFromHealth(readyBody) {
6197
+ const health = readyHealth(readyBody);
6198
+ if (health?.["listening"] === true) return "live";
6199
+ if (health?.["listening"] === false) return "not_responding";
6200
+ return "unknown";
6201
+ }
6202
+ function previewHttpStatusFromHealth(readyBody) {
6203
+ const status = readyHealth(readyBody)?.["status"];
6204
+ return typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599 ? status : null;
6205
+ }
6206
+ async function notListeningError(port, sitePath, probe) {
6207
+ const where = `http://127.0.0.1:${port}${sitePath}`;
6208
+ if (probe.status !== null) {
6209
+ return new PreviewCommandError(
6210
+ `The site at ${where} answered HTTP ${probe.status}. Fix the server error, then retry.`,
6211
+ PREVIEW_EXIT_NOT_LISTENING
6212
+ );
6213
+ }
6214
+ if (probe.timedOut) {
6215
+ return new PreviewCommandError(noAnswerMessage(port, probe, where, "retry"), PREVIEW_EXIT_NOT_LISTENING);
6216
+ }
6217
+ const ipv6 = await probeLocalSite(port, sitePath, void 0, ["::1"]);
6218
+ if (ipv6.status !== null) {
6219
+ return new PreviewCommandError(
6220
+ `The site answers only on [::1]:${port}, and the preview reaches it on 127.0.0.1. It is running: do not start it again. Bind it to 127.0.0.1 or 0.0.0.0 instead (for example --host 127.0.0.1), restart it, then retry.`,
6221
+ PREVIEW_EXIT_NOT_LISTENING
6222
+ );
6223
+ }
6224
+ return new PreviewCommandError(noAnswerMessage(port, probe, where, "retry"), PREVIEW_EXIT_NOT_LISTENING);
6225
+ }
6226
+ var SERVE_MAX_ARGS = 64;
6227
+ var SERVE_MAX_ARG_LENGTH = 2048;
6228
+ var EPHEMERAL_PREFIXES = ["/tmp/", "/var/tmp/", "/dev/shm/", "/run/"];
6229
+ function parseServeCommand(raw) {
6230
+ const argv = [];
6231
+ let current = "";
6232
+ let inWord = false;
6233
+ let quote = null;
6234
+ const text2 = String(raw ?? "");
6235
+ for (let index = 0; index < text2.length; index += 1) {
6236
+ const ch = text2[index];
6237
+ if (quote === "'") {
6238
+ if (ch === "'") quote = null;
6239
+ else current += ch;
6240
+ continue;
6241
+ }
6242
+ if (quote === '"') {
6243
+ if (ch === '"') {
6244
+ quote = null;
6245
+ } else if (ch === "\\" && index + 1 < text2.length && '"\\$`'.includes(text2[index + 1])) {
6246
+ current += text2[index + 1];
6247
+ index += 1;
6248
+ } else if (ch === "$" || ch === "`") {
6249
+ throw new Error(`--serve-cmd takes one command and its arguments, not shell syntax (found ${ch} in quotes).`);
6250
+ } else {
6251
+ current += ch;
6252
+ }
6253
+ continue;
6254
+ }
6255
+ if (ch === "'" || ch === '"') {
6256
+ quote = ch;
6257
+ inWord = true;
6258
+ continue;
6259
+ }
6260
+ if (ch === "\\") {
6261
+ if (index + 1 >= text2.length) throw new Error("--serve-cmd ends with a lone backslash.");
6262
+ current += text2[index + 1];
6263
+ index += 1;
6264
+ inWord = true;
6265
+ continue;
6266
+ }
6267
+ if (/\s/.test(ch)) {
6268
+ if (ch === "\n" || ch === "\r") {
6269
+ throw new Error("--serve-cmd takes one command on one line.");
6270
+ }
6271
+ if (inWord) {
6272
+ argv.push(current);
6273
+ current = "";
6274
+ inWord = false;
6275
+ }
6276
+ continue;
6277
+ }
6278
+ if ("|&;<>()$`".includes(ch)) {
6279
+ throw new Error(
6280
+ `--serve-cmd takes one command and its arguments, not shell syntax (found ${ch}). Put a pipeline or several commands in a script and pass the script.`
6281
+ );
6282
+ }
6283
+ current += ch;
6284
+ inWord = true;
6285
+ }
6286
+ if (quote) throw new Error("--serve-cmd has an unterminated quote.");
6287
+ if (inWord) argv.push(current);
6288
+ if (argv.length === 0) throw new Error("--serve-cmd is empty.");
6289
+ if (argv.length > SERVE_MAX_ARGS || argv.some((arg) => arg.length > SERVE_MAX_ARG_LENGTH || arg.includes("\0"))) {
6290
+ throw new Error(`--serve-cmd must have at most ${SERVE_MAX_ARGS} arguments of at most ${SERVE_MAX_ARG_LENGTH} characters.`);
6291
+ }
6292
+ return argv;
6293
+ }
6294
+ function realDir(dir) {
6295
+ try {
6296
+ const resolved = fs8.realpathSync(dir);
6297
+ return fs8.statSync(resolved).isDirectory() ? resolved : null;
6298
+ } catch {
6299
+ return null;
6300
+ }
6301
+ }
6302
+ function resolveWorkspaceRoot(env, projectId2) {
6303
+ const explicit = env["WORKSPACE_PROJECT_DIR"]?.trim();
6304
+ if (explicit) return realDir(explicit);
6305
+ const base = env["WORKSPACE_DIR"]?.trim();
6306
+ if (!base || !projectId2) return null;
6307
+ return realDir(path9.join(base, projectId2));
6308
+ }
6309
+ function workspaceRelativeDir(root, dir) {
6310
+ const resolved = realDir(dir);
6311
+ if (!resolved) return null;
6312
+ const relative = path9.relative(root, resolved);
6313
+ if (relative === "") return ".";
6314
+ if (relative.startsWith("..") || path9.isAbsolute(relative)) return null;
6315
+ return relative.split(path9.sep).join("/");
6316
+ }
6317
+ function listeningSocketInodes(procRoot, port) {
6318
+ const inodes = /* @__PURE__ */ new Set();
6319
+ for (const table of ["net/tcp", "net/tcp6"]) {
6320
+ let text2 = "";
6321
+ try {
6322
+ text2 = fs8.readFileSync(path9.join(procRoot, table), "utf8");
6323
+ } catch {
6324
+ continue;
6325
+ }
6326
+ for (const line of text2.split("\n").slice(1)) {
6327
+ const fields = line.trim().split(/\s+/);
6328
+ if (fields.length < 10) continue;
6329
+ const local = fields[1] ?? "";
6330
+ const portHex = local.slice(local.lastIndexOf(":") + 1);
6331
+ if (fields[3] === "0A" && Number.parseInt(portHex, 16) === port && fields[9] !== "0") {
6332
+ inodes.add(fields[9]);
6333
+ }
6334
+ }
6335
+ }
6336
+ return inodes;
6337
+ }
6338
+ function detectListeningProcess(port, procRoot = "/proc") {
6339
+ const inodes = listeningSocketInodes(procRoot, port);
6340
+ if (inodes.size === 0) return null;
6341
+ let pids = [];
6342
+ try {
6343
+ pids = fs8.readdirSync(procRoot).filter((name) => /^\d+$/.test(name)).map(Number).sort((a, b) => a - b);
6344
+ } catch {
6345
+ return null;
6346
+ }
6347
+ for (const pid of pids) {
6348
+ let fds = [];
6349
+ try {
6350
+ fds = fs8.readdirSync(path9.join(procRoot, String(pid), "fd"));
6351
+ } catch {
6352
+ continue;
6353
+ }
6354
+ const holds = fds.some((fd) => {
6355
+ try {
6356
+ const target = fs8.readlinkSync(path9.join(procRoot, String(pid), "fd", fd));
6357
+ const match = /^socket:\[(\d+)\]$/.exec(target);
6358
+ return Boolean(match && inodes.has(match[1]));
6359
+ } catch {
6360
+ return false;
6361
+ }
6362
+ });
6363
+ if (!holds) continue;
6364
+ try {
6365
+ const argv = fs8.readFileSync(path9.join(procRoot, String(pid), "cmdline"), "utf8").split("\0");
6366
+ while (argv.length > 0 && argv[argv.length - 1] === "") argv.pop();
6367
+ const cwd = fs8.readlinkSync(path9.join(procRoot, String(pid), "cwd"));
6368
+ if (argv.length === 0) continue;
6369
+ return { pid, argv, cwd };
6370
+ } catch {
6371
+ continue;
6372
+ }
6373
+ }
6374
+ return null;
6375
+ }
6376
+ var SECRET_FLAG = /^-{1,2}(?!-|no-)[\w.-]*(token|secret|passw(or)?d|passwd|api[-_]?key|access[-_]?key|private[-_]?key|credentials?|auth(?![a-z]))[\w.-]*$/i;
6377
+ var SECRET_ENV = /^(?=[A-Za-z_])\w*(TOKEN|SECRET|PASSWORD|PASSWD|API_?KEY|ACCESS_?KEY|PRIVATE_?KEY|CREDENTIALS?)[\w]*=./i;
6378
+ var SECRET_VALUE = /^(sk|pk|rk)[-_][A-Za-z0-9_-]{12,}|^gh[pousr]_[A-Za-z0-9]{20,}|^xox[abposr]-|^AKIA[0-9A-Z]{12,}|^eyJ[A-Za-z0-9_-]{20,}\./;
6379
+ var URL_USERINFO = /^[a-z][a-z0-9+.-]*:\/\/[^/\s@]*:[^/\s@]*@/i;
6380
+ function secretInServeCommand(argv) {
6381
+ for (let i = 0; i < argv.length; i += 1) {
6382
+ const arg = argv[i];
6383
+ const eq = arg.indexOf("=");
6384
+ const flag = arg.startsWith("-") ? eq > 0 ? arg.slice(0, eq) : arg : null;
6385
+ if (flag && SECRET_FLAG.test(flag)) {
6386
+ const value = eq > 0 ? arg.slice(eq + 1) : argv[i + 1];
6387
+ if (value && !value.startsWith("-")) return flag;
6388
+ }
6389
+ if (SECRET_ENV.test(arg)) return arg.slice(0, arg.indexOf("="));
6390
+ const bare = eq > 0 && arg.startsWith("-") ? arg.slice(eq + 1) : arg;
6391
+ if (URL_USERINFO.test(bare)) return "a URL with a password";
6392
+ if (SECRET_VALUE.test(bare)) return "a key";
6393
+ }
6394
+ return null;
6395
+ }
6396
+ function parseExplicitServeCommand(raw) {
6397
+ const argv = parseServeCommand(raw);
6398
+ const secret = secretInServeCommand(argv);
6399
+ if (secret) {
6400
+ throw new Error(
6401
+ `--serve-cmd carries what looks like a secret (${secret}), which is not recorded. Have the site read it from an env file and pass the command without it.`
6402
+ );
6403
+ }
6404
+ return argv;
6405
+ }
6406
+ function resolvePreviewServe(params) {
6407
+ const env = params.env ?? process.env;
6408
+ const root = resolveWorkspaceRoot(env, params.projectId);
6409
+ const explicit = params.serveCmd?.trim();
6410
+ if (explicit) {
6411
+ const argv = parseExplicitServeCommand(explicit);
6412
+ if (!root) {
6413
+ throw new Error("--serve-cmd needs the space's workspace (WORKSPACE_DIR); run it inside the space.");
6414
+ }
6415
+ const cwd2 = workspaceRelativeDir(root, params.cwd ?? process.cwd());
6416
+ if (!cwd2) {
6417
+ throw new Error(`--serve-cmd runs from the current directory, which must be inside the space's workspace (${root}).`);
6418
+ }
6419
+ return { serve: { argv, cwd: cwd2 } };
6420
+ }
6421
+ const notRestartable = (note) => ({ serve: null, note });
6422
+ const platform = params.platform ?? process.platform;
6423
+ if (platform !== "linux" && !params.procRoot) {
6424
+ return notRestartable("the start command can only be read on a Linux space; pass --serve-cmd");
6425
+ }
6426
+ if (!root) return notRestartable("this is not running inside a space's workspace");
6427
+ const found = detectListeningProcess(params.port, params.procRoot);
6428
+ if (!found) return notRestartable(`could not see which process listens on port ${params.port}; pass --serve-cmd`);
6429
+ const cwd = workspaceRelativeDir(root, found.cwd);
6430
+ if (!cwd) {
6431
+ return notRestartable(`the site runs from ${found.cwd}, outside the workspace, which does not survive a sleep`);
6432
+ }
6433
+ const ephemeral = found.argv.find((arg) => EPHEMERAL_PREFIXES.some((prefix) => arg.startsWith(prefix)));
6434
+ if (ephemeral) {
6435
+ return notRestartable(`its command uses ${ephemeral}, which does not survive a sleep; pass --serve-cmd`);
6436
+ }
6437
+ if (found.argv.length > SERVE_MAX_ARGS || found.argv.some((arg) => arg.length > SERVE_MAX_ARG_LENGTH)) {
6438
+ return notRestartable("its command line is too long to record; pass --serve-cmd");
6439
+ }
6440
+ const secret = secretInServeCommand(found.argv);
6441
+ if (secret) {
6442
+ return notRestartable(
6443
+ `its command line carries what looks like a secret (${secret}), which is not recorded; read it from an env file and pass --serve-cmd`
6444
+ );
6445
+ }
6446
+ return { serve: { argv: found.argv, cwd } };
6447
+ }
6448
+ async function createAndStartClient(params) {
6449
+ const { opts, port, title, sitePath, projectId: projectId2, retryCommand, auth, controllerUrl, serverUrl, log, serve } = params;
6450
+ const conversationId = opts.conversationId?.trim() || void 0;
6451
+ const created = await controllerRequest({
6452
+ controllerUrl,
6453
+ auth,
6454
+ method: "POST",
6455
+ route: previewsRoute(projectId2),
6456
+ body: {
6457
+ runtimeId: readEnv("RUNTIME_ID"),
6458
+ runtimeLeaseId: readEnv("RUNTIME_LEASE_ID"),
6459
+ conversationId,
6460
+ port,
6461
+ path: sitePath,
6462
+ title,
6463
+ metadata: { source: "instafy-cli" },
6464
+ ...serve ? { serve } : {}
6465
+ },
6466
+ retryCommand,
6467
+ notFoundMeansUnavailable: true
6468
+ });
6469
+ const grant = created.body && !Array.isArray(created.body) ? created.body : {};
6470
+ const previewId = stringField(grant, "previewId");
6471
+ if (!previewId) {
6472
+ throw new Error("Preview response is missing previewId.");
6473
+ }
6474
+ const workdir = workdirFor(previewId);
6475
+ const logFile = path9.join(instafyDir(), "cli-preview-logs", `preview-${safePathSegment2(previewId)}.log`);
6476
+ const configPath2 = configPathFor(workdir);
6477
+ const hostSuffix = parseViewerHostSuffix(grant["viewerHostSuffix"]);
6478
+ const studioOrigin = parseStudioOrigin(grant["studioOrigin"]);
6479
+ const state = readStateFile2();
6480
+ const current = state.previews.find((preview) => preview.previewId === previewId) ?? null;
6481
+ let pid = null;
6482
+ let binary = current?.binary;
6483
+ try {
6484
+ const credentials = grant["credentials"] && typeof grant["credentials"] === "object" ? grant["credentials"] : {};
6485
+ const configBody = buildRatholeConfig(credentials, port);
6486
+ fs8.mkdirSync(workdir, { recursive: true });
6487
+ fs8.mkdirSync(path9.dirname(logFile), { recursive: true });
6488
+ if (current && readFileOrNull(configPath2) === configBody && isRatholeClientFor(current.pid, configPath2, current.binary)) {
6489
+ pid = current.pid;
6490
+ }
6491
+ for (const stale of state.previews) {
6492
+ if (stale.previewId === previewId) {
6493
+ if (pid === null) await stopClient(stale);
6494
+ continue;
6495
+ }
6496
+ if (stale.localPort !== port) continue;
6497
+ await stopClient(stale);
6498
+ forgetPreview(stale.previewId);
6499
+ if (cleanUrl(stale.controllerUrl) === serverUrl) {
6500
+ await closePreviewOnServer(controllerUrl, auth, stale.projectId, stale.previewId).catch(() => {
6501
+ });
6502
+ }
6503
+ }
6504
+ if (pid === null) {
6505
+ const rathole = await resolveRathole(opts.ratholeBin, log);
6506
+ fs8.writeFileSync(configPath2, configBody, { encoding: "utf8", mode: 384 });
6507
+ binary = rathole;
6508
+ const started = await spawnRatholeDetached({ rathole, configPath: configPath2, workdir, logFile });
6509
+ if (started.exit !== null) {
6510
+ const suffix = started.logTail ? `
6511
+
6512
+ Last logs:
6513
+ ${started.logTail}` : "";
6514
+ throw new Error(`Preview tunnel exited (${started.exit}).${suffix}`);
6515
+ }
6516
+ pid = started.pid;
6517
+ } else {
6518
+ log(`Reusing the running preview client (pid ${pid}).`);
6519
+ }
6520
+ } catch (error) {
6521
+ if (pid !== null) await stopRatholeClient(pid, configPath2, binary);
6522
+ forgetPreview(previewId);
6523
+ await closePreviewOnServer(controllerUrl, auth, projectId2, previewId).catch(() => {
6524
+ });
6525
+ const message = error instanceof Error ? error.message : String(error);
6526
+ const [first, ...rest] = message.split("\n");
6527
+ const closed = [`${first} The preview was closed.`, ...rest].join("\n");
6528
+ if (error instanceof PreviewCommandError) throw new PreviewCommandError(closed, error.exitCode);
6529
+ throw new Error(closed);
6530
+ }
6531
+ const entry = {
6532
+ previewId,
6533
+ projectId: projectId2,
6534
+ title,
6535
+ localPort: port,
6536
+ path: sitePath,
6537
+ controllerUrl: serverUrl,
6538
+ pid,
6539
+ logFile,
6540
+ workdir,
6541
+ startedAt: current?.pid === pid ? current.startedAt : (/* @__PURE__ */ new Date()).toISOString(),
6542
+ ...binary ? { binary } : {},
6543
+ ...hostSuffix ? { hostSuffix } : {},
6544
+ ...studioOrigin ? { studioOrigin } : {}
6545
+ };
6546
+ upsertPreviewState(entry);
6547
+ return { grant, previewId, entry, current };
6548
+ }
6549
+ async function previewOpen(opts) {
6550
+ const port = parsePreviewPort(opts.port);
6551
+ const title = parsePreviewTitle(opts.title);
6552
+ const sitePath = parsePreviewPath(opts.path);
6553
+ if (opts.serveCmd?.trim()) parseExplicitServeCommand(opts.serveCmd);
6554
+ const projectId2 = resolveProject({ project: opts.project });
6555
+ const retryCommand = `instafy preview open --port ${port} --title ${shellQuote2(title)}`;
6556
+ const { auth, controllerUrl } = resolveAuth(opts, retryCommand);
6557
+ const serverUrl = cleanUrl(controllerUrl);
6558
+ const log = (message) => console.error(kleur8.cyan(message));
6559
+ const probe = await probeLocalSite(port, sitePath, void 0, RATHOLE_LOCAL_HOSTS);
6560
+ if (!probe.listening) {
6561
+ throw await notListeningError(port, sitePath, probe);
6562
+ }
6563
+ const { serve, note: restartNote } = resolvePreviewServe({ port, serveCmd: opts.serveCmd, projectId: projectId2, cwd: opts.cwd });
6564
+ const { grant, previewId, entry } = await withStateLock(
6565
+ () => createAndStartClient({ opts, port, title, sitePath, projectId: projectId2, retryCommand, auth, controllerUrl, serverUrl, log, serve })
6566
+ );
6567
+ let ready;
6568
+ try {
6569
+ ready = await controllerRequest({
6570
+ controllerUrl,
6571
+ auth,
6572
+ method: "POST",
6573
+ route: previewsRoute(projectId2, previewId, "ready"),
6574
+ body: {},
6575
+ retryCommand
6576
+ });
6577
+ } catch (error) {
6578
+ if (error instanceof PreviewCommandError && error.exitCode === PREVIEW_EXIT_UNAVAILABLE) {
6579
+ await withStateLock(async () => {
6580
+ await stopClient(entry);
6581
+ forgetPreview(previewId);
6582
+ });
6583
+ }
6584
+ throw error;
6585
+ }
6586
+ const readyBody = ready.body && !Array.isArray(ready.body) ? ready.body : {};
6587
+ const restartable = serve !== null && grant["restartable"] === true;
6588
+ return {
6589
+ previewId,
6590
+ title,
6591
+ port,
6592
+ path: sitePath,
6593
+ status: previewStatusFromHealth(readyBody),
6594
+ httpStatus: previewHttpStatusFromHealth(readyBody),
6595
+ messageId: stringField(readyBody, "messageId"),
6596
+ viewerHostSuffix: parseViewerHostSuffix(grant["viewerHostSuffix"]) ?? null,
6597
+ restartable,
6598
+ ...restartable ? {} : { restartNote: restartNote ?? "the server did not record a start command" }
6599
+ };
6600
+ }
6601
+ function toPreviewSummary(raw) {
6602
+ if (!raw || typeof raw !== "object") return null;
6603
+ const record2 = raw;
6604
+ const previewId = stringField(record2, "previewId", "id");
6605
+ if (!previewId) return null;
6606
+ return {
6607
+ previewId,
6608
+ title: stringField(record2, "title") ?? "",
6609
+ port: numberField(record2, "port", "localPort") ?? 0,
6610
+ path: stringField(record2, "path") ?? "/",
6611
+ status: stringField(record2, "status") ?? "unknown"
6612
+ };
6613
+ }
6614
+ async function previewList(opts) {
6615
+ const projectId2 = resolveProject({ project: opts.project });
6616
+ const { auth, controllerUrl } = resolveAuth(opts, "instafy preview list");
6617
+ const listed = await controllerRequest({
6618
+ controllerUrl,
6619
+ auth,
6620
+ method: "GET",
6621
+ route: previewsRoute(projectId2),
6622
+ retryCommand: "instafy preview list",
6623
+ notFoundMeansUnavailable: true
6624
+ });
6625
+ const rows = Array.isArray(listed.body) ? listed.body : listed.body && Array.isArray(listed.body["previews"]) ? listed.body["previews"] : [];
6626
+ return rows.map(toPreviewSummary).filter((row) => row !== null);
6627
+ }
6628
+ async function previewClose(opts) {
6629
+ const previewId = opts.previewId?.trim() ?? "";
6630
+ if (!previewId) {
6631
+ throw new Error("Preview id is required. Use `instafy preview list` to find it.");
6632
+ }
6633
+ const entry = await withStateLock(async () => {
6634
+ const removed = removePreviewState(previewId);
6635
+ if (removed) await stopClient(removed);
6636
+ return removed;
6637
+ });
6638
+ const projectId2 = entry?.projectId ?? resolveProject({ project: opts.project });
6639
+ try {
6640
+ const { auth, controllerUrl } = resolveAuth(
6641
+ { ...opts, controllerUrl: opts.controllerUrl ?? entry?.controllerUrl },
6642
+ `instafy preview close ${previewId}`
6643
+ );
6644
+ await closePreviewOnServer(controllerUrl, auth, projectId2, previewId);
6645
+ } catch (error) {
6646
+ if (entry) await withStateLock(async () => upsertPreviewState({ ...entry, pid: -1 }));
6647
+ throw error;
6648
+ }
6649
+ removeWorkdir(previewId);
6650
+ return { ok: true, previewId };
6651
+ }
6652
+ function printPreviewOpen(result, json) {
6653
+ if (json) {
6654
+ console.log(JSON.stringify(result, null, 2));
6655
+ return;
6656
+ }
6657
+ const label = result.status === "live" ? kleur8.green("Ready to preview") : result.status === "not_responding" ? kleur8.yellow("Not responding yet") : kleur8.yellow("Preview opened (the server did not report whether the site answers)");
6658
+ console.log(`${label}: ${result.title} (port ${result.port}). Open it from the card in the conversation.`);
6659
+ if (result.restartable) {
6660
+ console.log(kleur8.gray("Open starts the site again after the space sleeps."));
6661
+ } else {
6662
+ console.log(kleur8.yellow(`After the space sleeps, Open cannot start the site by itself: ${result.restartNote}.`));
6663
+ }
6664
+ console.log(kleur8.gray(`previewId=${result.previewId}`));
6665
+ if (result.httpStatus !== null && result.httpStatus >= 400) {
6666
+ console.log(kleur8.yellow(`The site answered HTTP ${result.httpStatus} through the preview.`));
6667
+ }
6668
+ if (result.viewerHostSuffix) {
6669
+ console.log(kleur8.gray(`viewerHostSuffix=${result.viewerHostSuffix}`));
6670
+ }
6671
+ console.log("");
6672
+ console.log("Next:");
6673
+ console.log(`- ${kleur8.cyan("instafy preview list")}`);
6674
+ console.log(`- ${kleur8.cyan(`instafy preview close ${result.previewId}`)}`);
6675
+ }
6676
+ function printPreviewList(previews, json) {
6677
+ if (json) {
6678
+ console.log(JSON.stringify(previews, null, 2));
6679
+ return;
6680
+ }
6681
+ if (previews.length === 0) {
6682
+ console.log(kleur8.yellow("No previews found."));
6683
+ return;
6684
+ }
6685
+ for (const preview of previews) {
6686
+ console.log(
6687
+ `${preview.previewId} \xB7 ${preview.title || "(untitled)"} \xB7 port=${preview.port}${preview.path === "/" ? "" : ` \xB7 path=${preview.path}`} \xB7 ${preview.status}`
6688
+ );
6689
+ }
6690
+ }
6691
+
5666
6692
  // src/config-command.ts
5667
6693
  init_config();
5668
6694
  init_config();
5669
- import kleur8 from "kleur";
6695
+ import kleur9 from "kleur";
5670
6696
  function normalizeKey(raw) {
5671
6697
  const key = raw.trim().toLowerCase().replace(/_/g, "-");
5672
6698
  if (key === "controller-url" || key === "controllerurl" || key === "server-url") {
@@ -5708,12 +6734,12 @@ function clearConfig(key) {
5708
6734
  }
5709
6735
  }
5710
6736
  function configPath(options) {
5711
- const path16 = getInstafyConfigPath();
6737
+ const path19 = getInstafyConfigPath();
5712
6738
  if (options?.json) {
5713
- console.log(JSON.stringify({ path: path16 }, null, 2));
6739
+ console.log(JSON.stringify({ path: path19 }, null, 2));
5714
6740
  return;
5715
6741
  }
5716
- console.log(path16);
6742
+ console.log(path19);
5717
6743
  }
5718
6744
  function configList(options) {
5719
6745
  const config = readInstafyCliConfig();
@@ -5730,13 +6756,13 @@ function configList(options) {
5730
6756
  console.log(JSON.stringify(payload, null, 2));
5731
6757
  return;
5732
6758
  }
5733
- console.log(kleur8.green("Instafy CLI config"));
6759
+ console.log(kleur9.green("Instafy CLI config"));
5734
6760
  console.log(`Path: ${payload.path}`);
5735
- console.log(`controller-url: ${payload.controllerUrl ?? kleur8.yellow("(not set)")}`);
6761
+ console.log(`controller-url: ${payload.controllerUrl ?? kleur9.yellow("(not set)")}`);
5736
6762
  console.log(`controller-url (effective): ${payload.effectiveControllerUrl}`);
5737
- console.log(`studio-url: ${payload.studioUrl ?? kleur8.yellow("(not set)")}`);
5738
- console.log(`access-token: ${payload.accessTokenSet ? kleur8.green("(set)") : kleur8.yellow("(not set)")}`);
5739
- console.log(`refresh-token: ${payload.refreshTokenSet ? kleur8.green("(set)") : kleur8.yellow("(not set)")}`);
6763
+ console.log(`studio-url: ${payload.studioUrl ?? kleur9.yellow("(not set)")}`);
6764
+ console.log(`access-token: ${payload.accessTokenSet ? kleur9.green("(set)") : kleur9.yellow("(not set)")}`);
6765
+ console.log(`refresh-token: ${payload.refreshTokenSet ? kleur9.green("(set)") : kleur9.yellow("(not set)")}`);
5740
6766
  if (payload.updatedAt) {
5741
6767
  console.log(`updated-at: ${payload.updatedAt}`);
5742
6768
  }
@@ -5765,7 +6791,7 @@ function configSet(params) {
5765
6791
  console.log(JSON.stringify({ key, value, path: getInstafyConfigPath() }, null, 2));
5766
6792
  return;
5767
6793
  }
5768
- console.log(kleur8.green(`Set ${key}`));
6794
+ console.log(kleur9.green(`Set ${key}`));
5769
6795
  console.log(value);
5770
6796
  }
5771
6797
  function configUnset(params) {
@@ -5775,7 +6801,7 @@ function configUnset(params) {
5775
6801
  console.log(JSON.stringify({ ok: true, key }, null, 2));
5776
6802
  return;
5777
6803
  }
5778
- console.log(kleur8.green(`Unset ${key}`));
6804
+ console.log(kleur9.green(`Unset ${key}`));
5779
6805
  }
5780
6806
 
5781
6807
  // src/index.ts
@@ -5784,27 +6810,27 @@ init_config();
5784
6810
  // src/git-wrapper.ts
5785
6811
  import { spawnSync as spawnSync4 } from "node:child_process";
5786
6812
  import { randomUUID as randomUUID2 } from "node:crypto";
5787
- import fs8 from "node:fs";
5788
- import path9 from "node:path";
5789
- import kleur9 from "kleur";
6813
+ import fs9 from "node:fs";
6814
+ import path10 from "node:path";
6815
+ import kleur10 from "kleur";
5790
6816
  function pathExists(candidate) {
5791
6817
  try {
5792
- fs8.statSync(candidate);
6818
+ fs9.statSync(candidate);
5793
6819
  return true;
5794
6820
  } catch {
5795
6821
  return false;
5796
6822
  }
5797
6823
  }
5798
6824
  function findInstafyGitContext(startDir) {
5799
- let current = path9.resolve(startDir);
5800
- const root = path9.parse(current).root;
6825
+ let current = path10.resolve(startDir);
6826
+ const root = path10.parse(current).root;
5801
6827
  while (true) {
5802
- const gitDir = path9.join(current, ".instafy", ".git");
6828
+ const gitDir = path10.join(current, ".instafy", ".git");
5803
6829
  if (pathExists(gitDir)) {
5804
6830
  return { workTree: current, gitDir };
5805
6831
  }
5806
6832
  if (current === root) break;
5807
- current = path9.dirname(current);
6833
+ current = path10.dirname(current);
5808
6834
  }
5809
6835
  return null;
5810
6836
  }
@@ -5851,9 +6877,9 @@ function validateGitArgs(userArgs) {
5851
6877
  }
5852
6878
  if (arg === "--git-dir" || arg.startsWith("--git-dir=") || arg === "--work-tree" || arg.startsWith("--work-tree=") || arg === "-C") {
5853
6879
  throw new Error(
5854
- `Unsupported argument "${arg}". Use ${kleur9.cyan(
6880
+ `Unsupported argument "${arg}". Use ${kleur10.cyan(
5855
6881
  "cd"
5856
- )} instead; ${kleur9.cyan("instafy git")} manages --git-dir/--work-tree automatically.`
6882
+ )} instead; ${kleur10.cyan("instafy git")} manages --git-dir/--work-tree automatically.`
5857
6883
  );
5858
6884
  }
5859
6885
  if (arg === "-c" || arg === "--config-env" || arg === "--exec-path" || arg === "--namespace") {
@@ -5960,7 +6986,7 @@ function collectDirtyPaths(context, options) {
5960
6986
  return unique;
5961
6987
  }
5962
6988
  function findEmbeddedGitDirs(context, pathSpecs) {
5963
- const instafyGitDir = path9.resolve(context.gitDir);
6989
+ const instafyGitDir = path10.resolve(context.gitDir);
5964
6990
  const seen = /* @__PURE__ */ new Set();
5965
6991
  for (const rawPath of pathSpecs) {
5966
6992
  const normalized = normalizePathSpec(rawPath).replace(/\/+$/, "");
@@ -5969,11 +6995,11 @@ function findEmbeddedGitDirs(context, pathSpecs) {
5969
6995
  }
5970
6996
  let cursor = normalized;
5971
6997
  while (cursor && cursor !== ".") {
5972
- const candidate = path9.join(context.workTree, ...cursor.split("/"), ".git");
5973
- if (pathExists(candidate) && path9.resolve(candidate) !== instafyGitDir) {
5974
- seen.add(path9.resolve(candidate));
6998
+ const candidate = path10.join(context.workTree, ...cursor.split("/"), ".git");
6999
+ if (pathExists(candidate) && path10.resolve(candidate) !== instafyGitDir) {
7000
+ seen.add(path10.resolve(candidate));
5975
7001
  }
5976
- const next = path9.posix.dirname(cursor);
7002
+ const next = path10.posix.dirname(cursor);
5977
7003
  if (next === "." || next === cursor) {
5978
7004
  break;
5979
7005
  }
@@ -5982,8 +7008,8 @@ function findEmbeddedGitDirs(context, pathSpecs) {
5982
7008
  }
5983
7009
  const candidates = Array.from(seen);
5984
7010
  candidates.sort((a, b) => {
5985
- const depthA = a.split(path9.sep).length;
5986
- const depthB = b.split(path9.sep).length;
7011
+ const depthA = a.split(path10.sep).length;
7012
+ const depthB = b.split(path10.sep).length;
5987
7013
  return depthB - depthA;
5988
7014
  });
5989
7015
  return candidates;
@@ -5993,16 +7019,16 @@ function hideEmbeddedGitDirs(context, pathSpecs) {
5993
7019
  if (candidates.length === 0) {
5994
7020
  return [];
5995
7021
  }
5996
- const stagingBase = path9.join(context.workTree, ".instafy", "origin-staging", "embedded-git");
5997
- fs8.mkdirSync(stagingBase, { recursive: true });
7022
+ const stagingBase = path10.join(context.workTree, ".instafy", "origin-staging", "embedded-git");
7023
+ fs9.mkdirSync(stagingBase, { recursive: true });
5998
7024
  const renames = [];
5999
7025
  for (const candidate of candidates) {
6000
- let hidden = path9.join(stagingBase, randomUUID2());
7026
+ let hidden = path10.join(stagingBase, randomUUID2());
6001
7027
  for (let attempt = 0; attempt < 8; attempt += 1) {
6002
7028
  if (!pathExists(hidden)) break;
6003
- hidden = path9.join(stagingBase, randomUUID2());
7029
+ hidden = path10.join(stagingBase, randomUUID2());
6004
7030
  }
6005
- fs8.renameSync(candidate, hidden);
7031
+ fs9.renameSync(candidate, hidden);
6006
7032
  renames.push({ original: candidate, hidden });
6007
7033
  }
6008
7034
  return renames;
@@ -6012,7 +7038,7 @@ function restoreEmbeddedGitDirs(renames) {
6012
7038
  const rename = renames[index];
6013
7039
  if (!rename) continue;
6014
7040
  try {
6015
- fs8.renameSync(rename.hidden, rename.original);
7041
+ fs9.renameSync(rename.hidden, rename.original);
6016
7042
  } catch {
6017
7043
  }
6018
7044
  }
@@ -6082,15 +7108,15 @@ function runInstafyGit(userArgs, options) {
6082
7108
  console.log("instafy git <git-args...>");
6083
7109
  console.log("");
6084
7110
  console.log(
6085
- `Runs ${kleur9.cyan("git")} against the Instafy canonical repo at ${kleur9.cyan(
7111
+ `Runs ${kleur10.cyan("git")} against the Instafy canonical repo at ${kleur10.cyan(
6086
7112
  ".instafy/.git"
6087
7113
  )} (auto-detected by walking up from cwd).`
6088
7114
  );
6089
7115
  console.log("");
6090
7116
  console.log("Example:");
6091
- console.log(` ${kleur9.cyan("instafy git status")}`);
6092
- console.log(` ${kleur9.cyan("instafy git add -A")}`);
6093
- console.log(` ${kleur9.cyan('instafy git commit -am "instafy: checkpoint"')}`);
7117
+ console.log(` ${kleur10.cyan("instafy git status")}`);
7118
+ console.log(` ${kleur10.cyan("instafy git add -A")}`);
7119
+ console.log(` ${kleur10.cyan('instafy git commit -am "instafy: checkpoint"')}`);
6094
7120
  return 0;
6095
7121
  }
6096
7122
  const context = findInstafyGitContext(cwd);
@@ -6100,8 +7126,8 @@ function runInstafyGit(userArgs, options) {
6100
7126
  "No Instafy canonical git checkout found (expected .instafy/.git).",
6101
7127
  "",
6102
7128
  "Tips:",
6103
- `- Run this inside a git-canonical workspace (where ${kleur9.cyan(".instafy/.git")} exists).`,
6104
- `- If you're trying to operate on a user repo, use normal ${kleur9.cyan("git")} (it uses .git).`
7129
+ `- Run this inside a git-canonical workspace (where ${kleur10.cyan(".instafy/.git")} exists).`,
7130
+ `- If you're trying to operate on a user repo, use normal ${kleur10.cyan("git")} (it uses .git).`
6105
7131
  ].join("\n")
6106
7132
  );
6107
7133
  }
@@ -6376,7 +7402,7 @@ async function runInstafyGitSync(args, options) {
6376
7402
  }
6377
7403
  } catch (error) {
6378
7404
  const message = error instanceof Error ? error.message : String(error);
6379
- console.warn(kleur9.yellow(`[instafy git sync] ${message}; falling back to environment token.`));
7405
+ console.warn(kleur10.yellow(`[instafy git sync] ${message}; falling back to environment token.`));
6380
7406
  }
6381
7407
  }
6382
7408
  if (!originToken) {
@@ -6424,7 +7450,7 @@ async function runInstafyGitSync(args, options) {
6424
7450
  } catch (error) {
6425
7451
  const message = error instanceof Error ? error.message : String(error);
6426
7452
  console.warn(
6427
- kleur9.yellow(
7453
+ kleur10.yellow(
6428
7454
  `[instafy git sync] failed to recover from invalid origin token: ${message}`
6429
7455
  )
6430
7456
  );
@@ -6438,18 +7464,18 @@ async function runInstafyGitSync(args, options) {
6438
7464
  console.log(JSON.stringify(saveReportJson(report, notSaved2), null, 2));
6439
7465
  } else {
6440
7466
  for (const sentence of notSaved2) {
6441
- console.error(kleur9.red(`${sentence}.`));
7467
+ console.error(kleur10.red(`${sentence}.`));
6442
7468
  }
6443
7469
  }
6444
7470
  return 1;
6445
7471
  }
6446
7472
  const suffix = text2.trim() ? `: ${text2.trim()}` : "";
6447
- console.error(kleur9.red(`Origin git sync failed (${response.status} ${response.statusText})${suffix}`));
7473
+ console.error(kleur10.red(`Origin git sync failed (${response.status} ${response.statusText})${suffix}`));
6448
7474
  return 1;
6449
7475
  }
6450
7476
  const rev = report.rev ?? "";
6451
7477
  if (!rev) {
6452
- console.error(kleur9.red("Origin git sync response missing rev field."));
7478
+ console.error(kleur10.red("Origin git sync response missing rev field."));
6453
7479
  return 1;
6454
7480
  }
6455
7481
  const notSaved = notSavedSentences(report, { failedWhole: false });
@@ -6458,7 +7484,7 @@ async function runInstafyGitSync(args, options) {
6458
7484
  } else {
6459
7485
  console.log(rev);
6460
7486
  for (const sentence of notSaved) {
6461
- console.error(kleur9.yellow(`${sentence}.`));
7487
+ console.error(kleur10.yellow(`${sentence}.`));
6462
7488
  }
6463
7489
  }
6464
7490
  return notSaved.length > 0 ? 1 : 0;
@@ -6477,21 +7503,21 @@ function parseSaveReport(text2) {
6477
7503
  payload = {};
6478
7504
  }
6479
7505
  const conflictedPaths = Array.isArray(payload["conflictedPaths"]) ? payload["conflictedPaths"].flatMap((entry) => {
6480
- const path16 = optionalString(entry);
6481
- return path16 ? [path16] : [];
7506
+ const path19 = optionalString(entry);
7507
+ return path19 ? [path19] : [];
6482
7508
  }) : [];
6483
7509
  const rejectedPaths = Array.isArray(payload["rejectedPaths"]) ? payload["rejectedPaths"].flatMap((entry) => {
6484
7510
  if (typeof entry === "string") {
6485
- const path17 = optionalString(entry);
6486
- return path17 ? [{ path: path17, reason: "", keptSavedVersion: false }] : [];
7511
+ const path20 = optionalString(entry);
7512
+ return path20 ? [{ path: path20, reason: "", keptSavedVersion: false }] : [];
6487
7513
  }
6488
7514
  if (!entry || typeof entry !== "object") return [];
6489
7515
  const record2 = entry;
6490
- const path16 = optionalString(record2["path"]);
6491
- if (!path16) return [];
7516
+ const path19 = optionalString(record2["path"]);
7517
+ if (!path19) return [];
6492
7518
  return [
6493
7519
  {
6494
- path: path16,
7520
+ path: path19,
6495
7521
  reason: optionalString(record2["reason"]) ?? "",
6496
7522
  keptSavedVersion: record2["keptSavedVersion"] === true
6497
7523
  }
@@ -6571,11 +7597,554 @@ function saveReportJson(report, notSaved) {
6571
7597
  };
6572
7598
  }
6573
7599
 
7600
+ // src/bind.ts
7601
+ import { spawn as spawn4 } from "node:child_process";
7602
+ import fs10 from "node:fs";
7603
+ import path11 from "node:path";
7604
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
7605
+ var BIN_REL = path11.join("bin", "instafy-bind.mjs");
7606
+ function resolveBindBin(env = process.env) {
7607
+ const fromEnv = env.INSTAFY_BIND_BIN?.trim();
7608
+ if (fromEnv) return fs10.existsSync(fromEnv) ? path11.resolve(fromEnv) : null;
7609
+ const here = path11.dirname(fileURLToPath2(import.meta.url));
7610
+ for (const candidate of [path11.join(here, "bind", BIN_REL), path11.resolve(here, "..", "..", "instafy-bind", BIN_REL)]) {
7611
+ if (fs10.existsSync(candidate)) return candidate;
7612
+ }
7613
+ return null;
7614
+ }
7615
+ function runBind(args) {
7616
+ const bin = resolveBindBin();
7617
+ if (!bin) {
7618
+ process.stderr.write(
7619
+ "instafy bind: the bundled binder (dist/bind) was not found; rebuild the CLI, or set INSTAFY_BIND_BIN to packages/instafy-bind/bin/instafy-bind.mjs, or run it directly: node packages/instafy-bind/bin/instafy-bind.mjs <command>\n"
7620
+ );
7621
+ return Promise.resolve(1);
7622
+ }
7623
+ return new Promise((resolve) => {
7624
+ const child = spawn4(process.execPath, [bin, ...args], { stdio: "inherit", env: process.env });
7625
+ const forward = (signal) => {
7626
+ if (!child.killed) child.kill(signal);
7627
+ };
7628
+ process.on("SIGINT", forward);
7629
+ process.on("SIGTERM", forward);
7630
+ child.on("error", (error) => {
7631
+ process.stderr.write(`instafy bind: ${error.message}
7632
+ `);
7633
+ resolve(1);
7634
+ });
7635
+ child.on("exit", (code, signal) => {
7636
+ process.off("SIGINT", forward);
7637
+ process.off("SIGTERM", forward);
7638
+ resolve(code ?? (signal ? 130 : 1));
7639
+ });
7640
+ });
7641
+ }
7642
+
7643
+ // src/site-check.ts
7644
+ import { spawnSync as spawnSync5 } from "node:child_process";
7645
+ import { once } from "node:events";
7646
+ import fs11 from "node:fs";
7647
+ import http3 from "node:http";
7648
+ import { createRequire as createRequire3 } from "node:module";
7649
+ import os6 from "node:os";
7650
+ import path12 from "node:path";
7651
+ var CHECK_EXIT_OK = 0;
7652
+ var CHECK_EXIT_PROBLEMS = 1;
7653
+ var CHECK_EXIT_NOT_LISTENING = PREVIEW_EXIT_NOT_LISTENING;
7654
+ var CHECK_EXIT_NO_BROWSER = 4;
7655
+ var CHECK_EXIT_USAGE = 64;
7656
+ var CHECK_EXIT_INTERNAL = 70;
7657
+ var MAX_PATH_LENGTH2 = 512;
7658
+ var MAX_LIST = 20;
7659
+ var MAX_TEXT = 300;
7660
+ var CHROMIUM_ARGS = ["--use-angle=swiftshader", "--enable-unsafe-swiftshader", "--ignore-gpu-blocklist", "--disable-dev-shm-usage"];
7661
+ var SOFTWARE_GL_NOISE = /GL Driver Message|GPU stall due to ReadPixels|READ-usage buffer was written|WebGL: too many errors|Automatic fallback to software WebGL/;
7662
+ var PHONE_UA = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1";
7663
+ var VIEWPORTS = [
7664
+ { name: "desktop", width: 1280, height: 800, mobile: false },
7665
+ { name: "phone", width: 390, height: 844, mobile: true }
7666
+ ];
7667
+ var TRUSTED_PLAYWRIGHT_PATH_ENV = "INSTAFY_SHARED_BROWSER_PLAYWRIGHT_PATH";
7668
+ var TRUSTED_NODE_MODULES_ROOT_ENV = "INSTAFY_SHARED_BROWSER_TRUSTED_NODE_MODULES_ROOT";
7669
+ function isPlaywrightDir(dir) {
7670
+ try {
7671
+ const pkg2 = JSON.parse(fs11.readFileSync(path12.join(dir, "package.json"), "utf8"));
7672
+ return pkg2.name === "playwright" || pkg2.name === "playwright-core";
7673
+ } catch {
7674
+ return false;
7675
+ }
7676
+ }
7677
+ function playwrightIn(nodeModules) {
7678
+ const found = [];
7679
+ for (const name of ["playwright", "playwright-core"]) {
7680
+ const dir = path12.join(nodeModules, name);
7681
+ if (isPlaywrightDir(dir)) found.push(dir);
7682
+ }
7683
+ try {
7684
+ const store = path12.join(nodeModules, ".pnpm");
7685
+ for (const entry of fs11.readdirSync(store).filter((e) => /^playwright(-core)?@\d/.test(e)).sort().reverse()) {
7686
+ const name = entry.startsWith("playwright-core@") ? "playwright-core" : "playwright";
7687
+ const dir = path12.join(store, entry, "node_modules", name);
7688
+ if (isPlaywrightDir(dir)) found.push(dir);
7689
+ }
7690
+ } catch {
7691
+ }
7692
+ return found;
7693
+ }
7694
+ var npmRootCache;
7695
+ function npmGlobalRoot() {
7696
+ if (npmRootCache !== void 0) return npmRootCache;
7697
+ try {
7698
+ const out = spawnSync5("npm", ["root", "-g"], { cwd: path12.parse(process.cwd()).root, encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] });
7699
+ npmRootCache = out.status === 0 && out.stdout.trim() ? out.stdout.trim() : null;
7700
+ } catch {
7701
+ npmRootCache = null;
7702
+ }
7703
+ return npmRootCache;
7704
+ }
7705
+ function defaultGlobalRoots(env = process.env) {
7706
+ const roots = [];
7707
+ for (const entry of (env.NODE_PATH ?? "").split(path12.delimiter)) if (path12.isAbsolute(entry.trim())) roots.push(entry.trim());
7708
+ const npmRoot = npmGlobalRoot();
7709
+ if (npmRoot && path12.isAbsolute(npmRoot)) roots.push(npmRoot);
7710
+ roots.push("/usr/lib/node_modules", "/usr/local/lib/node_modules", "/opt/homebrew/lib/node_modules");
7711
+ return [...new Set(roots)];
7712
+ }
7713
+ function locateBrowserEngine(o = {}) {
7714
+ const env = o.env ?? process.env;
7715
+ const tried = [];
7716
+ const trustedPath = env[TRUSTED_PLAYWRIGHT_PATH_ENV]?.trim();
7717
+ if (trustedPath) {
7718
+ if (path12.isAbsolute(trustedPath) && isPlaywrightDir(trustedPath)) return { engine: { playwright: trustedPath, source: "trusted" }, tried };
7719
+ tried.push(`$${TRUSTED_PLAYWRIGHT_PATH_ENV} is not an absolute path to a playwright package`);
7720
+ }
7721
+ const trustedRoot = env[TRUSTED_NODE_MODULES_ROOT_ENV]?.trim();
7722
+ if (trustedRoot) {
7723
+ const found = path12.isAbsolute(trustedRoot) ? playwrightIn(trustedRoot) : [];
7724
+ if (found.length) return { engine: { playwright: found[0], source: "trusted" }, tried };
7725
+ tried.push(`no playwright in $${TRUSTED_NODE_MODULES_ROOT_ENV}`);
7726
+ }
7727
+ for (const root of o.globalRoots ?? defaultGlobalRoots(env)) {
7728
+ const found = playwrightIn(root);
7729
+ if (found.length) return { engine: { playwright: found[0], source: "global" }, tried };
7730
+ tried.push(`none in ${root}`);
7731
+ }
7732
+ if (!o.workspace) {
7733
+ tried.push("the workspace was not searched (--workspace-playwright)");
7734
+ return { engine: null, tried };
7735
+ }
7736
+ let dir = path12.resolve(o.cwd ?? process.cwd());
7737
+ for (let i = 0; i < 16; i += 1) {
7738
+ const found = playwrightIn(path12.join(dir, "node_modules"));
7739
+ if (found.length) return { engine: { playwright: found[0], source: "workspace" }, tried };
7740
+ const up = path12.dirname(dir);
7741
+ if (up === dir) break;
7742
+ dir = up;
7743
+ }
7744
+ tried.push(`no node_modules/playwright from ${o.cwd ?? process.cwd()} upwards`);
7745
+ return { engine: null, tried };
7746
+ }
7747
+ function chromiumCandidates(env = process.env, exists = fs11.existsSync) {
7748
+ const out = [];
7749
+ const browsersRoots = [env.PLAYWRIGHT_BROWSERS_PATH, "/ms-playwright", path12.join(os6.homedir(), ".cache", "ms-playwright")].filter((x) => Boolean(x && x !== "0"));
7750
+ for (const root of [...new Set(browsersRoots)]) {
7751
+ let entries = [];
7752
+ try {
7753
+ entries = fs11.readdirSync(root).sort().reverse();
7754
+ } catch {
7755
+ continue;
7756
+ }
7757
+ for (const e of entries.filter((x) => x.startsWith("chromium_headless_shell-"))) {
7758
+ out.push(path12.join(root, e, "chrome-headless-shell-linux64", "chrome-headless-shell"));
7759
+ out.push(path12.join(root, e, "chrome-linux", "headless_shell"));
7760
+ }
7761
+ for (const e of entries.filter((x) => /^chromium-\d/.test(x))) {
7762
+ out.push(path12.join(root, e, "chrome-linux64", "chrome"), path12.join(root, e, "chrome-linux", "chrome"));
7763
+ }
7764
+ }
7765
+ out.push(
7766
+ "/usr/bin/chromium",
7767
+ "/usr/bin/chromium-browser",
7768
+ "/usr/bin/google-chrome",
7769
+ "/usr/bin/google-chrome-stable",
7770
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
7771
+ "/Applications/Chromium.app/Contents/MacOS/Chromium"
7772
+ );
7773
+ return out.filter((p) => exists(p));
7774
+ }
7775
+ function loadPlaywright(engine) {
7776
+ const req = createRequire3(path12.join(engine.playwright, "package.json"));
7777
+ const mod = req(engine.playwright);
7778
+ const pw = mod?.chromium ? mod : mod?.default;
7779
+ if (!pw?.chromium) throw new Error(`${engine.playwright} has no chromium export`);
7780
+ return pw;
7781
+ }
7782
+ async function launchChromium(pw, env = process.env) {
7783
+ const errors = [];
7784
+ try {
7785
+ const browser = await pw.chromium.launch({ headless: true, args: CHROMIUM_ARGS });
7786
+ return { browser, executable: "playwright" };
7787
+ } catch (error) {
7788
+ errors.push(firstLine(error));
7789
+ }
7790
+ for (const executablePath of chromiumCandidates(env)) {
7791
+ try {
7792
+ const browser = await pw.chromium.launch({ headless: true, executablePath, args: CHROMIUM_ARGS });
7793
+ return { browser, executable: executablePath };
7794
+ } catch (error) {
7795
+ errors.push(`${executablePath}: ${firstLine(error)}`);
7796
+ }
7797
+ }
7798
+ throw new NoBrowserError(`No Chromium would start (${errors.join("; ") || "none found"})`);
7799
+ }
7800
+ var NoBrowserError = class extends Error {
7801
+ };
7802
+ function firstLine(error) {
7803
+ return String(error?.message ?? error).split("\n")[0].slice(0, MAX_TEXT);
7804
+ }
7805
+ function parseCheckPath(raw) {
7806
+ const value = raw?.trim() || "/";
7807
+ if (!value.startsWith("/") || value.startsWith("//") || value.length > MAX_PATH_LENGTH2 || /[\s\\]/.test(value)) {
7808
+ throw new PreviewCommandError("Path must start with / and contain no spaces or backslashes (a ?query and #hash are fine).", CHECK_EXIT_USAGE);
7809
+ }
7810
+ return value;
7811
+ }
7812
+ function siteUrlFor(siteOrigin, sitePath) {
7813
+ const url = new URL(sitePath, siteOrigin);
7814
+ if (url.origin !== siteOrigin) throw new PreviewCommandError("Path must stay on the site's own origin.", CHECK_EXIT_USAGE);
7815
+ return url.href;
7816
+ }
7817
+ function isSiteUrl(url, siteOrigin) {
7818
+ try {
7819
+ return new URL(url).origin === siteOrigin;
7820
+ } catch {
7821
+ return false;
7822
+ }
7823
+ }
7824
+ function makeDefaultOutDir(port) {
7825
+ return fs11.mkdtempSync(path12.join(os6.tmpdir(), `instafy-preview-check-${port}-`));
7826
+ }
7827
+ function escapeAttribute(value) {
7828
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
7829
+ }
7830
+ function framePage(siteUrl) {
7831
+ return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
7832
+ <title>instafy preview check</title>
7833
+ <style>html,body{margin:0;height:100%;background:#fff}iframe{display:block;border:0;width:100%;height:100%}</style>
7834
+ <iframe id="site" src="${escapeAttribute(siteUrl)}" allow="fullscreen; clipboard-write"></iframe>`;
7835
+ }
7836
+ async function startFrameHost(siteUrl) {
7837
+ const body = framePage(siteUrl);
7838
+ const server = http3.createServer((req, res) => {
7839
+ if (req.url !== "/") {
7840
+ res.writeHead(404, { "content-type": "text/plain" });
7841
+ res.end();
7842
+ return;
7843
+ }
7844
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
7845
+ res.end(body);
7846
+ });
7847
+ server.listen(0, "127.0.0.1");
7848
+ await once(server, "listening");
7849
+ const { port } = server.address();
7850
+ return { url: `http://localhost:${port}/`, close: () => server.close() };
7851
+ }
7852
+ function blankVerdict(input3) {
7853
+ if (input3.framed === "refused") {
7854
+ return { blank: true, reason: "the site refuses to be framed (X-Frame-Options or CSP frame-ancestors), so the frame stays empty" };
7855
+ }
7856
+ if (input3.framed === "missing") return { blank: true, reason: "the page did not load" };
7857
+ if (input3.dominant !== null && input3.dominant >= 0.995 && input3.text === 0) return { blank: true, reason: "the screen is a single colour" };
7858
+ if (input3.text === 0 && input3.media === 0 && input3.elements < 3) return { blank: true, reason: "the page has no text, images or canvas" };
7859
+ return { blank: false, reason: null };
7860
+ }
7861
+ async function measureDominantShare(measure) {
7862
+ try {
7863
+ const share = await measure();
7864
+ return Number.isFinite(share) && share >= 0 && share <= 1 ? share : null;
7865
+ } catch {
7866
+ return null;
7867
+ }
7868
+ }
7869
+ function incompleteBlankCheck(dominant, text2) {
7870
+ if (dominant !== null) return null;
7871
+ return text2 === 0 ? { error: true, message: "blank check incomplete: could not read the screenshot, and the page has no text to show it drew anything" } : { error: false, message: "blank check incomplete: could not read the screenshot, so a single-colour screen would not be caught" };
7872
+ }
7873
+ var FRAME_SNAPSHOT = () => {
7874
+ const visible = (el) => {
7875
+ const r = el.getBoundingClientRect();
7876
+ if (r.width < 2 || r.height < 2 || r.bottom < 0 || r.right < 0 || r.top > innerHeight || r.left > innerWidth) return false;
7877
+ const s = getComputedStyle(el);
7878
+ return s.visibility !== "hidden" && s.display !== "none" && Number(s.opacity) > 0.05;
7879
+ };
7880
+ const body = document.body;
7881
+ const text2 = (body?.innerText ?? "").replace(/\s+/g, " ").trim().length;
7882
+ let media = 0;
7883
+ let elements = 0;
7884
+ for (const el of Array.from(body?.querySelectorAll("*") ?? [])) {
7885
+ if (/^(SCRIPT|STYLE|NOSCRIPT|TEMPLATE|LINK|META)$/.test(el.tagName)) continue;
7886
+ if (!visible(el)) continue;
7887
+ elements += 1;
7888
+ if (/^(CANVAS|IMG|SVG|VIDEO|IFRAME|PICTURE|OBJECT|EMBED)$/i.test(el.tagName)) media += 1;
7889
+ }
7890
+ return { text: text2, media, elements, title: document.title };
7891
+ };
7892
+ var DOMINANT_COLOUR = async (b64) => {
7893
+ const img = new Image();
7894
+ img.src = `data:image/png;base64,${b64}`;
7895
+ await img.decode();
7896
+ const w = 160;
7897
+ const h = Math.max(1, Math.round(img.height / img.width * w));
7898
+ const c = document.createElement("canvas");
7899
+ c.width = w;
7900
+ c.height = h;
7901
+ const g = c.getContext("2d");
7902
+ g.imageSmoothingEnabled = false;
7903
+ g.drawImage(img, 0, 0, w, h);
7904
+ const data = g.getImageData(0, 0, w, h).data;
7905
+ const counts = /* @__PURE__ */ new Map();
7906
+ let max = 0;
7907
+ for (let i = 0; i < data.length; i += 4) {
7908
+ const key = data[i] >> 3 << 10 | data[i + 1] >> 3 << 5 | data[i + 2] >> 3;
7909
+ const n = (counts.get(key) ?? 0) + 1;
7910
+ counts.set(key, n);
7911
+ if (n > max) max = n;
7912
+ }
7913
+ return max / (w * h);
7914
+ };
7915
+ async function checkViewport(browser, target, vp, siteOrigin, outDir, timeoutMs, waitMs) {
7916
+ const errors = [];
7917
+ const warnings = [];
7918
+ const context = await browser.newContext({
7919
+ viewport: { width: vp.width, height: vp.height },
7920
+ deviceScaleFactor: vp.mobile ? 2 : 1,
7921
+ isMobile: vp.mobile,
7922
+ hasTouch: vp.mobile,
7923
+ ...vp.mobile ? { userAgent: PHONE_UA } : {},
7924
+ reducedMotion: "reduce",
7925
+ serviceWorkers: "block",
7926
+ acceptDownloads: false
7927
+ });
7928
+ const t0 = Date.now();
7929
+ let status = null;
7930
+ try {
7931
+ const page = await context.newPage();
7932
+ const ours = (u) => isSiteUrl(u, siteOrigin);
7933
+ const shortUrl = (u) => (ours(u) ? u.slice(siteOrigin.length) || "/" : u).slice(0, 200);
7934
+ page.on("console", (m) => {
7935
+ const text2 = String(m.text());
7936
+ if (/^Failed to load resource/.test(text2)) return;
7937
+ if (SOFTWARE_GL_NOISE.test(text2)) return;
7938
+ if (/^Refused to display .* in a frame/.test(text2)) return;
7939
+ if (m.type() === "error") errors.push(`console.error: ${text2.slice(0, MAX_TEXT)}`);
7940
+ });
7941
+ page.on("pageerror", (e) => errors.push(`uncaught: ${String(e?.message ?? e).split("\n")[0].slice(0, MAX_TEXT)}`));
7942
+ let refused = false;
7943
+ page.on("requestfailed", (r) => {
7944
+ const u = String(r.url());
7945
+ if (/\/favicon\.ico(\?|$)/.test(u)) return;
7946
+ const why = r.failure()?.errorText ?? "failed";
7947
+ if (target.framed && ours(u) && r.isNavigationRequest() && /ERR_BLOCKED_BY_RESPONSE/.test(why)) {
7948
+ refused = true;
7949
+ return;
7950
+ }
7951
+ if (/ERR_ABORTED/.test(why)) return;
7952
+ (ours(u) ? errors : warnings).push(`request failed: ${shortUrl(u)} (${why})`);
7953
+ });
7954
+ page.on("response", (r) => {
7955
+ const u = String(r.url());
7956
+ const s = Number(r.status());
7957
+ if (r.request().isNavigationRequest() && ours(u) && status === null) status = s;
7958
+ if (s < 400 || /\/favicon\.ico(\?|$)/.test(u)) return;
7959
+ (ours(u) ? errors : warnings).push(`HTTP ${s}: ${shortUrl(u)}`);
7960
+ });
7961
+ await page.goto(target.pageUrl, { waitUntil: "load", timeout: timeoutMs });
7962
+ await page.waitForLoadState("networkidle", { timeout: Math.min(8e3, timeoutMs) }).catch(() => {
7963
+ warnings.push("the network was still busy after 8 s (screenshot taken anyway)");
7964
+ });
7965
+ const frame = target.framed ? page.frames().find((f) => f !== page.mainFrame() && isSiteUrl(String(f.url()), siteOrigin)) : page.mainFrame();
7966
+ let framed = "ok";
7967
+ let snap = { text: 0, media: 0, elements: 0, title: "" };
7968
+ if (refused) framed = "refused";
7969
+ else if (!frame) framed = "missing";
7970
+ else {
7971
+ await frame.evaluate(() => document.fonts?.ready).catch(() => {
7972
+ });
7973
+ await page.waitForTimeout(waitMs);
7974
+ snap = await frame.evaluate(FRAME_SNAPSHOT).catch(() => snap);
7975
+ }
7976
+ if (status !== null && status >= 400) framed = "missing";
7977
+ const file = path12.join(outDir, `${vp.name}.png`);
7978
+ const png = await page.screenshot({ path: file });
7979
+ const dominant = await measureDominantShare(async () => {
7980
+ const helper = await context.newPage();
7981
+ try {
7982
+ return await helper.evaluate(DOMINANT_COLOUR, png.toString("base64"));
7983
+ } finally {
7984
+ await helper.close().catch(() => {
7985
+ });
7986
+ }
7987
+ });
7988
+ const blank = blankVerdict({ dominant, text: snap.text, media: snap.media, elements: snap.elements, framed });
7989
+ if (blank.blank) errors.push(`blank page: ${blank.reason}`);
7990
+ else {
7991
+ const incomplete = incompleteBlankCheck(dominant, snap.text);
7992
+ if (incomplete) (incomplete.error ? errors : warnings).push(incomplete.message);
7993
+ }
7994
+ return {
7995
+ name: vp.name,
7996
+ width: vp.width,
7997
+ height: vp.height,
7998
+ screenshot: file,
7999
+ title: snap.title.slice(0, 200),
8000
+ status,
8001
+ ms: Date.now() - t0,
8002
+ blank,
8003
+ errors: [...new Set(errors)].slice(0, MAX_LIST),
8004
+ warnings: [...new Set(warnings)].slice(0, MAX_LIST)
8005
+ };
8006
+ } catch (error) {
8007
+ errors.push(`the page did not load: ${firstLine(error)}`);
8008
+ return {
8009
+ name: vp.name,
8010
+ width: vp.width,
8011
+ height: vp.height,
8012
+ screenshot: null,
8013
+ title: "",
8014
+ status,
8015
+ ms: Date.now() - t0,
8016
+ blank: { blank: true, reason: "the page did not load" },
8017
+ errors: [...new Set(errors)].slice(0, MAX_LIST),
8018
+ warnings: [...new Set(warnings)].slice(0, MAX_LIST)
8019
+ };
8020
+ } finally {
8021
+ await context.close().catch(() => {
8022
+ });
8023
+ }
8024
+ }
8025
+ async function runSiteCheck(opts) {
8026
+ const env = opts.env ?? process.env;
8027
+ const port = parsePreviewPort(opts.port, env);
8028
+ const sitePath = parseCheckPath(opts.path);
8029
+ const timeoutMs = opts.timeoutMs ?? 3e4;
8030
+ const viewports = VIEWPORTS.filter((v) => !opts.phone || v.name === "phone");
8031
+ let outDir = null;
8032
+ if (opts.outDir !== void 0) {
8033
+ outDir = path12.resolve(opts.outDir);
8034
+ fs11.mkdirSync(outDir, { recursive: true });
8035
+ for (const v of VIEWPORTS) fs11.rmSync(path12.join(outDir, `${v.name}.png`), { force: true });
8036
+ }
8037
+ const probePath = sitePath.replace(/[?#].*$/, "") || "/";
8038
+ const probe = await probeLocalSite(port, probePath, timeoutMs);
8039
+ if (!probe.host) {
8040
+ return {
8041
+ code: CHECK_EXIT_NOT_LISTENING,
8042
+ report: null,
8043
+ message: noAnswerMessage(port, probe, `port ${port} at 127.0.0.1 or [::1]`, "check again")
8044
+ };
8045
+ }
8046
+ const siteOrigin = `http://${probe.host}:${port}`;
8047
+ const url = siteUrlFor(siteOrigin, sitePath);
8048
+ const { engine, tried } = locateBrowserEngine({ env, cwd: opts.cwd, globalRoots: opts.globalRoots, workspace: opts.workspacePlaywright });
8049
+ if (!engine) {
8050
+ return { code: CHECK_EXIT_NO_BROWSER, report: null, message: `SKIPPED, nothing was checked: Playwright not found (${tried.join("; ")}).` };
8051
+ }
8052
+ let launched;
8053
+ try {
8054
+ launched = await launchChromium(loadPlaywright(engine), env);
8055
+ } catch (error) {
8056
+ return { code: CHECK_EXIT_NO_BROWSER, report: null, message: `SKIPPED, nothing was checked: ${firstLine(error)}` };
8057
+ }
8058
+ const reports = [];
8059
+ let host = null;
8060
+ try {
8061
+ const shots = outDir ?? makeDefaultOutDir(port);
8062
+ host = opts.framed ? await startFrameHost(url) : null;
8063
+ const target = { pageUrl: host?.url ?? url, framed: Boolean(opts.framed) };
8064
+ for (const vp of viewports) {
8065
+ reports.push(await checkViewport(launched.browser, target, vp, siteOrigin, shots, timeoutMs, opts.waitMs ?? 600));
8066
+ }
8067
+ } finally {
8068
+ await launched.browser.close().catch(() => {
8069
+ });
8070
+ host?.close();
8071
+ }
8072
+ const problems = reports.reduce((n, r) => n + r.errors.length, 0);
8073
+ const report = {
8074
+ ok: problems === 0,
8075
+ url,
8076
+ title: reports.find((r) => r.title)?.title ?? "",
8077
+ framed: Boolean(opts.framed),
8078
+ engine: { playwright: engine.playwright, chromium: launched.executable },
8079
+ viewports: reports,
8080
+ problems
8081
+ };
8082
+ return { code: problems ? CHECK_EXIT_PROBLEMS : CHECK_EXIT_OK, report };
8083
+ }
8084
+ function formatSiteCheck(report) {
8085
+ const lines = [];
8086
+ lines.push(`${report.ok ? "\u2713" : "\u2717"} ${report.url}${report.framed ? " (framed)" : ""} \xB7 "${report.title || "(no title)"}"`);
8087
+ for (const r of report.viewports) {
8088
+ const timing = `${(r.ms / 1e3).toFixed(1)} s${r.status ? `, HTTP ${r.status}` : ""}`;
8089
+ lines.push(`${r.errors.length ? "\u2717" : "\u2713"} ${r.name} ${r.width}\xD7${r.height}: ${r.screenshot ?? "no screenshot"} (${timing})`);
8090
+ for (const e of r.errors) lines.push(` error: ${e}`);
8091
+ for (const w of r.warnings.slice(0, 8)) lines.push(` warn: ${w}`);
8092
+ }
8093
+ lines.push(
8094
+ report.ok ? "No problems found. Open the screenshots and check layout, legibility and overlap on both sizes." : "Fix every error above, reload the site, and run the check again. Open the screenshots either way."
8095
+ );
8096
+ return lines.join("\n");
8097
+ }
8098
+ async function runPreviewCheckCli(opts, io = { out: console.log, err: console.error }) {
8099
+ const env = io.env ?? process.env;
8100
+ const num = (raw, name, min, max) => {
8101
+ if (raw === void 0) return void 0;
8102
+ const n = Number(raw);
8103
+ if (!Number.isInteger(n) || n < min || n > max) throw new PreviewCommandError(`${name} must be a whole number from ${min} to ${max}.`, CHECK_EXIT_USAGE);
8104
+ return n;
8105
+ };
8106
+ try {
8107
+ if (opts.port === void 0) throw new PreviewCommandError("--port <port> is required.", CHECK_EXIT_USAGE);
8108
+ const result = await runSiteCheck({
8109
+ port: parsePreviewPort(opts.port, env),
8110
+ path: opts.path,
8111
+ phone: opts.phone,
8112
+ framed: opts.framed,
8113
+ workspacePlaywright: opts.workspacePlaywright,
8114
+ outDir: opts.out,
8115
+ timeoutMs: num(opts.timeout, "--timeout", 1e3, 12e4),
8116
+ waitMs: num(opts.wait, "--wait", 0, 3e4),
8117
+ env
8118
+ });
8119
+ if (!result.report) {
8120
+ if (opts.json) io.out(JSON.stringify({ ok: false, exitCode: result.code, error: result.message, skipped: result.code === CHECK_EXIT_NO_BROWSER }));
8121
+ else io.err(result.message ?? "check failed");
8122
+ return result.code;
8123
+ }
8124
+ io.out(opts.json ? JSON.stringify(result.report) : formatSiteCheck(result.report));
8125
+ return result.code;
8126
+ } catch (error) {
8127
+ const known = error instanceof PreviewCommandError;
8128
+ const code = known ? error.exitCode : CHECK_EXIT_INTERNAL;
8129
+ const message = known ? firstLine(error) : `The check itself failed, so nothing was checked: ${firstLine(error)}`;
8130
+ if (opts.json) io.out(JSON.stringify({ ok: false, exitCode: code, error: message, skipped: !known }));
8131
+ else io.err(message);
8132
+ return code;
8133
+ }
8134
+ }
8135
+ function registerPreviewCheckCommand(preview) {
8136
+ return preview.command("check").description(
8137
+ "Load a local site in a headless browser and report its title, errors, failed requests and a blank page; writes desktop and phone screenshots (exit 1: problems, 2: nothing answering, 3: port not allowed, 4: no browser, 64: bad options, 70: the check itself failed)"
8138
+ ).option("--port <port>", "Local port the site listens on (1024-65535)").option("--path <path>", "Path to load (default /); a ?query and #hash are fine").option("--phone", "Only the phone size (390\xD7844); default: desktop 1280\xD7800 and phone").option("--framed", "Load the site inside a frame on another origin, and report a site that refuses to be framed").option("--workspace-playwright", "Also look for Playwright in this project's node_modules (runs the project's own copy)").option("--out <dir>", "Folder for desktop.png and phone.png (default: a new private folder under the temp folder each run; the report names it)").option("--timeout <ms>", "Page load timeout in ms (default 30000)").option("--wait <ms>", "Extra settle time after load in ms (default 600)").option("--json", "Output JSON").action(async (opts) => {
8139
+ process.exitCode = await runPreviewCheckCli(opts);
8140
+ });
8141
+ }
8142
+
6574
8143
  // src/api.ts
6575
8144
  init_config();
6576
8145
  init_errors();
6577
8146
  init_controller_fetch();
6578
- import fs9 from "node:fs";
8147
+ import fs12 from "node:fs";
6579
8148
  function normalizeUrl5(raw) {
6580
8149
  const value = (raw ?? "").trim();
6581
8150
  if (!value) return "http://127.0.0.1:8788";
@@ -6652,7 +8221,7 @@ function parseHeader(raw) {
6652
8221
  }
6653
8222
  function parseJsonBody(options) {
6654
8223
  if (options.jsonFile) {
6655
- const raw = fs9.readFileSync(options.jsonFile, "utf8");
8224
+ const raw = fs12.readFileSync(options.jsonFile, "utf8");
6656
8225
  const parsed = JSON.parse(raw);
6657
8226
  return JSON.stringify(parsed);
6658
8227
  }
@@ -6796,7 +8365,7 @@ async function requestControllerApi(options) {
6796
8365
 
6797
8366
  // src/conversations.ts
6798
8367
  init_project_manifest();
6799
- import kleur10 from "kleur";
8368
+ import kleur11 from "kleur";
6800
8369
  var DEFAULT_CONVERSATION_LIMIT = 50;
6801
8370
  var DEFAULT_MESSAGE_LIMIT = 80;
6802
8371
  var MAX_LIMIT = 200;
@@ -7103,7 +8672,7 @@ async function buildSearchMatches(conversations, options) {
7103
8672
  }));
7104
8673
  }
7105
8674
  function formatConversationMatch(match) {
7106
- const lines = [`- ${kleur10.green(match.title)} (${match.id})`];
8675
+ const lines = [`- ${kleur11.green(match.title)} (${match.id})`];
7107
8676
  const details = [];
7108
8677
  if (match.matchedIn.length > 0) {
7109
8678
  details.push(`match: ${match.matchedIn.join(", ")}`);
@@ -7115,7 +8684,7 @@ function formatConversationMatch(match) {
7115
8684
  lines.push(` ${details.join(" \xB7 ")}`);
7116
8685
  }
7117
8686
  if (match.preview) {
7118
- lines.push(` ${kleur10.gray(summarizeContent(match.preview))}`);
8687
+ lines.push(` ${kleur11.gray(summarizeContent(match.preview))}`);
7119
8688
  }
7120
8689
  return lines.join("\n");
7121
8690
  }
@@ -7174,16 +8743,16 @@ async function createConversation(options) {
7174
8743
  console.log(JSON.stringify(payload, null, 2));
7175
8744
  return;
7176
8745
  }
7177
- console.log(kleur10.green("Created conversation"));
7178
- console.log(`${kleur10.gray("ID:")} ${conversationId}`);
8746
+ console.log(kleur11.green("Created conversation"));
8747
+ console.log(`${kleur11.gray("ID:")} ${conversationId}`);
7179
8748
  if (title) {
7180
- console.log(`${kleur10.gray("Title:")} ${title}`);
8749
+ console.log(`${kleur11.gray("Title:")} ${title}`);
7181
8750
  }
7182
8751
  if (parentConversationId) {
7183
- console.log(`${kleur10.gray("Parent:")} ${parentConversationId}`);
8752
+ console.log(`${kleur11.gray("Parent:")} ${parentConversationId}`);
7184
8753
  }
7185
8754
  if (threadKind) {
7186
- console.log(`${kleur10.gray("Thread kind:")} ${threadKind}`);
8755
+ console.log(`${kleur11.gray("Thread kind:")} ${threadKind}`);
7187
8756
  }
7188
8757
  }
7189
8758
  async function listConversations(options) {
@@ -7210,10 +8779,10 @@ async function listConversations(options) {
7210
8779
  return;
7211
8780
  }
7212
8781
  if (items.length === 0) {
7213
- console.log(kleur10.yellow("No conversations found for this space."));
8782
+ console.log(kleur11.yellow("No conversations found for this space."));
7214
8783
  return;
7215
8784
  }
7216
- console.log(kleur10.green(`Conversations (${items.length})`));
8785
+ console.log(kleur11.green(`Conversations (${items.length})`));
7217
8786
  for (const item of items) {
7218
8787
  console.log(formatConversationMatch({ ...item, score: 0, matchedIn: [] }));
7219
8788
  }
@@ -7235,10 +8804,10 @@ async function searchConversations(options) {
7235
8804
  return matches;
7236
8805
  }
7237
8806
  if (matches.length === 0) {
7238
- console.log(kleur10.yellow(`No conversations matched "${options.query}".`));
8807
+ console.log(kleur11.yellow(`No conversations matched "${options.query}".`));
7239
8808
  return matches;
7240
8809
  }
7241
- console.log(kleur10.green(`Matches for "${options.query}"`));
8810
+ console.log(kleur11.green(`Matches for "${options.query}"`));
7242
8811
  for (const match of matches.slice(0, Math.min(limit, matches.length))) {
7243
8812
  console.log(formatConversationMatch(match));
7244
8813
  }
@@ -7356,27 +8925,27 @@ async function showConversation(options) {
7356
8925
  );
7357
8926
  return;
7358
8927
  }
7359
- console.log(kleur10.green(selected.title));
7360
- console.log(`${kleur10.gray("ID:")} ${selected.id}`);
8928
+ console.log(kleur11.green(selected.title));
8929
+ console.log(`${kleur11.gray("ID:")} ${selected.id}`);
7361
8930
  if (selected.updatedAt) {
7362
- console.log(`${kleur10.gray("Updated:")} ${selected.updatedAt}`);
8931
+ console.log(`${kleur11.gray("Updated:")} ${selected.updatedAt}`);
7363
8932
  }
7364
8933
  if (selected.preview) {
7365
- console.log(`${kleur10.gray("Preview:")} ${summarizeContent(selected.preview, 240)}`);
8934
+ console.log(`${kleur11.gray("Preview:")} ${summarizeContent(selected.preview, 240)}`);
7366
8935
  }
7367
8936
  console.log("");
7368
8937
  if (transcript?.hasMore) {
7369
- console.log(kleur10.yellow(`Older rows remain; continue with --transcript --cursor ${transcript.nextCursor}.`));
8938
+ console.log(kleur11.yellow(`Older rows remain; continue with --transcript --cursor ${transcript.nextCursor}.`));
7370
8939
  }
7371
8940
  if (messages.length === 0) {
7372
- console.log(kleur10.yellow(transcript ? "No transcript messages in the scanned rows." : "No messages found in this conversation."));
8941
+ console.log(kleur11.yellow(transcript ? "No transcript messages in the scanned rows." : "No messages found in this conversation."));
7373
8942
  return;
7374
8943
  }
7375
8944
  for (const message of messages.slice().reverse()) {
7376
8945
  const role = (message.role ?? "unknown").trim().toLowerCase();
7377
8946
  const createdAt = trimOrNull(message.createdAt) ?? trimOrNull(message.created_at);
7378
8947
  const header = createdAt ? `${role} \xB7 ${createdAt}` : role;
7379
- console.log(kleur10.cyan(`[${header}${transcript ? ` \xB7 ${message.id}` : ""}]`));
8948
+ console.log(kleur11.cyan(`[${header}${transcript ? ` \xB7 ${message.id}` : ""}]`));
7380
8949
  console.log(transcript ? message.content : summarizeContent(message.content ?? "", 4e3));
7381
8950
  console.log("");
7382
8951
  }
@@ -7473,9 +9042,9 @@ async function historyConversations(options) {
7473
9042
 
7474
9043
  // src/hardware-bindings.ts
7475
9044
  init_project_manifest();
7476
- import fs10 from "node:fs";
7477
- import path10 from "node:path";
7478
- import kleur11 from "kleur";
9045
+ import fs13 from "node:fs";
9046
+ import path13 from "node:path";
9047
+ import kleur12 from "kleur";
7479
9048
  var SERIAL_HARDWARE_PROVIDER_ID = "hardware.serial";
7480
9049
  var DEFAULT_SERIAL_CAPABILITIES = [
7481
9050
  "hardware_serial_list",
@@ -7485,7 +9054,7 @@ var SUPPORTED_HARDWARE_CAPABILITIES = new Set(
7485
9054
  DEFAULT_SERIAL_CAPABILITIES
7486
9055
  );
7487
9056
  function resolveHardwareBindingContext(startDir) {
7488
- const lookup = findProjectManifest(path10.resolve(startDir));
9057
+ const lookup = findProjectManifest(path13.resolve(startDir));
7489
9058
  if (!lookup.manifest || !lookup.path) {
7490
9059
  throw new Error(
7491
9060
  "Linked space manifest required. Run `instafy space init` in this folder or pass --path <dir>."
@@ -7495,17 +9064,17 @@ function resolveHardwareBindingContext(startDir) {
7495
9064
  if (!projectId2) {
7496
9065
  throw new Error("Linked space manifest is missing a spaceId.");
7497
9066
  }
7498
- const instafyDir = path10.dirname(lookup.path);
9067
+ const instafyDir2 = path13.dirname(lookup.path);
7499
9068
  return {
7500
- bindingsPath: path10.join(instafyDir, "hardware-bindings.json"),
9069
+ bindingsPath: path13.join(instafyDir2, "hardware-bindings.json"),
7501
9070
  projectId: projectId2
7502
9071
  };
7503
9072
  }
7504
9073
  function readHardwareBindingStore(bindingsPath) {
7505
- if (!fs10.existsSync(bindingsPath)) {
9074
+ if (!fs13.existsSync(bindingsPath)) {
7506
9075
  return { version: 1, bindings: {} };
7507
9076
  }
7508
- const raw = JSON.parse(fs10.readFileSync(bindingsPath, "utf8"));
9077
+ const raw = JSON.parse(fs13.readFileSync(bindingsPath, "utf8"));
7509
9078
  const bindings = raw && typeof raw.bindings === "object" && raw.bindings !== null ? raw.bindings : {};
7510
9079
  return {
7511
9080
  version: 1,
@@ -7513,8 +9082,8 @@ function readHardwareBindingStore(bindingsPath) {
7513
9082
  };
7514
9083
  }
7515
9084
  function writeHardwareBindingStore(bindingsPath, store) {
7516
- fs10.mkdirSync(path10.dirname(bindingsPath), { recursive: true });
7517
- fs10.writeFileSync(bindingsPath, JSON.stringify(store, null, 2), "utf8");
9085
+ fs13.mkdirSync(path13.dirname(bindingsPath), { recursive: true });
9086
+ fs13.writeFileSync(bindingsPath, JSON.stringify(store, null, 2), "utf8");
7518
9087
  }
7519
9088
  function normalizeCapabilities(providerId, values) {
7520
9089
  const requested = Array.isArray(values) && values.length > 0 ? values : providerId === SERIAL_HARDWARE_PROVIDER_ID ? DEFAULT_SERIAL_CAPABILITIES : [];
@@ -7542,18 +9111,18 @@ function normalizeDevices(values) {
7542
9111
  if (!trimmed) {
7543
9112
  continue;
7544
9113
  }
7545
- const resolved = path10.resolve(trimmed);
9114
+ const resolved = path13.resolve(trimmed);
7546
9115
  unique.set(resolved, {
7547
9116
  kind: "serial_device",
7548
9117
  id: resolved,
7549
9118
  path: resolved,
7550
- displayName: path10.basename(resolved)
9119
+ displayName: path13.basename(resolved)
7551
9120
  });
7552
9121
  }
7553
9122
  return [...unique.values()];
7554
9123
  }
7555
9124
  function printHardwareBinding(record2) {
7556
- console.log(kleur11.cyan(`Hardware provider: ${record2.providerId}`));
9125
+ console.log(kleur12.cyan(`Hardware provider: ${record2.providerId}`));
7557
9126
  console.log(`Status: ${record2.status}`);
7558
9127
  console.log(`Project: ${record2.projectId}`);
7559
9128
  console.log(`Capabilities: ${record2.grantedCapabilities.join(", ") || "(none)"}`);
@@ -7599,10 +9168,10 @@ function showHardwareBinding(options) {
7599
9168
  return;
7600
9169
  }
7601
9170
  if (bindings.length === 0) {
7602
- console.log(kleur11.yellow("No hardware bindings recorded for this space."));
9171
+ console.log(kleur12.yellow("No hardware bindings recorded for this space."));
7603
9172
  return;
7604
9173
  }
7605
- console.log(kleur11.cyan(`Hardware bindings for ${context.projectId}:`));
9174
+ console.log(kleur12.cyan(`Hardware bindings for ${context.projectId}:`));
7606
9175
  for (const record2 of bindings) {
7607
9176
  console.log(`- ${record2.providerId} (${record2.status})`);
7608
9177
  }
@@ -7635,9 +9204,9 @@ function grantHardwareBinding(options) {
7635
9204
  console.log(JSON.stringify(record2));
7636
9205
  return;
7637
9206
  }
7638
- console.log(kleur11.green(`Hardware binding granted: ${providerId}`));
9207
+ console.log(kleur12.green(`Hardware binding granted: ${providerId}`));
7639
9208
  printHardwareBinding(record2);
7640
- console.log(kleur11.cyan(`Stored at ${context.bindingsPath}`));
9209
+ console.log(kleur12.cyan(`Stored at ${context.bindingsPath}`));
7641
9210
  }
7642
9211
  function revokeHardwareBinding(options) {
7643
9212
  const providerId = options.providerId.trim();
@@ -7656,12 +9225,12 @@ function revokeHardwareBinding(options) {
7656
9225
  console.log(JSON.stringify({ revoked: true, providerId }));
7657
9226
  return;
7658
9227
  }
7659
- console.log(kleur11.green(`Hardware binding revoked: ${providerId}`));
7660
- console.log(kleur11.cyan(`Updated ${context.bindingsPath}`));
9228
+ console.log(kleur12.green(`Hardware binding revoked: ${providerId}`));
9229
+ console.log(kleur12.cyan(`Updated ${context.bindingsPath}`));
7661
9230
  }
7662
9231
 
7663
9232
  // src/agents.ts
7664
- import kleur12 from "kleur";
9233
+ import kleur13 from "kleur";
7665
9234
  init_project_manifest();
7666
9235
  var DEFAULT_CONTEXT_LIMIT = 50;
7667
9236
  var MAX_CONTEXT_LIMIT = 200;
@@ -7770,20 +9339,107 @@ async function agentsList(options) {
7770
9339
  return;
7771
9340
  }
7772
9341
  if (!agents.length) {
7773
- console.log(kleur12.yellow("No agents found."));
9342
+ console.log(kleur13.yellow("No agents found."));
7774
9343
  return;
7775
9344
  }
7776
- console.log(kleur12.bold("Agents"));
9345
+ console.log(kleur13.bold("Agents"));
7777
9346
  for (const agent of agents) {
7778
9347
  const provider = trimOrNull2(agent.provider);
7779
9348
  const model = trimOrNull2(agent.model);
7780
9349
  const details = [provider, model].filter(Boolean).join(" \xB7 ");
7781
9350
  console.log(
7782
- `- ${kleur12.cyan(`@${agent.handle}`)} ${displayName(agent)}${details ? ` (${details})` : ""}`
9351
+ `- ${kleur13.cyan(`@${agent.handle}`)} ${displayName(agent)}${details ? ` (${details})` : ""}`
7783
9352
  );
7784
9353
  console.log(` id: ${agent.id}`);
7785
9354
  }
7786
9355
  }
9356
+ var agentSettingFields = [
9357
+ { key: "maxAgentsPerPlan", flag: "--max-agents-per-plan", label: "Maximum agents per plan" },
9358
+ { key: "maxConcurrentAgents", flag: "--max-concurrent-agents", label: "Maximum concurrent team workers across this space" }
9359
+ ];
9360
+ function parseAgentSetting(value, label) {
9361
+ if (!/^[1-9]\d*$/.test(value.trim()) || !Number.isSafeInteger(Number(value))) {
9362
+ throw new Error(`${label} must be a positive whole number.`);
9363
+ }
9364
+ return Number(value);
9365
+ }
9366
+ async function agentSettings(options) {
9367
+ const patch = {};
9368
+ for (const field of agentSettingFields) {
9369
+ if (options[field.key] !== void 0) {
9370
+ patch[field.key] = parseAgentSetting(options[field.key], field.flag);
9371
+ }
9372
+ }
9373
+ if (options.interactive) {
9374
+ if (options.json || Object.keys(patch).length) {
9375
+ throw new Error("Use --interactive without --json or setting flags.");
9376
+ }
9377
+ if (!process.stdin.isTTY || !process.stdout.isTTY || process.env.CI === "true") {
9378
+ throw new Error("--interactive requires a terminal. Use --max-agents-per-plan or --max-concurrent-agents instead.");
9379
+ }
9380
+ }
9381
+ const projectId2 = resolveProjectId6(options.project);
9382
+ const request = {
9383
+ path: `/projects/${encodeURIComponent(projectId2)}/multi-agent-settings`,
9384
+ controllerUrl: options.controllerUrl,
9385
+ accessToken: options.accessToken,
9386
+ serviceToken: options.serviceToken
9387
+ };
9388
+ let settings = await requestControllerApiJson({ ...request, method: "GET" });
9389
+ if ((options.interactive || Object.keys(patch).length) && !settings.canManage) {
9390
+ throw new Error("You do not have permission to manage this space's multi-agent settings.");
9391
+ }
9392
+ if (options.interactive) {
9393
+ const { isCancel, text: text2 } = await import("@clack/prompts");
9394
+ console.log("These settings are shared by everyone in this space. Runtime capacity can limit concurrency further; lead and root jobs are excluded.");
9395
+ for (const field of agentSettingFields) {
9396
+ const bounds = settings.limits[field.key];
9397
+ const entered = await text2({
9398
+ message: `${field.label} (${bounds.min}\u2013${bounds.max}; default ${bounds.default})`,
9399
+ placeholder: String(settings[field.key]),
9400
+ defaultValue: String(settings[field.key]),
9401
+ validate(value2) {
9402
+ try {
9403
+ const parsed = parseAgentSetting(value2 || String(settings[field.key]), field.label);
9404
+ if (parsed < bounds.min || parsed > bounds.max) {
9405
+ return `Enter a whole number from ${bounds.min} to ${bounds.max}.`;
9406
+ }
9407
+ } catch (error) {
9408
+ return error.message;
9409
+ }
9410
+ }
9411
+ });
9412
+ if (isCancel(entered)) throw new Error("Cancelled. No settings were changed.");
9413
+ const value = parseAgentSetting(String(entered), field.label);
9414
+ if (value !== settings[field.key]) patch[field.key] = value;
9415
+ }
9416
+ }
9417
+ for (const field of agentSettingFields) {
9418
+ const value = patch[field.key];
9419
+ const bounds = settings.limits[field.key];
9420
+ if (value !== void 0 && (value < bounds.min || value > bounds.max)) {
9421
+ throw new Error(`${field.flag} must be between ${bounds.min} and ${bounds.max}.`);
9422
+ }
9423
+ }
9424
+ if (Object.keys(patch).length) {
9425
+ settings = await requestControllerApiJson({
9426
+ ...request,
9427
+ method: "PATCH",
9428
+ jsonBody: patch
9429
+ });
9430
+ }
9431
+ if (options.json) {
9432
+ console.log(JSON.stringify(settings, null, 2));
9433
+ return;
9434
+ }
9435
+ console.log(kleur13.bold(`Multi-agent settings for space ${projectId2}`));
9436
+ for (const field of agentSettingFields) {
9437
+ const bounds = settings.limits[field.key];
9438
+ console.log(`${field.label}: ${settings[field.key]} (range ${bounds.min}\u2013${bounds.max}; default ${bounds.default})`);
9439
+ }
9440
+ console.log("Shared across this space. Lead/root jobs are excluded from the concurrent worker limit; runtime capacity can limit concurrency further.");
9441
+ if (!settings.canManage) console.log("Read-only: your role cannot change these settings.");
9442
+ }
7787
9443
  async function agentContextsList(options) {
7788
9444
  const projectId2 = resolveProjectId6(options.project);
7789
9445
  const contexts = await requestControllerApiJson({
@@ -7799,15 +9455,15 @@ async function agentContextsList(options) {
7799
9455
  return;
7800
9456
  }
7801
9457
  if (!contexts.length) {
7802
- console.log(kleur12.yellow("No agent context cards found."));
9458
+ console.log(kleur13.yellow("No agent context cards found."));
7803
9459
  return;
7804
9460
  }
7805
- console.log(kleur12.bold("Agent context"));
9461
+ console.log(kleur13.bold("Agent context"));
7806
9462
  for (const card of contexts) {
7807
9463
  const title = trimOrNull2(card.title);
7808
9464
  const updatedAt = trimOrNull2(card.updatedAt) ?? trimOrNull2(card.updated_at);
7809
9465
  console.log(
7810
- `- ${kleur12.cyan(contextAgentLabel(card))} ${kleur12.gray(`[${contextScope(card)}]`)}${title ? ` ${title}` : ""}`
9466
+ `- ${kleur13.cyan(contextAgentLabel(card))} ${kleur13.gray(`[${contextScope(card)}]`)}${title ? ` ${title}` : ""}`
7811
9467
  );
7812
9468
  console.log(` ${summarize(card.context)}`);
7813
9469
  if (updatedAt) {
@@ -7844,7 +9500,7 @@ async function agentContextPut(options) {
7844
9500
  return;
7845
9501
  }
7846
9502
  console.log(
7847
- `${kleur12.green("Saved")} ${kleur12.cyan(contextAgentLabel(saved))} context for ${kleur12.gray(contextScope(saved))}.`
9503
+ `${kleur13.green("Saved")} ${kleur13.cyan(contextAgentLabel(saved))} context for ${kleur13.gray(contextScope(saved))}.`
7848
9504
  );
7849
9505
  }
7850
9506
  function workerLabel(worker) {
@@ -7870,7 +9526,7 @@ async function agentPlanGroupStatus(options) {
7870
9526
  console.log(JSON.stringify(status, null, 2));
7871
9527
  return;
7872
9528
  }
7873
- console.log(kleur12.bold(`Plan group ${trimOrNull2(status.groupId) ?? groupId}`));
9529
+ console.log(kleur13.bold(`Plan group ${trimOrNull2(status.groupId) ?? groupId}`));
7874
9530
  const conversationId = trimOrNull2(status.conversationId);
7875
9531
  if (conversationId) {
7876
9532
  console.log(` conversation: ${conversationId}`);
@@ -7880,13 +9536,13 @@ async function agentPlanGroupStatus(options) {
7880
9536
  );
7881
9537
  const workers = status.workers ?? [];
7882
9538
  if (!workers.length) {
7883
- console.log(kleur12.yellow("No worker jobs found."));
9539
+ console.log(kleur13.yellow("No worker jobs found."));
7884
9540
  return;
7885
9541
  }
7886
9542
  for (const worker of workers) {
7887
9543
  const marker = worker.terminal ? "terminal" : "running";
7888
9544
  const details = [trimOrNull2(worker.status) ?? "unknown", marker, trimOrNull2(worker.outcome)].filter(Boolean).join(" \xB7 ");
7889
- console.log(`- ${kleur12.cyan(workerLabel(worker))} ${kleur12.gray(`[${details}]`)}`);
9545
+ console.log(`- ${kleur13.cyan(workerLabel(worker))} ${kleur13.gray(`[${details}]`)}`);
7890
9546
  const scope = trimOrNull2(worker.scopeSummary);
7891
9547
  if (scope) {
7892
9548
  console.log(` scope: ${scope}`);
@@ -7897,7 +9553,7 @@ async function agentPlanGroupStatus(options) {
7897
9553
  }
7898
9554
  const errorMessage = trimOrNull2(worker.errorMessage);
7899
9555
  if (errorMessage) {
7900
- console.log(` ${kleur12.red(`error: ${summarize(errorMessage)}`)}`);
9556
+ console.log(` ${kleur13.red(`error: ${summarize(errorMessage)}`)}`);
7901
9557
  }
7902
9558
  }
7903
9559
  }
@@ -7924,11 +9580,11 @@ async function agentJobsCancel(options) {
7924
9580
  const canceledJobs = result.canceledJobIds?.length ?? 0;
7925
9581
  const canceledRuns = result.canceledRunIds?.length ?? 0;
7926
9582
  if (!canceledJobs && !canceledRuns) {
7927
- console.log(kleur12.yellow(`Nothing to cancel for ${target} (no queued or leased jobs).`));
9583
+ console.log(kleur13.yellow(`Nothing to cancel for ${target} (no queued or leased jobs).`));
7928
9584
  return;
7929
9585
  }
7930
9586
  console.log(
7931
- `${kleur12.green("Canceled")} ${canceledJobs} job(s) and ${canceledRuns} run(s) for ${target}.`
9587
+ `${kleur13.green("Canceled")} ${canceledJobs} job(s) and ${canceledRuns} run(s) for ${target}.`
7932
9588
  );
7933
9589
  }
7934
9590
 
@@ -8080,7 +9736,7 @@ function isAssistantReplyCandidate(message, runId, options) {
8080
9736
  }
8081
9737
  return typeof message.content === "string" && message.content.trim().length > 0;
8082
9738
  }
8083
- async function sleep(ms) {
9739
+ async function sleep2(ms) {
8084
9740
  await new Promise((resolve) => setTimeout(resolve, ms));
8085
9741
  }
8086
9742
  async function dispatchPrompt(options) {
@@ -8188,7 +9844,7 @@ async function waitForAssistantReply(conversationId, runId, options) {
8188
9844
  break;
8189
9845
  }
8190
9846
  }
8191
- await sleep(pollMs);
9847
+ await sleep2(pollMs);
8192
9848
  }
8193
9849
  const finalMessages = await fetchConversationMessages2(conversationId, options);
8194
9850
  const assistant = findLatestAssistantReply(finalMessages, runId, options);
@@ -8234,8 +9890,8 @@ async function chatPrompt(options) {
8234
9890
  }
8235
9891
 
8236
9892
  // src/recommendations.ts
8237
- import fs11 from "node:fs";
8238
- import path11 from "node:path";
9893
+ import fs14 from "node:fs";
9894
+ import path14 from "node:path";
8239
9895
  init_project_manifest();
8240
9896
  var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
8241
9897
  var MAX_FILE_BYTES = 65536;
@@ -8264,26 +9920,26 @@ function text(value, label, max) {
8264
9920
  }
8265
9921
  function readProposalFile(file) {
8266
9922
  const manifestPath = findProjectManifest(process.cwd()).path;
8267
- const root = fs11.realpathSync(manifestPath ? path11.dirname(path11.dirname(manifestPath)) : process.cwd());
8268
- const selected = path11.resolve(file);
8269
- const canonical = path11.join(fs11.realpathSync(path11.dirname(selected)), path11.basename(selected));
8270
- const relative = path11.relative(root, canonical);
8271
- if (relative === ".." || relative.startsWith(`..${path11.sep}`) || path11.isAbsolute(relative)) {
9923
+ const root = fs14.realpathSync(manifestPath ? path14.dirname(path14.dirname(manifestPath)) : process.cwd());
9924
+ const selected = path14.resolve(file);
9925
+ const canonical = path14.join(fs14.realpathSync(path14.dirname(selected)), path14.basename(selected));
9926
+ const relative = path14.relative(root, canonical);
9927
+ if (relative === ".." || relative.startsWith(`..${path14.sep}`) || path14.isAbsolute(relative)) {
8272
9928
  throw new Error("Recommendation file must stay inside the active Instafy workspace.");
8273
9929
  }
8274
- const initial = fs11.lstatSync(canonical);
9930
+ const initial = fs14.lstatSync(canonical);
8275
9931
  if (!initial.isFile() || initial.isSymbolicLink()) throw new Error("Recommendation file must be a regular file, not a symlink.");
8276
- const descriptor = fs11.openSync(canonical, fs11.constants.O_RDONLY | (fs11.constants.O_NOFOLLOW ?? 0));
9932
+ const descriptor = fs14.openSync(canonical, fs14.constants.O_RDONLY | (fs14.constants.O_NOFOLLOW ?? 0));
8277
9933
  let bytes;
8278
9934
  try {
8279
- const opened = fs11.fstatSync(descriptor);
9935
+ const opened = fs14.fstatSync(descriptor);
8280
9936
  if (!opened.isFile() || opened.dev !== initial.dev || opened.ino !== initial.ino || opened.size > MAX_FILE_BYTES) {
8281
9937
  throw new Error(`Recommendation file must be a regular file no larger than ${MAX_FILE_BYTES} bytes.`);
8282
9938
  }
8283
- bytes = fs11.readFileSync(descriptor);
9939
+ bytes = fs14.readFileSync(descriptor);
8284
9940
  if (bytes.length > MAX_FILE_BYTES) throw new Error(`Recommendation file must be ${MAX_FILE_BYTES} bytes or smaller.`);
8285
9941
  } finally {
8286
- fs11.closeSync(descriptor);
9942
+ fs14.closeSync(descriptor);
8287
9943
  }
8288
9944
  return bytes;
8289
9945
  }
@@ -8615,11 +10271,11 @@ async function showConversationContext(options) {
8615
10271
  }
8616
10272
 
8617
10273
  // src/invitations.ts
8618
- import kleur13 from "kleur";
8619
- import path12 from "node:path";
10274
+ import kleur14 from "kleur";
10275
+ import path15 from "node:path";
8620
10276
  init_project_manifest();
8621
10277
  function resolveStartDir(rawPath) {
8622
- return path12.resolve(rawPath ?? process.cwd());
10278
+ return path15.resolve(rawPath ?? process.cwd());
8623
10279
  }
8624
10280
  function trimOrNull3(value) {
8625
10281
  const trimmed = value?.trim() ?? "";
@@ -8761,7 +10417,7 @@ async function inviteSpaceMember(options) {
8761
10417
  return normalized;
8762
10418
  }
8763
10419
  const scopeLabel = orgName ? `${orgName}` : "this space";
8764
- console.log(kleur13.green(`Invitation sent to ${normalized.email}`));
10420
+ console.log(kleur14.green(`Invitation sent to ${normalized.email}`));
8765
10421
  console.log(`Scope: ${scopeLabel}`);
8766
10422
  console.log(`Role: ${normalized.role}`);
8767
10423
  if (normalized.expiresAt) {
@@ -8821,7 +10477,7 @@ async function setSpaceMemberRole(options) {
8821
10477
  return normalized;
8822
10478
  }
8823
10479
  console.log(
8824
- kleur13.green(
10480
+ kleur14.green(
8825
10481
  normalized.status === "updated" ? `Updated ${normalized.email} to ${normalized.role}` : `${normalized.email} already has role ${normalized.role}`
8826
10482
  )
8827
10483
  );
@@ -8860,7 +10516,7 @@ async function setSpaceMemberRole(options) {
8860
10516
  return normalized;
8861
10517
  }
8862
10518
  console.log(
8863
- kleur13.green(
10519
+ kleur14.green(
8864
10520
  normalized.status === "updated" ? `Updated pending invite for ${normalized.email} to ${normalized.role}` : `Pending invite for ${normalized.email} already has role ${normalized.role}`
8865
10521
  )
8866
10522
  );
@@ -8910,7 +10566,7 @@ async function sendSpaceInvitation(options) {
8910
10566
  init_config();
8911
10567
  init_controller_fetch();
8912
10568
  init_errors();
8913
- import kleur14 from "kleur";
10569
+ import kleur15 from "kleur";
8914
10570
  var DEFAULT_STUDIO_URL = "https://instafy.dev";
8915
10571
  var ORG_ROLES = ["owner", "admin", "builder", "viewer"];
8916
10572
  var INVITE_LINK_ROLES = ["builder", "viewer"];
@@ -9065,7 +10721,7 @@ function printTable(headers, rows) {
9065
10721
  (header, index) => Math.max(header.length, ...rows.map((row) => (row[index] ?? "").length))
9066
10722
  );
9067
10723
  const format = (cells) => cells.map((cell, index) => (cell ?? "").padEnd(widths[index])).join(" ").trimEnd();
9068
- console.log(kleur14.bold(format(headers)));
10724
+ console.log(kleur15.bold(format(headers)));
9069
10725
  for (const row of rows) {
9070
10726
  console.log(format(row));
9071
10727
  }
@@ -9094,7 +10750,7 @@ async function teamMembersList(options) {
9094
10750
  return;
9095
10751
  }
9096
10752
  if (members.length === 0) {
9097
- console.log(kleur14.gray("No members."));
10753
+ console.log(kleur15.gray("No members."));
9098
10754
  return;
9099
10755
  }
9100
10756
  const rows = members.map((member) => [
@@ -9150,7 +10806,7 @@ If they already have an Instafy account, add them directly with:
9150
10806
  );
9151
10807
  return;
9152
10808
  }
9153
- console.log(kleur14.green(`Invitation sent to ${invitation.email}`));
10809
+ console.log(kleur15.green(`Invitation sent to ${invitation.email}`));
9154
10810
  console.log(`Role: ${invitation.role}`);
9155
10811
  if (invitation.expiresAt) {
9156
10812
  console.log(`Expires: ${invitation.expiresAt}`);
@@ -9181,7 +10837,7 @@ async function teamInviteLink(options) {
9181
10837
  console.log(JSON.stringify({ inviteLink, acceptUrl }, null, 2));
9182
10838
  return;
9183
10839
  }
9184
- console.log(kleur14.green("Shareable invite link created."));
10840
+ console.log(kleur15.green("Shareable invite link created."));
9185
10841
  console.log(`Role: ${inviteLink.role}`);
9186
10842
  console.log(`Link: ${acceptUrl}`);
9187
10843
  console.log(`Token: ${inviteLink.token}`);
@@ -9213,11 +10869,11 @@ async function teamInvites(options) {
9213
10869
  return;
9214
10870
  }
9215
10871
  if (invitations.length === 0 && inviteLinks.length === 0) {
9216
- console.log(kleur14.gray("No pending invitations or invite links."));
10872
+ console.log(kleur15.gray("No pending invitations or invite links."));
9217
10873
  return;
9218
10874
  }
9219
10875
  if (invitations.length > 0) {
9220
- console.log(kleur14.bold("Email invitations"));
10876
+ console.log(kleur15.bold("Email invitations"));
9221
10877
  const rows = invitations.map((invitation) => [
9222
10878
  shortId(invitation.id),
9223
10879
  invitation.email,
@@ -9228,7 +10884,7 @@ async function teamInvites(options) {
9228
10884
  }
9229
10885
  if (inviteLinks.length > 0) {
9230
10886
  if (invitations.length > 0) console.log("");
9231
- console.log(kleur14.bold("Invite links (shareable, grant join on sign-in)"));
10887
+ console.log(kleur15.bold("Invite links (shareable, grant join on sign-in)"));
9232
10888
  const rows = inviteLinks.map((link) => [
9233
10889
  shortId(link.id),
9234
10890
  link.role,
@@ -9259,7 +10915,7 @@ async function teamAddMember(options) {
9259
10915
  return;
9260
10916
  }
9261
10917
  const label = (member.email ?? "").trim() || member.userId;
9262
- console.log(kleur14.green(`Added ${label} as ${member.role}.`));
10918
+ console.log(kleur15.green(`Added ${label} as ${member.role}.`));
9263
10919
  }
9264
10920
  async function teamAccept(options) {
9265
10921
  const auth = resolveControllerAuth4(options, "instafy team accept");
@@ -9274,7 +10930,7 @@ async function teamAccept(options) {
9274
10930
  return;
9275
10931
  }
9276
10932
  const name = (payload?.orgName ?? "").trim() || payload?.orgSlug || payload?.orgId || "team";
9277
- console.log(kleur14.green(`Joined ${name} as ${payload?.role ?? "member"}.`));
10933
+ console.log(kleur15.green(`Joined ${name} as ${payload?.role ?? "member"}.`));
9278
10934
  }
9279
10935
  async function confirmDestructive(message, yes, json) {
9280
10936
  if (yes) return;
@@ -9308,7 +10964,7 @@ async function teamRevokeInvite(options) {
9308
10964
  console.log(JSON.stringify({ ok: true, invitationId }, null, 2));
9309
10965
  return;
9310
10966
  }
9311
- console.log(kleur14.green(`Revoked invitation ${invitationId}.`));
10967
+ console.log(kleur15.green(`Revoked invitation ${invitationId}.`));
9312
10968
  }
9313
10969
  async function teamRevokeLink(options) {
9314
10970
  const auth = resolveControllerAuth4(options, "instafy team revoke-link");
@@ -9327,17 +10983,17 @@ async function teamRevokeLink(options) {
9327
10983
  console.log(JSON.stringify({ ok: true, inviteLinkId }, null, 2));
9328
10984
  return;
9329
10985
  }
9330
- console.log(kleur14.green(`Revoked invite link ${inviteLinkId}.`));
10986
+ console.log(kleur15.green(`Revoked invite link ${inviteLinkId}.`));
9331
10987
  }
9332
10988
 
9333
10989
  // src/provider-bindings.ts
9334
10990
  init_project_manifest();
9335
- import fs12 from "node:fs";
9336
- import path13 from "node:path";
10991
+ import fs15 from "node:fs";
10992
+ import path16 from "node:path";
9337
10993
  import { pathToFileURL } from "node:url";
9338
- import kleur15 from "kleur";
10994
+ import kleur16 from "kleur";
9339
10995
  function resolveBindingContext(startDir) {
9340
- const lookup = findProjectManifest(path13.resolve(startDir));
10996
+ const lookup = findProjectManifest(path16.resolve(startDir));
9341
10997
  if (!lookup.manifest || !lookup.path) {
9342
10998
  throw new Error(
9343
10999
  "Linked space manifest required. Run `instafy space init` in this folder or pass --path <dir>."
@@ -9347,19 +11003,19 @@ function resolveBindingContext(startDir) {
9347
11003
  if (!projectId2) {
9348
11004
  throw new Error("Linked space manifest is missing a spaceId.");
9349
11005
  }
9350
- const instafyDir = path13.dirname(lookup.path);
9351
- const projectRoot = path13.dirname(instafyDir);
11006
+ const instafyDir2 = path16.dirname(lookup.path);
11007
+ const projectRoot = path16.dirname(instafyDir2);
9352
11008
  return {
9353
- bindingsPath: path13.join(instafyDir, "provider-bindings.json"),
11009
+ bindingsPath: path16.join(instafyDir2, "provider-bindings.json"),
9354
11010
  projectId: projectId2,
9355
11011
  projectRoot
9356
11012
  };
9357
11013
  }
9358
11014
  function readBindingStore(bindingsPath) {
9359
- if (!fs12.existsSync(bindingsPath)) {
11015
+ if (!fs15.existsSync(bindingsPath)) {
9360
11016
  return { version: 1, bindings: {} };
9361
11017
  }
9362
- const raw = JSON.parse(fs12.readFileSync(bindingsPath, "utf8"));
11018
+ const raw = JSON.parse(fs15.readFileSync(bindingsPath, "utf8"));
9363
11019
  const bindings = raw && typeof raw.bindings === "object" && raw.bindings !== null ? raw.bindings : {};
9364
11020
  return {
9365
11021
  version: 1,
@@ -9367,8 +11023,8 @@ function readBindingStore(bindingsPath) {
9367
11023
  };
9368
11024
  }
9369
11025
  function writeBindingStore(bindingsPath, store) {
9370
- fs12.mkdirSync(path13.dirname(bindingsPath), { recursive: true });
9371
- fs12.writeFileSync(bindingsPath, JSON.stringify(store, null, 2), "utf8");
11026
+ fs15.mkdirSync(path16.dirname(bindingsPath), { recursive: true });
11027
+ fs15.writeFileSync(bindingsPath, JSON.stringify(store, null, 2), "utf8");
9372
11028
  }
9373
11029
  function normalizeCapabilities2(values) {
9374
11030
  const requested = Array.isArray(values) && values.length > 0 ? values : ["project_content_read", "project_content_write"];
@@ -9391,7 +11047,7 @@ function deriveStatus(capabilities) {
9391
11047
  return "bound_read_only";
9392
11048
  }
9393
11049
  function printBinding(record2) {
9394
- console.log(kleur15.cyan(`Provider: ${record2.providerId}`));
11050
+ console.log(kleur16.cyan(`Provider: ${record2.providerId}`));
9395
11051
  console.log(`Status: ${record2.status}`);
9396
11052
  console.log(`Project: ${record2.projectId}`);
9397
11053
  console.log(`Root URI: ${record2.rootUri}`);
@@ -9433,10 +11089,10 @@ function showProviderBinding(options) {
9433
11089
  return;
9434
11090
  }
9435
11091
  if (bindings.length === 0) {
9436
- console.log(kleur15.yellow("No provider bindings recorded for this space."));
11092
+ console.log(kleur16.yellow("No provider bindings recorded for this space."));
9437
11093
  return;
9438
11094
  }
9439
- console.log(kleur15.cyan(`Bindings for ${context.projectId}:`));
11095
+ console.log(kleur16.cyan(`Bindings for ${context.projectId}:`));
9440
11096
  for (const record2 of bindings) {
9441
11097
  console.log(`- ${record2.providerId} (${record2.status})`);
9442
11098
  }
@@ -9468,9 +11124,9 @@ function grantProviderBinding(options) {
9468
11124
  console.log(JSON.stringify(record2));
9469
11125
  return;
9470
11126
  }
9471
- console.log(kleur15.green(`Provider binding granted: ${providerId}`));
11127
+ console.log(kleur16.green(`Provider binding granted: ${providerId}`));
9472
11128
  printBinding(record2);
9473
- console.log(kleur15.cyan(`Stored at ${context.bindingsPath}`));
11129
+ console.log(kleur16.cyan(`Stored at ${context.bindingsPath}`));
9474
11130
  }
9475
11131
  function revokeProviderBinding(options) {
9476
11132
  const providerId = options.providerId.trim();
@@ -9489,12 +11145,12 @@ function revokeProviderBinding(options) {
9489
11145
  console.log(JSON.stringify({ revoked: true, providerId }));
9490
11146
  return;
9491
11147
  }
9492
- console.log(kleur15.green(`Provider binding revoked: ${providerId}`));
9493
- console.log(kleur15.cyan(`Updated ${context.bindingsPath}`));
11148
+ console.log(kleur16.green(`Provider binding revoked: ${providerId}`));
11149
+ console.log(kleur16.cyan(`Updated ${context.bindingsPath}`));
9494
11150
  }
9495
11151
 
9496
11152
  // src/providers.ts
9497
- import kleur16 from "kleur";
11153
+ import kleur17 from "kleur";
9498
11154
 
9499
11155
  // ../provider-client/index.js
9500
11156
  var DEFAULT_PROVIDER_HOST_BASE_URL = "http://127.0.0.1:8797";
@@ -9566,12 +11222,12 @@ function resolveFetchImplementation(fetchImpl) {
9566
11222
  function createProviderHostClient(options = {}) {
9567
11223
  const fetchImpl = resolveFetchImplementation(options.fetch);
9568
11224
  const baseUrl = normalizeProviderHostBaseUrl(options.baseUrl);
9569
- async function jsonRequest(path16, init) {
9570
- const response = await fetchImpl(`${baseUrl}${path16}`, init);
11225
+ async function jsonRequest(path19, init) {
11226
+ const response = await fetchImpl(`${baseUrl}${path19}`, init);
9571
11227
  const json = await response.json();
9572
11228
  if (!response.ok || json?.ok === false) {
9573
11229
  throw new Error(
9574
- json?.error || json?.stderr || `provider host request failed for ${path16}`
11230
+ json?.error || json?.stderr || `provider host request failed for ${path19}`
9575
11231
  );
9576
11232
  }
9577
11233
  return json;
@@ -9664,7 +11320,7 @@ function formatProviderLabel(provider) {
9664
11320
  return provider.title?.trim().length ? provider.title.trim() : provider.id;
9665
11321
  }
9666
11322
  function printProviderSummary(provider) {
9667
- console.log(kleur16.green(formatProviderLabel(provider)));
11323
+ console.log(kleur17.green(formatProviderLabel(provider)));
9668
11324
  console.log(` id: ${provider.id}`);
9669
11325
  if (provider.description) {
9670
11326
  console.log(` description: ${provider.description}`);
@@ -9676,7 +11332,7 @@ function printProviderSummary(provider) {
9676
11332
  console.log(` type: ${provider.providerType}`);
9677
11333
  }
9678
11334
  console.log(
9679
- ` discoverable: ${provider.discoverable !== false ? kleur16.green("yes") : kleur16.yellow("no")}`
11335
+ ` discoverable: ${provider.discoverable !== false ? kleur17.green("yes") : kleur17.yellow("no")}`
9680
11336
  );
9681
11337
  if (provider.capabilityIds?.length) {
9682
11338
  console.log(` capabilities: ${provider.capabilityIds.join(", ")}`);
@@ -9688,7 +11344,7 @@ function printProviderSummary(provider) {
9688
11344
  console.log(` resources: ${provider.resourceUris.join(", ")}`);
9689
11345
  }
9690
11346
  if (provider.error) {
9691
- console.log(` error: ${kleur16.yellow(provider.error)}`);
11347
+ console.log(` error: ${kleur17.yellow(provider.error)}`);
9692
11348
  }
9693
11349
  }
9694
11350
  function parseCommandJson(commandJson) {
@@ -9708,10 +11364,10 @@ async function providersList(options = {}) {
9708
11364
  });
9709
11365
  return;
9710
11366
  }
9711
- console.log(kleur16.green("Local providers"));
11367
+ console.log(kleur17.green("Local providers"));
9712
11368
  console.log(`Host: ${providerHostUrl}`);
9713
11369
  if (!result.providers.length) {
9714
- console.log(kleur16.yellow("No providers discovered."));
11370
+ console.log(kleur17.yellow("No providers discovered."));
9715
11371
  return;
9716
11372
  }
9717
11373
  for (const provider of result.providers) {
@@ -9728,7 +11384,7 @@ async function providersDiscover(params) {
9728
11384
  });
9729
11385
  return;
9730
11386
  }
9731
- console.log(kleur16.green(`Provider discovery: ${params.providerId}`));
11387
+ console.log(kleur17.green(`Provider discovery: ${params.providerId}`));
9732
11388
  console.log(`Host: ${providerHostUrl}`);
9733
11389
  console.log(JSON.stringify(result.provider ?? result, null, 2));
9734
11390
  }
@@ -9742,10 +11398,10 @@ async function providersRead(params) {
9742
11398
  });
9743
11399
  return;
9744
11400
  }
9745
- console.log(kleur16.green(`Provider resource: ${params.uri}`));
11401
+ console.log(kleur17.green(`Provider resource: ${params.uri}`));
9746
11402
  console.log(`Host: ${providerHostUrl}`);
9747
11403
  if (result.exists === false) {
9748
- console.log(kleur16.yellow("Resource does not exist."));
11404
+ console.log(kleur17.yellow("Resource does not exist."));
9749
11405
  return;
9750
11406
  }
9751
11407
  console.log(JSON.stringify(result.value ?? result, null, 2));
@@ -9771,7 +11427,7 @@ async function providersProbe(params) {
9771
11427
  });
9772
11428
  return;
9773
11429
  }
9774
- console.log(kleur16.green(`Provider transport probe: ${params.providerId}`));
11430
+ console.log(kleur17.green(`Provider transport probe: ${params.providerId}`));
9775
11431
  console.log(`Host: ${providerHostUrl}`);
9776
11432
  console.log(JSON.stringify(result, null, 2));
9777
11433
  }
@@ -9873,9 +11529,9 @@ async function diagnosticsRunResult(options) {
9873
11529
  }
9874
11530
 
9875
11531
  // src/support.ts
9876
- import fs13 from "node:fs";
11532
+ import fs16 from "node:fs";
9877
11533
  import { randomUUID as randomUUID3 } from "node:crypto";
9878
- import path14 from "node:path";
11534
+ import path17 from "node:path";
9879
11535
  init_project_manifest();
9880
11536
  var MAX_SUMMARY_CHARS = 500;
9881
11537
  var MAX_DETAILS_BYTES = 2e4;
@@ -9924,34 +11580,34 @@ function requiredCursorUuid(value, flag) {
9924
11580
  return id;
9925
11581
  }
9926
11582
  function readRegularFile(filePath, maxBytes, label) {
9927
- const resolved = path14.resolve(filePath);
11583
+ const resolved = path17.resolve(filePath);
9928
11584
  const manifestPath = findProjectManifest(process.cwd()).path;
9929
- const workspaceRoot = manifestPath ? path14.dirname(path14.dirname(manifestPath)) : process.cwd();
9930
- const canonicalRoot = fs13.realpathSync(workspaceRoot);
9931
- const canonicalParent = fs13.realpathSync(path14.dirname(resolved));
9932
- const canonicalTarget = path14.join(canonicalParent, path14.basename(resolved));
9933
- const relativeTarget = path14.relative(canonicalRoot, canonicalTarget);
9934
- if (relativeTarget === ".." || relativeTarget.startsWith(`..${path14.sep}`) || path14.isAbsolute(relativeTarget)) {
11585
+ const workspaceRoot = manifestPath ? path17.dirname(path17.dirname(manifestPath)) : process.cwd();
11586
+ const canonicalRoot = fs16.realpathSync(workspaceRoot);
11587
+ const canonicalParent = fs16.realpathSync(path17.dirname(resolved));
11588
+ const canonicalTarget = path17.join(canonicalParent, path17.basename(resolved));
11589
+ const relativeTarget = path17.relative(canonicalRoot, canonicalTarget);
11590
+ if (relativeTarget === ".." || relativeTarget.startsWith(`..${path17.sep}`) || path17.isAbsolute(relativeTarget)) {
9935
11591
  throw new Error(`${label} must stay inside the active Instafy workspace.`);
9936
11592
  }
9937
- const initialStat = fs13.lstatSync(canonicalTarget);
11593
+ const initialStat = fs16.lstatSync(canonicalTarget);
9938
11594
  if (initialStat.isSymbolicLink() || !initialStat.isFile()) {
9939
11595
  throw new Error(`${label} must be a regular file, not a symlink.`);
9940
11596
  }
9941
- const noFollow = typeof fs13.constants.O_NOFOLLOW === "number" ? fs13.constants.O_NOFOLLOW : 0;
9942
- const descriptor = fs13.openSync(canonicalTarget, fs13.constants.O_RDONLY | noFollow);
11597
+ const noFollow = typeof fs16.constants.O_NOFOLLOW === "number" ? fs16.constants.O_NOFOLLOW : 0;
11598
+ const descriptor = fs16.openSync(canonicalTarget, fs16.constants.O_RDONLY | noFollow);
9943
11599
  try {
9944
- const openedStat = fs13.fstatSync(descriptor);
11600
+ const openedStat = fs16.fstatSync(descriptor);
9945
11601
  if (!openedStat.isFile() || openedStat.dev !== initialStat.dev || openedStat.ino !== initialStat.ino || openedStat.size > maxBytes) {
9946
11602
  throw new Error(`${label} must be a regular file no larger than ${maxBytes} bytes.`);
9947
11603
  }
9948
- const bytes = fs13.readFileSync(descriptor);
11604
+ const bytes = fs16.readFileSync(descriptor);
9949
11605
  if (bytes.length > maxBytes) {
9950
11606
  throw new Error(`${label} must be ${maxBytes} bytes or smaller.`);
9951
11607
  }
9952
11608
  return bytes;
9953
11609
  } finally {
9954
- fs13.closeSync(descriptor);
11610
+ fs16.closeSync(descriptor);
9955
11611
  }
9956
11612
  }
9957
11613
  function readJsonFile(filePath, maxBytes, label) {
@@ -10025,7 +11681,7 @@ function resolveScreenshots(filePaths = []) {
10025
11681
  );
10026
11682
  }
10027
11683
  return {
10028
- fileName: path14.basename(filePath),
11684
+ fileName: path17.basename(filePath),
10029
11685
  mediaType,
10030
11686
  dataBase64: bytes.toString("base64"),
10031
11687
  byteLength: bytes.length
@@ -10423,7 +12079,7 @@ function failWithGroupHelp(command) {
10423
12079
  process.exitCode = 1;
10424
12080
  });
10425
12081
  }
10426
- var require4 = createRequire3(import.meta.url);
12082
+ var require4 = createRequire4(import.meta.url);
10427
12083
  var pkg = require4("../package.json");
10428
12084
  function addServerUrlOptions(command) {
10429
12085
  return command.option(
@@ -10508,7 +12164,7 @@ program.command("login").description("Log in and save an access token for future
10508
12164
  json: opts.json
10509
12165
  });
10510
12166
  } catch (error) {
10511
- console.error(kleur19.red(String(error)));
12167
+ console.error(kleur20.red(String(error)));
10512
12168
  process.exit(1);
10513
12169
  }
10514
12170
  });
@@ -10516,7 +12172,7 @@ program.command("logout").description("Clear the saved CLI access token").option
10516
12172
  try {
10517
12173
  await logout({ json: opts.json, profile: opts.profile });
10518
12174
  } catch (error) {
10519
- console.error(kleur19.red(String(error)));
12175
+ console.error(kleur20.red(String(error)));
10520
12176
  process.exit(1);
10521
12177
  }
10522
12178
  });
@@ -10540,7 +12196,7 @@ chatCommand.option("--json", "Output JSON").action(async (promptParts, opts) =>
10540
12196
  json: opts.json
10541
12197
  });
10542
12198
  } catch (error) {
10543
- console.error(kleur19.red(String(error)));
12199
+ console.error(kleur20.red(String(error)));
10544
12200
  process.exit(1);
10545
12201
  }
10546
12202
  });
@@ -10564,7 +12220,7 @@ function registerSpaceCommand(command) {
10564
12220
  json: opts.json
10565
12221
  });
10566
12222
  } catch (error) {
10567
- console.error(kleur19.red(String(error)));
12223
+ console.error(kleur20.red(String(error)));
10568
12224
  process.exit(1);
10569
12225
  }
10570
12226
  });
@@ -10580,7 +12236,7 @@ function registerSpaceCommand(command) {
10580
12236
  json: opts.json
10581
12237
  });
10582
12238
  } catch (error) {
10583
- console.error(kleur19.red(String(error)));
12239
+ console.error(kleur20.red(String(error)));
10584
12240
  process.exit(1);
10585
12241
  }
10586
12242
  });
@@ -10594,7 +12250,7 @@ function registerSpaceCommand(command) {
10594
12250
  json: opts.json
10595
12251
  });
10596
12252
  } catch (error) {
10597
- console.error(kleur19.red(String(error)));
12253
+ console.error(kleur20.red(String(error)));
10598
12254
  process.exit(1);
10599
12255
  }
10600
12256
  });
@@ -10617,7 +12273,7 @@ function registerSpaceCommand(command) {
10617
12273
  json: opts.json
10618
12274
  });
10619
12275
  } catch (error) {
10620
- console.error(kleur19.red(String(error)));
12276
+ console.error(kleur20.red(String(error)));
10621
12277
  process.exit(1);
10622
12278
  }
10623
12279
  });
@@ -10629,7 +12285,7 @@ function registerSpaceCommand(command) {
10629
12285
  json: opts.json
10630
12286
  });
10631
12287
  } catch (error) {
10632
- console.error(kleur19.red(String(error)));
12288
+ console.error(kleur20.red(String(error)));
10633
12289
  process.exit(1);
10634
12290
  }
10635
12291
  });
@@ -10651,7 +12307,7 @@ function registerSpaceCommand(command) {
10651
12307
  json: opts.json
10652
12308
  });
10653
12309
  } catch (error) {
10654
- console.error(kleur19.red(String(error)));
12310
+ console.error(kleur20.red(String(error)));
10655
12311
  process.exit(1);
10656
12312
  }
10657
12313
  });
@@ -10667,7 +12323,7 @@ function registerSpaceCommand(command) {
10667
12323
  json: opts.json
10668
12324
  });
10669
12325
  } catch (error) {
10670
- console.error(kleur19.red(String(error)));
12326
+ console.error(kleur20.red(String(error)));
10671
12327
  process.exit(1);
10672
12328
  }
10673
12329
  });
@@ -10693,7 +12349,7 @@ function registerSpaceCommand(command) {
10693
12349
  json: opts.json
10694
12350
  });
10695
12351
  } catch (error) {
10696
- console.error(kleur19.red(String(error)));
12352
+ console.error(kleur20.red(String(error)));
10697
12353
  process.exit(1);
10698
12354
  }
10699
12355
  });
@@ -10718,7 +12374,7 @@ function registerSpaceCommand(command) {
10718
12374
  json: opts.json
10719
12375
  });
10720
12376
  } catch (error) {
10721
- console.error(kleur19.red(String(error)));
12377
+ console.error(kleur20.red(String(error)));
10722
12378
  process.exit(1);
10723
12379
  }
10724
12380
  });
@@ -10738,7 +12394,7 @@ hardwareBindingsCommand.command("show").description("Show hardware bindings for
10738
12394
  json: opts.json
10739
12395
  });
10740
12396
  } catch (error) {
10741
- console.error(kleur19.red(String(error)));
12397
+ console.error(kleur20.red(String(error)));
10742
12398
  process.exit(1);
10743
12399
  }
10744
12400
  });
@@ -10758,7 +12414,7 @@ hardwareBindingsCommand.command("grant").description("Grant a local hardware pro
10758
12414
  json: opts.json
10759
12415
  });
10760
12416
  } catch (error) {
10761
- console.error(kleur19.red(String(error)));
12417
+ console.error(kleur20.red(String(error)));
10762
12418
  process.exit(1);
10763
12419
  }
10764
12420
  });
@@ -10770,7 +12426,7 @@ hardwareBindingsCommand.command("revoke").description("Revoke a local hardware b
10770
12426
  json: opts.json
10771
12427
  });
10772
12428
  } catch (error) {
10773
- console.error(kleur19.red(String(error)));
12429
+ console.error(kleur20.red(String(error)));
10774
12430
  process.exit(1);
10775
12431
  }
10776
12432
  });
@@ -10784,7 +12440,7 @@ hardwareSerialCommand.command("list").description("List local serial devices vis
10784
12440
  scanDirs: opts.scanDir
10785
12441
  });
10786
12442
  } catch (error) {
10787
- console.error(kleur19.red(String(error)));
12443
+ console.error(kleur20.red(String(error)));
10788
12444
  process.exit(1);
10789
12445
  }
10790
12446
  });
@@ -10796,7 +12452,7 @@ hardwareSerialCommand.command("probe").description("Probe host permissions for o
10796
12452
  json: opts.json
10797
12453
  });
10798
12454
  } catch (error) {
10799
- console.error(kleur19.red(String(error)));
12455
+ console.error(kleur20.red(String(error)));
10800
12456
  process.exit(1);
10801
12457
  }
10802
12458
  });
@@ -10808,7 +12464,7 @@ hardwareCommand.command("opportunities").description("List host-local IO opportu
10808
12464
  scanDirs: opts.scanDir
10809
12465
  });
10810
12466
  } catch (error) {
10811
- console.error(kleur19.red(String(error)));
12467
+ console.error(kleur20.red(String(error)));
10812
12468
  process.exit(1);
10813
12469
  }
10814
12470
  });
@@ -10821,7 +12477,7 @@ hardwareCommand.command("run").description("Run one available host-local IO acti
10821
12477
  json: opts.json
10822
12478
  });
10823
12479
  } catch (error) {
10824
- console.error(kleur19.red(String(error)));
12480
+ console.error(kleur20.red(String(error)));
10825
12481
  process.exit(1);
10826
12482
  }
10827
12483
  });
@@ -10836,7 +12492,7 @@ providersListCommand.option("--json", "Output JSON").action(async (opts) => {
10836
12492
  json: opts.json
10837
12493
  });
10838
12494
  } catch (error) {
10839
- console.error(kleur19.red(String(error)));
12495
+ console.error(kleur20.red(String(error)));
10840
12496
  process.exit(1);
10841
12497
  }
10842
12498
  });
@@ -10850,7 +12506,7 @@ providersDiscoverCommand.option("--json", "Output JSON").action(async (providerI
10850
12506
  json: opts.json
10851
12507
  });
10852
12508
  } catch (error) {
10853
- console.error(kleur19.red(String(error)));
12509
+ console.error(kleur20.red(String(error)));
10854
12510
  process.exit(1);
10855
12511
  }
10856
12512
  });
@@ -10865,7 +12521,7 @@ providersReadCommand.option("--json", "Output JSON").action(async (providerId, u
10865
12521
  json: opts.json
10866
12522
  });
10867
12523
  } catch (error) {
10868
- console.error(kleur19.red(String(error)));
12524
+ console.error(kleur20.red(String(error)));
10869
12525
  process.exit(1);
10870
12526
  }
10871
12527
  });
@@ -10888,7 +12544,7 @@ providersProbeCommand.option("--json", "Output JSON").action(async (providerId,
10888
12544
  json: opts.json
10889
12545
  });
10890
12546
  } catch (error) {
10891
- console.error(kleur19.red(String(error)));
12547
+ console.error(kleur20.red(String(error)));
10892
12548
  process.exit(1);
10893
12549
  }
10894
12550
  });
@@ -10908,7 +12564,7 @@ for (const operation of ["list", "submit"]) {
10908
12564
  if (operation === "list") await recommendationsList({ ...common, limit: opts.limit });
10909
12565
  else await recommendationsSubmit({ ...common, file: opts.file });
10910
12566
  } catch (error) {
10911
- console.error(kleur19.red(String(error)));
12567
+ console.error(kleur20.red(String(error)));
10912
12568
  process.exit(1);
10913
12569
  }
10914
12570
  });
@@ -10932,13 +12588,33 @@ for (const operation of ["current", "dismiss", "remind"]) {
10932
12588
  else if (operation === "dismiss") await recommendationsDismiss(common);
10933
12589
  else await recommendationsRemind({ ...common, at: opts.at, timezone: opts.timezone });
10934
12590
  } catch (error) {
10935
- console.error(kleur19.red(String(error)));
12591
+ console.error(kleur20.red(String(error)));
10936
12592
  process.exit(1);
10937
12593
  }
10938
12594
  });
10939
12595
  }
10940
- var agentsCommand = program.command("agents").description("Inspect agents and scoped agent context");
12596
+ var agentsCommand = program.command("agents").description("Inspect agents, shared multi-agent settings, and scoped agent context");
10941
12597
  failWithGroupHelp(agentsCommand);
12598
+ var agentsSettingsCommand = agentsCommand.command("settings").description("Inspect or change the space's shared multi-agent planning and concurrency limits");
12599
+ addSpaceOption(agentsSettingsCommand, "Space UUID (defaults to .instafy/space.json or SPACE_ID)");
12600
+ addServerUrlOptions(agentsSettingsCommand);
12601
+ addAccessTokenOptions(agentsSettingsCommand, "Instafy access token");
12602
+ agentsSettingsCommand.option("--max-agents-per-plan <n>", "Set the maximum team workers in one plan (default: 64; range: 1\u2013256)").option("--max-concurrent-agents <n>", "Set the maximum running team workers across this space (default: 8; range: 1\u201364)").option("--interactive", "Interactively edit both shared settings").option("--json", "Output JSON (noninteractive)").action(async (opts) => {
12603
+ try {
12604
+ await agentSettings({
12605
+ project: resolveSpaceIdOption(opts),
12606
+ controllerUrl: opts.serverUrl,
12607
+ accessToken: opts.accessToken,
12608
+ maxAgentsPerPlan: opts.maxAgentsPerPlan,
12609
+ maxConcurrentAgents: opts.maxConcurrentAgents,
12610
+ interactive: opts.interactive,
12611
+ json: opts.json
12612
+ });
12613
+ } catch (error) {
12614
+ console.error(kleur20.red(String(error)));
12615
+ process.exit(1);
12616
+ }
12617
+ });
10942
12618
  var agentsListCommand = agentsCommand.command("list").description("List agents available to this account");
10943
12619
  addSpaceOption(
10944
12620
  agentsListCommand,
@@ -10955,7 +12631,7 @@ agentsListCommand.option("--json", "Output JSON").action(async (opts) => {
10955
12631
  json: opts.json
10956
12632
  });
10957
12633
  } catch (error) {
10958
- console.error(kleur19.red(String(error)));
12634
+ console.error(kleur20.red(String(error)));
10959
12635
  process.exit(1);
10960
12636
  }
10961
12637
  });
@@ -10983,7 +12659,7 @@ agentsContextListCommand.option("--agent <handle>", "Filter by agent handle (for
10983
12659
  json: opts.json
10984
12660
  });
10985
12661
  } catch (error) {
10986
- console.error(kleur19.red(String(error)));
12662
+ console.error(kleur20.red(String(error)));
10987
12663
  process.exit(1);
10988
12664
  }
10989
12665
  });
@@ -11009,7 +12685,7 @@ agentsContextPutCommand.action(async (contextParts, opts) => {
11009
12685
  json: opts.json
11010
12686
  });
11011
12687
  } catch (error) {
11012
- console.error(kleur19.red(String(error)));
12688
+ console.error(kleur20.red(String(error)));
11013
12689
  process.exit(1);
11014
12690
  }
11015
12691
  });
@@ -11025,7 +12701,7 @@ agentsStatusCommand.option("--json", "Output JSON").action(async (groupId, opts)
11025
12701
  json: opts.json
11026
12702
  });
11027
12703
  } catch (error) {
11028
- console.error(kleur19.red(String(error)));
12704
+ console.error(kleur20.red(String(error)));
11029
12705
  process.exit(1);
11030
12706
  }
11031
12707
  });
@@ -11043,7 +12719,7 @@ agentsCancelCommand.option("--json", "Output JSON").action(async (opts) => {
11043
12719
  json: opts.json
11044
12720
  });
11045
12721
  } catch (error) {
11046
- console.error(kleur19.red(String(error)));
12722
+ console.error(kleur20.red(String(error)));
11047
12723
  process.exit(1);
11048
12724
  }
11049
12725
  });
@@ -11065,7 +12741,7 @@ secretsListCommand.option("--json", "Output JSON").action(async (opts) => {
11065
12741
  json: opts.json
11066
12742
  });
11067
12743
  } catch (error) {
11068
- console.error(kleur19.red(String(error)));
12744
+ console.error(kleur20.red(String(error)));
11069
12745
  process.exit(1);
11070
12746
  }
11071
12747
  });
@@ -11086,7 +12762,7 @@ secretsGetCommand.option("--json", "Output JSON").action(async (nameOrId, opts)
11086
12762
  json: opts.json
11087
12763
  });
11088
12764
  } catch (error) {
11089
- console.error(kleur19.red(String(error)));
12765
+ console.error(kleur20.red(String(error)));
11090
12766
  process.exit(1);
11091
12767
  }
11092
12768
  });
@@ -11111,7 +12787,7 @@ secretsPutCommand.option("--json", "Output JSON").action(async (name, opts) => {
11111
12787
  json: opts.json
11112
12788
  });
11113
12789
  } catch (error) {
11114
- console.error(kleur19.red(String(error)));
12790
+ console.error(kleur20.red(String(error)));
11115
12791
  process.exit(1);
11116
12792
  }
11117
12793
  });
@@ -11132,7 +12808,7 @@ secretsRevokeCommand.option("--json", "Output JSON").action(async (nameOrId, opt
11132
12808
  json: opts.json
11133
12809
  });
11134
12810
  } catch (error) {
11135
- console.error(kleur19.red(String(error)));
12811
+ console.error(kleur20.red(String(error)));
11136
12812
  process.exit(1);
11137
12813
  }
11138
12814
  });
@@ -11166,7 +12842,7 @@ automationsListCommand.option("--json", "Output JSON").action(async (opts) => {
11166
12842
  json: opts.json
11167
12843
  });
11168
12844
  } catch (error) {
11169
- console.error(kleur19.red(String(error)));
12845
+ console.error(kleur20.red(String(error)));
11170
12846
  process.exit(1);
11171
12847
  }
11172
12848
  });
@@ -11212,7 +12888,7 @@ automationsCreateCommand.option("--json", "Output JSON").action(async (opts) =>
11212
12888
  json: opts.json
11213
12889
  });
11214
12890
  } catch (error) {
11215
- console.error(kleur19.red(String(error)));
12891
+ console.error(kleur20.red(String(error)));
11216
12892
  process.exit(1);
11217
12893
  }
11218
12894
  });
@@ -11258,7 +12934,7 @@ automationsUpdateCommand.option("--json", "Output JSON").action(async (automatio
11258
12934
  json: opts.json
11259
12935
  });
11260
12936
  } catch (error) {
11261
- console.error(kleur19.red(String(error)));
12937
+ console.error(kleur20.red(String(error)));
11262
12938
  process.exit(1);
11263
12939
  }
11264
12940
  });
@@ -11275,7 +12951,7 @@ automationsPauseCommand.option("--json", "Output JSON").action(async (automation
11275
12951
  json: opts.json
11276
12952
  });
11277
12953
  } catch (error) {
11278
- console.error(kleur19.red(String(error)));
12954
+ console.error(kleur20.red(String(error)));
11279
12955
  process.exit(1);
11280
12956
  }
11281
12957
  });
@@ -11292,7 +12968,7 @@ automationsResumeCommand.option("--json", "Output JSON").action(async (automatio
11292
12968
  json: opts.json
11293
12969
  });
11294
12970
  } catch (error) {
11295
- console.error(kleur19.red(String(error)));
12971
+ console.error(kleur20.red(String(error)));
11296
12972
  process.exit(1);
11297
12973
  }
11298
12974
  });
@@ -11308,7 +12984,7 @@ automationsRunCommand.option("--json", "Output JSON").action(async (automationId
11308
12984
  json: opts.json
11309
12985
  });
11310
12986
  } catch (error) {
11311
- console.error(kleur19.red(String(error)));
12987
+ console.error(kleur20.red(String(error)));
11312
12988
  process.exit(1);
11313
12989
  }
11314
12990
  });
@@ -11324,7 +13000,7 @@ automationsDeleteCommand.option("--json", "Output JSON").action(async (automatio
11324
13000
  json: opts.json
11325
13001
  });
11326
13002
  } catch (error) {
11327
- console.error(kleur19.red(String(error)));
13003
+ console.error(kleur20.red(String(error)));
11328
13004
  process.exit(1);
11329
13005
  }
11330
13006
  });
@@ -11342,7 +13018,7 @@ credentialsListCommand.option("--json", "Output JSON").action(async (opts) => {
11342
13018
  json: opts.json
11343
13019
  });
11344
13020
  } catch (error) {
11345
- console.error(kleur19.red(String(error)));
13021
+ console.error(kleur20.red(String(error)));
11346
13022
  process.exit(1);
11347
13023
  }
11348
13024
  });
@@ -11361,7 +13037,7 @@ credentialsTestCommand.option("--json", "Output JSON").action(async (idOrPrefix,
11361
13037
  process.exitCode = 1;
11362
13038
  }
11363
13039
  } catch (error) {
11364
- console.error(kleur19.red(String(error)));
13040
+ console.error(kleur20.red(String(error)));
11365
13041
  process.exit(1);
11366
13042
  }
11367
13043
  });
@@ -11392,7 +13068,7 @@ credentialsDefaultCommand.option("--json", "Output JSON").action(async (idOrPref
11392
13068
  json: opts.json
11393
13069
  });
11394
13070
  } catch (error) {
11395
- console.error(kleur19.red(String(error)));
13071
+ console.error(kleur20.red(String(error)));
11396
13072
  process.exit(1);
11397
13073
  }
11398
13074
  });
@@ -11409,7 +13085,7 @@ credentialsRevokeCommand.option("--json", "Output JSON").action(async (idOrPrefi
11409
13085
  json: opts.json
11410
13086
  });
11411
13087
  } catch (error) {
11412
- console.error(kleur19.red(String(error)));
13088
+ console.error(kleur20.red(String(error)));
11413
13089
  process.exit(1);
11414
13090
  }
11415
13091
  });
@@ -11419,7 +13095,7 @@ configCommand.command("path").description("Print the config file path").option("
11419
13095
  try {
11420
13096
  configPath({ json: opts.json });
11421
13097
  } catch (error) {
11422
- console.error(kleur19.red(String(error)));
13098
+ console.error(kleur20.red(String(error)));
11423
13099
  process.exit(1);
11424
13100
  }
11425
13101
  });
@@ -11427,7 +13103,7 @@ configCommand.command("list").description("List saved configuration").option("--
11427
13103
  try {
11428
13104
  configList({ json: opts.json });
11429
13105
  } catch (error) {
11430
- console.error(kleur19.red(String(error)));
13106
+ console.error(kleur20.red(String(error)));
11431
13107
  process.exit(1);
11432
13108
  }
11433
13109
  });
@@ -11435,7 +13111,7 @@ configCommand.command("get").description("Get a config value (controller-url, st
11435
13111
  try {
11436
13112
  configGet({ key, json: opts.json });
11437
13113
  } catch (error) {
11438
- console.error(kleur19.red(String(error)));
13114
+ console.error(kleur20.red(String(error)));
11439
13115
  process.exit(1);
11440
13116
  }
11441
13117
  });
@@ -11443,7 +13119,7 @@ configCommand.command("set").description("Set a config value (controller-url, st
11443
13119
  try {
11444
13120
  configSet({ key, value, json: opts.json });
11445
13121
  } catch (error) {
11446
- console.error(kleur19.red(String(error)));
13122
+ console.error(kleur20.red(String(error)));
11447
13123
  process.exit(1);
11448
13124
  }
11449
13125
  });
@@ -11451,7 +13127,7 @@ configCommand.command("unset").description("Unset a config value (controller-url
11451
13127
  try {
11452
13128
  configUnset({ key, json: opts.json });
11453
13129
  } catch (error) {
11454
- console.error(kleur19.red(String(error)));
13130
+ console.error(kleur20.red(String(error)));
11455
13131
  process.exit(1);
11456
13132
  }
11457
13133
  });
@@ -11471,7 +13147,7 @@ runtimeStartCommand.option("--supabase-access-token <token>", "Supabase session
11471
13147
  controllerAccessToken: opts.accessToken
11472
13148
  });
11473
13149
  } catch (error) {
11474
- console.error(kleur19.red(String(error)));
13150
+ console.error(kleur20.red(String(error)));
11475
13151
  process.exit(1);
11476
13152
  }
11477
13153
  });
@@ -11479,7 +13155,7 @@ runtimeCommand.command("status").description("Show runtime health").option("--js
11479
13155
  try {
11480
13156
  await runtimeStatus({ json: opts.json });
11481
13157
  } catch (error) {
11482
- console.error(kleur19.red(String(error)));
13158
+ console.error(kleur20.red(String(error)));
11483
13159
  process.exit(1);
11484
13160
  }
11485
13161
  });
@@ -11487,7 +13163,7 @@ runtimeCommand.command("stop").description("Stop the local Instafy runtime").opt
11487
13163
  try {
11488
13164
  await runtimeStop({ json: opts.json });
11489
13165
  } catch (error) {
11490
- console.error(kleur19.red(String(error)));
13166
+ console.error(kleur20.red(String(error)));
11491
13167
  process.exit(1);
11492
13168
  }
11493
13169
  });
@@ -11513,7 +13189,7 @@ runtimeTokenCommand.option("--runtime-id <uuid>", "Runtime ID to bind token to")
11513
13189
  json: opts.json
11514
13190
  });
11515
13191
  } catch (error) {
11516
- console.error(kleur19.red(String(error)));
13192
+ console.error(kleur20.red(String(error)));
11517
13193
  process.exit(1);
11518
13194
  }
11519
13195
  });
@@ -11535,7 +13211,7 @@ diagnosticsRuntimeEventsCommand.action(async (opts) => {
11535
13211
  accessToken: opts.accessToken
11536
13212
  });
11537
13213
  } catch (error) {
11538
- console.error(kleur19.red(String(error)));
13214
+ console.error(kleur20.red(String(error)));
11539
13215
  process.exit(1);
11540
13216
  }
11541
13217
  });
@@ -11550,7 +13226,7 @@ diagnosticsRunResultCommand.action(async (runId, opts) => {
11550
13226
  accessToken: opts.accessToken
11551
13227
  });
11552
13228
  } catch (error) {
11553
- console.error(kleur19.red(String(error)));
13229
+ console.error(kleur20.red(String(error)));
11554
13230
  process.exit(1);
11555
13231
  }
11556
13232
  });
@@ -11588,7 +13264,7 @@ supportReportCommand.action(async (summaryParts, opts) => {
11588
13264
  json: opts.json
11589
13265
  });
11590
13266
  } catch (error) {
11591
- console.error(kleur19.red(String(error)));
13267
+ console.error(kleur20.red(String(error)));
11592
13268
  process.exit(1);
11593
13269
  }
11594
13270
  });
@@ -11623,7 +13299,7 @@ supportListCommand.action(async (opts) => {
11623
13299
  json: opts.json
11624
13300
  });
11625
13301
  } catch (error) {
11626
- console.error(kleur19.red(String(error)));
13302
+ console.error(kleur20.red(String(error)));
11627
13303
  process.exit(1);
11628
13304
  }
11629
13305
  });
@@ -11639,7 +13315,7 @@ supportShowCommand.action(async (reportId, opts) => {
11639
13315
  json: opts.json
11640
13316
  });
11641
13317
  } catch (error) {
11642
- console.error(kleur19.red(String(error)));
13318
+ console.error(kleur20.red(String(error)));
11643
13319
  process.exit(1);
11644
13320
  }
11645
13321
  });
@@ -11664,7 +13340,7 @@ supportMessagesCommand.action(async (reportId, opts) => {
11664
13340
  json: opts.json
11665
13341
  });
11666
13342
  } catch (error) {
11667
- console.error(kleur19.red(String(error)));
13343
+ console.error(kleur20.red(String(error)));
11668
13344
  process.exit(1);
11669
13345
  }
11670
13346
  });
@@ -11685,11 +13361,12 @@ supportReplyCommand.action(async (reportId, messageParts, opts) => {
11685
13361
  json: opts.json
11686
13362
  });
11687
13363
  } catch (error) {
11688
- console.error(kleur19.red(String(error)));
13364
+ console.error(kleur20.red(String(error)));
11689
13365
  process.exit(1);
11690
13366
  }
11691
13367
  });
11692
13368
  program.command("git").description("Run git against the Instafy canonical checkout (.instafy/.git)").allowUnknownOption(true);
13369
+ program.command("bind").description("Bound sites: serve a website whose clicks run scripts and stream records back to every viewer (instafy-bind)").allowUnknownOption(true);
11693
13370
  var tunnelCommand = program.command("tunnel").description("Create and manage shareable tunnels");
11694
13371
  failWithGroupHelp(tunnelCommand);
11695
13372
  var tunnelStartCommand = tunnelCommand.command("start").description("Create (or reuse) a shareable tunnel URL for a local port (defaults to detached)").option("--port <port>", "Local port to expose (default 3000)").option("--name <name>", "Stable tunnel name (default web)").option("--rotate", "Rotate the stable tunnel hostname before starting");
@@ -11730,19 +13407,19 @@ tunnelStartCommand.option("--no-detach", "Run in foreground until interrupted").
11730
13407
  console.log(JSON.stringify(started, null, 2));
11731
13408
  return;
11732
13409
  }
11733
- console.log(kleur19.green(`Tunnel started: ${started.url} (tunnelId=${started.tunnelId})`));
11734
- console.log(kleur19.gray(`pid=${started.pid} \xB7 port=${started.localPort}`));
11735
- console.log(kleur19.gray(`log: ${started.logFile}`));
13410
+ console.log(kleur20.green(`Tunnel started: ${started.url} (tunnelId=${started.tunnelId})`));
13411
+ console.log(kleur20.gray(`pid=${started.pid} \xB7 port=${started.localPort}`));
13412
+ console.log(kleur20.gray(`log: ${started.logFile}`));
11736
13413
  console.log("");
11737
13414
  console.log("Next:");
11738
- console.log(`- ${kleur19.cyan(`instafy tunnel list`)}`);
11739
- console.log(`- ${kleur19.cyan(`instafy tunnel logs ${started.tunnelId} --follow`)}`);
11740
- console.log(`- ${kleur19.cyan(`instafy tunnel stop ${started.tunnelId}`)}`);
13415
+ console.log(`- ${kleur20.cyan(`instafy tunnel list`)}`);
13416
+ console.log(`- ${kleur20.cyan(`instafy tunnel logs ${started.tunnelId} --follow`)}`);
13417
+ console.log(`- ${kleur20.cyan(`instafy tunnel stop ${started.tunnelId}`)}`);
11741
13418
  if (process.platform !== "win32") {
11742
- console.log(kleur19.gray(`(or) tail -n 200 -f ${started.logFile}`));
13419
+ console.log(kleur20.gray(`(or) tail -n 200 -f ${started.logFile}`));
11743
13420
  }
11744
13421
  } catch (error) {
11745
- console.error(kleur19.red(String(error)));
13422
+ console.error(kleur20.red(String(error)));
11746
13423
  process.exit(1);
11747
13424
  }
11748
13425
  });
@@ -11754,14 +13431,14 @@ tunnelCommand.command("list").description("List local tunnel sessions started by
11754
13431
  return;
11755
13432
  }
11756
13433
  if (tunnels.length === 0) {
11757
- console.log(kleur19.yellow("No tunnels found."));
13434
+ console.log(kleur20.yellow("No tunnels found."));
11758
13435
  return;
11759
13436
  }
11760
13437
  for (const tunnel of tunnels) {
11761
13438
  console.log(`${tunnel.tunnelId} \xB7 ${tunnel.url} \xB7 port=${tunnel.localPort} \xB7 pid=${tunnel.pid}`);
11762
13439
  }
11763
13440
  } catch (error) {
11764
- console.error(kleur19.red(String(error)));
13441
+ console.error(kleur20.red(String(error)));
11765
13442
  process.exit(1);
11766
13443
  }
11767
13444
  });
@@ -11777,9 +13454,9 @@ tunnelCommand.command("stop").description("Stop a local tunnel session and revok
11777
13454
  console.log(JSON.stringify(result, null, 2));
11778
13455
  return;
11779
13456
  }
11780
- console.log(kleur19.green(`Tunnel stopped: ${result.tunnelId}`));
13457
+ console.log(kleur20.green(`Tunnel stopped: ${result.tunnelId}`));
11781
13458
  } catch (error) {
11782
- console.error(kleur19.red(String(error)));
13459
+ console.error(kleur20.red(String(error)));
11783
13460
  process.exit(1);
11784
13461
  }
11785
13462
  });
@@ -11793,10 +13470,81 @@ tunnelCommand.command("logs").description("Show logs for a local tunnel session"
11793
13470
  json: Boolean(opts.json)
11794
13471
  });
11795
13472
  } catch (error) {
11796
- console.error(kleur19.red(String(error)));
13473
+ console.error(kleur20.red(String(error)));
11797
13474
  process.exit(1);
11798
13475
  }
11799
13476
  });
13477
+ function exitWithPreviewError(error) {
13478
+ console.error(kleur20.red(error instanceof Error ? error.message : String(error)));
13479
+ process.exit(error instanceof PreviewCommandError ? error.exitCode : 1);
13480
+ }
13481
+ var previewCommand = program.command("preview").description("Look at sites running in this space, and show them in Studio as a private Ready to preview card");
13482
+ failWithGroupHelp(previewCommand);
13483
+ registerPreviewCheckCommand(previewCommand);
13484
+ var previewOpenCommand = previewCommand.command("open").description(
13485
+ "Open a private preview of a local port (exit 2: nothing listening, 3: port not allowed, 5: previews not available)"
13486
+ ).requiredOption("--port <port>", "Local port the site listens on (1024-65535)").requiredOption("--title <title>", "Card title (at most 120 characters)").option("--path <path>", "Path the preview opens at (default /)");
13487
+ addSpaceOption(previewOpenCommand, "Space UUID (defaults to .instafy/space.json or SPACE_ID)");
13488
+ addServerUrlOptions(previewOpenCommand);
13489
+ addAccessTokenOptions(previewOpenCommand, "Instafy access token (defaults to the job token, then the saved `instafy login` token)");
13490
+ previewOpenCommand.option("--conversation <id>", "Conversation for the card (only needed without a job token)").option("--rathole-bin <path>", "Path to rathole binary (or set RATHOLE_BIN)").option(
13491
+ "--serve-cmd <command>",
13492
+ "How to start the site again after the space sleeps, run from the current directory (one command, no shell); default: read from the process listening on the port"
13493
+ ).option("--json", "Output JSON").action(async (opts) => {
13494
+ try {
13495
+ const result = await previewOpen({
13496
+ project: resolveSpaceIdOption(opts),
13497
+ controllerUrl: opts.serverUrl,
13498
+ controllerToken: opts.accessToken,
13499
+ conversationId: opts.conversation,
13500
+ port: opts.port,
13501
+ title: opts.title,
13502
+ path: opts.path,
13503
+ ratholeBin: opts.ratholeBin,
13504
+ serveCmd: opts.serveCmd
13505
+ });
13506
+ printPreviewOpen(result, Boolean(opts.json));
13507
+ } catch (error) {
13508
+ exitWithPreviewError(error);
13509
+ }
13510
+ });
13511
+ var previewListCommand = previewCommand.command("list").description("List this space's previews");
13512
+ addSpaceOption(previewListCommand, "Space UUID (defaults to .instafy/space.json or SPACE_ID)");
13513
+ addServerUrlOptions(previewListCommand);
13514
+ addAccessTokenOptions(previewListCommand, "Instafy access token (defaults to the job token, then the saved `instafy login` token)");
13515
+ previewListCommand.option("--json", "Output JSON").action(async (opts) => {
13516
+ try {
13517
+ const previews = await previewList({
13518
+ project: resolveSpaceIdOption(opts),
13519
+ controllerUrl: opts.serverUrl,
13520
+ controllerToken: opts.accessToken
13521
+ });
13522
+ printPreviewList(previews, Boolean(opts.json));
13523
+ } catch (error) {
13524
+ exitWithPreviewError(error);
13525
+ }
13526
+ });
13527
+ var previewCloseCommand = previewCommand.command("close").description("Close a preview: stop its local client and revoke it").argument("<previewId>", "Preview ID (from `instafy preview list`)");
13528
+ addSpaceOption(previewCloseCommand, "Space UUID (defaults to .instafy/space.json or SPACE_ID)");
13529
+ addServerUrlOptions(previewCloseCommand);
13530
+ addAccessTokenOptions(previewCloseCommand, "Instafy access token (defaults to the job token, then the saved `instafy login` token)");
13531
+ previewCloseCommand.option("--json", "Output JSON").action(async (previewId, opts) => {
13532
+ try {
13533
+ const result = await previewClose({
13534
+ previewId,
13535
+ project: resolveSpaceIdOption(opts),
13536
+ controllerUrl: opts.serverUrl,
13537
+ controllerToken: opts.accessToken
13538
+ });
13539
+ if (opts.json) {
13540
+ console.log(JSON.stringify(result, null, 2));
13541
+ return;
13542
+ }
13543
+ console.log(kleur20.green(`Preview closed: ${result.previewId}`));
13544
+ } catch (error) {
13545
+ exitWithPreviewError(error);
13546
+ }
13547
+ });
11800
13548
  var orgCommand = program.command("team").alias("org").description("Team utilities");
11801
13549
  failWithGroupHelp(orgCommand);
11802
13550
  var orgListCommand = orgCommand.command("list").description("List teams for your account");
@@ -11809,7 +13557,7 @@ orgListCommand.option("--access-token <token>", "Instafy or Supabase access toke
11809
13557
  json: opts.json
11810
13558
  });
11811
13559
  } catch (error) {
11812
- console.error(kleur19.red(String(error)));
13560
+ console.error(kleur20.red(String(error)));
11813
13561
  process.exit(1);
11814
13562
  }
11815
13563
  });
@@ -11830,7 +13578,7 @@ teamMembersCommand.action(async (opts) => {
11830
13578
  json: opts.json
11831
13579
  });
11832
13580
  } catch (error) {
11833
- console.error(kleur19.red(String(error)));
13581
+ console.error(kleur20.red(String(error)));
11834
13582
  process.exit(1);
11835
13583
  }
11836
13584
  });
@@ -11849,7 +13597,7 @@ teamInviteCommand.option("--role <role>", "owner, admin, builder, or viewer (def
11849
13597
  json: opts.json
11850
13598
  });
11851
13599
  } catch (error) {
11852
- console.error(kleur19.red(String(error)));
13600
+ console.error(kleur20.red(String(error)));
11853
13601
  process.exit(1);
11854
13602
  }
11855
13603
  });
@@ -11868,7 +13616,7 @@ teamInviteLinkCommand.option("--role <role>", "builder or viewer (default: build
11868
13616
  json: opts.json
11869
13617
  });
11870
13618
  } catch (error) {
11871
- console.error(kleur19.red(String(error)));
13619
+ console.error(kleur20.red(String(error)));
11872
13620
  process.exit(1);
11873
13621
  }
11874
13622
  });
@@ -11885,7 +13633,7 @@ teamInvitesCommand.action(async (opts) => {
11885
13633
  json: opts.json
11886
13634
  });
11887
13635
  } catch (error) {
11888
- console.error(kleur19.red(String(error)));
13636
+ console.error(kleur20.red(String(error)));
11889
13637
  process.exit(1);
11890
13638
  }
11891
13639
  });
@@ -11904,7 +13652,7 @@ teamAddMemberCommand.requiredOption("--user-id <uuid>", "User id of an existing
11904
13652
  json: opts.json
11905
13653
  });
11906
13654
  } catch (error) {
11907
- console.error(kleur19.red(String(error)));
13655
+ console.error(kleur20.red(String(error)));
11908
13656
  process.exit(1);
11909
13657
  }
11910
13658
  });
@@ -11922,7 +13670,7 @@ teamAcceptCommand.action(async (token, opts) => {
11922
13670
  json: opts.json
11923
13671
  });
11924
13672
  } catch (error) {
11925
- console.error(kleur19.red(String(error)));
13673
+ console.error(kleur20.red(String(error)));
11926
13674
  process.exit(1);
11927
13675
  }
11928
13676
  });
@@ -11941,7 +13689,7 @@ teamRevokeInviteCommand.option("--yes", "Skip the confirmation prompt").action(a
11941
13689
  json: opts.json
11942
13690
  });
11943
13691
  } catch (error) {
11944
- console.error(kleur19.red(String(error)));
13692
+ console.error(kleur20.red(String(error)));
11945
13693
  process.exit(1);
11946
13694
  }
11947
13695
  });
@@ -11960,7 +13708,7 @@ teamRevokeLinkCommand.option("--yes", "Skip the confirmation prompt").action(asy
11960
13708
  json: opts.json
11961
13709
  });
11962
13710
  } catch (error) {
11963
- console.error(kleur19.red(String(error)));
13711
+ console.error(kleur20.red(String(error)));
11964
13712
  process.exit(1);
11965
13713
  }
11966
13714
  });
@@ -11985,17 +13733,17 @@ profileCommand.command("list").description("List saved CLI profiles").option("--
11985
13733
  return;
11986
13734
  }
11987
13735
  if (profiles.length === 0) {
11988
- console.log(kleur19.yellow("No profiles found."));
11989
- console.log(`Create one with: ${kleur19.cyan("instafy login --profile <name>")}`);
13736
+ console.log(kleur20.yellow("No profiles found."));
13737
+ console.log(`Create one with: ${kleur20.cyan("instafy login --profile <name>")}`);
11990
13738
  return;
11991
13739
  }
11992
- console.log(kleur19.green("Instafy CLI profiles"));
13740
+ console.log(kleur20.green("Instafy CLI profiles"));
11993
13741
  for (const profile of profiles) {
11994
- const token = profile.accessTokenSet ? kleur19.green("token") : kleur19.yellow("no-token");
11995
- console.log(`- ${kleur19.cyan(profile.name)} (${token})`);
13742
+ const token = profile.accessTokenSet ? kleur20.green("token") : kleur20.yellow("no-token");
13743
+ console.log(`- ${kleur20.cyan(profile.name)} (${token})`);
11996
13744
  }
11997
13745
  } catch (error) {
11998
- console.error(kleur19.red(String(error)));
13746
+ console.error(kleur20.red(String(error)));
11999
13747
  process.exit(1);
12000
13748
  }
12001
13749
  });
@@ -12015,7 +13763,7 @@ historyMessagesCommand.option("--limit <n>", "Max messages to return (1-200, def
12015
13763
  pretty: opts.pretty
12016
13764
  });
12017
13765
  } catch (error) {
12018
- console.error(kleur19.red(String(error)));
13766
+ console.error(kleur20.red(String(error)));
12019
13767
  process.exit(1);
12020
13768
  }
12021
13769
  });
@@ -12032,7 +13780,7 @@ historyRunsCommand.option("--limit <n>", "Max runs to return (1-200, default: 50
12032
13780
  pretty: opts.pretty
12033
13781
  });
12034
13782
  } catch (error) {
12035
- console.error(kleur19.red(String(error)));
13783
+ console.error(kleur20.red(String(error)));
12036
13784
  process.exit(1);
12037
13785
  }
12038
13786
  });
@@ -12049,7 +13797,7 @@ historyConversationsCommand.option("--limit <n>", "Max conversations to return (
12049
13797
  pretty: opts.pretty
12050
13798
  });
12051
13799
  } catch (error) {
12052
- console.error(kleur19.red(String(error)));
13800
+ console.error(kleur20.red(String(error)));
12053
13801
  process.exit(1);
12054
13802
  }
12055
13803
  });
@@ -12070,7 +13818,7 @@ conversationCreateCommand.action(async (opts) => {
12070
13818
  json: opts.json
12071
13819
  });
12072
13820
  } catch (error) {
12073
- console.error(kleur19.red(String(error)));
13821
+ console.error(kleur20.red(String(error)));
12074
13822
  process.exit(1);
12075
13823
  }
12076
13824
  });
@@ -12088,7 +13836,7 @@ conversationListCommand.action(async (opts) => {
12088
13836
  json: opts.json
12089
13837
  });
12090
13838
  } catch (error) {
12091
- console.error(kleur19.red(String(error)));
13839
+ console.error(kleur20.red(String(error)));
12092
13840
  process.exit(1);
12093
13841
  }
12094
13842
  });
@@ -12107,7 +13855,7 @@ conversationSearchCommand.action(async (queryParts, opts) => {
12107
13855
  json: opts.json
12108
13856
  });
12109
13857
  } catch (error) {
12110
- console.error(kleur19.red(String(error)));
13858
+ console.error(kleur20.red(String(error)));
12111
13859
  process.exit(1);
12112
13860
  }
12113
13861
  });
@@ -12131,7 +13879,7 @@ conversationGrepCommand.action(async (parts, opts) => {
12131
13879
  });
12132
13880
  process.exitCode = found ? 0 : 1;
12133
13881
  } catch (error) {
12134
- console.error(kleur19.red(String(error)));
13882
+ console.error(kleur20.red(String(error)));
12135
13883
  process.exitCode = 2;
12136
13884
  }
12137
13885
  });
@@ -12151,7 +13899,7 @@ conversationContextCommand.action(async (conversationId, messageId, opts) => {
12151
13899
  accessToken: opts.accessToken
12152
13900
  });
12153
13901
  } catch (error) {
12154
- console.error(kleur19.red(String(error)));
13902
+ console.error(kleur20.red(String(error)));
12155
13903
  process.exitCode = 2;
12156
13904
  }
12157
13905
  });
@@ -12173,7 +13921,7 @@ conversationShowCommand.action(async (targetParts, opts) => {
12173
13921
  json: opts.json
12174
13922
  });
12175
13923
  } catch (error) {
12176
- console.error(kleur19.red(String(error)));
13924
+ console.error(kleur20.red(String(error)));
12177
13925
  process.exit(1);
12178
13926
  }
12179
13927
  });
@@ -12183,6 +13931,10 @@ async function runCli(argv = process.argv) {
12183
13931
  return;
12184
13932
  }
12185
13933
  const args = argv.slice(2);
13934
+ if (args[0] === "bind") {
13935
+ process.exitCode = await runBind(args.slice(1));
13936
+ return;
13937
+ }
12186
13938
  if (args[0] === "git") {
12187
13939
  if (args[1] === "credential") {
12188
13940
  try {