@opennous/cli 0.16.0 → 0.17.0
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/CHANGELOG.md +10 -0
- package/README.md +11 -0
- package/package.json +1 -1
- package/src/index.js +112 -0
- package/src/runner.js +205 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @opennous/cli changelog
|
|
2
2
|
|
|
3
|
+
## 0.17.0
|
|
4
|
+
|
|
5
|
+
### Added: `runner`, reading on your own plan all the time
|
|
6
|
+
|
|
7
|
+
`backfill` reads your history once. `runner` keeps going: it reads your new calls and email on your
|
|
8
|
+
own Claude or ChatGPT plan as they come in, with the same no-tools sandbox. `runner install` starts it
|
|
9
|
+
now and every time you log in (launchd on macOS, systemd on Linux); `runner status` and
|
|
10
|
+
`runner uninstall` check on it and remove it. When your plan hits its limit it waits and carries on
|
|
11
|
+
by itself. The app's Settings → Your plan shows it as connected (GET-1289).
|
|
12
|
+
|
|
3
13
|
## 0.16.0
|
|
4
14
|
|
|
5
15
|
### Added: your Intelligence report after `backfill`
|
package/README.md
CHANGED
|
@@ -12,6 +12,17 @@ npx @opennous/cli@latest init
|
|
|
12
12
|
|
|
13
13
|
Then tell your agent *"set up my Ownable workspace"* — it reads your project, finds your ICP (or drafts one), and syncs it. On self-host, add `--url https://api.yourdomain.com`.
|
|
14
14
|
|
|
15
|
+
## Read on your own plan
|
|
16
|
+
|
|
17
|
+
Reading your calls and email runs on your own Claude Code or Codex plan, not on credits:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx @opennous/cli@latest backfill # your history, once
|
|
21
|
+
npx @opennous/cli@latest runner install # what's new, from now on; starts at login
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`runner status` says whether it's running, `runner uninstall` stops it. Settings → Your plan in the app shows each runner and what's waiting to be read.
|
|
25
|
+
|
|
15
26
|
## Event tracking
|
|
16
27
|
|
|
17
28
|
`nous track init` detects your stack (Next.js, Node, Python), writes a small `nous.js` (or `nous.py`) module with three helpers, and updates your `.env`:
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -650,6 +650,118 @@ program
|
|
|
650
650
|
if (reportUrl) console.log(`Your Intelligence report is ready: ${reportUrl}`);
|
|
651
651
|
});
|
|
652
652
|
|
|
653
|
+
// `nous runner` (GET-1293): keeps reading your new calls and email on your own plan. `install`
|
|
654
|
+
// starts it at login; the app's "Your plan" page shows it as connected once it has polled.
|
|
655
|
+
const runnerCmd = program
|
|
656
|
+
.command("runner")
|
|
657
|
+
.description("Keep reading your new calls and email on your own Claude or ChatGPT plan")
|
|
658
|
+
.option("--agent <agent>", "claude or codex (default: whichever you're signed in to)")
|
|
659
|
+
.option("--workspace <id>", "the workspace to read; the app's command fills this in")
|
|
660
|
+
.option("--url <url>", "API base URL (default: https://api.opennous.cloud)")
|
|
661
|
+
.action(async (opts) => {
|
|
662
|
+
const { detectAgents, liveRun, NO_AGENT } = await import("./backfill.js");
|
|
663
|
+
const { runRunner } = await import("./runner.js");
|
|
664
|
+
const agent = await pickAgent(opts.agent, detectAgents, NO_AGENT);
|
|
665
|
+
const key = await runnerKey(opts);
|
|
666
|
+
const api = apiClient(key);
|
|
667
|
+
const service = process.env.NOUS_RUNNER_SERVICE === "1";
|
|
668
|
+
const stamp = (line) => (service ? `${new Date().toISOString()} ${line}` : line);
|
|
669
|
+
console.log(stamp(`Reading on your ${agent} plan. ${service ? "" : "Stop with Ctrl-C; `nous runner install` keeps it going on its own."}`.trim()));
|
|
670
|
+
await runRunner({
|
|
671
|
+
agent, version, run: liveRun(), log: (l) => console.log(stamp(l)),
|
|
672
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
673
|
+
api: {
|
|
674
|
+
poll: (body) => api.softPost("/v2/runner/poll", body),
|
|
675
|
+
answer: (id, body) => api.softPost(`/v2/runner/calls/${id}`, body),
|
|
676
|
+
},
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
runnerCmd
|
|
681
|
+
.command("install")
|
|
682
|
+
.description("Start the runner now and every time you log in")
|
|
683
|
+
.option("--agent <agent>", "claude or codex (default: whichever you're signed in to)")
|
|
684
|
+
.option("--workspace <id>", "the workspace to read; the app's command fills this in")
|
|
685
|
+
.option("--url <url>", "API base URL (default: https://api.opennous.cloud)")
|
|
686
|
+
.action(async (opts) => {
|
|
687
|
+
const { detectAgents, NO_AGENT } = await import("./backfill.js");
|
|
688
|
+
const { installService } = await import("./runner.js");
|
|
689
|
+
const agent = await pickAgent(opts.agent, detectAgents, NO_AGENT);
|
|
690
|
+
const key = await runnerKey(opts);
|
|
691
|
+
// One poll before installing, so a bad key or plan fails here and not silently in the background.
|
|
692
|
+
try {
|
|
693
|
+
await apiClient(key).softPost("/v2/runner/poll", { agent, host: hostname(), version, limit: 0 });
|
|
694
|
+
} catch (e) {
|
|
695
|
+
console.error(`Couldn't connect: ${e.message}`);
|
|
696
|
+
process.exit(1);
|
|
697
|
+
}
|
|
698
|
+
try {
|
|
699
|
+
const where = installService({ agent, workspace: opts.workspace ?? null });
|
|
700
|
+
console.log(`Connected. The runner reads on your ${agent} plan and starts every time you log in.`);
|
|
701
|
+
if (where.log) console.log(`Log: ${where.log}`);
|
|
702
|
+
console.log("Check on it with `npx @opennous/cli@latest runner status`. Stop it with `npx @opennous/cli@latest runner uninstall`.");
|
|
703
|
+
} catch (e) {
|
|
704
|
+
console.error(e.message);
|
|
705
|
+
process.exit(1);
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
runnerCmd
|
|
710
|
+
.command("uninstall")
|
|
711
|
+
.description("Stop the runner and stop it starting at login")
|
|
712
|
+
.action(async () => {
|
|
713
|
+
const { uninstallService } = await import("./runner.js");
|
|
714
|
+
console.log(uninstallService() ? "Stopped. It won't start at login any more." : "No runner is installed on this machine.");
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
runnerCmd
|
|
718
|
+
.command("status")
|
|
719
|
+
.description("Whether the runner is installed and running on this machine")
|
|
720
|
+
.action(async () => {
|
|
721
|
+
const { serviceStatus } = await import("./runner.js");
|
|
722
|
+
const s = serviceStatus();
|
|
723
|
+
if (!s.installed) { console.log("Not installed. Run `npx @opennous/cli@latest runner install`."); return; }
|
|
724
|
+
console.log(s.running ? "Running." : "Installed, but not running right now.");
|
|
725
|
+
if (s.log) console.log(`Log: ${s.log}`);
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
/** The agent to read with: the one asked for, or whichever is signed in. Exits with what to do. */
|
|
729
|
+
async function pickAgent(asked, detectAgents, noAgent) {
|
|
730
|
+
const agents = detectAgents();
|
|
731
|
+
const agent = asked ?? agents[0];
|
|
732
|
+
if (!agent || !agents.includes(agent)) {
|
|
733
|
+
console.error(asked ? `You're not signed in to ${asked}. Run \`${asked === "codex" ? "codex login" : "claude"}\` first.` : noAgent);
|
|
734
|
+
process.exit(1);
|
|
735
|
+
}
|
|
736
|
+
return agent;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/** The key the runner reads with: for --workspace when given (signing in to it if needed), else the saved one. */
|
|
740
|
+
async function runnerKey(opts) {
|
|
741
|
+
if (opts.url) writeConfig({ ...readConfig(), apiUrl: opts.url });
|
|
742
|
+
if (!hasKey()) await browserLogin(opts.url);
|
|
743
|
+
const cfg = readConfig();
|
|
744
|
+
const apiUrl = opts.url || process.env.NOUS_API_URL || cfg.apiUrl || "https://api.opennous.cloud";
|
|
745
|
+
let key = process.env.NOUS_API_KEY || cfg.apiKey;
|
|
746
|
+
if (!opts.workspace) return key;
|
|
747
|
+
const kept = cfg.workspaceKeys?.[opts.workspace];
|
|
748
|
+
for (const k of [key, kept].filter(Boolean)) {
|
|
749
|
+
if ((await fetchWhoami(apiUrl, k))?.workspace?.id === opts.workspace) {
|
|
750
|
+
if (k !== kept) writeConfig({ ...cfg, workspaceKeys: { ...cfg.workspaceKeys, [opts.workspace]: k } });
|
|
751
|
+
return k;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
console.log("Sign in to the workspace this runner is for.");
|
|
755
|
+
key = await browserLogin(opts.url, { save: false });
|
|
756
|
+
if ((await fetchWhoami(apiUrl, key))?.workspace?.id !== opts.workspace) {
|
|
757
|
+
console.error("That sign-in is for another workspace. Open the right workspace in the app, then run this again.");
|
|
758
|
+
process.exit(1);
|
|
759
|
+
}
|
|
760
|
+
const saved = readConfig();
|
|
761
|
+
writeConfig({ ...saved, workspaceKeys: { ...saved.workspaceKeys, [opts.workspace]: key } });
|
|
762
|
+
return key;
|
|
763
|
+
}
|
|
764
|
+
|
|
653
765
|
program
|
|
654
766
|
.command("logout")
|
|
655
767
|
.description("Sign out — remove the saved API key")
|
package/src/runner.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// `nous runner` (GET-1293): reads your new calls and email on your own Claude or ChatGPT plan, all
|
|
2
|
+
// the time. It is `nous backfill`'s loop that never ends: it polls the server, answers each model
|
|
3
|
+
// call with the `claude` or `codex` CLI you are signed in to (no tools, the same sandbox as backfill:
|
|
4
|
+
// a call's prompt holds other people's words), and posts the answer back. The server decides what to
|
|
5
|
+
// read; this only answers. `nous runner install` starts it at login, so it keeps going on its own.
|
|
6
|
+
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { homedir, hostname, platform } from "node:os";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { commandFor, parseAnswer, isPlanLimit } from "./backfill.js";
|
|
12
|
+
|
|
13
|
+
export const WORKERS = 3;
|
|
14
|
+
/** Busy: there is a read going, so ask again soon. */
|
|
15
|
+
export const BUSY_MS = 2000;
|
|
16
|
+
/** Idle: nothing going. A heartbeat still has to land well inside the server's two minutes. */
|
|
17
|
+
export const IDLE_MS = 20_000;
|
|
18
|
+
/** At the plan's limit: take nothing, keep the heartbeat. */
|
|
19
|
+
export const LIMITED_POLL_MS = 60_000;
|
|
20
|
+
export const LIMIT_WAIT_MS = 30 * 60_000;
|
|
21
|
+
const RETRIES = 2;
|
|
22
|
+
const ERROR_BACKOFF_MS = 30_000;
|
|
23
|
+
|
|
24
|
+
export const LABEL = "cloud.opennous.runner";
|
|
25
|
+
export const RUN_COMMAND = "npx -y @opennous/cli@latest runner";
|
|
26
|
+
|
|
27
|
+
/** What the status line says about the read in progress, or null. Pure. */
|
|
28
|
+
export function readingLine(reading) {
|
|
29
|
+
if (!reading) return null;
|
|
30
|
+
const what = reading.live ? "Reading what's new" : "Reading your history";
|
|
31
|
+
if (reading.status === "paused_own_model") return `${what}: picking it back up.`;
|
|
32
|
+
if (reading.finished) return `${what}: wrapping up.`;
|
|
33
|
+
if (reading.total) return `${what}: ${reading.done ?? 0} of ${reading.total}.`;
|
|
34
|
+
return `${what}.`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The loop. Injected: `api` ({ poll, answer }), `run` (one command → stdout), `sleep`, `log`.
|
|
39
|
+
* `stop()` → true ends it (tests; the real one runs until the process is stopped).
|
|
40
|
+
*/
|
|
41
|
+
export async function runRunner({ api, agent, host = hostname(), version = null, run, sleep, log, now = Date.now,
|
|
42
|
+
workers = WORKERS, limitWaitMs = LIMIT_WAIT_MS, stop = () => false }) {
|
|
43
|
+
let limitedUntil = 0, answered = 0, lastLine = null, lastError = null, toldBackfill = false;
|
|
44
|
+
const answerOne = async (call, models) => {
|
|
45
|
+
for (let attempt = 0; ; attempt++) {
|
|
46
|
+
try {
|
|
47
|
+
const { text, usage } = parseAnswer(agent, await run(commandFor(agent, models[agent], call)));
|
|
48
|
+
await api.answer(call.id, { answer: text, model: models[agent].model, usage });
|
|
49
|
+
answered++;
|
|
50
|
+
return;
|
|
51
|
+
} catch (err) {
|
|
52
|
+
// The plan's limit: drop the call (its lease runs out and it comes back) and stop taking
|
|
53
|
+
// more until the limit lifts. The heartbeat keeps going, so the app shows why.
|
|
54
|
+
if (isPlanLimit(err.message)) {
|
|
55
|
+
if (now() >= limitedUntil) log(`Your ${agent} plan hit its limit. Reading waits ${Math.round(limitWaitMs / 60000)} minutes, then carries on by itself.`);
|
|
56
|
+
limitedUntil = Math.max(limitedUntil, now() + limitWaitMs);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (attempt >= RETRIES) { await api.answer(call.id, { error: err.message }).catch(() => {}); return; }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
while (!stop()) {
|
|
64
|
+
let page;
|
|
65
|
+
try {
|
|
66
|
+
page = await api.poll({ agent, host, version, answered, limit: workers,
|
|
67
|
+
limited_until: limitedUntil > now() ? new Date(limitedUntil).toISOString() : null });
|
|
68
|
+
if (lastError) { log("Back online."); lastError = null; }
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (err.message !== lastError) log(`Waiting on Ownable: ${err.message}. Trying again every ${ERROR_BACKOFF_MS / 1000} seconds.`);
|
|
71
|
+
lastError = err.message;
|
|
72
|
+
await sleep(ERROR_BACKOFF_MS);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (page.needs_backfill && !toldBackfill) {
|
|
76
|
+
toldBackfill = true;
|
|
77
|
+
log("Your history hasn't been read yet. Run `npx @opennous/cli@latest backfill` once; after that this keeps it current.");
|
|
78
|
+
}
|
|
79
|
+
const line = readingLine(page.reading);
|
|
80
|
+
if (line && line !== lastLine) { log(line); }
|
|
81
|
+
lastLine = line;
|
|
82
|
+
const calls = page.calls ?? [];
|
|
83
|
+
if (!calls.length) { await sleep(limitedUntil > now() ? LIMITED_POLL_MS : page.reading ? BUSY_MS : IDLE_MS); continue; }
|
|
84
|
+
await Promise.all(calls.map((c) => answerOne(c, page.models)));
|
|
85
|
+
}
|
|
86
|
+
return { answered };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Start at login ───────────────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/** The absolute path of a command, or null. */
|
|
92
|
+
export function which(cmd, run = spawnSync) {
|
|
93
|
+
const r = run("/bin/sh", ["-c", `command -v ${cmd}`], { encoding: "utf8" });
|
|
94
|
+
const p = r.status === 0 ? String(r.stdout).trim().split("\n")[0] : "";
|
|
95
|
+
return p.startsWith("/") ? p : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** PATH for the service: where node, npx and the agent CLIs live, then the usual places. Pure. */
|
|
99
|
+
export function servicePath(binaries) {
|
|
100
|
+
const dirs = [...new Set(binaries.filter(Boolean).map((b) => dirname(b)))];
|
|
101
|
+
for (const d of ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"]) if (!dirs.includes(d)) dirs.push(d);
|
|
102
|
+
return dirs.join(":");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const xml = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
106
|
+
|
|
107
|
+
/** The launchd agent for macOS. Pure. */
|
|
108
|
+
export function launchdPlist({ npx, agent, workspace = null, path, log }) {
|
|
109
|
+
const args = [npx, "-y", "@opennous/cli@latest", "runner", "--agent", agent, ...(workspace ? ["--workspace", workspace] : [])];
|
|
110
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
111
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
112
|
+
<plist version="1.0">
|
|
113
|
+
<dict>
|
|
114
|
+
<key>Label</key><string>${LABEL}</string>
|
|
115
|
+
<key>ProgramArguments</key>
|
|
116
|
+
<array>
|
|
117
|
+
${args.map((a) => ` <string>${xml(a)}</string>`).join("\n")}
|
|
118
|
+
</array>
|
|
119
|
+
<key>EnvironmentVariables</key>
|
|
120
|
+
<dict>
|
|
121
|
+
<key>PATH</key><string>${xml(path)}</string>
|
|
122
|
+
<key>NOUS_RUNNER_SERVICE</key><string>1</string>
|
|
123
|
+
</dict>
|
|
124
|
+
<key>RunAtLoad</key><true/>
|
|
125
|
+
<key>KeepAlive</key><true/>
|
|
126
|
+
<key>ThrottleInterval</key><integer>30</integer>
|
|
127
|
+
<key>StandardOutPath</key><string>${xml(log)}</string>
|
|
128
|
+
<key>StandardErrorPath</key><string>${xml(log)}</string>
|
|
129
|
+
</dict>
|
|
130
|
+
</plist>
|
|
131
|
+
`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The systemd user unit for Linux. Pure. */
|
|
135
|
+
export function systemdUnit({ npx, agent, workspace = null, path }) {
|
|
136
|
+
const args = [npx, "-y", "@opennous/cli@latest", "runner", "--agent", agent, ...(workspace ? ["--workspace", workspace] : [])];
|
|
137
|
+
return `[Unit]
|
|
138
|
+
Description=Ownable runner: reads on your own ${agent === "codex" ? "ChatGPT" : "Claude"} plan
|
|
139
|
+
After=network-online.target
|
|
140
|
+
|
|
141
|
+
[Service]
|
|
142
|
+
ExecStart=${args.join(" ")}
|
|
143
|
+
Environment=PATH=${path}
|
|
144
|
+
Environment=NOUS_RUNNER_SERVICE=1
|
|
145
|
+
Restart=always
|
|
146
|
+
RestartSec=30
|
|
147
|
+
|
|
148
|
+
[Install]
|
|
149
|
+
WantedBy=default.target
|
|
150
|
+
`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Where the service files live on this machine. Pure. */
|
|
154
|
+
export function servicePaths(os = platform(), home = homedir()) {
|
|
155
|
+
if (os === "darwin") return { kind: "launchd", file: join(home, "Library", "LaunchAgents", `${LABEL}.plist`), log: join(home, "Library", "Logs", "opennous-runner.log") };
|
|
156
|
+
if (os === "linux") return { kind: "systemd", file: join(home, ".config", "systemd", "user", "opennous-runner.service"), log: null };
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Install and start the runner as a service. Returns { kind, file, log } or throws with what to do. */
|
|
161
|
+
export function installService({ agent, workspace = null, run = spawnSync, os = platform(), home = homedir() } = {}) {
|
|
162
|
+
const where = servicePaths(os, home);
|
|
163
|
+
if (!where) throw new Error(`Starting at login isn't supported on ${os} yet. Run \`${RUN_COMMAND}\` in a terminal you keep open instead.`);
|
|
164
|
+
const npx = which("npx", run);
|
|
165
|
+
if (!npx) throw new Error("Couldn't find npx. Install Node.js 20 or later, then run this again.");
|
|
166
|
+
const path = servicePath([npx, which("node", run), which(agent, run)]);
|
|
167
|
+
mkdirSync(dirname(where.file), { recursive: true });
|
|
168
|
+
if (where.kind === "launchd") {
|
|
169
|
+
mkdirSync(dirname(where.log), { recursive: true });
|
|
170
|
+
writeFileSync(where.file, launchdPlist({ npx, agent, workspace, path, log: where.log }));
|
|
171
|
+
run("launchctl", ["unload", where.file], { stdio: "ignore" });
|
|
172
|
+
const r = run("launchctl", ["load", "-w", where.file], { encoding: "utf8" });
|
|
173
|
+
if (r.status !== 0) throw new Error(`launchctl couldn't start it: ${(r.stderr || "").trim()}`);
|
|
174
|
+
} else {
|
|
175
|
+
writeFileSync(where.file, systemdUnit({ npx, agent, workspace, path }));
|
|
176
|
+
run("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
|
|
177
|
+
const r = run("systemctl", ["--user", "enable", "--now", "opennous-runner.service"], { encoding: "utf8" });
|
|
178
|
+
if (r.status !== 0) throw new Error(`systemd couldn't start it: ${(r.stderr || "").trim()}`);
|
|
179
|
+
// Keeps it running after logout, where the machine allows it. Best effort.
|
|
180
|
+
run("loginctl", ["enable-linger"], { stdio: "ignore" });
|
|
181
|
+
}
|
|
182
|
+
return where;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Stop and remove the service. True when there was one. */
|
|
186
|
+
export function uninstallService({ run = spawnSync, os = platform(), home = homedir() } = {}) {
|
|
187
|
+
const where = servicePaths(os, home);
|
|
188
|
+
if (!where || !existsSync(where.file)) return false;
|
|
189
|
+
if (where.kind === "launchd") run("launchctl", ["unload", "-w", where.file], { stdio: "ignore" });
|
|
190
|
+
else {
|
|
191
|
+
run("systemctl", ["--user", "disable", "--now", "opennous-runner.service"], { stdio: "ignore" });
|
|
192
|
+
}
|
|
193
|
+
rmSync(where.file, { force: true });
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Whether the service is installed, and running as far as the OS says. */
|
|
198
|
+
export function serviceStatus({ run = spawnSync, os = platform(), home = homedir() } = {}) {
|
|
199
|
+
const where = servicePaths(os, home);
|
|
200
|
+
if (!where || !existsSync(where.file)) return { installed: false, running: false, ...(where ?? {}) };
|
|
201
|
+
const r = where.kind === "launchd"
|
|
202
|
+
? run("launchctl", ["list", LABEL], { encoding: "utf8" })
|
|
203
|
+
: run("systemctl", ["--user", "is-active", "opennous-runner.service"], { encoding: "utf8" });
|
|
204
|
+
return { installed: true, running: r.status === 0, ...where };
|
|
205
|
+
}
|