@jive-ai/cli 0.0.44 → 0.0.46
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 +448 -1704
- package/dist/{service-4H4YceKv.mjs → service-MMjLsA9C.mjs} +158 -42
- package/package.json +3 -2
|
@@ -2,21 +2,31 @@ import { E as WS_URL, T as GRAPHQL_API_URL, w as API_URL } from "./index.mjs";
|
|
|
2
2
|
import fs from "fs/promises";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import os from "os";
|
|
5
|
-
import { exec, spawn } from "child_process";
|
|
5
|
+
import { exec, spawn, spawnSync } from "child_process";
|
|
6
6
|
import { promisify } from "util";
|
|
7
7
|
|
|
8
8
|
//#region src/lib/service/systemd.ts
|
|
9
9
|
const execAsync$1 = promisify(exec);
|
|
10
|
+
const SERVICE_NAME = "jive-task-runner";
|
|
11
|
+
const SYSTEM_SERVICE_PATH = `/etc/systemd/system/${SERVICE_NAME}.service`;
|
|
12
|
+
const USER_SERVICE_DIR = path.join(os.homedir(), ".config", "systemd", "user");
|
|
13
|
+
const LEGACY_USER_SERVICE_PATH = path.join(USER_SERVICE_DIR, `${SERVICE_NAME}.service`);
|
|
14
|
+
const ENV_DIR = "/etc/jive";
|
|
15
|
+
const ENV_FILE_PATH = `${ENV_DIR}/runner.env`;
|
|
10
16
|
const SERVICE_TEMPLATE = `[Unit]
|
|
11
17
|
Description=Jive Task Runner
|
|
12
18
|
Documentation=https://getjive.app/docs
|
|
13
|
-
After=network-online.target
|
|
19
|
+
After=network-online.target docker.service
|
|
14
20
|
Wants=network-online.target
|
|
21
|
+
Requires=docker.service
|
|
15
22
|
|
|
16
23
|
[Service]
|
|
17
24
|
Type=simple
|
|
25
|
+
User={{SERVICE_USER}}
|
|
26
|
+
Group={{SERVICE_GROUP}}
|
|
27
|
+
WorkingDirectory={{WORKING_DIRECTORY}}
|
|
18
28
|
ExecStart={{JIVE_BINARY_PATH}} task-runner start
|
|
19
|
-
Restart=
|
|
29
|
+
Restart=always
|
|
20
30
|
RestartSec=30
|
|
21
31
|
TimeoutStartSec=90
|
|
22
32
|
TimeoutStopSec=30
|
|
@@ -24,13 +34,10 @@ StartLimitBurst=5
|
|
|
24
34
|
StartLimitIntervalSec=10m
|
|
25
35
|
KillMode=mixed
|
|
26
36
|
Environment="PATH={{NODE_BIN_PATH}}:/usr/local/bin:/usr/bin:/bin"
|
|
27
|
-
Environment="JIVE_API_KEY={{JIVE_API_KEY}}"
|
|
28
|
-
Environment="ANTHROPIC_API_KEY={{ANTHROPIC_API_KEY}}"
|
|
29
|
-
Environment="JIVE_TEAM_ID={{JIVE_TEAM_ID}}"
|
|
30
|
-
Environment="JIVE_RUNNER_ID={{JIVE_RUNNER_ID}}"
|
|
31
37
|
Environment="JIVE_API_URL={{JIVE_API_URL}}"
|
|
32
38
|
Environment="JIVE_WS_URL={{JIVE_WS_URL}}"
|
|
33
39
|
Environment="JIVE_GRAPHQL_API_URL={{JIVE_GRAPHQL_API_URL}}"
|
|
40
|
+
EnvironmentFile={{ENV_FILE_PATH}}
|
|
34
41
|
|
|
35
42
|
StandardOutput=journal
|
|
36
43
|
StandardError=journal
|
|
@@ -43,16 +50,64 @@ ProtectKernelTunables=true
|
|
|
43
50
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
|
44
51
|
|
|
45
52
|
[Install]
|
|
46
|
-
WantedBy=
|
|
53
|
+
WantedBy=multi-user.target
|
|
47
54
|
`;
|
|
55
|
+
/**
|
|
56
|
+
* Check if the current process is running as root (UID 0)
|
|
57
|
+
*/
|
|
58
|
+
function isRunningAsRoot() {
|
|
59
|
+
return process.getuid?.() === 0;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Re-execute the current command with sudo, preserving PATH and environment.
|
|
63
|
+
* This allows users to just run `jive task-runner install-service` without
|
|
64
|
+
* needing to manually invoke sudo with the correct environment.
|
|
65
|
+
*/
|
|
66
|
+
function reExecWithSudo() {
|
|
67
|
+
const nodePath = process.execPath;
|
|
68
|
+
const scriptPath = process.argv[1];
|
|
69
|
+
const args = process.argv.slice(2);
|
|
70
|
+
console.log("Root privileges required. Re-running with sudo...\n");
|
|
71
|
+
const result = spawnSync("sudo", [
|
|
72
|
+
"--preserve-env=PATH,HOME",
|
|
73
|
+
`SUDO_USER=${process.env.USER}`,
|
|
74
|
+
`SUDO_UID=${process.getuid?.() || 1e3}`,
|
|
75
|
+
`SUDO_GID=${process.getgid?.() || 1e3}`,
|
|
76
|
+
nodePath,
|
|
77
|
+
scriptPath,
|
|
78
|
+
...args
|
|
79
|
+
], {
|
|
80
|
+
stdio: "inherit",
|
|
81
|
+
env: process.env
|
|
82
|
+
});
|
|
83
|
+
process.exit(result.status ?? 1);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get the original user info when running with sudo
|
|
87
|
+
* Returns the user who invoked sudo, not root
|
|
88
|
+
*/
|
|
89
|
+
function getCurrentUser() {
|
|
90
|
+
return {
|
|
91
|
+
uid: parseInt(process.env.SUDO_UID || String(process.getuid?.() || 1e3), 10),
|
|
92
|
+
gid: parseInt(process.env.SUDO_GID || String(process.getgid?.() || 1e3), 10),
|
|
93
|
+
username: process.env.SUDO_USER || process.env.USER || "jive",
|
|
94
|
+
homeDir: process.env.SUDO_USER ? `/home/${process.env.SUDO_USER}` : os.homedir()
|
|
95
|
+
};
|
|
96
|
+
}
|
|
48
97
|
var SystemdServiceManager = class {
|
|
49
|
-
servicePath;
|
|
50
|
-
serviceName =
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
98
|
+
servicePath = SYSTEM_SERVICE_PATH;
|
|
99
|
+
serviceName = SERVICE_NAME;
|
|
100
|
+
/**
|
|
101
|
+
* Check for legacy user service and handle migration.
|
|
102
|
+
* Call this BEFORE starting any spinners since it may prompt the user.
|
|
103
|
+
*/
|
|
104
|
+
async checkAndMigrateLegacyService() {
|
|
105
|
+
if (!isRunningAsRoot()) reExecWithSudo();
|
|
106
|
+
await this.migrateFromUserService();
|
|
54
107
|
}
|
|
55
108
|
async install() {
|
|
109
|
+
if (!isRunningAsRoot()) reExecWithSudo();
|
|
110
|
+
const user = getCurrentUser();
|
|
56
111
|
const { getRunnerConfig } = await import("./tasks-Py86q1u7.mjs");
|
|
57
112
|
const { getCredentials } = await import("./config-7rVDmj2u.mjs");
|
|
58
113
|
const runnerConfig = await getRunnerConfig();
|
|
@@ -72,41 +127,86 @@ var SystemdServiceManager = class {
|
|
|
72
127
|
if (resolvedPath.includes(pattern)) throw new Error(`Detected unstable path for jive binary: ${resolvedPath}\nThis path contains '${pattern}' which may not persist across reboots.\n\nIf you're using fnm, try:\n 1. Run: fnm exec --using=default -- npm install -g @jive-ai/cli\n 2. Then run install-service again from a fresh terminal`);
|
|
73
128
|
if (resolvedNodePath.includes(pattern)) throw new Error(`Detected unstable path for node binary: ${resolvedNodePath}\nThis path contains '${pattern}' which may not persist across reboots.\n\nIf you're using fnm, ensure you have a default node version set:\n fnm default <version>`);
|
|
74
129
|
}
|
|
75
|
-
|
|
76
|
-
JIVE_BINARY_PATH: resolvedPath,
|
|
77
|
-
NODE_BIN_PATH: nodeBinDir,
|
|
130
|
+
await this.createEnvironmentFile({
|
|
78
131
|
JIVE_API_KEY: credentials.token,
|
|
79
132
|
ANTHROPIC_API_KEY: credentials.anthropicApiKey || "",
|
|
80
133
|
JIVE_TEAM_ID: runnerConfig.teamId,
|
|
81
|
-
JIVE_RUNNER_ID: runnerConfig.id.toString()
|
|
134
|
+
JIVE_RUNNER_ID: runnerConfig.id.toString()
|
|
135
|
+
});
|
|
136
|
+
const variables = {
|
|
137
|
+
SERVICE_USER: user.username,
|
|
138
|
+
SERVICE_GROUP: user.username,
|
|
139
|
+
WORKING_DIRECTORY: user.homeDir,
|
|
140
|
+
JIVE_BINARY_PATH: resolvedPath,
|
|
141
|
+
NODE_BIN_PATH: nodeBinDir,
|
|
82
142
|
JIVE_API_URL: process.env.JIVE_API_URL || API_URL,
|
|
83
143
|
JIVE_WS_URL: process.env.JIVE_WS_URL || WS_URL,
|
|
84
|
-
JIVE_GRAPHQL_API_URL: process.env.JIVE_GRAPHQL_API_URL || GRAPHQL_API_URL
|
|
144
|
+
JIVE_GRAPHQL_API_URL: process.env.JIVE_GRAPHQL_API_URL || GRAPHQL_API_URL,
|
|
145
|
+
ENV_FILE_PATH
|
|
85
146
|
};
|
|
86
147
|
let serviceContent = SERVICE_TEMPLATE;
|
|
87
148
|
for (const [key, value] of Object.entries(variables)) serviceContent = serviceContent.replace(new RegExp(`{{${key}}}`, "g"), value);
|
|
88
|
-
|
|
89
|
-
await fs.mkdir(serviceDir, {
|
|
90
|
-
recursive: true,
|
|
91
|
-
mode: 493
|
|
92
|
-
});
|
|
93
|
-
const dirMode = (await fs.stat(serviceDir)).mode & 511;
|
|
94
|
-
if (dirMode > 493) throw new Error(`Systemd user directory has overly permissive permissions: ${dirMode.toString(8)}\nExpected 0755 or stricter. Fix with: chmod 755 ${serviceDir}`);
|
|
95
|
-
await fs.writeFile(this.servicePath, serviceContent, { mode: 384 });
|
|
149
|
+
await fs.writeFile(this.servicePath, serviceContent, { mode: 420 });
|
|
96
150
|
try {
|
|
97
|
-
await execAsync$1("systemctl
|
|
98
|
-
await execAsync$1(`systemctl
|
|
151
|
+
await execAsync$1("systemctl daemon-reload", { timeout: 3e4 });
|
|
152
|
+
await execAsync$1(`systemctl enable ${this.serviceName}`, { timeout: 3e4 });
|
|
99
153
|
} catch (error) {
|
|
100
154
|
try {
|
|
101
155
|
await fs.unlink(this.servicePath);
|
|
102
|
-
await
|
|
156
|
+
await fs.unlink(ENV_FILE_PATH);
|
|
157
|
+
await execAsync$1("systemctl daemon-reload", { timeout: 3e4 });
|
|
103
158
|
} catch (cleanupError) {
|
|
104
159
|
console.error("Failed to clean up after installation failure:", cleanupError);
|
|
105
160
|
}
|
|
106
161
|
throw new Error(`Service installation failed: ${error.message}\nPartial installation has been rolled back.`);
|
|
107
162
|
}
|
|
108
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Create the environment file with sensitive credentials
|
|
166
|
+
*/
|
|
167
|
+
async createEnvironmentFile(vars) {
|
|
168
|
+
await fs.mkdir(ENV_DIR, {
|
|
169
|
+
recursive: true,
|
|
170
|
+
mode: 493
|
|
171
|
+
});
|
|
172
|
+
const content = Object.entries(vars).map(([key, value]) => `${key}=${value}`).join("\n") + "\n";
|
|
173
|
+
await fs.writeFile(ENV_FILE_PATH, content, { mode: 384 });
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Check for and migrate from legacy user service
|
|
177
|
+
*/
|
|
178
|
+
async migrateFromUserService() {
|
|
179
|
+
try {
|
|
180
|
+
await fs.access(LEGACY_USER_SERVICE_PATH);
|
|
181
|
+
} catch {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const { default: prompts } = await import("prompts");
|
|
185
|
+
console.log("\n⚠ Found existing user service at:");
|
|
186
|
+
console.log(` ${LEGACY_USER_SERVICE_PATH}\n`);
|
|
187
|
+
const { migrate } = await prompts({
|
|
188
|
+
type: "confirm",
|
|
189
|
+
name: "migrate",
|
|
190
|
+
message: "Migrate to system service? (This will remove the old user service)",
|
|
191
|
+
initial: true
|
|
192
|
+
});
|
|
193
|
+
if (!migrate) throw new Error("Migration cancelled. Remove the user service first or choose to migrate.");
|
|
194
|
+
const user = getCurrentUser();
|
|
195
|
+
console.log("Stopping and removing legacy user service...");
|
|
196
|
+
try {
|
|
197
|
+
await execAsync$1(`sudo -u ${user.username} systemctl --user stop ${this.serviceName}`, { timeout: 3e4 });
|
|
198
|
+
} catch {}
|
|
199
|
+
try {
|
|
200
|
+
await execAsync$1(`sudo -u ${user.username} systemctl --user disable ${this.serviceName}`, { timeout: 3e4 });
|
|
201
|
+
} catch {}
|
|
202
|
+
await fs.unlink(LEGACY_USER_SERVICE_PATH);
|
|
203
|
+
try {
|
|
204
|
+
await execAsync$1(`sudo -u ${user.username} systemctl --user daemon-reload`, { timeout: 3e4 });
|
|
205
|
+
} catch {}
|
|
206
|
+
console.log("Legacy user service removed successfully.\n");
|
|
207
|
+
}
|
|
109
208
|
async uninstall() {
|
|
209
|
+
if (!isRunningAsRoot()) reExecWithSudo();
|
|
110
210
|
try {
|
|
111
211
|
await this.stop();
|
|
112
212
|
} catch (error) {
|
|
@@ -114,7 +214,7 @@ var SystemdServiceManager = class {
|
|
|
114
214
|
else console.warn(`Warning: Failed to stop service: ${error.message}`);
|
|
115
215
|
}
|
|
116
216
|
try {
|
|
117
|
-
await execAsync$1(`systemctl
|
|
217
|
+
await execAsync$1(`systemctl disable ${this.serviceName}`, { timeout: 3e4 });
|
|
118
218
|
} catch (error) {
|
|
119
219
|
if (error.stderr?.includes("No such file") || error.message?.includes("not be found")) {} else if (error.code === "EACCES") console.warn("Warning: Permission denied when disabling service");
|
|
120
220
|
else console.warn(`Warning: Failed to disable service: ${error.message}`);
|
|
@@ -124,16 +224,24 @@ var SystemdServiceManager = class {
|
|
|
124
224
|
} catch (error) {
|
|
125
225
|
if (error.code !== "ENOENT") throw new Error(`Failed to remove service file: ${error.message}`);
|
|
126
226
|
}
|
|
127
|
-
|
|
227
|
+
try {
|
|
228
|
+
await fs.unlink(ENV_FILE_PATH);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (error.code !== "ENOENT") console.warn(`Warning: Failed to remove environment file: ${error.message}`);
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
await fs.rmdir(ENV_DIR);
|
|
234
|
+
} catch {}
|
|
235
|
+
await execAsync$1("systemctl daemon-reload", { timeout: 3e4 });
|
|
128
236
|
}
|
|
129
237
|
async start() {
|
|
130
|
-
await execAsync$1(`systemctl
|
|
238
|
+
await execAsync$1(`systemctl start ${this.serviceName}`, { timeout: 3e4 });
|
|
131
239
|
}
|
|
132
240
|
async stop() {
|
|
133
|
-
await execAsync$1(`systemctl
|
|
241
|
+
await execAsync$1(`systemctl stop ${this.serviceName}`, { timeout: 3e4 });
|
|
134
242
|
}
|
|
135
243
|
async restart() {
|
|
136
|
-
await execAsync$1(`systemctl
|
|
244
|
+
await execAsync$1(`systemctl restart ${this.serviceName}`, { timeout: 3e4 });
|
|
137
245
|
}
|
|
138
246
|
async status() {
|
|
139
247
|
if (!await this.isInstalled()) return {
|
|
@@ -142,11 +250,11 @@ var SystemdServiceManager = class {
|
|
|
142
250
|
enabled: false
|
|
143
251
|
};
|
|
144
252
|
try {
|
|
145
|
-
const { stdout } = await execAsync$1(`systemctl
|
|
253
|
+
const { stdout } = await execAsync$1(`systemctl status ${this.serviceName} --no-pager`, { timeout: 3e4 });
|
|
146
254
|
const running = stdout.includes("Active: active (running)");
|
|
147
255
|
const pid = this.extractPid(stdout);
|
|
148
256
|
const uptime = this.extractUptime(stdout);
|
|
149
|
-
const { stdout: isEnabledOutput } = await execAsync$1(`systemctl
|
|
257
|
+
const { stdout: isEnabledOutput } = await execAsync$1(`systemctl is-enabled ${this.serviceName}`, { timeout: 3e4 });
|
|
150
258
|
return {
|
|
151
259
|
installed: true,
|
|
152
260
|
running,
|
|
@@ -155,7 +263,7 @@ var SystemdServiceManager = class {
|
|
|
155
263
|
pid
|
|
156
264
|
};
|
|
157
265
|
} catch (error) {
|
|
158
|
-
const { stdout: isEnabledOutput } = await execAsync$1(`systemctl
|
|
266
|
+
const { stdout: isEnabledOutput } = await execAsync$1(`systemctl is-enabled ${this.serviceName}`, { timeout: 3e4 }).catch(() => ({ stdout: "disabled" }));
|
|
159
267
|
return {
|
|
160
268
|
installed: true,
|
|
161
269
|
running: false,
|
|
@@ -164,11 +272,7 @@ var SystemdServiceManager = class {
|
|
|
164
272
|
}
|
|
165
273
|
}
|
|
166
274
|
async logs(options) {
|
|
167
|
-
const args = [
|
|
168
|
-
"--user",
|
|
169
|
-
"-u",
|
|
170
|
-
this.serviceName
|
|
171
|
-
];
|
|
275
|
+
const args = ["-u", this.serviceName];
|
|
172
276
|
if (options?.follow) args.push("-f");
|
|
173
277
|
if (options?.lines) args.push("-n", options.lines.toString());
|
|
174
278
|
const logsProcess = spawn("journalctl", args, { stdio: "inherit" });
|
|
@@ -240,6 +344,18 @@ async function validateServiceInstallation() {
|
|
|
240
344
|
const checks = [];
|
|
241
345
|
let canInstall = true;
|
|
242
346
|
let hasWarnings = false;
|
|
347
|
+
if (!isRunningAsRoot()) {
|
|
348
|
+
checks.push({
|
|
349
|
+
name: "Root privileges",
|
|
350
|
+
status: "warning",
|
|
351
|
+
message: "Will prompt for sudo password"
|
|
352
|
+
});
|
|
353
|
+
hasWarnings = true;
|
|
354
|
+
} else checks.push({
|
|
355
|
+
name: "Root privileges",
|
|
356
|
+
status: "success",
|
|
357
|
+
message: "Running as root"
|
|
358
|
+
});
|
|
243
359
|
try {
|
|
244
360
|
const { getRunnerConfig } = await import("./tasks-Py86q1u7.mjs");
|
|
245
361
|
const runnerConfig = await getRunnerConfig();
|
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.46",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
16
16
|
"typecheck": "tsc --noEmit",
|
|
17
17
|
"build": "tsdown && npm pack && npm install -g jive-ai-cli-*.tgz",
|
|
18
|
-
"docker:
|
|
18
|
+
"docker:clean": "docker rmi jiveai/task:latest jiveai/task:$npm_package_version",
|
|
19
|
+
"docker:build": "bun run build && npm run docker:clean && .docker/build.sh",
|
|
19
20
|
"docker:push": ".docker/build.sh --push",
|
|
20
21
|
"prepublishOnly": "npm run typecheck && npm run build"
|
|
21
22
|
},
|