@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.
- package/dist/index.mjs +366 -55
- 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
|
|
3350
|
-
const
|
|
3351
|
-
const
|
|
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.
|
|
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("
|
|
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
|
-
|
|
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: `
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
4713
|
+
tunnel.once("url", (url) => {
|
|
4632
4714
|
clearTimeout(timeout);
|
|
4633
4715
|
resolve(url);
|
|
4634
4716
|
});
|
|
4635
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
4714
|
-
|
|
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
|
-
|
|
4717
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|