@jait/gateway 0.1.8 → 0.1.9

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/bin/jait.mjs +260 -2
  2. package/package.json +1 -1
package/bin/jait.mjs CHANGED
@@ -9,18 +9,42 @@
9
9
  * jait --host 127.0.0.1 Bind to specific host
10
10
  * jait --help Show help
11
11
  * jait --version Show version
12
+ * jait daemon install Install systemd user service
13
+ * jait daemon start Start the service
14
+ * jait daemon stop Stop the service
15
+ * jait daemon restart Restart the service
16
+ * jait daemon status Show service status
17
+ * jait daemon uninstall Remove the systemd service
18
+ * jait daemon logs Tail service logs
12
19
  */
13
20
 
14
- import { readFileSync, existsSync } from "node:fs";
21
+ import { readFileSync, existsSync, writeFileSync, mkdirSync, unlinkSync } from "node:fs";
15
22
  import { resolve, dirname, join } from "node:path";
16
- import { homedir } from "node:os";
23
+ import { homedir, platform } from "node:os";
17
24
  import { fileURLToPath } from "node:url";
25
+ import { execSync } from "node:child_process";
18
26
 
19
27
  const __dirname = dirname(fileURLToPath(import.meta.url));
20
28
  const pkg = JSON.parse(
21
29
  readFileSync(resolve(__dirname, "..", "package.json"), "utf8"),
22
30
  );
23
31
 
32
+ // ── Constants ───────────────────────────────────────────────────────
33
+
34
+ const SERVICE_NAME = "jait-gateway";
35
+ const JAIT_DIR = join(homedir(), ".jait");
36
+ const ENV_PATH = join(JAIT_DIR, ".env");
37
+ const LOG_PATH = join(JAIT_DIR, "gateway.log");
38
+ const ERR_LOG_PATH = join(JAIT_DIR, "gateway.err.log");
39
+
40
+ function systemdUnitDir() {
41
+ return join(homedir(), ".config", "systemd", "user");
42
+ }
43
+
44
+ function systemdUnitPath() {
45
+ return join(systemdUnitDir(), `${SERVICE_NAME}.service`);
46
+ }
47
+
24
48
  // ── Helpers ─────────────────────────────────────────────────────────
25
49
 
