@jive-ai/cli 0.0.39 → 0.0.41

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 +353 -46
  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
  /**
@@ -158,6 +161,18 @@ async function isProjectInitialized() {
158
161
  const API_URL = process.env.JIVE_API_URL || "https://getjive.app";
159
162
  const GRAPHQL_API_URL = process.env.JIVE_GRAPHQL_API_URL || "https://api.getjive.app/graphql";
160
163
  const WS_URL = process.env.JIVE_WS_URL || "wss://api.getjive.app";
164
+ /**
165
+ * Detect if running in local development mode based on configured URLs or NODE_ENV.
166
+ * Used to enable dev-only features like localhost iframe embedding.
167
+ */
168
+ function isDevMode() {
169
+ const localCheck = [
170
+ "localhost",
171
+ "127.0.0.1",
172
+ "host.docker.internal"
173
+ ];
174
+ return process.env.NODE_ENV === "development" || localCheck.some((check) => API_URL.includes(check)) || localCheck.some((check) => WS_URL.includes(check));
175
+ }
161
176
 
162
177
  //#endregion
163
178
  //#region src/commands/auth.ts
@@ -3346,10 +3361,9 @@ async function spawnTaskDocker(ctx, config$2, opts) {
3346
3361
  envVars.push("-e", `JIVE_RUNNER_ID=${config$2.id}`);
3347
3362
  envVars.push("-e", `JIVE_PROJECT_ID=${ctx.projectId}`);
3348
3363
  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;
3364
+ const dockerApiUrl = isDevMode() ? API_URL.replace("localhost", "host.docker.internal") : API_URL;
3365
+ const dockerGraphqlApiUrl = isDevMode() ? GRAPHQL_API_URL.replace("localhost", "host.docker.internal") : GRAPHQL_API_URL;
3366
+ const dockerWsUrl = isDevMode() ? WS_URL.replace("localhost", "host.docker.internal") : WS_URL;
3353
3367
  envVars.push("-e", `JIVE_API_URL=${dockerApiUrl}`);
3354
3368
  envVars.push("-e", `JIVE_GRAPHQL_API_URL=${dockerGraphqlApiUrl}`);
3355
3369
  envVars.push("-e", `JIVE_WS_URL=${dockerWsUrl}`);
@@ -3366,7 +3380,7 @@ async function spawnTaskDocker(ctx, config$2, opts) {
3366
3380
  ];
3367
3381
  console.log(chalk.dim("Using Sysbox runtime for isolated Docker-in-Docker"));
3368
3382
  dockerArgs.push("--network", "bridge");
3369
- if (isDevMode) {
3383
+ if (isDevMode()) {
3370
3384
  dockerArgs.push("--add-host", "host.docker.internal:host-gateway");
3371
3385
  dockerArgs.push("--pull", "never");
3372
3386
  }
@@ -3430,7 +3444,7 @@ async function createGraphQLClient() {
3430
3444
 
3431
3445
  //#endregion
3432
3446
  //#region package.json
3433
- var version = "0.0.39";
3447
+ var version = "0.0.41";
3434
3448
 
3435
3449
  //#endregion
3436
3450
  //#region src/runner/index.ts
@@ -3602,17 +3616,13 @@ var TaskRunner = class {
3602
3616
  return this.taskProcesses[taskId.toString()] = new Promise((resolve) => {
3603
3617
  (this.config.type === "docker" ? spawnTaskDocker(ctx, this.config, {
3604
3618
  onMessage: (message) => this.onTaskMessage(ctx.taskId, message),
3605
- onError: () => {
3606
- this.taskProcesses[ctx.taskId.toString()] = null;
3607
- },
3619
+ onError: () => {},
3608
3620
  onClose: () => {
3609
3621
  this.taskProcesses[ctx.taskId.toString()] = null;
3610
3622
  }
3611
3623
  }) : Promise.resolve(spawnTask(ctx, {
3612
3624
  onMessage: (message) => this.onTaskMessage(ctx.taskId, message),
3613
- onError: () => {
3614
- this.taskProcesses[ctx.taskId.toString()] = null;
3615
- },
3625
+ onError: () => {},
3616
3626
  onClose: () => {
3617
3627
  this.taskProcesses[ctx.taskId.toString()] = null;
3618
3628
  }
@@ -3974,14 +3984,6 @@ async function commitChanges(commitMessage, branch) {
3974
3984
  await execAsync$2(`git push origin ${branch}`);
3975
3985
  }
3976
3986
  /**
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
3987
  * Tool: jive-tasks__create_pull_request
3986
3988
  * Create a pull request or merge request for the task
3987
3989
  */
@@ -4124,18 +4126,12 @@ function createTasksSdkServer(task) {
4124
4126
  };
4125
4127
  }
4126
4128
  }),
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) => {
4129
+ 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
4130
  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}`);
4131
+ await task.setup();
4136
4132
  return { content: [{
4137
4133
  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.`
4134
+ text: `Preview refreshed successfully`
4139
4135
  }] };
4140
4136
  } catch (error$1) {
4141
4137
  return {
@@ -4262,6 +4258,67 @@ function getClaudeSessionPath(directory, sessionId) {
4262
4258
  return path.join(process.env.HOME || "~", ".claude", "projects", sanitizedDir, `${sessionId}.jsonl`);
4263
4259
  }
4264
4260
 
4261
+ //#endregion
4262
+ //#region src/runner/PreviewProxy.ts
4263
+ 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";
4264
+ var PreviewProxy = class {
4265
+ server = null;
4266
+ proxy = null;
4267
+ constructor(config$2) {
4268
+ this.config = config$2;
4269
+ }
4270
+ log(message) {
4271
+ debugLog(`[PreviewProxy:${this.config.name}] ${message}`);
4272
+ }
4273
+ async start() {
4274
+ this.proxy = httpProxy.createProxyServer({
4275
+ target: `http://localhost:${this.config.targetPort}`,
4276
+ ws: true
4277
+ });
4278
+ this.proxy.on("proxyRes", (proxyRes, req, res) => {
4279
+ delete proxyRes.headers["x-frame-options"];
4280
+ delete proxyRes.headers["X-Frame-Options"];
4281
+ let csp = proxyRes.headers["content-security-policy"] || "";
4282
+ csp = csp.replace(/frame-ancestors[^;]*;?/gi, "").trim();
4283
+ const frameAncestors = `frame-ancestors ${ALLOWED_FRAME_ANCESTORS}`;
4284
+ csp = csp ? `${csp}; ${frameAncestors}` : frameAncestors;
4285
+ debugLog(`[PreviewProxy:${this.config.name}] CSP: ${csp}`);
4286
+ proxyRes.headers["content-security-policy"] = csp;
4287
+ });
4288
+ this.proxy.on("error", (err, req, res) => {
4289
+ this.log(`Proxy error: ${err.message}`);
4290
+ if (res instanceof http.ServerResponse && !res.headersSent) {
4291
+ res.writeHead(502, { "Content-Type": "text/plain" });
4292
+ res.end("Proxy error: " + err.message);
4293
+ }
4294
+ });
4295
+ this.server = http.createServer((req, res) => {
4296
+ this.proxy.web(req, res);
4297
+ });
4298
+ this.server.on("upgrade", (req, socket, head) => {
4299
+ this.proxy.ws(req, socket, head);
4300
+ });
4301
+ await new Promise((resolve, reject) => {
4302
+ this.server.listen(this.config.proxyPort, () => {
4303
+ this.log(`Proxy listening on port ${this.config.proxyPort}, forwarding to ${this.config.targetPort}`);
4304
+ resolve();
4305
+ });
4306
+ this.server.on("error", reject);
4307
+ });
4308
+ }
4309
+ stop() {
4310
+ if (this.server) {
4311
+ this.server.close();
4312
+ this.server = null;
4313
+ }
4314
+ if (this.proxy) {
4315
+ this.proxy.close();
4316
+ this.proxy = null;
4317
+ }
4318
+ this.log("Proxy stopped");
4319
+ }
4320
+ };
4321
+
4265
4322
  //#endregion
4266
4323
  //#region src/runner/Task.ts
4267
4324
  const ACK_TIMEOUT_MS = 1e3;
@@ -4276,7 +4333,9 @@ var Task = class {
4276
4333
  pendingAcks = /* @__PURE__ */ new Map();
4277
4334
  pendingPermissions = /* @__PURE__ */ new Map();
4278
4335
  pendingQuestions = /* @__PURE__ */ new Map();
4279
- tunnel = null;
4336
+ tunnels = /* @__PURE__ */ new Map();
4337
+ proxies = /* @__PURE__ */ new Map();
4338
+ primaryTunnelHost = null;
4280
4339
  fileWatcher = null;
4281
4340
  lastFilePosition = 0;
4282
4341
  currentPendingLineUuid = null;
@@ -4284,6 +4343,7 @@ var Task = class {
4284
4343
  lastActivityTime = Date.now();
4285
4344
  idleWatchdogInterval = null;
4286
4345
  idleTimeoutDisabled = false;
4346
+ serveProcess = null;
4287
4347
  get defaultBranch() {
4288
4348
  const tasksConfig = getTasksConfigSync();
4289
4349
  if (!tasksConfig) {
@@ -4373,7 +4433,8 @@ var Task = class {
4373
4433
  }
4374
4434
  this.stopHeartbeat();
4375
4435
  this.stopSessionFileWatcher();
4376
- this.cleanupCloudflareTunnel();
4436
+ this.cleanupServeProcess();
4437
+ this.cleanupAllTunnels();
4377
4438
  this.debugLog("Task process exiting due to idle timeout");
4378
4439
  process.exit(0);
4379
4440
  }
@@ -4390,6 +4451,7 @@ var Task = class {
4390
4451
  process.exit(1);
4391
4452
  }
4392
4453
  this.debugLog(`✓ Git repository and session file setup complete`);
4454
+ await this.setup();
4393
4455
  this.startHeartbeat();
4394
4456
  this.startIdleWatchdog();
4395
4457
  this.sendStatusUpdate("idle");
@@ -4400,13 +4462,28 @@ var Task = class {
4400
4462
  this.stopHeartbeat();
4401
4463
  this.stopIdleWatchdog();
4402
4464
  this.stopSessionFileWatcher();
4403
- this.cleanupCloudflareTunnel();
4465
+ this.cleanupServeProcess();
4466
+ this.cleanupAllTunnels();
4404
4467
  this.sendStatusUpdate("exited");
4405
4468
  };
4406
4469
  process.on("exit", cleanup);
4407
4470
  process.on("SIGINT", cleanup);
4408
4471
  process.on("SIGTERM", cleanup);
4409
4472
  }
4473
+ /**
4474
+ * Stop the serve process if it's running
4475
+ */
4476
+ cleanupServeProcess() {
4477
+ if (this.serveProcess) {
4478
+ try {
4479
+ if (this.serveProcess.pid) process.kill(-this.serveProcess.pid, "SIGTERM");
4480
+ this.debugLog("Serve process stopped");
4481
+ } catch (error$1) {
4482
+ this.debugLog(`Warning: Failed to stop serve process: ${error$1.message}`);
4483
+ }
4484
+ this.serveProcess = null;
4485
+ }
4486
+ }
4410
4487
  stdinBuffer = "";
4411
4488
  listenForMessages() {
4412
4489
  process.stdin.on("data", (data) => {
@@ -4611,41 +4688,44 @@ var Task = class {
4611
4688
  this.debugLog("Idle timeout disabled");
4612
4689
  }
4613
4690
  /**
4614
- * Set up Cloudflare tunnel for exposing local services to the internet.
4691
+ * Set up a single Cloudflare tunnel for exposing local services to the internet.
4692
+ * This is the legacy method for backwards compatibility when no tunnels are configured.
4615
4693
  * Sets HOST and PORT environment variables for use by the task.
4616
4694
  */
4617
4695
  async ensureCloudflareTunnel() {
4618
- this.cleanupCloudflareTunnel();
4619
- this.debugLog("Setting up Cloudflare tunnel...");
4696
+ this.cleanupAllTunnels();
4697
+ this.debugLog("Setting up Cloudflare tunnel (legacy mode)...");
4620
4698
  const idealPort = parseInt(process.env.PORT ?? "", 10);
4621
4699
  if (!isNaN(idealPort)) this.debugLog(`Trying ideal port: ${idealPort}`);
4622
4700
  const port = await getPort({ port: idealPort });
4623
4701
  this.debugLog(`Selected available port: ${port}`);
4624
4702
  process.env.PORT = port.toString();
4625
4703
  try {
4626
- this.tunnel = Tunnel.quick(`http://localhost:${port}`);
4704
+ const tunnel = Tunnel.quick(`http://localhost:${port}`);
4627
4705
  const tunnelUrl = await new Promise((resolve, reject) => {
4628
4706
  const timeout = setTimeout(() => {
4629
4707
  reject(/* @__PURE__ */ new Error("Cloudflare tunnel URL resolution timed out after 30 seconds"));
4630
4708
  }, 3e4);
4631
- this.tunnel.once("url", (url) => {
4709
+ tunnel.once("url", (url) => {
4632
4710
  clearTimeout(timeout);
4633
4711
  resolve(url);
4634
4712
  });
4635
- this.tunnel.once("exit", (code) => {
4713
+ tunnel.once("exit", (code) => {
4636
4714
  clearTimeout(timeout);
4637
4715
  reject(/* @__PURE__ */ new Error(`Cloudflare tunnel exited unexpectedly with code ${code}`));
4638
4716
  });
4639
4717
  });
4640
4718
  const host = new URL(tunnelUrl).host;
4641
4719
  process.env.JIVE_PUBLIC_HOST = host;
4720
+ this.tunnels.set("_legacy", tunnel);
4721
+ this.primaryTunnelHost = host;
4642
4722
  this.debugLog(`Cloudflare tunnel established: Host: ${host}, Port: ${port}`);
4643
4723
  return {
4644
4724
  port,
4645
4725
  host
4646
4726
  };
4647
4727
  } catch (error$1) {
4648
- this.cleanupCloudflareTunnel();
4728
+ this.cleanupAllTunnels();
4649
4729
  return { error: error$1 };
4650
4730
  }
4651
4731
  }
@@ -4708,18 +4788,100 @@ Host gitlab.com
4708
4788
  this.debugLog("✓ SSH authentication configured with deploy key");
4709
4789
  }
4710
4790
  /**
4711
- * Stop the Cloudflare tunnel and clean up resources
4791
+ * Stop all Cloudflare tunnels, proxies, and clean up resources
4792
+ */
4793
+ cleanupAllTunnels() {
4794
+ if (this.proxies.size > 0) {
4795
+ this.debugLog(`Stopping ${this.proxies.size} preview proxy(ies)...`);
4796
+ for (const [name$1, proxy] of this.proxies) try {
4797
+ proxy.stop();
4798
+ this.debugLog(`Preview proxy '${name$1}' stopped`);
4799
+ } catch (error$1) {
4800
+ console.warn(chalk.yellow(`Warning: Failed to stop preview proxy '${name$1}': ${error$1.message}`));
4801
+ }
4802
+ this.proxies.clear();
4803
+ }
4804
+ if (this.tunnels.size === 0) return;
4805
+ this.debugLog(`Stopping ${this.tunnels.size} Cloudflare tunnel(s)...`);
4806
+ for (const [name$1, tunnel] of this.tunnels) try {
4807
+ tunnel.stop();
4808
+ this.debugLog(`Cloudflare tunnel '${name$1}' stopped`);
4809
+ } catch (error$1) {
4810
+ console.warn(chalk.yellow(`Warning: Failed to stop Cloudflare tunnel '${name$1}': ${error$1.message}`));
4811
+ }
4812
+ this.tunnels.clear();
4813
+ this.primaryTunnelHost = null;
4814
+ }
4815
+ /**
4816
+ * Set up all configured tunnels from .jiverc config.
4817
+ * Sets environment variables for each tunnel:
4818
+ * - {NAME}_PORT = actual port used (for dev server to bind to)
4819
+ * - JIVE_{NAME}_HOST or custom envVarName = tunnel host
4820
+ *
4821
+ * When embedded !== false (default), a proxy server is inserted between
4822
+ * the tunnel and the dev server to modify CSP headers for iframe embedding.
4712
4823
  */
4713
- cleanupCloudflareTunnel() {
4714
- if (this.tunnel) {
4824
+ async setupTunnels(tunnelConfigs) {
4825
+ this.debugLog(`Setting up ${tunnelConfigs.length} tunnel(s)...`);
4826
+ this.cleanupAllTunnels();
4827
+ const primaryTunnel = tunnelConfigs.find((t$2) => t$2.primary) ?? tunnelConfigs[0];
4828
+ for (const config$2 of tunnelConfigs) {
4829
+ const name$1 = config$2.name.toUpperCase();
4830
+ const useProxy = config$2.embedded !== false;
4715
4831
  try {
4716
- this.tunnel.stop();
4717
- this.debugLog("Cloudflare tunnel stopped");
4832
+ let tunnelPort;
4833
+ let targetPort;
4834
+ if (useProxy) {
4835
+ const proxyPort = await getPort({ port: config$2.port + 1 });
4836
+ targetPort = await getPort({ port: config$2.port });
4837
+ tunnelPort = proxyPort;
4838
+ this.debugLog(`Tunnel '${config$2.name}': proxy on ${proxyPort}, dev server on ${targetPort}`);
4839
+ const proxy = new PreviewProxy({
4840
+ proxyPort,
4841
+ targetPort,
4842
+ name: config$2.name
4843
+ });
4844
+ await proxy.start();
4845
+ this.proxies.set(config$2.name, proxy);
4846
+ } else {
4847
+ tunnelPort = await getPort({ port: config$2.port });
4848
+ targetPort = tunnelPort;
4849
+ this.debugLog(`Tunnel '${config$2.name}': direct connection on port ${tunnelPort} (proxy disabled)`);
4850
+ }
4851
+ process.env[`${name$1}_PORT`] = targetPort.toString();
4852
+ const tunnel = Tunnel.quick(`http://localhost:${tunnelPort}`);
4853
+ const tunnelUrl = await new Promise((resolve, reject) => {
4854
+ const timeout = setTimeout(() => {
4855
+ reject(/* @__PURE__ */ new Error(`Cloudflare tunnel '${config$2.name}' URL resolution timed out after 30 seconds`));
4856
+ }, 3e4);
4857
+ tunnel.once("url", (url) => {
4858
+ clearTimeout(timeout);
4859
+ resolve(url);
4860
+ });
4861
+ tunnel.once("exit", (code) => {
4862
+ clearTimeout(timeout);
4863
+ reject(/* @__PURE__ */ new Error(`Cloudflare tunnel '${config$2.name}' exited unexpectedly with code ${code}`));
4864
+ });
4865
+ });
4866
+ const host = new URL(tunnelUrl).host;
4867
+ const hostEnvVar = config$2.envVarName ?? `JIVE_${name$1}_HOST`;
4868
+ process.env[hostEnvVar] = host;
4869
+ if (config$2 === primaryTunnel) {
4870
+ this.primaryTunnelHost = host;
4871
+ process.env.JIVE_PUBLIC_HOST = host;
4872
+ process.env.PORT = targetPort.toString();
4873
+ this.debugLog(`Tunnel '${config$2.name}' is primary (previewUrl)`);
4874
+ }
4875
+ this.tunnels.set(config$2.name, tunnel);
4876
+ const proxyStatus = useProxy ? " (proxy enabled)" : " (proxy disabled)";
4877
+ this.debugLog(`✓ Tunnel '${config$2.name}' established: ${hostEnvVar}=${host}, ${name$1}_PORT=${targetPort}${proxyStatus}`);
4718
4878
  } catch (error$1) {
4719
- console.warn(chalk.yellow(`Warning: Failed to stop Cloudflare tunnel: ${error$1.message}`));
4879
+ this.debugLog(`✗ Failed to set up tunnel '${config$2.name}': ${error$1.message}`);
4880
+ this.cleanupAllTunnels();
4881
+ throw error$1;
4720
4882
  }
4721
- this.tunnel = null;
4722
4883
  }
4884
+ this.debugLog(`✓ All ${tunnelConfigs.length} tunnel(s) established`);
4723
4885
  }
4724
4886
  async setupGitRepository() {
4725
4887
  let { repository } = this.ctx;
@@ -4783,6 +4945,151 @@ Host gitlab.com
4783
4945
  await execAsync$1(`git status`);
4784
4946
  this.debugLog(`✓ Git repository setup complete`);
4785
4947
  }
4948
+ /**
4949
+ * Read and parse the .jiverc.yml or .jiverc.json config file from the project root.
4950
+ * Returns null if neither file exists.
4951
+ */
4952
+ async readJivercConfig() {
4953
+ const { directory } = this.ctx;
4954
+ const ymlPath = path$1.join(directory, ".jiverc.yml");
4955
+ const jsonPath = path$1.join(directory, ".jiverc.json");
4956
+ try {
4957
+ if (existsSync(ymlPath)) {
4958
+ this.debugLog(`Found config file: .jiverc.yml`);
4959
+ const content = await promises.readFile(ymlPath, "utf-8");
4960
+ return yaml.load(content);
4961
+ }
4962
+ if (existsSync(jsonPath)) {
4963
+ this.debugLog(`Found config file: .jiverc.json`);
4964
+ const content = await promises.readFile(jsonPath, "utf-8");
4965
+ return JSON.parse(content);
4966
+ }
4967
+ this.debugLog(`No .jiverc.yml or .jiverc.json found`);
4968
+ return null;
4969
+ } catch (error$1) {
4970
+ this.debugLog(`Error reading jiverc config: ${error$1.message}`);
4971
+ return null;
4972
+ }
4973
+ }
4974
+ /**
4975
+ * Run a shell command and return success/failure status.
4976
+ * Logs stdout/stderr appropriately.
4977
+ */
4978
+ async runSetupCommand(command, label) {
4979
+ this.debugLog(`Running ${label} command: ${command}`);
4980
+ try {
4981
+ const { stdout, stderr } = await execAsync$1(command, {
4982
+ cwd: this.ctx.directory,
4983
+ env: process.env,
4984
+ maxBuffer: 10 * 1024 * 1024
4985
+ });
4986
+ if (stdout) this.debugLog(`${label} stdout:\n${stdout}`);
4987
+ if (stderr) this.debugLog(`${label} stderr:\n${stderr}`);
4988
+ this.debugLog(`✓ ${label} command completed successfully`);
4989
+ return true;
4990
+ } catch (error$1) {
4991
+ this.debugLog(`✗ ${label} command failed: ${error$1.message}`);
4992
+ if (error$1.stdout) this.debugLog(`stdout:\n${error$1.stdout}`);
4993
+ if (error$1.stderr) this.debugLog(`stderr:\n${error$1.stderr}`);
4994
+ return false;
4995
+ }
4996
+ }
4997
+ /**
4998
+ * Run project setup based on .jiverc config file.
4999
+ * - Sets up tunnels first (if configured) so env vars are available
5000
+ * - Runs setup command synchronously if defined
5001
+ * - Starts serve command if defined
5002
+ *
5003
+ * Note: this is also called by the 'refresh_preview' tool.
5004
+ */
5005
+ async setup() {
5006
+ this.cleanupServeProcess();
5007
+ this.cleanupAllTunnels();
5008
+ try {
5009
+ const config$2 = await this.readJivercConfig();
5010
+ if (!config$2) {
5011
+ this.debugLog("No jiverc config found, skipping setup");
5012
+ return;
5013
+ }
5014
+ if (config$2.tunnels && config$2.tunnels.length > 0) await this.setupTunnels(config$2.tunnels);
5015
+ if (config$2.commands?.setup) {
5016
+ this.debugLog("Running setup command...");
5017
+ if (!await this.runSetupCommand(config$2.commands.setup, "setup")) this.debugLog("Setup command failed, continuing anyway...");
5018
+ }
5019
+ if (config$2.commands?.serve) await this.serve();
5020
+ } catch (error$1) {
5021
+ this.debugLog(`Error during setup: ${error$1.message}`);
5022
+ }
5023
+ }
5024
+ /**
5025
+ * Start the serve command from .jiverc config.
5026
+ * Tunnels should already be set up by setup() - this just starts the serve process.
5027
+ * If tunnels aren't set up yet (e.g., serve() called directly), it will set them up.
5028
+ * Updates the task's previewUrl using the primary tunnel.
5029
+ */
5030
+ async serve() {
5031
+ try {
5032
+ const config$2 = await this.readJivercConfig();
5033
+ if (!config$2?.commands?.serve) {
5034
+ this.debugLog("No serve command configured in .jiverc");
5035
+ return;
5036
+ }
5037
+ const serveCommand = config$2.commands.serve;
5038
+ this.debugLog(`Starting serve command: ${serveCommand}`);
5039
+ this.cleanupServeProcess();
5040
+ if (this.tunnels.size === 0 && config$2.tunnels && config$2.tunnels.length > 0) await this.setupTunnels(config$2.tunnels);
5041
+ if (this.tunnels.size === 0) {
5042
+ this.debugLog("No tunnels configured, setting up legacy single tunnel...");
5043
+ const tunnelResult = await this.ensureCloudflareTunnel();
5044
+ if ("error" in tunnelResult) {
5045
+ this.debugLog(`Failed to establish Cloudflare tunnel: ${tunnelResult.error}`);
5046
+ return;
5047
+ }
5048
+ this.primaryTunnelHost = tunnelResult.host;
5049
+ }
5050
+ this.serveProcess = spawn(serveCommand, [], {
5051
+ cwd: this.ctx.directory,
5052
+ env: process.env,
5053
+ shell: true,
5054
+ detached: true,
5055
+ stdio: [
5056
+ "ignore",
5057
+ "pipe",
5058
+ "pipe"
5059
+ ]
5060
+ });
5061
+ this.serveProcess.unref();
5062
+ this.serveProcess.stdout?.on("data", (data) => {
5063
+ this.debugLog(`[serve] ${data.toString().trim()}`);
5064
+ });
5065
+ this.serveProcess.stderr?.on("data", (data) => {
5066
+ this.debugLog(`[serve:err] ${data.toString().trim()}`);
5067
+ });
5068
+ this.serveProcess.on("error", (error$1) => {
5069
+ this.debugLog(`[serve] Process error: ${error$1.message}`);
5070
+ });
5071
+ this.serveProcess.on("exit", (code, signal) => {
5072
+ this.debugLog(`[serve] Process exited with code ${code}, signal ${signal}`);
5073
+ this.serveProcess = null;
5074
+ });
5075
+ this.disableIdleTimeout();
5076
+ if (this.primaryTunnelHost) {
5077
+ const previewUrl = `https://${this.primaryTunnelHost}`;
5078
+ this.debugLog(`✓ Serve command started in background`);
5079
+ this.debugLog(`Public URL: ${previewUrl}`);
5080
+ try {
5081
+ this.debugLog("Waiting 5 seconds to ensure tunnel is ready...");
5082
+ await new Promise((resolve) => setTimeout(resolve, 5e3));
5083
+ await getApiClient().updateTask(this.id, { previewUrl });
5084
+ this.debugLog(`✓ Task previewUrl updated to ${previewUrl}`);
5085
+ } catch (error$1) {
5086
+ this.debugLog(`Failed to update task previewUrl: ${error$1.message}`);
5087
+ }
5088
+ } else this.debugLog(`✓ Serve command started in background (no tunnel configured)`);
5089
+ } catch (error$1) {
5090
+ this.debugLog(`Error during serve: ${error$1.message}`);
5091
+ }
5092
+ }
4786
5093
  get claudeSessionFilePath() {
4787
5094
  return path$1.join(process.env.HOME || "~", ".claude", "projects", this.ctx.directory.replace(/\//g, "-"), `${this.ctx.claudeSessionId}.jsonl`);
4788
5095
  }
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.41",
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",