@jive-ai/cli 0.0.39 → 0.0.40

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 (2) hide show
  1. package/dist/index.mjs +366 -55
  2. package/package.json +6 -2
package/dist/index.mjs CHANGED
@@ -16,12 +16,15 @@ import WebSocket from "ws";
16
16
  import { decode, encode } from "js-base64";
17
17
  import { GraphQLClient } from "graphql-request";
18
18
  import { createSdkMcpServer, query, tool } from "@anthropic-ai/claude-agent-sdk";
19
+ import yaml from "js-yaml";
19
20
  import { z } from "zod";
20
21
  import dedent from "dedent";
21
22
  import { exists } from "fs-extra";
22
23
  import chokidar from "chokidar";
23
24
  import { Tunnel } from "cloudflared";
24
25
  import getPort from "get-port";
26
+ import http from "http";
27
+ import httpProxy from "http-proxy";
25
28
 
26
29
  //#region src/lib/config.ts
27
30
  /**
@@ -153,11 +156,36 @@ async function isProjectInitialized() {
153
156
  return await getProjectConfig() !== null;
154
157
  }
155
158
 
159
+ //#endregion
160
+ //#region src/runner/task/debugLog.ts
161
+ function debugLog(message) {
162
+ console.log(JSON.stringify({
163
+ type: "debug_log",
164
+ message
165
+ }).replace(/\n/g, "\\n"));
166
+ }
167
+
156
168
  //#endregion
157
169
  //#region src/constants.ts
158
170
  const API_URL = process.env.JIVE_API_URL || "https://getjive.app";
159
171
  const GRAPHQL_API_URL = process.env.JIVE_GRAPHQL_API_URL || "https://api.getjive.app/graphql";
160
172
  const WS_URL = process.env.JIVE_WS_URL || "wss://api.getjive.app";
173
+ /**
174
+ * Detect if running in local development mode based on configured URLs or NODE_ENV.
175
+ * Used to enable dev-only features like localhost iframe embedding.
176
+ */
177
+ function isDevMode() {
178
+ const localCheck = [
179
+ "localhost",
180
+ "127.0.0.1",
181
+ "host.docker.internal"
182
+ ];
183
+ return process.env.NODE_ENV === "development" || localCheck.some((check) => API_URL.includes(check)) || localCheck.some((check) => WS_URL.includes(check));
184
+ }
185
+ debugLog(`[Constants] isDevMode: ${isDevMode()}`);
186
+ debugLog(`[Constants] NODE_ENV: ${process.env.NODE_ENV}`);
187
+ debugLog(`[Constants] API_URL: ${API_URL}`);
188
+ debugLog(`[Constants] WS_URL: ${WS_URL}`);
161
189
 
162
190
  //#endregion
163
191
  //#region src/commands/auth.ts
