@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.
- package/dist/index.mjs +353 -46
- 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
|
|
3350
|
-
const
|
|
3351
|
-
const
|
|
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.
|
|
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("
|
|
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
|
-
|
|
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: `
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
4709
|
+
tunnel.once("url", (url) => {
|
|
4632
4710
|
clearTimeout(timeout);
|
|
4633
4711
|
resolve(url);
|
|
4634
4712
|
});
|
|
4635
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
4714
|
-
|
|
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
|
-
|
|
4717
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|