26
50
  function printBanner() {
@@ -38,6 +62,7 @@ function printBanner() {
38
62
  function printHelp() {
39
63
  printBanner();
40
64
  console.log(`Usage: jait [options]
65
+ jait daemon <command>
41
66
 
42
67
  Options:
43
68
  --port <number> Port to listen on (default: 8000, env: PORT)
@@ -46,6 +71,15 @@ Options:
46
71
  --version, -v Show version number
47
72
  --help, -h Show this help message
48
73
 
74
+ Daemon commands (Linux systemd):
75
+ daemon install Install systemd user service (auto-starts on boot)
76
+ daemon uninstall Remove systemd user service
77
+ daemon start Start the service
78
+ daemon stop Stop the service
79
+ daemon restart Restart the service
80
+ daemon status Show service status + health check
81
+ daemon logs Tail service logs (journalctl)
82
+
49
83
  Environment files are loaded in order (first found wins):
50
84
  1. --env flag path
51
85
  2. ./.env (current directory)
@@ -56,11 +90,235 @@ See https://github.com/JakobWl/Jait for full documentation.
56
90
  `);
57
91
  }
58
92
 
93
+ function run(cmd, { silent = false } = {}) {
94
+ try {
95
+ return execSync(cmd, {
96
+ encoding: "utf8",
97
+ stdio: silent ? "pipe" : "inherit",
98
+ });
99
+ } catch (err) {
100
+ if (silent) return err.stdout || "";
101
+ throw err;
102
+ }
103
+ }
104
+
105
+ function runSilent(cmd) {
106
+ return run(cmd, { silent: true }).trim();
107
+ }
108
+
109
+ // ── Daemon commands ─────────────────────────────────────────────────
110
+
111
+ function ensureLinux() {
112
+ if (platform() !== "linux") {
113
+ console.error("Error: daemon commands are only supported on Linux (systemd).");
114
+ process.exit(1);
115
+ }
116
+ }
117
+
118
+ function resolveNodePath() {
119
+ try {
120
+ return runSilent("which node");
121
+ } catch {
122
+ return "/usr/bin/node";
123
+ }
124
+ }
125
+
126
+ function resolveJaitBin() {
127
+ return resolve(__dirname, "jait.mjs");
128
+ }
129
+
130
+ function buildUnit({ port, host, envPath } = {}) {
131
+ const nodePath = resolveNodePath();
132
+ const jaitBin = resolveJaitBin();
133
+ const envFlag = envPath && existsSync(envPath) ? envPath : ENV_PATH;
134
+
135
+ const execArgs = [nodePath, jaitBin];
136
+ if (existsSync(envFlag)) execArgs.push("--env", envFlag);
137
+ if (port) execArgs.push("--port", String(port));
138
+ if (host) execArgs.push("--host", host);
139
+
140
+ const execStart = execArgs.join(" ");
141
+
142
+ return `[Unit]
143
+ Description=Jait AI Gateway
144
+ After=network-online.target
145
+ Wants=network-online.target
146
+
147
+ [Service]
148
+ ExecStart=${execStart}
149
+ Restart=always
150
+ RestartSec=5
151
+ KillMode=process
152
+ StandardOutput=journal
153
+ StandardError=journal
154
+
155
+ [Install]
156
+ WantedBy=default.target
157
+ `;
158
+ }
159
+
160
+ function daemonInstall(flags) {
161
+ ensureLinux();
162
+ const unitDir = systemdUnitDir();
163
+ const unitPath = systemdUnitPath();
164
+
165
+ // Ensure directories exist
166
+ mkdirSync(unitDir, { recursive: true });
167
+ mkdirSync(JAIT_DIR, { recursive: true });
168
+
169
+ const unit = buildUnit(flags);
170
+ writeFileSync(unitPath, unit, "utf8");
171
+ console.log(` Wrote ${unitPath}`);
172
+
173
+ // Enable user lingering so service runs without active login session
174
+ try {
175
+ const user = runSilent("whoami");
176
+ run(`loginctl enable-linger ${user}`, { silent: true });
177
+ console.log(` Enabled linger for user ${user}`);
178
+ } catch {
179
+ console.warn(" Warning: could not enable lingering (service may stop on logout)");
180
+ }
181
+
182
+ // Reload systemd and enable the service
183
+ run(`systemctl --user daemon-reload`, { silent: true });
184
+ run(`systemctl --user enable ${SERVICE_NAME}`, { silent: true });
185
+ console.log(` Service ${SERVICE_NAME} installed and enabled`);
186
+ console.log("");
187
+ console.log(" Run 'jait daemon start' to start the gateway.");
188
+ }
189
+
190
+ function daemonUninstall() {
191
+ ensureLinux();
192
+ const unitPath = systemdUnitPath();
193
+
194
+ try {
195
+ run(`systemctl --user stop ${SERVICE_NAME}`, { silent: true });
196
+ run(`systemctl --user disable ${SERVICE_NAME}`, { silent: true });
197
+ } catch { /* may not be running */ }
198
+
199
+ if (existsSync(unitPath)) {
200
+ unlinkSync(unitPath);
201
+ run(`systemctl --user daemon-reload`, { silent: true });
202
+ console.log(` Removed ${unitPath}`);
203
+ } else {
204
+ console.log(" Service not installed.");
205
+ }
206
+ }
207
+
208
+ function daemonStart() {
209
+ ensureLinux();
210
+ run(`systemctl --user start ${SERVICE_NAME}`);
211
+ console.log(` ${SERVICE_NAME} started`);
212
+ }
213
+
214
+ function daemonStop() {
215
+ ensureLinux();
216
+ run(`systemctl --user stop ${SERVICE_NAME}`);
217
+ console.log(` ${SERVICE_NAME} stopped`);
218
+ }
219
+
220
+ function daemonRestart() {
221
+ ensureLinux();
222
+ run(`systemctl --user restart ${SERVICE_NAME}`);
223
+ console.log(` ${SERVICE_NAME} restarted`);
224
+ }
225
+
226
+ function daemonStatus() {
227
+ ensureLinux();
228
+
229
+ // Show systemd status
230
+ const status = runSilent(
231
+ `systemctl --user show ${SERVICE_NAME} -p ActiveState,SubState,MainPID --no-pager`
232
+ );
233
+ const fields = {};
234
+ for (const line of status.split("\n")) {
235
+ const eq = line.indexOf("=");
236
+ if (eq > 0) fields[line.slice(0, eq)] = line.slice(eq + 1);
237
+ }
238
+
239
+ const active = fields.ActiveState || "unknown";
240
+ const sub = fields.SubState || "unknown";
241
+ const pid = fields.MainPID || "0";
242
+
243
+ console.log(` Service: ${SERVICE_NAME}`);
244
+ console.log(` State: ${active} (${sub})`);
245
+ console.log(` PID: ${pid === "0" ? "—" : pid}`);
246
+
247
+ // Health check
248
+ if (active === "active") {
249
+ try {
250
+ const port = process.env.PORT || "8000";
251
+ const health = runSilent(
252
+ `curl -sf --max-time 3 http://127.0.0.1:${port}/health`
253
+ );
254
+ const data = JSON.parse(health);
255
+ console.log(` Version: ${data.version}`);
256
+ console.log(` Healthy: ${data.healthy ? "yes" : "no"}`);
257
+ console.log(` Uptime: ${data.uptime}s`);
258
+ } catch {
259
+ console.log(" Health: unreachable (gateway may still be starting)");
260
+ }
261
+ }
262
+ }
263
+
264
+ function daemonLogs() {
265
+ ensureLinux();
266
+ try {
267
+ execSync(
268
+ `journalctl --user -u ${SERVICE_NAME} -f --no-pager -n 100`,
269
+ { stdio: "inherit" },
270
+ );
271
+ } catch {
272
+ // user Ctrl-C
273
+ }
274
+ }
275
+
59
276
  // ── Argument parsing ────────────────────────────────────────────────
60
277
 
61
278
  const args = process.argv.slice(2);
62
279
  const flags = {};
63
280
 
281
+ // Check for daemon subcommand first
282
+ if (args[0] === "daemon") {
283
+ const subCmd = args[1];
284
+ // Parse remaining flags for daemon install
285
+ for (let i = 2; i < args.length; i++) {
286
+ if (args[i] === "--port" && args[i + 1]) flags.port = args[++i];
287
+ else if (args[i] === "--host" && args[i + 1]) flags.host = args[++i];
288
+ else if (args[i] === "--env" && args[i + 1]) flags.envPath = args[++i];
289
+ }
290
+
291
+ printBanner();
292
+ switch (subCmd) {
293
+ case "install":
294
+ daemonInstall(flags);
295
+ break;
296
+ case "uninstall":
297
+ daemonUninstall();
298
+ break;
299
+ case "start":
300
+ daemonStart();
301
+ break;
302
+ case "stop":
303
+ daemonStop();
304
+ break;
305
+ case "restart":
306
+ daemonRestart();
307
+ break;
308
+ case "status":
309
+ daemonStatus();
310
+ break;
311
+ case "logs":
312
+ daemonLogs();
313
+ break;
314
+ default:
315
+ console.log("Unknown daemon command:", subCmd || "(none)");
316
+ console.log("Available: install, uninstall, start, stop, restart, status, logs");
317
+ process.exit(1);
318
+ }
319
+ process.exit(0);
320
+ }
321
+
64
322
  for (let i = 0; i < args.length; i++) {
65
323
  const arg = args[i];
66
324
  if (arg === "--help" || arg === "-h") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jait/gateway",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Jait AI gateway — local-first AI coding agent with terminal, filesystem, and browser control",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",