@@ -3346,10 +3374,9 @@ async function spawnTaskDocker(ctx, config$2, opts) {
3346
3374
  envVars.push("-e", `JIVE_RUNNER_ID=${config$2.id}`);
3347
3375
  envVars.push("-e", `JIVE_PROJECT_ID=${ctx.projectId}`);
3348
3376
  envVars.push("-e", `JIVE_TASK_ID=${ctx.taskId}`);
3349
- const isDevMode = API_URL.includes("localhost") || process.env.NODE_ENV === "development";
3350
- const dockerApiUrl = isDevMode ? API_URL.replace("localhost", "host.docker.internal") : API_URL;
3351
- const dockerGraphqlApiUrl = isDevMode ? GRAPHQL_API_URL.replace("localhost", "host.docker.internal") : GRAPHQL_API_URL;
3352
- const dockerWsUrl = isDevMode ? WS_URL.replace("localhost", "host.docker.internal") : WS_URL;
3377
+ const dockerApiUrl = isDevMode() ? API_URL.replace("localhost", "host.docker.internal") : API_URL;
3378
+ const dockerGraphqlApiUrl = isDevMode() ? GRAPHQL_API_URL.replace("localhost", "host.docker.internal") : GRAPHQL_API_URL;
3379
+ const dockerWsUrl = isDevMode() ? WS_URL.replace("localhost", "host.docker.internal") : WS_URL;
3353
3380
  envVars.push("-e", `JIVE_API_URL=${dockerApiUrl}`);
3354
3381
  envVars.push("-e", `JIVE_GRAPHQL_API_URL=${dockerGraphqlApiUrl}`);
3355
3382
  envVars.push("-e", `JIVE_WS_URL=${dockerWsUrl}`);
@@ -3366,7 +3393,7 @@ async function spawnTaskDocker(ctx, config$2, opts) {
3366
3393
  ];
3367
3394
  console.log(chalk.dim("Using Sysbox runtime for isolated Docker-in-Docker"));
3368
3395
  dockerArgs.push("--network", "bridge");
3369
- if (isDevMode) {
3396
+ if (isDevMode()) {
3370
3397
  dockerArgs.push("--add-host", "host.docker.internal:host-gateway");
3371
3398
  dockerArgs.push("--pull", "never");
3372
3399
  }
@@ -3430,7 +3457,7 @@ async function createGraphQLClient() {
3430
3457
 
3431
3458
  //#endregion
3432
3459
  //#region package.json
3433
- var version = "0.0.39";
3460
+ var version = "0.0.40";
3434
3461
 
3435
3462
  //#endregion
3436
3463
  //#region src/runner/index.ts
@@ -3602,17 +3629,13 @@ var TaskRunner = class {
3602
3629
  return this.taskProcesses[taskId.toString()] = new Promise((resolve) => {
3603
3630
  (this.config.type === "docker" ? spawnTaskDocker(ctx, this.config, {
3604
3631
  onMessage: (message) => this.onTaskMessage(ctx.taskId, message),
3605
- onError: () => {
3606
- this.taskProcesses[ctx.taskId.toString()] = null;
3607
- },
3632
+ onError: () => {},
3608
3633
  onClose: () => {
3609
3634
  this.taskProcesses[ctx.taskId.toString()] = null;
3610
3635
  }
3611
3636
  }) : Promise.resolve(spawnTask(ctx, {
3612
3637
  onMessage: (message) => this.onTaskMessage(ctx.taskId, message),
3613
- onError: () => {
3614
- this.taskProcesses[ctx.taskId.toString()] = null;
3615
- },
3638
+ onError: () => {},
3616
3639
  onClose: () => {
3617
3640
  this.taskProcesses[ctx.taskId.toString()] = null;
3618
3641
  }
@@ -3826,15 +3849,6 @@ var TaskRunner = class {
3826
3849
  }
3827
3850
  };
3828
3851
 
3829
- //#endregion
3830
- //#region src/runner/task/debugLog.ts
3831
- function debugLog(message) {
3832
- console.log(JSON.stringify({
3833
- type: "debug_log",
3834
- message
3835
- }).replace(/\n/g, "\\n"));
3836
- }
3837
-
3838
3852
  //#endregion
3839
3853
  //#region src/runner/query.ts
3840
3854
  async function queryClaude(prompt, mcpServer, opts) {
@@ -3974,14 +3988,6 @@ async function commitChanges(commitMessage, branch) {
3974
3988
  await execAsync$2(`git push origin ${branch}`);
3975
3989
  }
3976
3990
  /**
3977
- * Tool: jive-tasks__set_task_url
3978
- * Set the preview URL for a task
3979
- */
3980
- async function setTaskUrl(taskId, url) {
3981
- if (!context) throw new Error("Task context not initialized");
3982
- await getApiClient().updateTask(taskId, { previewUrl: url });
3983
- }
3984
- /**
3985
3991
  * Tool: jive-tasks__create_pull_request
3986
3992
  * Create a pull request or merge request for the task
3987
3993
  */
@@ -4124,18 +4130,12 @@ function createTasksSdkServer(task) {
4124
4130
  };
4125
4131
  }
4126
4132
  }),
4127
- tool("setup_cloudflare_tunnel", "Run this before serving an application for user/automated testing. Spin up a cloudflare tunnel and returns the public host and port.", { taskId: z.number() }, async (args) => {
4133
+ tool("refresh_preview", "Reads the .jiverc file, regenerates the specified cloudflare tunnels and starts the serve command. Call this if the user reports the preview is not working or otherwise inaccessible.", {}, async () => {
4128
4134
  try {
4129
- const result = await task.ensureCloudflareTunnel();
4130
- if ("error" in result) return { content: [{
4131
- type: "text",
4132
- text: `Failed to set up cloudflare tunnel, error: ${result.error}`
4133
- }] };
4134
- task.disableIdleTimeout();
4135
- await setTaskUrl(args.taskId, `https://${result.host}`);
4135
+ await task.setup();
4136
4136
  return { content: [{
4137
4137
  type: "text",
4138
- text: `Tunnel set up successfully. Host: ${result.host} Port: ${result.port}. If you start a server it may be accessible to the public.`
4138
+ text: `Preview refreshed successfully`
4139
4139
  }] };
4140
4140
  } catch (error$1) {
4141
4141
  return {
@@ -4262,6 +4262,67 @@ function getClaudeSessionPath(directory, sessionId) {
4262
4262
  return path.join(process.env.HOME || "~", ".claude", "projects", sanitizedDir, `${sessionId}.jsonl`);
4263
4263
  }
4264
4264
 
4265
+ //#endregion
4266
+ //#region src/runner/PreviewProxy.ts
4267
+ const ALLOWED_FRAME_ANCESTORS = isDevMode() ? "'self' https://*.getjive.app https://getjive.app http://localhost:* http://127.0.0.1:*" : "'self' https://*.getjive.app https://getjive.app";
4268
+ var PreviewProxy = class {
4269
+ server = null;
4270
+ proxy = null;
4271
+ constructor(config$2) {
4272
+ this.config = config$2;
4273
+ }
4274
+ log(message) {
4275
+ debugLog(`[PreviewProxy:${this.config.name}] ${message}`);
4276
+ }
4277
+ async start() {
4278
+ this.proxy = httpProxy.createProxyServer({
4279
+ target: `http://localhost:${this.config.targetPort}`,
4280
+ ws: true
4281
+ });
4282
+ this.proxy.on("proxyRes", (proxyRes, req, res) => {
4283
+ delete proxyRes.headers["x-frame-options"];
4284
+ delete proxyRes.headers["X-Frame-Options"];
4285
+ let csp = proxyRes.headers["content-security-policy"] || "";
4286
+ csp = csp.replace(/frame-ancestors[^;]*;?/gi, "").trim();
4287
+ const frameAncestors = `frame-ancestors ${ALLOWED_FRAME_ANCESTORS}`;
4288
+ csp = csp ? `${csp}; ${frameAncestors}` : frameAncestors;
4289
+ debugLog(`[PreviewProxy:${this.config.name}] CSP: ${csp}`);
4290
+ proxyRes.headers["content-security-policy"] = csp;
4291
+ });
4292
+ this.proxy.on("error", (err, req, res) => {
4293
+ this.log(`Proxy error: ${err.message}`);
4294
+ if (res instanceof http.ServerResponse && !res.headersSent) {
4295
+ res.writeHead(502, { "Content-Type": "text/plain" });
4296
+ res.end("Proxy error: " + err.message);
4297
+ }
4298
+ });
4299
+ this.server = http.createServer((req, res) => {
4300
+ this.proxy.web(req, res);
4301
+ });
4302
+ this.server.on("upgrade", (req, socket, head) => {
4303
+ this.proxy.ws(req, socket, head);
4304
+ });
4305
+ await new Promise((resolve, reject) => {
4306
+ this.server.listen(this.config.proxyPort, () => {
4307
+ this.log(`Proxy listening on port ${this.config.proxyPort}, forwarding to ${this.config.targetPort}`);
4308
+ resolve();
4309
+ });
4310
+ this.server.on("error", reject);
4311
+ });
4312
+ }
4313
+ stop() {
4314
+ if (this.server) {
4315
+ this.server.close();
4316
+ this.server = null;
4317
+ }
4318
+ if (this.proxy) {
4319
+ this.proxy.close();
4320
+ this.proxy = null;
4321
+ }
4322
+ this.log("Proxy stopped");
4323
+ }
4324
+ };
4325
+
4265
4326
  //#endregion
4266
4327
  //#region src/runner/Task.ts
4267
4328
  const ACK_TIMEOUT_MS = 1e3;
@@ -4276,7 +4337,9 @@ var Task = class {
4276
4337
  pendingAcks = /* @__PURE__ */ new Map();
4277
4338
  pendingPermissions = /* @__PURE__ */ new Map();
4278
4339
  pendingQuestions = /* @__PURE__ */ new Map();
4279
- tunnel = null;
4340
+ tunnels = /* @__PURE__ */ new Map();
4341
+ proxies = /* @__PURE__ */ new Map();
4342
+ primaryTunnelHost = null;
4280
4343
  fileWatcher = null;
4281
4344
  lastFilePosition = 0;
4282
4345
  currentPendingLineUuid = null;
@@ -4284,6 +4347,7 @@ var Task = class {
4284
4347
  lastActivityTime = Date.now();
4285
4348
  idleWatchdogInterval = null;
4286
4349
  idleTimeoutDisabled = false;
4350
+ serveProcess = null;
4287
4351
  get defaultBranch() {
4288
4352
  const tasksConfig = getTasksConfigSync();
4289
4353
  if (!tasksConfig) {
@@ -4373,7 +4437,8 @@ var Task = class {
4373
4437
  }
4374
4438
  this.stopHeartbeat();
4375
4439
  this.stopSessionFileWatcher();
4376
- this.cleanupCloudflareTunnel();
4440
+ this.cleanupServeProcess();
4441
+ this.cleanupAllTunnels();
4377
4442
  this.debugLog("Task process exiting due to idle timeout");
4378
4443
  process.exit(0);
4379
4444
  }
@@ -4390,6 +4455,7 @@ var Task = class {
4390
4455
  process.exit(1);
4391
4456
  }
4392
4457
  this.debugLog(`✓ Git repository and session file setup complete`);
4458
+ await this.setup();
4393
4459
  this.startHeartbeat();
4394
4460
  this.startIdleWatchdog();
4395
4461
  this.sendStatusUpdate("idle");
@@ -4400,13 +4466,28 @@ var Task = class {
4400
4466
  this.stopHeartbeat();
4401
4467
  this.stopIdleWatchdog();
4402
4468
  this.stopSessionFileWatcher();
4403
- this.cleanupCloudflareTunnel();
4469
+ this.cleanupServeProcess();
4470
+ this.cleanupAllTunnels();
4404
4471
  this.sendStatusUpdate("exited");
4405
4472
  };
4406
4473
  process.on("exit", cleanup);
4407
4474
  process.on("SIGINT", cleanup);
4408
4475
  process.on("SIGTERM", cleanup);
4409
4476
  }
4477
+ /**
4478
+ * Stop the serve process if it's running
4479
+ */
4480
+ cleanupServeProcess() {
4481
+ if (this.serveProcess) {
4482
+ try {
4483
+ if (this.serveProcess.pid) process.kill(-this.serveProcess.pid, "SIGTERM");
4484
+ this.debugLog("Serve process stopped");
4485
+ } catch (error$1) {
4486
+ this.debugLog(`Warning: Failed to stop serve process: ${error$1.message}`);
4487
+ }
4488
+ this.serveProcess = null;
4489
+ }
4490
+ }
4410
4491
  stdinBuffer = "";
4411
4492
  listenForMessages() {
4412
4493
  process.stdin.on("data", (data) => {
@@ -4611,41 +4692,44 @@ var Task = class {
4611
4692
  this.debugLog("Idle timeout disabled");
4612
4693
  }
4613
4694
  /**
4614
- * Set up Cloudflare tunnel for exposing local services to the internet.
4695
+ * Set up a single Cloudflare tunnel for exposing local services to the internet.
4696
+ * This is the legacy method for backwards compatibility when no tunnels are configured.
4615
4697
  * Sets HOST and PORT environment variables for use by the task.
4616
4698
  */
4617
4699
  async ensureCloudflareTunnel() {
4618
- this.cleanupCloudflareTunnel();
4619
- this.debugLog("Setting up Cloudflare tunnel...");
4700
+ this.cleanupAllTunnels();
4701
+ this.debugLog("Setting up Cloudflare tunnel (legacy mode)...");
4620
4702
  const idealPort = parseInt(process.env.PORT ?? "", 10);
4621
4703
  if (!isNaN(idealPort)) this.debugLog(`Trying ideal port: ${idealPort}`);
4622
4704
  const port = await getPort({ port: idealPort });
4623
4705
  this.debugLog(`Selected available port: ${port}`);
4624
4706
  process.env.PORT = port.toString();
4625
4707
  try {
4626
- this.tunnel = Tunnel.quick(`http://localhost:${port}`);
4708
+ const tunnel = Tunnel.quick(`http://localhost:${port}`);
4627
4709
  const tunnelUrl = await new Promise((resolve, reject) => {
4628
4710
  const timeout = setTimeout(() => {
4629
4711
  reject(/* @__PURE__ */ new Error("Cloudflare tunnel URL resolution timed out after 30 seconds"));
4630
4712
  }, 3e4);
4631
- this.tunnel.once("url", (url) => {
4713
+ tunnel.once("url", (url) => {
4632
4714
  clearTimeout(timeout);
4633
4715
  resolve(url);
4634
4716
  });
4635
- this.tunnel.once("exit", (code) => {
4717
+ tunnel.once("exit", (code) => {
4636
4718
  clearTimeout(timeout);
4637
4719
  reject(/* @__PURE__ */ new Error(`Cloudflare tunnel exited unexpectedly with code ${code}`));
4638
4720
  });
4639
4721
  });
4640
4722
  const host = new URL(tunnelUrl).host;
4641
4723
  process.env.JIVE_PUBLIC_HOST = host;
4724
+ this.tunnels.set("_legacy", tunnel);
4725
+ this.primaryTunnelHost = host;
4642
4726
  this.debugLog(`Cloudflare tunnel established: Host: ${host}, Port: ${port}`);
4643
4727
  return {
4644
4728
  port,
4645
4729
  host
4646
4730
  };
4647
4731
  } catch (error$1) {
4648
- this.cleanupCloudflareTunnel();
4732
+ this.cleanupAllTunnels();
4649
4733
  return { error: error$1 };
4650
4734
  }
4651
4735
  }
@@ -4708,18 +4792,100 @@ Host gitlab.com
4708
4792
  this.debugLog("✓ SSH authentication configured with deploy key");
4709
4793
  }
4710
4794
  /**
4711
- * Stop the Cloudflare tunnel and clean up resources
4795
+ * Stop all Cloudflare tunnels, proxies, and clean up resources
4796
+ */
4797
+ cleanupAllTunnels() {
4798
+ if (this.proxies.size > 0) {
4799
+ this.debugLog(`Stopping ${this.proxies.size} preview proxy(ies)...`);
4800
+ for (const [name$1, proxy] of this.proxies) try {
4801
+ proxy.stop();
4802
+ this.debugLog(`Preview proxy '${name$1}' stopped`);
4803
+ } catch (error$1) {
4804
+ console.warn(chalk.yellow(`Warning: Failed to stop preview proxy '${name$1}': ${error$1.message}`));
4805
+ }
4806
+ this.proxies.clear();
4807
+ }
4808
+ if (this.tunnels.size === 0) return;
4809
+ this.debugLog(`Stopping ${this.tunnels.size} Cloudflare tunnel(s)...`);
4810
+ for (const [name$1, tunnel] of this.tunnels) try {
4811
+ tunnel.stop();
4812
+ this.debugLog(`Cloudflare tunnel '${name$1}' stopped`);
4813
+ } catch (error$1) {
4814
+ console.warn(chalk.yellow(`Warning: Failed to stop Cloudflare tunnel '${name$1}': ${error$1.message}`));
4815
+ }
4816
+ this.tunnels.clear();
4817
+ this.primaryTunnelHost = null;
4818
+ }
4819
+ /**
4820
+ * Set up all configured tunnels from .jiverc config.
4821
+ * Sets environment variables for each tunnel:
4822
+ * - {NAME}_PORT = actual port used (for dev server to bind to)
4823
+ * - JIVE_{NAME}_HOST or custom envVarName = tunnel host
4824
+ *
4825
+ * When embedded !== false (default), a proxy server is inserted between
4826
+ * the tunnel and the dev server to modify CSP headers for iframe embedding.
4712
4827
  */
4713
- cleanupCloudflareTunnel() {
4714
- if (this.tunnel) {
4828
+ async setupTunnels(tunnelConfigs) {
4829
+ this.debugLog(`Setting up ${tunnelConfigs.length} tunnel(s)...`);
4830
+ this.cleanupAllTunnels();
4831
+ const primaryTunnel = tunnelConfigs.find((t$2) => t$2.primary) ?? tunnelConfigs[0];
4832
+ for (const config$2 of tunnelConfigs) {
4833
+ const name$1 = config$2.name.toUpperCase();
4834
+ const useProxy = config$2.embedded !== false;
4715
4835
  try {
4716
- this.tunnel.stop();
4717
- this.debugLog("Cloudflare tunnel stopped");
4836
+ let tunnelPort;
4837
+ let targetPort;
4838
+ if (useProxy) {
4839
+ const proxyPort = await getPort({ port: config$2.port + 1 });
4840
+ targetPort = await getPort({ port: config$2.port });
4841
+ tunnelPort = proxyPort;
4842
+ this.debugLog(`Tunnel '${config$2.name}': proxy on ${proxyPort}, dev server on ${targetPort}`);
4843
+ const proxy = new PreviewProxy({
4844
+ proxyPort,
4845
+ targetPort,
4846
+ name: config$2.name
4847
+ });
4848
+ await proxy.start();
4849
+ this.proxies.set(config$2.name, proxy);
4850
+ } else {
4851
+ tunnelPort = await getPort({ port: config$2.port });
4852
+ targetPort = tunnelPort;
4853
+ this.debugLog(`Tunnel '${config$2.name}': direct connection on port ${tunnelPort} (proxy disabled)`);
4854
+ }
4855
+ process.env[`${name$1}_PORT`] = targetPort.toString();
4856
+ const tunnel = Tunnel.quick(`http://localhost:${tunnelPort}`);
4857
+ const tunnelUrl = await new Promise((resolve, reject) => {
4858
+ const timeout = setTimeout(() => {
4859
+ reject(/* @__PURE__ */ new Error(`Cloudflare tunnel '${config$2.name}' URL resolution timed out after 30 seconds`));
4860
+ }, 3e4);
4861
+ tunnel.once("url", (url) => {
4862
+ clearTimeout(timeout);
4863
+ resolve(url);
4864
+ });
4865
+ tunnel.once("exit", (code) => {
4866
+ clearTimeout(timeout);
4867
+ reject(/* @__PURE__ */ new Error(`Cloudflare tunnel '${config$2.name}' exited unexpectedly with code ${code}`));
4868
+ });
4869
+ });
4870
+ const host = new URL(tunnelUrl).host;
4871
+ const hostEnvVar = config$2.envVarName ?? `JIVE_${name$1}_HOST`;
4872
+ process.env[hostEnvVar] = host;
4873
+ if (config$2 === primaryTunnel) {
4874
+ this.primaryTunnelHost = host;
4875
+ process.env.JIVE_PUBLIC_HOST = host;
4876
+ process.env.PORT = targetPort.toString();
4877
+ this.debugLog(`Tunnel '${config$2.name}' is primary (previewUrl)`);
4878
+ }
4879
+ this.tunnels.set(config$2.name, tunnel);
4880
+ const proxyStatus = useProxy ? " (proxy enabled)" : " (proxy disabled)";
4881
+ this.debugLog(`✓ Tunnel '${config$2.name}' established: ${hostEnvVar}=${host}, ${name$1}_PORT=${targetPort}${proxyStatus}`);
4718
4882
  } catch (error$1) {
4719
- console.warn(chalk.yellow(`Warning: Failed to stop Cloudflare tunnel: ${error$1.message}`));
4883
+ this.debugLog(`✗ Failed to set up tunnel '${config$2.name}': ${error$1.message}`);
4884
+ this.cleanupAllTunnels();
4885
+ throw error$1;
4720
4886
  }
4721
- this.tunnel = null;
4722
4887
  }
4888
+ this.debugLog(`✓ All ${tunnelConfigs.length} tunnel(s) established`);
4723
4889
  }
4724
4890
  async setupGitRepository() {
4725
4891
  let { repository } = this.ctx;
@@ -4783,6 +4949,151 @@ Host gitlab.com
4783
4949
  await execAsync$1(`git status`);
4784
4950
  this.debugLog(`✓ Git repository setup complete`);
4785
4951
  }
4952
+ /**
4953
+ * Read and parse the .jiverc.yml or .jiverc.json config file from the project root.
4954
+ * Returns null if neither file exists.
4955
+ */
4956
+ async readJivercConfig() {
4957
+ const { directory } = this.ctx;
4958
+ const ymlPath = path$1.join(directory, ".jiverc.yml");
4959
+ const jsonPath = path$1.join(directory, ".jiverc.json");
4960
+ try {
4961
+ if (existsSync(ymlPath)) {
4962
+ this.debugLog(`Found config file: .jiverc.yml`);
4963
+ const content = await promises.readFile(ymlPath, "utf-8");
4964
+ return yaml.load(content);
4965
+ }
4966
+ if (existsSync(jsonPath)) {
4967
+ this.debugLog(`Found config file: .jiverc.json`);
4968
+ const content = await promises.readFile(jsonPath, "utf-8");
4969
+ return JSON.parse(content);
4970
+ }
4971
+ this.debugLog(`No .jiverc.yml or .jiverc.json found`);
4972
+ return null;
4973
+ } catch (error$1) {
4974
+ this.debugLog(`Error reading jiverc config: ${error$1.message}`);
4975
+ return null;
4976
+ }
4977
+ }
4978
+ /**
4979
+ * Run a shell command and return success/failure status.
4980
+ * Logs stdout/stderr appropriately.
4981
+ */
4982
+ async runSetupCommand(command, label) {
4983
+ this.debugLog(`Running ${label} command: ${command}`);
4984
+ try {
4985
+ const { stdout, stderr } = await execAsync$1(command, {
4986
+ cwd: this.ctx.directory,
4987
+ env: process.env,
4988
+ maxBuffer: 10 * 1024 * 1024
4989
+ });
4990
+ if (stdout) this.debugLog(`${label} stdout:\n${stdout}`);
4991
+ if (stderr) this.debugLog(`${label} stderr:\n${stderr}`);
4992
+ this.debugLog(`✓ ${label} command completed successfully`);
4993
+ return true;
4994
+ } catch (error$1) {
4995
+ this.debugLog(`✗ ${label} command failed: ${error$1.message}`);
4996
+ if (error$1.stdout) this.debugLog(`stdout:\n${error$1.stdout}`);
4997
+ if (error$1.stderr) this.debugLog(`stderr:\n${error$1.stderr}`);
4998
+ return false;
4999
+ }
5000
+ }
5001
+ /**
5002
+ * Run project setup based on .jiverc config file.
5003
+ * - Sets up tunnels first (if configured) so env vars are available
5004
+ * - Runs setup command synchronously if defined
5005
+ * - Starts serve command if defined
5006
+ *
5007
+ * Note: this is also called by the 'refresh_preview' tool.
5008
+ */
5009
+ async setup() {
5010
+ this.cleanupServeProcess();
5011
+ this.cleanupAllTunnels();
5012
+ try {
5013
+ const config$2 = await this.readJivercConfig();
5014
+ if (!config$2) {
5015
+ this.debugLog("No jiverc config found, skipping setup");
5016
+ return;
5017
+ }
5018
+ if (config$2.tunnels && config$2.tunnels.length > 0) await this.setupTunnels(config$2.tunnels);
5019
+ if (config$2.commands?.setup) {
5020
+ this.debugLog("Running setup command...");
5021
+ if (!await this.runSetupCommand(config$2.commands.setup, "setup")) this.debugLog("Setup command failed, continuing anyway...");
5022
+ }
5023
+ if (config$2.commands?.serve) await this.serve();
5024
+ } catch (error$1) {
5025
+ this.debugLog(`Error during setup: ${error$1.message}`);
5026
+ }
5027
+ }
5028
+ /**
5029
+ * Start the serve command from .jiverc config.
5030
+ * Tunnels should already be set up by setup() - this just starts the serve process.
5031
+ * If tunnels aren't set up yet (e.g., serve() called directly), it will set them up.
5032
+ * Updates the task's previewUrl using the primary tunnel.
5033
+ */
5034
+ async serve() {
5035
+ try {
5036
+ const config$2 = await this.readJivercConfig();
5037
+ if (!config$2?.commands?.serve) {
5038
+ this.debugLog("No serve command configured in .jiverc");
5039
+ return;
5040
+ }
5041
+ const serveCommand = config$2.commands.serve;
5042
+ this.debugLog(`Starting serve command: ${serveCommand}`);
5043
+ this.cleanupServeProcess();
5044
+ if (this.tunnels.size === 0 && config$2.tunnels && config$2.tunnels.length > 0) await this.setupTunnels(config$2.tunnels);
5045
+ if (this.tunnels.size === 0) {
5046
+ this.debugLog("No tunnels configured, setting up legacy single tunnel...");
5047
+ const tunnelResult = await this.ensureCloudflareTunnel();
5048
+ if ("error" in tunnelResult) {
5049
+ this.debugLog(`Failed to establish Cloudflare tunnel: ${tunnelResult.error}`);
5050
+ return;
5051
+ }
5052
+ this.primaryTunnelHost = tunnelResult.host;
5053
+ }
5054
+ this.serveProcess = spawn(serveCommand, [], {
5055
+ cwd: this.ctx.directory,
5056
+ env: process.env,
5057
+ shell: true,
5058
+ detached: true,
5059
+ stdio: [
5060
+ "ignore",
5061
+ "pipe",
5062
+ "pipe"
5063
+ ]
5064
+ });
5065
+ this.serveProcess.unref();
5066
+ this.serveProcess.stdout?.on("data", (data) => {
5067
+ this.debugLog(`[serve] ${data.toString().trim()}`);
5068
+ });
5069
+ this.serveProcess.stderr?.on("data", (data) => {
5070
+ this.debugLog(`[serve:err] ${data.toString().trim()}`);
5071
+ });
5072
+ this.serveProcess.on("error", (error$1) => {
5073
+ this.debugLog(`[serve] Process error: ${error$1.message}`);
5074
+ });
5075
+ this.serveProcess.on("exit", (code, signal) => {
5076
+ this.debugLog(`[serve] Process exited with code ${code}, signal ${signal}`);
5077
+ this.serveProcess = null;
5078
+ });
5079
+ this.disableIdleTimeout();
5080
+ if (this.primaryTunnelHost) {
5081
+ const previewUrl = `https://${this.primaryTunnelHost}`;
5082
+ this.debugLog(`✓ Serve command started in background`);
5083
+ this.debugLog(`Public URL: ${previewUrl}`);
5084
+ try {
5085
+ this.debugLog("Waiting 5 seconds to ensure tunnel is ready...");
5086
+ await new Promise((resolve) => setTimeout(resolve, 5e3));
5087
+ await getApiClient().updateTask(this.id, { previewUrl });
5088
+ this.debugLog(`✓ Task previewUrl updated to ${previewUrl}`);
5089
+ } catch (error$1) {
5090
+ this.debugLog(`Failed to update task previewUrl: ${error$1.message}`);
5091
+ }
5092
+ } else this.debugLog(`✓ Serve command started in background (no tunnel configured)`);
5093
+ } catch (error$1) {
5094
+ this.debugLog(`Error during serve: ${error$1.message}`);
5095
+ }
5096
+ }
4786
5097
  get claudeSessionFilePath() {
4787
5098
  return path$1.join(process.env.HOME || "~", ".claude", "projects", this.ctx.directory.replace(/\//g, "-"), `${this.ctx.claudeSessionId}.jsonl`);
4788
5099
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@jive-ai/cli",
4
- "version": "0.0.39",
4
+ "version": "0.0.40",
5
5
  "main": "index.js",
6
6
  "files": [
7
7
  "dist",
@@ -16,7 +16,7 @@
16
16
  "typecheck": "tsc --noEmit",
17
17
  "build": "tsdown && npm pack && npm install -g jive-ai-cli-*.tgz",
18
18
  "docker:build": "touch jive-ai-cli-0.0.0.tgz && docker build -t jiveai/task:latest . && rm -f jive-ai-cli-*.tgz",
19
- "docker:build:local": "docker rmi jiveai/task:$npm_package_version jiveai/task:latest; bun run build && docker build --build-arg USE_LOCAL=true -t jiveai/task:latest -t jiveai/task:$npm_package_version .",
19
+ "docker:build:local": "docker rmi jiveai/task:$npm_package_version jiveai/task:latest; bun run build && docker build --no-cache --build-arg USE_LOCAL=true -t jiveai/task:latest -t jiveai/task:$npm_package_version .",
20
20
  "docker:build:public": "touch jive-ai-cli-0.0.0.tgz && docker buildx build --platform linux/amd64 -t jiveai/task:latest -t jiveai/task:$npm_package_version . && rm -f jive-ai-cli-*.tgz",
21
21
  "docker:push": "docker buildx build --platform linux/amd64 --push -t jiveai/task:latest -t jiveai/task:$npm_package_version .",
22
22
  "docker:publish": "touch jive-ai-cli-0.0.0.tgz && docker buildx build --platform linux/amd64 --push -t jiveai/task:latest -t jiveai/task:$npm_package_version . && rm -f jive-ai-cli-*.tgz",
@@ -27,6 +27,8 @@
27
27
  "description": "",
28
28
  "devDependencies": {
29
29
  "@types/fs-extra": "^11.0.4",
30
+ "@types/http-proxy": "^1.17.16",
31
+ "@types/js-yaml": "^4.0.9",
30
32
  "@types/node": "^24.10.1",
31
33
  "@types/prompts": "^2.4.9",
32
34
  "@types/ws": "^8.5.13",
@@ -46,8 +48,10 @@
46
48
  "get-port": "^7.1.0",
47
49
  "graphql": "^16.12.0",
48
50
  "graphql-request": "^7.4.0",
51
+ "http-proxy": "^1.18.1",
49
52
  "gray-matter": "^4.0.3",
50
53
  "js-base64": "^3.7.8",
54
+ "js-yaml": "^3.14.2",
51
55
  "ora": "^9.0.0",
52
56
  "prompts": "^2.4.2",
53
57
  "typescript": "^5.9.3",