sparkecoder 0.1.100 → 0.1.103

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 (102) hide show
  1. package/dist/cli.js +432 -44
  2. package/dist/cli.js.map +1 -1
  3. package/dist/index.js +5 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/server/index.js +5 -1
  6. package/dist/server/index.js.map +1 -1
  7. package/dist/tools/index.d.ts +1 -1
  8. package/package.json +16 -15
  9. package/web/.next/BUILD_ID +1 -1
  10. package/web/.next/standalone/web/.next/BUILD_ID +1 -1
  11. package/web/.next/standalone/web/.next/build-manifest.json +2 -2
  12. package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
  13. package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
  14. package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
  15. package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  16. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  17. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  18. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  19. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  20. package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
  21. package/web/.next/standalone/web/.next/server/app/_not-found.rsc +1 -1
  22. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  23. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  24. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  25. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  26. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  27. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  28. package/web/.next/standalone/web/.next/server/app/agents.html +1 -1
  29. package/web/.next/standalone/web/.next/server/app/agents.rsc +1 -1
  30. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents/__PAGE__.segment.rsc +1 -1
  31. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents.segment.rsc +1 -1
  32. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p.segment.rsc +1 -1
  33. package/web/.next/standalone/web/.next/server/app/agents.segments/_full.segment.rsc +1 -1
  34. package/web/.next/standalone/web/.next/server/app/agents.segments/_head.segment.rsc +1 -1
  35. package/web/.next/standalone/web/.next/server/app/agents.segments/_index.segment.rsc +1 -1
  36. package/web/.next/standalone/web/.next/server/app/agents.segments/_tree.segment.rsc +1 -1
  37. package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
  38. package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +1 -1
  39. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +1 -1
  40. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
  41. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +1 -1
  42. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +1 -1
  43. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
  44. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
  45. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
  46. package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
  47. package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +1 -1
  48. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +1 -1
  49. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
  50. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +1 -1
  51. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +1 -1
  52. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
  53. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
  54. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
  55. package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
  56. package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +1 -1
  57. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +1 -1
  58. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
  59. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +1 -1
  60. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +1 -1
  61. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
  62. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
  63. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
  64. package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
  65. package/web/.next/standalone/web/.next/server/app/docs.rsc +1 -1
  66. package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +1 -1
  67. package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
  68. package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +1 -1
  69. package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +1 -1
  70. package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
  71. package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
  72. package/web/.next/standalone/web/.next/server/app/index.html +1 -1
  73. package/web/.next/standalone/web/.next/server/app/index.rsc +1 -1
  74. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +1 -1
  75. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +1 -1
  76. package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +1 -1
  77. package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  78. package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +1 -1
  79. package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  80. package/web/.next/standalone/web/.next/server/app/settings.html +1 -1
  81. package/web/.next/standalone/web/.next/server/app/settings.rsc +1 -1
  82. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings/__PAGE__.segment.rsc +1 -1
  83. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings.segment.rsc +1 -1
  84. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p.segment.rsc +1 -1
  85. package/web/.next/standalone/web/.next/server/app/settings.segments/_full.segment.rsc +1 -1
  86. package/web/.next/standalone/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  87. package/web/.next/standalone/web/.next/server/app/settings.segments/_index.segment.rsc +1 -1
  88. package/web/.next/standalone/web/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  89. package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
  90. package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
  91. package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
  92. package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
  93. package/web/.next/standalone/web/package-lock.json +7 -7
  94. /package/web/.next/standalone/web/.next/static/{eyxBRyMA8Zq36Pfen3Ylj → static/zSDiLDq3NGRZ_Ys5zvnML}/_buildManifest.js +0 -0
  95. /package/web/.next/standalone/web/.next/static/{eyxBRyMA8Zq36Pfen3Ylj → static/zSDiLDq3NGRZ_Ys5zvnML}/_clientMiddlewareManifest.json +0 -0
  96. /package/web/.next/standalone/web/.next/static/{eyxBRyMA8Zq36Pfen3Ylj → static/zSDiLDq3NGRZ_Ys5zvnML}/_ssgManifest.js +0 -0
  97. /package/web/.next/standalone/web/.next/static/{static/eyxBRyMA8Zq36Pfen3Ylj → zSDiLDq3NGRZ_Ys5zvnML}/_buildManifest.js +0 -0
  98. /package/web/.next/standalone/web/.next/static/{static/eyxBRyMA8Zq36Pfen3Ylj → zSDiLDq3NGRZ_Ys5zvnML}/_clientMiddlewareManifest.json +0 -0
  99. /package/web/.next/standalone/web/.next/static/{static/eyxBRyMA8Zq36Pfen3Ylj → zSDiLDq3NGRZ_Ys5zvnML}/_ssgManifest.js +0 -0
  100. /package/web/.next/static/{eyxBRyMA8Zq36Pfen3Ylj → zSDiLDq3NGRZ_Ys5zvnML}/_buildManifest.js +0 -0
  101. /package/web/.next/static/{eyxBRyMA8Zq36Pfen3Ylj → zSDiLDq3NGRZ_Ys5zvnML}/_clientMiddlewareManifest.json +0 -0
  102. /package/web/.next/static/{eyxBRyMA8Zq36Pfen3Ylj → zSDiLDq3NGRZ_Ys5zvnML}/_ssgManifest.js +0 -0
