@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.
@@ -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=on-failure
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=default.target
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 = "jive-task-runner";
51
- constructor() {
52
- const homeDir = os.homedir();
53
- this.servicePath = path.join(homeDir, ".config", "systemd", "user", `${this.serviceName}.service`);
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
- const variables = {
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
- const serviceDir = path.dirname(this.servicePath);
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 --user daemon-reload", { timeout: 3e4 });
98
- await execAsync$1(`systemctl --user enable ${this.serviceName}`, { timeout: 3e4 });
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 execAsync$1("systemctl --user daemon-reload", { timeout: 3e4 });
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 --user disable ${this.serviceName}`, { timeout: 3e4 });
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
- await execAsync$1("systemctl --user daemon-reload", { timeout: 3e4 });
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 --user start ${this.serviceName}`, { timeout: 3e4 });
238
+ await execAsync$1(`systemctl start ${this.serviceName}`, { timeout: 3e4 });
131
239
  }
132
240
  async stop() {
133
- await execAsync$1(`systemctl --user stop ${this.serviceName}`, { timeout: 3e4 });
241
+ await execAsync$1(`systemctl stop ${this.serviceName}`, { timeout: 3e4 });
134
242
  }
135
243
  async restart() {
136
- await execAsync$1(`systemctl --user restart ${this.serviceName}`, { timeout: 3e4 });
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 --user status ${this.serviceName} --no-pager`, { timeout: 3e4 });
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 --user is-enabled ${this.serviceName}`, { timeout: 3e4 });
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 --user is-enabled ${this.serviceName}`, { timeout: 3e4 }).catch(() => ({ stdout: "disabled" }));
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.44",
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:build": "bun run build && .docker/build.sh",
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
  },