package/dist/cli.js CHANGED
@@ -11575,6 +11575,83 @@ var init_scheduler = __esm({
11575
11575
  }
11576
11576
  });
11577
11577
 
11578
+ // src/cloudflare/api.ts
11579
+ var api_exports = {};
11580
+ __export(api_exports, {
11581
+ createRemoteTunnel: () => createRemoteTunnel,
11582
+ setTunnelConfig: () => setTunnelConfig,
11583
+ upsertDnsCname: () => upsertDnsCname
11584
+ });
11585
+ async function cf(cfg, method, path, body) {
11586
+ const res = await fetch(`${CF_API}${path}`, {
11587
+ method,
11588
+ headers: {
11589
+ Authorization: `Bearer ${cfg.apiToken}`,
11590
+ "Content-Type": "application/json"
11591
+ },
11592
+ body: body == null ? void 0 : JSON.stringify(body)
11593
+ });
11594
+ const json = await res.json().catch(() => ({}));
11595
+ if (!res.ok || json?.success === false) {
11596
+ const errMsg = json?.errors?.map((e) => `${e.code}: ${e.message}`).join("; ");
11597
+ throw new Error(
11598
+ `Cloudflare API ${method} ${path} failed (HTTP ${res.status}): ${errMsg ?? JSON.stringify(json).slice(0, 400)}`
11599
+ );
11600
+ }
11601
+ return json.result ?? json;
11602
+ }
11603
+ async function createRemoteTunnel(cfg, name) {
11604
+ const created = await cf(
11605
+ cfg,
11606
+ "POST",
11607
+ `/accounts/${cfg.accountId}/cfd_tunnel`,
11608
+ { name, config_src: "cloudflare" }
11609
+ );
11610
+ let token = created.token;
11611
+ if (!token) {
11612
+ token = await cf(cfg, "GET", `/accounts/${cfg.accountId}/cfd_tunnel/${created.id}/token`);
11613
+ }
11614
+ return { id: created.id, name: created.name, token };
11615
+ }
11616
+ async function setTunnelConfig(cfg, tunnelId, ingress) {
11617
+ await cf(cfg, "PUT", `/accounts/${cfg.accountId}/cfd_tunnel/${tunnelId}/configurations`, {
11618
+ config: { ingress: [...ingress, { service: "http_status:404" }] }
11619
+ });
11620
+ }
11621
+ async function upsertDnsCname(cfg, hostname, tunnelId) {
11622
+ if (!cfg.zoneId) throw new Error("zoneId required to manage DNS");
11623
+ const target = `${tunnelId}.cfargotunnel.com`;
11624
+ const existing = await cf(
11625
+ cfg,
11626
+ "GET",
11627
+ `/zones/${cfg.zoneId}/dns_records?name=${encodeURIComponent(hostname)}`
11628
+ );
11629
+ if (Array.isArray(existing) && existing.length > 0) {
11630
+ await cf(cfg, "PUT", `/zones/${cfg.zoneId}/dns_records/${existing[0].id}`, {
11631
+ type: "CNAME",
11632
+ name: hostname,
11633
+ content: target,
11634
+ proxied: true,
11635
+ ttl: 1
11636
+ });
11637
+ } else {
11638
+ await cf(cfg, "POST", `/zones/${cfg.zoneId}/dns_records`, {
11639
+ type: "CNAME",
11640
+ name: hostname,
11641
+ content: target,
11642
+ proxied: true,
11643
+ ttl: 1
11644
+ });
11645
+ }
11646
+ }
11647
+ var CF_API;
11648
+ var init_api = __esm({
11649
+ "src/cloudflare/api.ts"() {
11650
+ "use strict";
11651
+ CF_API = "https://api.cloudflare.com/client/v4";
11652
+ }
11653
+ });
11654
+
11578
11655
  // src/cli.ts
11579
11656
  import { Command } from "commander";
11580
11657
  import chalk from "chalk";
@@ -13104,7 +13181,11 @@ ${prompt}` });
13104
13181
  await writeSSE(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
13105
13182
  }
13106
13183
  if (!isAborted) {
13107
- await result.saveResponseMessages();
13184
+ try {
13185
+ await result.saveResponseMessages();
13186
+ } catch (err) {
13187
+ console.error("saveResponseMessages failed (continuing):", err?.message ?? err);
13188
+ }
13108
13189
  }
13109
13190
  if (isAborted) {
13110
13191
  await writeSSE(JSON.stringify({ type: "abort" }));
@@ -16828,53 +16909,360 @@ program.command("slack-setup").description("Interactively configure Slack integr
16828
16909
  process.exit(1);
16829
16910
  }
16830
16911
  });
16831
- program.command("cloudflared-setup").description("Print a copy-paste recipe for exposing this sparkecoder via cloudflared + Cloudflare Access").option("--port <port>", "Local sparkecoder web UI port", "6969").option("--api-port <port>", "Local sparkecoder API port", "3141").option("--hostname <host>", "Public hostname you want to assign (e.g. sf-mac-1.example.com)").option("--team <team>", "Your Cloudflare Access team subdomain (e.g. studyfetch -> studyfetch.cloudflareaccess.com)").option("--allowed-emails <emails>", "Comma-separated allow-listed emails").action(async (options) => {
16912
+ program.command("cloudflared-setup").description("Auto-detect cloudflared + set up a tunnel to this sparkecoder (interactive)").option("--port <port>", "Local sparkecoder web UI port", "6969").option("--api-port <port>", "Local sparkecoder API port", "3141").option("--hostname <host>", "Public hostname you want to assign (e.g. sf-mac-1.example.com)").option("--tunnel-name <name>", "Tunnel name to reuse or create", "sparkecoder").option("--team <team>", "Your Cloudflare Access team subdomain (e.g. studyfetch -> studyfetch.cloudflareaccess.com)").option("--allowed-emails <emails>", "Comma-separated allow-listed emails").option("-y, --yes", "Skip confirmations and do everything non-interactively", false).option("--print-only", "Skip auto-setup and just print the copy/paste recipe", false).option("--cf-api-token <token>", "Cloudflare API token (Account: Cloudflare Tunnel: Edit + Zone: DNS: Edit). Or set CF_API_TOKEN env").option("--cf-account-id <id>", "Cloudflare account ID. Or set CF_ACCOUNT_ID env").option("--cf-zone-id <id>", "Cloudflare zone ID for the hostname. Or set CF_ZONE_ID env").option("--remote", "Provision the tunnel via the remote sparkecoder server (server holds the Cloudflare credentials)", false).option("--setup-secret <secret>", "Tunnel setup secret (required when server has TUNNEL_SETUP_SECRET set). Or set SPARKECODER_TUNNEL_SECRET env").action(async (options) => {
16913
+ const { execSync: execSync2, spawnSync } = await import("child_process");
16914
+ const { homedir: homedir2 } = await import("os");
16915
+ const { copyFileSync } = await import("fs");
16832
16916
  const port = options.port || "6969";
16833
16917
  const apiPort = options.apiPort || "3141";
16834
- const hostname = options.hostname || "<your-public-hostname>";
16918
+ const tunnelName = options.tunnelName || "sparkecoder";
16835
16919
  const team = options.team || "<your-team>";
16836
16920
  const emails = (options.allowedEmails || "").split(",").map((s) => s.trim()).filter(Boolean);
16837
- console.log(chalk.bold("Cloudflared + Cloudflare Access setup\n"));
16838
- console.log(chalk.dim("1. Install cloudflared:"));
16839
- console.log(chalk.cyan(" brew install cloudflared # macOS"));
16840
- console.log(chalk.cyan(" # or download from https://github.com/cloudflare/cloudflared/releases\n"));
16841
- console.log(chalk.dim("2. Authenticate + create a named tunnel:"));
16842
- console.log(chalk.cyan(" cloudflared tunnel login"));
16843
- console.log(chalk.cyan(" cloudflared tunnel create sparkecoder"));
16844
- console.log(chalk.cyan(` cloudflared tunnel route dns sparkecoder ${hostname}
16845
- `));
16846
- console.log(chalk.dim("3. Create ~/.cloudflared/config.yml with:"));
16847
- console.log(chalk.cyan(`
16848
- tunnel: sparkecoder
16849
- credentials-file: /Users/$(whoami)/.cloudflared/<tunnel-id>.json
16850
- ingress:
16851
- - hostname: ${hostname}
16852
- service: http://localhost:${port}
16853
- - hostname: api-${hostname}
16854
- service: http://localhost:${apiPort}
16855
- - service: http_status:404
16856
- `));
16857
- console.log(chalk.dim("4. Run the tunnel (or set it up as a system service):"));
16858
- console.log(chalk.cyan(" cloudflared tunnel run sparkecoder\n"));
16859
- console.log(chalk.dim("5. In Cloudflare Zero Trust > Access > Applications:"));
16860
- console.log(chalk.dim(` - Add a self-hosted application for ${hostname}.`));
16861
- console.log(chalk.dim(' - Add an "Allow" policy with the emails you want.'));
16862
- console.log(chalk.dim(` - Note the application AUD tag.
16863
- `));
16864
- console.log(chalk.dim("6. Add an auth block to sparkecoder.config.json:"));
16865
- console.log(chalk.cyan(`
16866
- {
16867
- "auth": {
16868
- "cfAccess": {
16869
- "enabled": true,
16870
- "teamDomain": "${team}.cloudflareaccess.com",
16871
- "audTag": "<paste-aud-from-cf-dashboard>"
16872
- },
16873
- "allowedEmails": [${emails.length ? emails.map((e) => `"${e}"`).join(", ") : '"you@example.com"'}]
16874
- }
16875
- }
16876
- `));
16877
- console.log(chalk.dim("7. Slack endpoint (/api/slack/events) and /health are exempt from CF Access automatically."));
16921
+ const yes = !!options.yes;
16922
+ const ensureCloudflared = () => {
16923
+ try {
16924
+ return execSync2("command -v cloudflared", { stdio: ["ignore", "pipe", "ignore"] }).toString().trim() || null;
16925
+ } catch {
16926
+ return null;
16927
+ }
16928
+ };
16929
+ const installCloudflaredIfNeeded = async () => {
16930
+ let cfPath = ensureCloudflared();
16931
+ if (cfPath) return cfPath;
16932
+ console.log(chalk.yellow("cloudflared is not installed."));
16933
+ if (process.platform === "darwin") {
16934
+ try {
16935
+ execSync2("command -v brew", { stdio: "ignore" });
16936
+ } catch {
16937
+ return null;
16938
+ }
16939
+ console.log(chalk.dim("Installing via `brew install cloudflared`..."));
16940
+ spawnSync("brew", ["install", "cloudflared"], { stdio: "inherit" });
16941
+ cfPath = ensureCloudflared();
16942
+ }
16943
+ return cfPath;
16944
+ };
16945
+ const installAsService = (token) => {
16946
+ let r = spawnSync("cloudflared", ["service", "install", token], { stdio: "inherit" });
16947
+ if (r.status !== 0 && process.platform !== "win32") {
16948
+ console.log(chalk.dim("Retrying with sudo..."));
16949
+ r = spawnSync("sudo", ["cloudflared", "service", "install", token], { stdio: "inherit" });
16950
+ }
16951
+ return r.status === 0;
16952
+ };
16953
+ if (options.remote) {
16954
+ try {
16955
+ let config = loadConfig(options.config, process.cwd());
16956
+ let remoteUrl = config.resolvedRemoteServer.url;
16957
+ if (!remoteUrl) {
16958
+ console.log(chalk.red("No remoteServer.url configured. Run `sparkecoder login` (or set remoteServer.url in your config) first."));
16959
+ process.exitCode = 1;
16960
+ return;
16961
+ }
16962
+ let authKey3 = config.resolvedRemoteServer.authKey;
16963
+ if (!authKey3) {
16964
+ authKey3 = await ensureRemoteAuthKey(remoteUrl);
16965
+ }
16966
+ const setupSecret = options.setupSecret || process.env.SPARKECODER_TUNNEL_SECRET;
16967
+ const hostname = options.hostname;
16968
+ console.log(chalk.bold(
16969
+ hostname ? `Requesting tunnel for ${hostname} from remote server...` : "Requesting auto-generated tunnel from remote server..."
16970
+ ));
16971
+ const reqBody = {
16972
+ port: Number(apiPort),
16973
+ webPort: Number(port),
16974
+ name: tunnelName
16975
+ };
16976
+ if (hostname) reqBody.hostname = hostname;
16977
+ const res = await fetch(`${remoteUrl.replace(/\/$/, "")}/tunnels`, {
16978
+ method: "POST",
16979
+ headers: {
16980
+ "Content-Type": "application/json",
16981
+ Authorization: `Bearer ${authKey3}`,
16982
+ ...setupSecret ? { "X-Tunnel-Setup-Secret": setupSecret } : {}
16983
+ },
16984
+ body: JSON.stringify(reqBody)
16985
+ });
16986
+ if (!res.ok) {
16987
+ const body = await res.text();
16988
+ console.log(chalk.red(`Server refused tunnel creation (HTTP ${res.status}): ${body}`));
16989
+ process.exitCode = 1;
16990
+ return;
16991
+ }
16992
+ const { tunnelToken, hostname: provisionedHost, apiHostname } = await res.json();
16993
+ const cfPath = await installCloudflaredIfNeeded();
16994
+ if (!cfPath) {
16995
+ console.log(chalk.yellow("cloudflared not installed; run this once installed:"));
16996
+ console.log(chalk.cyan(` cloudflared service install ${tunnelToken}`));
16997
+ return;
16998
+ }
16999
+ console.log(chalk.bold("\nInstalling cloudflared as a service..."));
17000
+ const ok = installAsService(tunnelToken);
17001
+ if (!ok) {
17002
+ console.log(chalk.yellow("Service install failed; you can run it manually:"));
17003
+ console.log(chalk.cyan(` cloudflared tunnel run --token ${tunnelToken}`));
17004
+ }
17005
+ console.log("");
17006
+ console.log(chalk.green("Done."), "Your sparkecoder will be reachable at:");
17007
+ console.log(" Web:", chalk.cyan(`https://${provisionedHost}`));
17008
+ console.log(" API:", chalk.cyan(`https://${apiHostname}`));
17009
+ return;
17010
+ } catch (err) {
17011
+ console.error(chalk.red("Remote tunnel setup failed:"), err?.message || err);
17012
+ process.exitCode = 1;
17013
+ return;
17014
+ }
17015
+ }
17016
+ const cfToken = options.cfApiToken || process.env.CF_API_TOKEN;
17017
+ const cfAccount = options.cfAccountId || process.env.CF_ACCOUNT_ID;
17018
+ const cfZone = options.cfZoneId || process.env.CF_ZONE_ID;
17019
+ if (cfToken && cfAccount) {
17020
+ try {
17021
+ const hostname = options.hostname;
17022
+ if (!hostname) {
17023
+ console.log(chalk.red("--hostname is required in API mode."));
17024
+ process.exitCode = 1;
17025
+ return;
17026
+ }
17027
+ const cfPath = await installCloudflaredIfNeeded();
17028
+ if (!cfPath) {
17029
+ console.log(chalk.red("cloudflared is required. Install from https://github.com/cloudflare/cloudflared/releases and re-run."));
17030
+ process.exitCode = 1;
17031
+ return;
17032
+ }
17033
+ const { createRemoteTunnel: createRemoteTunnel2, setTunnelConfig: setTunnelConfig2, upsertDnsCname: upsertDnsCname2 } = await Promise.resolve().then(() => (init_api(), api_exports));
17034
+ const cfg = { apiToken: cfToken, accountId: cfAccount, zoneId: cfZone };
17035
+ console.log(chalk.bold("Creating tunnel via Cloudflare API..."));
17036
+ const created = await createRemoteTunnel2(cfg, tunnelName || `sparkecoder-${hostname}`);
17037
+ console.log(chalk.green("\u2713"), `tunnel created: ${created.id}`);
17038
+ await setTunnelConfig2(cfg, created.id, [
17039
+ { hostname, service: `http://localhost:${port}` },
17040
+ { hostname: `api-${hostname}`, service: `http://localhost:${apiPort}` }
17041
+ ]);
17042
+ console.log(chalk.green("\u2713"), "ingress config set");
17043
+ if (cfZone) {
17044
+ await upsertDnsCname2(cfg, hostname, created.id);
17045
+ await upsertDnsCname2(cfg, `api-${hostname}`, created.id);
17046
+ console.log(chalk.green("\u2713"), `DNS routes: ${hostname}, api-${hostname}`);
17047
+ } else {
17048
+ console.log(chalk.yellow("Skipping DNS (no --cf-zone-id). Add CNAMEs manually:"));
17049
+ console.log(chalk.dim(` ${hostname} CNAME -> ${created.id}.cfargotunnel.com (proxied)`));
17050
+ console.log(chalk.dim(` api-${hostname} CNAME -> ${created.id}.cfargotunnel.com (proxied)`));
17051
+ }
17052
+ console.log(chalk.bold("\nInstalling cloudflared as a service..."));
17053
+ installAsService(created.token);
17054
+ console.log("");
17055
+ console.log(chalk.green("Done."), "Your sparkecoder will be reachable at:");
17056
+ console.log(" Web:", chalk.cyan(`https://${hostname}`));
17057
+ console.log(" API:", chalk.cyan(`https://api-${hostname}`));
17058
+ return;
17059
+ } catch (err) {
17060
+ console.error(chalk.red("API-mode setup failed:"), err?.message || err);
17061
+ process.exitCode = 1;
17062
+ return;
17063
+ }
17064
+ }
17065
+ if (options.printOnly) {
17066
+ const hostname = options.hostname || "<your-public-hostname>";
17067
+ console.log(chalk.bold("Cloudflared + Cloudflare Access setup (manual)\n"));
17068
+ console.log(chalk.dim("1. brew install cloudflared"));
17069
+ console.log(chalk.dim("2. cloudflared tunnel login"));
17070
+ console.log(chalk.dim(`3. cloudflared tunnel create ${tunnelName}`));
17071
+ console.log(chalk.dim(`4. cloudflared tunnel route dns ${tunnelName} ${hostname}`));
17072
+ console.log(chalk.dim(`5. write ~/.cloudflared/config.yml pointing ${hostname} -> http://localhost:${port}, api-${hostname} -> http://localhost:${apiPort}`));
17073
+ console.log(chalk.dim(`6. cloudflared tunnel run ${tunnelName}`));
17074
+ console.log(chalk.dim("7. Cloudflare Zero Trust -> add a self-hosted Access app on the hostname"));
17075
+ console.log(chalk.dim("8. Paste the AUD into sparkecoder.config.json under auth.cfAccess"));
17076
+ return;
17077
+ }
17078
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
17079
+ const ask = (q, def) => new Promise((res) => {
17080
+ const prompt = def ? `${q} ${chalk.dim(`[${def}]`)} ` : `${q} `;
17081
+ rl.question(prompt, (a) => res((a || "").trim() || def || ""));
17082
+ });
17083
+ const confirm = async (q, def = true) => {
17084
+ if (yes) return true;
17085
+ const a = (await ask(`${q} ${chalk.dim(def ? "(Y/n)" : "(y/N)")}`)).toLowerCase();
17086
+ if (!a) return def;
17087
+ return a.startsWith("y");
17088
+ };
17089
+ const which = (bin) => {
17090
+ try {
17091
+ return execSync2(`command -v ${bin}`, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim() || null;
17092
+ } catch {
17093
+ return null;
17094
+ }
17095
+ };
17096
+ const run = (cmd, args, opts = {}) => {
17097
+ const r = spawnSync(cmd, args, { stdio: opts.inheritIO ? "inherit" : ["ignore", "pipe", "pipe"], encoding: "utf8" });
17098
+ if (opts.check && r.status !== 0) {
17099
+ throw new Error(`${cmd} ${args.join(" ")} failed (${r.status}): ${r.stderr || r.stdout}`);
17100
+ }
17101
+ return r;
17102
+ };
17103
+ try {
17104
+ console.log(chalk.bold("Cloudflared auto-setup\n"));
17105
+ let cfPath = which("cloudflared");
17106
+ if (!cfPath) {
17107
+ console.log(chalk.yellow("cloudflared is not installed."));
17108
+ if (process.platform === "darwin" && which("brew")) {
17109
+ if (await confirm("Install it now via `brew install cloudflared`?", true)) {
17110
+ run("brew", ["install", "cloudflared"], { inheritIO: true, check: true });
17111
+ cfPath = which("cloudflared");
17112
+ }
17113
+ }
17114
+ if (!cfPath) {
17115
+ console.log(chalk.red("Install cloudflared from https://github.com/cloudflare/cloudflared/releases and re-run."));
17116
+ rl.close();
17117
+ return;
17118
+ }
17119
+ }
17120
+ const verOut = run("cloudflared", ["--version"]).stdout?.toString().split("\n")[0] || "installed";
17121
+ console.log(chalk.green("\u2713"), "cloudflared:", chalk.dim(verOut));
17122
+ const cfDir = join16(homedir2(), ".cloudflared");
17123
+ const certPath = join16(cfDir, "cert.pem");
17124
+ if (!existsSync21(certPath)) {
17125
+ console.log(chalk.yellow("No Cloudflare login cert found."));
17126
+ if (await confirm("Run `cloudflared tunnel login` now (opens a browser)?", true)) {
17127
+ run("cloudflared", ["tunnel", "login"], { inheritIO: true, check: true });
17128
+ } else {
17129
+ console.log(chalk.red("Login required to continue."));
17130
+ rl.close();
17131
+ return;
17132
+ }
17133
+ } else {
17134
+ console.log(chalk.green("\u2713"), "logged in:", chalk.dim(certPath));
17135
+ }
17136
+ let tunnels = [];
17137
+ try {
17138
+ const out = run("cloudflared", ["tunnel", "list", "--output", "json"]).stdout || "[]";
17139
+ tunnels = JSON.parse(out);
17140
+ } catch {
17141
+ }
17142
+ let tunnel = tunnels.find((t) => t.name === tunnelName);
17143
+ if (tunnel) {
17144
+ console.log(chalk.green("\u2713"), `reusing existing tunnel "${tunnelName}":`, chalk.dim(tunnel.id));
17145
+ } else {
17146
+ if (!await confirm(`No tunnel named "${tunnelName}" found. Create it?`, true)) {
17147
+ rl.close();
17148
+ return;
17149
+ }
17150
+ const r = run("cloudflared", ["tunnel", "create", tunnelName], { inheritIO: true });
17151
+ if (r.status !== 0) {
17152
+ console.log(chalk.red("Tunnel create failed."));
17153
+ rl.close();
17154
+ return;
17155
+ }
17156
+ try {
17157
+ const out = run("cloudflared", ["tunnel", "list", "--output", "json"]).stdout || "[]";
17158
+ tunnels = JSON.parse(out);
17159
+ tunnel = tunnels.find((t) => t.name === tunnelName);
17160
+ } catch {
17161
+ }
17162
+ if (!tunnel) {
17163
+ console.log(chalk.red("Could not locate freshly-created tunnel. Check `cloudflared tunnel list`."));
17164
+ rl.close();
17165
+ return;
17166
+ }
17167
+ }
17168
+ const credsFile = tunnel.credentials_file || join16(cfDir, `${tunnel.id}.json`);
17169
+ if (!existsSync21(credsFile)) {
17170
+ console.log(chalk.yellow(`Credentials file not found at ${credsFile}. The tunnel may still work via cert.pem.`));
17171
+ }
17172
+ let hostname = options.hostname;
17173
+ if (!hostname) {
17174
+ hostname = await ask("Public hostname for this sparkecoder (e.g. sf-mac-1.example.com):");
17175
+ }
17176
+ if (!hostname) {
17177
+ console.log(chalk.red("A hostname is required to route DNS."));
17178
+ rl.close();
17179
+ return;
17180
+ }
17181
+ const dnsR = run("cloudflared", ["tunnel", "route", "dns", tunnelName, hostname]);
17182
+ if (dnsR.status === 0) {
17183
+ console.log(chalk.green("\u2713"), `DNS routed: ${hostname} -> ${tunnelName}`);
17184
+ } else {
17185
+ const err = (dnsR.stderr || dnsR.stdout || "").toString();
17186
+ if (/already exists|with the same/i.test(err)) {
17187
+ console.log(chalk.green("\u2713"), `DNS already routed for ${hostname}`);
17188
+ } else {
17189
+ console.log(chalk.yellow("DNS route warning:"), err.trim().split("\n").slice(-2).join(" "));
17190
+ }
17191
+ }
17192
+ const configPath = join16(cfDir, "config.yml");
17193
+ const configBody = `tunnel: ${tunnel.id}
17194
+ credentials-file: ${credsFile}
17195
+ ingress:
17196
+ - hostname: ${hostname}
17197
+ service: http://localhost:${port}
17198
+ - hostname: api-${hostname}
17199
+ service: http://localhost:${apiPort}
17200
+ - service: http_status:404
17201
+ `;
17202
+ let wroteConfig = false;
17203
+ if (existsSync21(configPath)) {
17204
+ const existing = readFileSync10(configPath, "utf8");
17205
+ if (existing.trim() === configBody.trim()) {
17206
+ console.log(chalk.green("\u2713"), `config.yml already up to date: ${configPath}`);
17207
+ wroteConfig = true;
17208
+ } else if (await confirm(`A different ${configPath} exists. Overwrite (a backup will be saved)?`, false)) {
17209
+ copyFileSync(configPath, `${configPath}.bak.${Date.now()}`);
17210
+ writeFileSync6(configPath, configBody);
17211
+ console.log(chalk.green("\u2713"), `wrote ${configPath} (previous saved as .bak.*)`);
17212
+ wroteConfig = true;
17213
+ } else {
17214
+ console.log(chalk.dim("Skipping config write. Suggested contents:"));
17215
+ console.log(chalk.cyan(configBody));
17216
+ }
17217
+ } else {
17218
+ writeFileSync6(configPath, configBody);
17219
+ console.log(chalk.green("\u2713"), `wrote ${configPath}`);
17220
+ wroteConfig = true;
17221
+ }
17222
+ let alreadyRunning = false;
17223
+ try {
17224
+ const psOut = execSync2("ps -axo command", { stdio: ["ignore", "pipe", "ignore"] }).toString();
17225
+ alreadyRunning = /cloudflared.*tunnel.*run/.test(psOut);
17226
+ } catch {
17227
+ }
17228
+ if (alreadyRunning) {
17229
+ console.log(chalk.green("\u2713"), "cloudflared tunnel process detected \u2014 you may need to restart it to pick up config changes.");
17230
+ } else if (wroteConfig) {
17231
+ if (await confirm("Install cloudflared as a background service (`sudo cloudflared service install`)?", false)) {
17232
+ run("sudo", ["cloudflared", "service", "install"], { inheritIO: true });
17233
+ } else {
17234
+ console.log(chalk.dim("Start it manually with:"), chalk.cyan(`cloudflared tunnel run ${tunnelName}`));
17235
+ }
17236
+ }
17237
+ console.log("");
17238
+ console.log(chalk.bold("Cloudflare Access (one manual step)"));
17239
+ console.log(chalk.dim(` 1. Open https://one.dash.cloudflare.com -> Access -> Applications -> Add an application.`));
17240
+ console.log(chalk.dim(` 2. Self-hosted, application domain = ${hostname} (and api-${hostname} if you also want the API protected).`));
17241
+ console.log(
17242
+ chalk.dim(' 3. Add an "Allow" policy with emails:'),
17243
+ chalk.cyan(emails.length ? emails.join(", ") : "you@example.com")
17244
+ );
17245
+ console.log(chalk.dim(" 4. Copy the application AUD tag from the dashboard."));
17246
+ console.log(chalk.dim(` 5. Paste it into sparkecoder.config.json:`));
17247
+ console.log(chalk.cyan(` {
17248
+ "auth": {
17249
+ "cfAccess": {
17250
+ "enabled": true,
17251
+ "teamDomain": "${team}.cloudflareaccess.com",
17252
+ "audTag": "<paste-aud-from-cf-dashboard>"
17253
+ },
17254
+ "allowedEmails": [${emails.length ? emails.map((e) => `"${e}"`).join(", ") : '"you@example.com"'}]
17255
+ }
17256
+ }`));
17257
+ console.log(chalk.dim(" (/api/slack/events, /api/inbox/* and /health are exempt from CF Access automatically.)"));
17258
+ console.log("");
17259
+ console.log(chalk.green("Done."), `Your sparkecoder should now be reachable at`, chalk.cyan(`https://${hostname}`));
17260
+ } catch (err) {
17261
+ console.error(chalk.red("Setup failed:"), err?.message || err);
17262
+ process.exitCode = 1;
17263
+ } finally {
17264
+ rl.close();
17265
+ }
16878
17266
  });
16879
17267
  program.command("sessions").description("List all sessions").option("-l, --limit <limit>", "Number of sessions to show", "20").option("-p, --port <port>", "Server port", "3141").option("-H, --host <host>", "Server host", "127.0.0.1").action(async (options) => {
16880
17268
  const baseUrl = `http://${options.host}:${options.port}`;