@hivefloor/cli 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Validate-QA
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,77 @@
1
- # Temporary Holding Version
1
+ # @hivefloor/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Connect your coding agents to [Hivefloor](https://hivefloor.worldofz.info): one floor that shows every agent on your machines (Claude Code, Codex, Cursor, Gemini CLI, Grok CLI), what each is working on, who is waiting for you, and what they cost in tokens. Agents also get task tools (claim, update, complete) over MCP.
4
+
5
+ The package installs one command, `hivefloor`. Node.js 22 or later.
6
+
7
+ ## Quick start
8
+
9
+ ```bash
10
+ npx @hivefloor/cli connect
11
+ ```
12
+
13
+ It prints a short code and opens your browser to approve this computer on Hivefloor (sign in there; the CLI never sees your password). Then it sets up the hooks and the background service. Your running agents show up on the Floor within a minute.
14
+
15
+ Install once instead of using `npx`:
16
+
17
+ ```bash
18
+ npm i -g @hivefloor/cli
19
+ hivefloor connect
20
+ ```
21
+
22
+ Headless box with no browser: run `connect --no-open`, then open the printed link on any device where you are signed in.
23
+
24
+ ## Commands
25
+
26
+ ```
27
+ hivefloor <command> [options]
28
+ npx @hivefloor/cli <command> [options] (no install)
29
+
30
+ connect Connect this computer once: every agent on it shows up (Claude profiles, Codex, ...)
31
+ --host URL --name NAME --no-open --no-service --yes
32
+ --agent | --tool claude|codex|cursor|gemini|grok|other connect one agent for one repo (MCP and tasks) instead
33
+ disconnect Remove everything connect added, the machine on the server included (--tool <name> for one agent tool)
34
+ status This machine and its service; the per-repo agent, its projects and live sessions
35
+ service run: the background service (started by your OS; you rarely run it by hand)
36
+ link Link this repo to another project (--project ID | --new NAME [--path PREFIX])
37
+ token Print the stored token for shell use (--reveal to print to a terminal)
38
+ mcp Stdio MCP server that proxies to Hivefloor (for tools without HTTP header support)
39
+ hook <tool> <event>: the hook runner the tools call (reads JSON on stdin)
40
+ help Show this help
41
+ --version Print the version
42
+ ```
43
+
44
+ The default server is `https://hivefloor.worldofz.info`; use `--host` or the `HIVEFLOOR_URL` environment variable for another one.
45
+
46
+ ## What `connect` changes on your computer
47
+
48
+ - `~/.config/hivefloor/` (`%APPDATA%\hivefloor\` on Windows): `machine.json` with the machine token (mode 0600, user-only ACL on Windows), a copy of this CLI that the hooks and the service run, and a small event spool and service log.
49
+ - Hooks in each Claude Code profile's `settings.json` and, if Codex is present, in its `hooks.json` and `config.toml`. Every entry is marked as Hivefloor's.
50
+ - A user-level background service: a launchd agent on macOS, a systemd user unit on Linux, a Scheduled Task on Windows. Skip it with `--no-service`.
51
+
52
+ `hivefloor disconnect` removes all of it and tells the server to delete the machine. Nothing is installed with administrator rights.
53
+
54
+ ## Privacy: what is sent to the server
55
+
56
+ Only to the Hivefloor server you connect to (the default host or `--host`), over HTTPS, with the machine token. There is no telemetry and no third-party service.
57
+
58
+ - **Connecting:** the machine name (your hostname unless you pass `--name`), OS and CLI version.
59
+ - **Hook events** (when an agent starts or ends a session, you submit a prompt, the agent stops, waits for you, or starts or stops a subagent): the tool, the event name, the session id, the model name, the git remote, branch and subfolder of the working directory, and for a stop or subagent event the first line of the agent's last message, with secrets masked and cut to 140 characters. A subagent's type and its short description (140 characters) are sent too. Your prompt text, file contents, tool inputs and outputs, and full transcripts are not sent.
60
+ - **Heartbeat** (about every 30 seconds): OS, CLI version, memory total and use, load average, and token and cost counters per agent profile. The counters are numbers read from the `usage` fields of your local Claude Code and Codex session files from the last 24 hours; message text is never read into what is sent.
61
+ - **Discovery** (about every minute): for each agent tool found running, its name, a profile key (for example `default` or `work`), a label such as `Claude Max (work)` (from the plan name in the profile's credentials file; no email, no token), the model, and how many agent processes are alive. No command lines, paths or prompts.
62
+ - **`link`, `--agent` mode and `mcp`:** the same repo details as above, and your tool's MCP calls to the server's `/mcp` endpoint.
63
+
64
+ The machine token stays on your computer (the file above, or the OS keychain in `--agent` mode) and is sent only to the host it was issued for.
65
+
66
+ ## Troubleshooting
67
+
68
+ - `hivefloor status` shows the machine, whether the service is alive and the last heartbeat.
69
+ - The service log is in the config directory above (`service.log`).
70
+ - The hooks never print and always exit 0, so they cannot break your agent.
71
+
72
+ ## Links
73
+
74
+ - Source and issues: https://github.com/Validate-QA/hivefloor
75
+ - Docs: https://hivefloor.worldofz.info
76
+
77
+ MIT License.
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/tools.ts
4
+ import fs from "fs";
5
+ import path from "path";
6
+ var REAL_TOOLS = ["claude", "codex", "cursor", "gemini", "grok"];
7
+ var TOOL_LABEL = {
8
+ claude: "Claude Code",
9
+ codex: "Codex",
10
+ cursor: "Cursor",
11
+ gemini: "Gemini CLI",
12
+ grok: "Grok CLI",
13
+ other: "Other"
14
+ };
15
+ var BINARIES = {
16
+ claude: ["claude"],
17
+ codex: ["codex"],
18
+ cursor: ["cursor-agent", "agent"],
19
+ gemini: ["gemini"],
20
+ grok: ["grok"]
21
+ };
22
+ function isTool(s) {
23
+ return s === "other" || REAL_TOOLS.includes(s);
24
+ }
25
+ function findBin(name, env) {
26
+ const exts = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
27
+ for (const dir of (env.PATH ?? "").split(path.delimiter)) {
28
+ if (!dir) continue;
29
+ for (const ext of exts) {
30
+ const p = path.join(dir, name + ext);
31
+ try {
32
+ if (fs.statSync(p).isFile()) {
33
+ fs.accessSync(p, fs.constants.X_OK);
34
+ return p;
35
+ }
36
+ } catch {
37
+ }
38
+ }
39
+ }
40
+ return void 0;
41
+ }
42
+ function detectTools(env) {
43
+ const found = [];
44
+ for (const tool of REAL_TOOLS) {
45
+ for (const name of BINARIES[tool]) {
46
+ const bin = findBin(name, env);
47
+ if (bin) {
48
+ found.push({ tool, bin });
49
+ break;
50
+ }
51
+ }
52
+ }
53
+ return found;
54
+ }
55
+ function toolFromEnv(env) {
56
+ if (env.GROK_SESSION_ID) return "grok";
57
+ if (env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT) return "claude";
58
+ if (env.CODEX_SANDBOX || env.CODEX_SESSION_ID || env.CODEX_THREAD_ID || env.CODEX_CI) return "codex";
59
+ if (env.CURSOR_AGENT || env.CURSOR_TRACE_ID || env.CURSOR_SESSION_ID) return "cursor";
60
+ if (env.GEMINI_CLI || env.GEMINI_SESSION_ID) return "gemini";
61
+ return void 0;
62
+ }
63
+
64
+ export {
65
+ REAL_TOOLS,
66
+ TOOL_LABEL,
67
+ isTool,
68
+ findBin,
69
+ detectTools,
70
+ toolFromEnv
71
+ };
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ atomicWrite,
4
+ configDir,
5
+ isRecord,
6
+ machineFile,
7
+ readText,
8
+ serviceStateFile
9
+ } from "./chunk-WNDUGQDX.js";
10
+
11
+ // src/machine.ts
12
+ import fs from "fs";
13
+ import os from "os";
14
+ function readMachine() {
15
+ const text = readText(machineFile());
16
+ if (!text) return void 0;
17
+ try {
18
+ const j = JSON.parse(text);
19
+ if (!isRecord(j) || typeof j.token !== "string" || typeof j.host !== "string" || !isRecord(j.machine)) return void 0;
20
+ const inst = isRecord(j.installed) ? j.installed : {};
21
+ return {
22
+ version: 1,
23
+ host: j.host,
24
+ machine: { id: String(j.machine.id ?? ""), name: String(j.machine.name ?? "") },
25
+ token: j.token,
26
+ connected_at: typeof j.connected_at === "string" ? j.connected_at : "",
27
+ ...j.revoked === true ? { revoked: true } : {},
28
+ installed: {
29
+ claude_dirs: Array.isArray(inst.claude_dirs) ? inst.claude_dirs.filter((x) => typeof x === "string") : [],
30
+ ...typeof inst.codex_dir === "string" ? { codex_dir: inst.codex_dir } : {},
31
+ ...isRecord(inst.service) && typeof inst.service.file === "string" && (inst.service.kind === "launchd" || inst.service.kind === "systemd" || inst.service.kind === "windows") ? { service: { kind: inst.service.kind, file: inst.service.file } } : {}
32
+ }
33
+ };
34
+ } catch {
35
+ return void 0;
36
+ }
37
+ }
38
+ function writeMachine(m) {
39
+ const dir = configDir();
40
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
41
+ fs.chmodSync(dir, 448);
42
+ try {
43
+ if (fs.lstatSync(machineFile()).isSymbolicLink()) {
44
+ throw new Error(`${machineFile()} is a symbolic link; the machine token is not written through links. Remove the link and run connect again.`);
45
+ }
46
+ } catch (e) {
47
+ if (e.code !== "ENOENT") throw e;
48
+ }
49
+ atomicWrite(machineFile(), JSON.stringify(m, null, 2) + "\n", 384);
50
+ }
51
+ var removeMachineFile = () => fs.rmSync(machineFile(), { force: true });
52
+ async function secureWindowsMachine(run, username = os.userInfo().username) {
53
+ if (!username) {
54
+ removeMachineFile();
55
+ throw new Error("could not determine the Windows user; the machine token was not kept");
56
+ }
57
+ const dir = configDir();
58
+ const d = await run("icacls", [dir, "/inheritance:r", "/grant:r", `${username}:(OI)(CI)F`]);
59
+ if (d.code === 0) {
60
+ const f = await run("icacls", [machineFile(), "/inheritance:r", "/grant:r", `${username}:F`]);
61
+ if (f.code === 0) return;
62
+ }
63
+ removeMachineFile();
64
+ throw new Error("could not restrict the Hivefloor config to your Windows user with icacls; the machine token was not kept");
65
+ }
66
+ var SERVICE_ALIVE_MS = 9e4;
67
+ function readServiceState() {
68
+ const text = readText(serviceStateFile());
69
+ if (!text) return void 0;
70
+ try {
71
+ const j = JSON.parse(text);
72
+ if (isRecord(j) && typeof j.beat === "number" && typeof j.pid === "number") return j;
73
+ } catch {
74
+ }
75
+ return void 0;
76
+ }
77
+ function writeServiceState(s) {
78
+ atomicWrite(serviceStateFile(), JSON.stringify(s) + "\n", 384);
79
+ }
80
+ var serviceAlive = (nowMs) => {
81
+ const s = readServiceState();
82
+ return s !== void 0 && nowMs - s.beat < SERVICE_ALIVE_MS;
83
+ };
84
+
85
+ export {
86
+ readMachine,
87
+ writeMachine,
88
+ removeMachineFile,
89
+ secureWindowsMachine,
90
+ SERVICE_ALIVE_MS,
91
+ readServiceState,
92
+ writeServiceState,
93
+ serviceAlive
94
+ };
@@ -0,0 +1,356 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ atomicWrite,
4
+ atomicWriteUtf16,
5
+ configDir,
6
+ credentialsFile,
7
+ home,
8
+ isRecord,
9
+ readText,
10
+ removeFile
11
+ } from "./chunk-WNDUGQDX.js";
12
+ import {
13
+ cliNpxCommand,
14
+ runCmd
15
+ } from "./chunk-RE2EQZWL.js";
16
+
17
+ // src/service/units.ts
18
+ import os from "os";
19
+ import path from "path";
20
+ var LAUNCHD_LABEL = "dev.hivefloor.machine";
21
+ var SYSTEMD_UNIT = "hivefloor.service";
22
+ var SCHTASKS_TASK = "Hivefloor Machine";
23
+ var xml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
24
+ var sdEscape = (s) => s.replace(/(["\\$])/g, "\\$1").replace(/%/g, "%%");
25
+ var sdQuote = (s) => /^[\w@+=:,./-]+$/.test(s) ? s : `"${sdEscape(s)}"`;
26
+ function launchdPlist(plan) {
27
+ const bin = path.dirname(plan.node);
28
+ return `<?xml version="1.0" encoding="UTF-8"?>
29
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
30
+ <plist version="1.0">
31
+ <dict>
32
+ <key>Label</key>
33
+ <string>${LAUNCHD_LABEL}</string>
34
+ <key>ProgramArguments</key>
35
+ <array>
36
+ <string>${xml(plan.node)}</string>
37
+ <string>${xml(plan.script)}</string>
38
+ <string>service</string>
39
+ <string>run</string>
40
+ </array>
41
+ <key>RunAtLoad</key>
42
+ <true/>
43
+ <key>KeepAlive</key>
44
+ <dict>
45
+ <key>SuccessfulExit</key>
46
+ <false/>
47
+ </dict>
48
+ <key>ThrottleInterval</key>
49
+ <integer>30</integer>
50
+ <key>EnvironmentVariables</key>
51
+ <dict>
52
+ <key>PATH</key>
53
+ <string>${xml(`${bin}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`)}</string>
54
+ </dict>
55
+ <key>StandardOutPath</key>
56
+ <string>/dev/null</string>
57
+ <key>StandardErrorPath</key>
58
+ <string>/dev/null</string>
59
+ </dict>
60
+ </plist>
61
+ `;
62
+ }
63
+ function systemdUnit(plan) {
64
+ return `[Unit]
65
+ Description=Hivefloor machine service
66
+ After=network-online.target
67
+
68
+ [Service]
69
+ ExecStart=${sdQuote(plan.node)} ${sdQuote(plan.script)} service run
70
+ Restart=on-failure
71
+ RestartSec=30
72
+ Environment="PATH=${sdEscape(`${path.dirname(plan.node)}:/usr/local/bin:/usr/bin:/bin`)}"
73
+
74
+ [Install]
75
+ WantedBy=default.target
76
+ `;
77
+ }
78
+ var winQuote = (s) => /\s/.test(s) ? `"${s}"` : s;
79
+ function windowsTaskExec(plan) {
80
+ return {
81
+ command: path.win32.normalize(plan.node),
82
+ arguments: `${winQuote(path.win32.normalize(plan.script))} service run`
83
+ };
84
+ }
85
+ function windowsUser(env = process.env) {
86
+ const name = env.USERNAME || os.userInfo().username;
87
+ return env.USERDOMAIN ? `${env.USERDOMAIN}\\${name}` : name;
88
+ }
89
+ function windowsTaskXml(plan, user) {
90
+ const ex = windowsTaskExec(plan);
91
+ return `<?xml version="1.0" encoding="UTF-16"?>
92
+ <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
93
+ <RegistrationInfo>
94
+ <Description>Hivefloor machine service</Description>
95
+ </RegistrationInfo>
96
+ <Triggers>
97
+ <LogonTrigger>
98
+ <Enabled>true</Enabled>
99
+ <UserId>${xml(user)}</UserId>
100
+ </LogonTrigger>
101
+ </Triggers>
102
+ <Principals>
103
+ <Principal id="Author">
104
+ <UserId>${xml(user)}</UserId>
105
+ <LogonType>InteractiveToken</LogonType>
106
+ <RunLevel>LeastPrivilege</RunLevel>
107
+ </Principal>
108
+ </Principals>
109
+ <Settings>
110
+ <AllowStartOnDemand>true</AllowStartOnDemand>
111
+ <RestartOnFailure>
112
+ <Interval>PT1M</Interval>
113
+ <Count>255</Count>
114
+ </RestartOnFailure>
115
+ <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
116
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
117
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
118
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
119
+ <Enabled>true</Enabled>
120
+ </Settings>
121
+ <Actions Context="Author">
122
+ <Exec>
123
+ <Command>${xml(ex.command)}</Command>
124
+ <Arguments>${xml(ex.arguments)}</Arguments>
125
+ </Exec>
126
+ </Actions>
127
+ </Task>
128
+ `;
129
+ }
130
+ var launchdFile = () => path.join(home(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
131
+ var systemdFile = () => path.join(home(), ".config", "systemd", "user", SYSTEMD_UNIT);
132
+ var windowsTaskFile = () => path.join(configDir(), "service-task.xml");
133
+ async function installService(platform, run, plan, uid, env = process.env) {
134
+ const res = { lines: [], warnings: [] };
135
+ if (platform === "darwin") {
136
+ const file = launchdFile();
137
+ atomicWrite(file, launchdPlist(plan), 420);
138
+ const domain = `gui/${uid ?? 501}`;
139
+ await run("launchctl", ["bootout", `${domain}/${LAUNCHD_LABEL}`]);
140
+ const r = await run("launchctl", ["bootstrap", domain, file]);
141
+ res.installed = { kind: "launchd", file };
142
+ if (r.code === 0) res.lines.push(`Service: launchd agent ${LAUNCHD_LABEL} (${file})`);
143
+ else res.warnings.push(`the service unit was written to ${file} but launchctl could not start it; log out and in, or run: ${cliNpxCommand("service run")}`);
144
+ return res;
145
+ }
146
+ if (platform === "linux") {
147
+ const file = systemdFile();
148
+ atomicWrite(file, systemdUnit(plan), 420);
149
+ res.installed = { kind: "systemd", file };
150
+ await run("systemctl", ["--user", "daemon-reload"]);
151
+ const r = await run("systemctl", ["--user", "enable", "--now", SYSTEMD_UNIT]);
152
+ if (r.code === 0) res.lines.push(`Service: systemd user unit ${SYSTEMD_UNIT} (${file})`);
153
+ else res.warnings.push(`the service unit was written to ${file} but systemd could not start it (no user session?); run: ${cliNpxCommand("service run")}`);
154
+ const lg = await run("loginctl", ["show-user", process.env.USER ?? "", "-p", "Linger"]);
155
+ if (lg.code === 0 && /Linger=no/.test(lg.stdout)) res.warnings.push("to keep reporting while you are logged out, run once: loginctl enable-linger");
156
+ return res;
157
+ }
158
+ if (platform === "win32") {
159
+ const file = windowsTaskFile();
160
+ atomicWriteUtf16(file, windowsTaskXml(plan, windowsUser(env)), 384);
161
+ const create = await run("schtasks", ["/Create", "/TN", SCHTASKS_TASK, "/XML", file, "/F"]);
162
+ removeFile(file);
163
+ if (create.code !== 0) {
164
+ res.warnings.push(`schtasks could not create the Scheduled Task: hooks post directly, and the machine shows offline; run connect again, or start it by hand with: ${cliNpxCommand("service run")}`);
165
+ return res;
166
+ }
167
+ res.installed = { kind: "windows", file: SCHTASKS_TASK };
168
+ const r = await run("schtasks", ["/Run", "/TN", SCHTASKS_TASK]);
169
+ if (r.code === 0) res.lines.push(`Service: Scheduled Task "${SCHTASKS_TASK}" (starts when you log in)`);
170
+ else res.warnings.push(`the Scheduled Task was created but schtasks could not start it; start it by hand with: schtasks /Run /TN "${SCHTASKS_TASK}"`);
171
+ return res;
172
+ }
173
+ res.warnings.push("no background service on this system yet: hooks post directly, and the machine shows offline");
174
+ return res;
175
+ }
176
+ function parseCsvRow(line) {
177
+ const out = [];
178
+ let field = "";
179
+ let quoted = false;
180
+ for (let i = 0; i < line.length; i++) {
181
+ const c = line[i];
182
+ if (quoted) {
183
+ if (c !== '"') field += c;
184
+ else if (line[i + 1] === '"') {
185
+ field += '"';
186
+ i++;
187
+ } else quoted = false;
188
+ } else if (c === '"') quoted = true;
189
+ else if (c === ",") {
190
+ out.push(field);
191
+ field = "";
192
+ } else field += c;
193
+ }
194
+ out.push(field);
195
+ return out;
196
+ }
197
+ async function windowsServiceStatus(run) {
198
+ const r = await run("schtasks", ["/Query", "/TN", SCHTASKS_TASK, "/FO", "CSV", "/NH"]);
199
+ if (r.code !== 0) return "missing";
200
+ const row = r.stdout.split(/\r?\n/).find((l) => l.trim() !== "");
201
+ if (!row) return "missing";
202
+ const status = (parseCsvRow(row)[2] ?? "").trim();
203
+ return /^running$/i.test(status) ? "running" : "installed";
204
+ }
205
+ async function uninstallService(svc, run, uid) {
206
+ if (svc.kind === "launchd") await run("launchctl", ["bootout", `gui/${uid ?? 501}/${LAUNCHD_LABEL}`]);
207
+ else if (svc.kind === "systemd") {
208
+ await run("systemctl", ["--user", "disable", "--now", SYSTEMD_UNIT]);
209
+ } else {
210
+ await run("schtasks", ["/End", "/TN", SCHTASKS_TASK]);
211
+ await run("schtasks", ["/Delete", "/TN", SCHTASKS_TASK, "/F"]);
212
+ }
213
+ if (svc.kind !== "windows") removeFile(svc.file);
214
+ if (svc.kind === "systemd") await run("systemctl", ["--user", "daemon-reload"]);
215
+ }
216
+
217
+ // src/credentials.ts
218
+ import fs from "fs";
219
+ var SERVICE = "hivefloor";
220
+ var realKeyring = async () => {
221
+ if (process.env.HIVEFLOOR_KEYRING === "0") return void 0;
222
+ try {
223
+ return await import("@napi-rs/keyring");
224
+ } catch {
225
+ return void 0;
226
+ }
227
+ };
228
+ var loadKeyring = realKeyring;
229
+ var account = (host, tool) => `${host}#${tool}`;
230
+ function readCredentials() {
231
+ const text = readText(credentialsFile());
232
+ const empty = { version: 1, hosts: {} };
233
+ if (!text) return empty;
234
+ try {
235
+ const j = JSON.parse(text);
236
+ if (isRecord(j) && isRecord(j.hosts)) return { version: 1, default_host: typeof j.default_host === "string" ? j.default_host : void 0, hosts: j.hosts };
237
+ } catch {
238
+ }
239
+ return empty;
240
+ }
241
+ function prepareConfigDir() {
242
+ const dir = configDir();
243
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
244
+ fs.chmodSync(dir, 448);
245
+ return dir;
246
+ }
247
+ function writeCredentials(c) {
248
+ prepareConfigDir();
249
+ const file = credentialsFile();
250
+ try {
251
+ if (fs.lstatSync(file).isSymbolicLink()) {
252
+ throw new Error(`${file} is a symbolic link; the token is not written through links. Remove the link and run connect again.`);
253
+ }
254
+ } catch (e) {
255
+ if (e.code !== "ENOENT") throw e;
256
+ }
257
+ atomicWrite(file, JSON.stringify(c, null, 2) + "\n", 384);
258
+ }
259
+ async function secureWindowsCredentials(run, username) {
260
+ if (!username) throw new Error("could not determine the Windows user; the token was not kept");
261
+ const dir = prepareConfigDir();
262
+ const d = await run("icacls", [dir, "/inheritance:r", "/grant:r", `${username}:(OI)(CI)F`]);
263
+ if (d.code !== 0) throw new Error("could not restrict the Hivefloor config to your Windows user with icacls; the token was not kept");
264
+ }
265
+ async function saveToken(host, tool, token, meta = {}, opts = {}) {
266
+ const creds = readCredentials();
267
+ const h = creds.hosts[host] ??= { tools: {} };
268
+ let storage = "file";
269
+ const kr = await loadKeyring();
270
+ if (kr) {
271
+ try {
272
+ const entry = new kr.Entry(SERVICE, account(host, tool));
273
+ entry.setPassword(token);
274
+ if (entry.getPassword() === token) storage = "keychain";
275
+ } catch {
276
+ storage = "file";
277
+ }
278
+ }
279
+ if (storage === "file" && (opts.platform ?? process.platform) === "win32") {
280
+ await secureWindowsCredentials(opts.run ?? runCmd, opts.username ?? windowsUser(process.env));
281
+ }
282
+ h.tools[tool] = { ...storage === "keychain" ? { keychain: true } : { token }, ...meta, saved_at: (/* @__PURE__ */ new Date()).toISOString() };
283
+ h.last_tool = tool;
284
+ creds.default_host = host;
285
+ writeCredentials(creds);
286
+ return storage;
287
+ }
288
+ function pickTool(h, tool) {
289
+ if (tool && h.tools[tool]) return tool;
290
+ if (h.last_tool && h.tools[h.last_tool]) return h.last_tool;
291
+ return Object.keys(h.tools)[0];
292
+ }
293
+ async function loadToken(host, tool) {
294
+ const h = readCredentials().hosts[host];
295
+ if (!h) return void 0;
296
+ const t = pickTool(h, tool);
297
+ const c = t ? h.tools[t] : void 0;
298
+ if (!t || !c) return void 0;
299
+ if (c.token) return { token: c.token, tool: t };
300
+ if (c.keychain) {
301
+ const kr = await loadKeyring();
302
+ if (!kr) return void 0;
303
+ try {
304
+ const v = new kr.Entry(SERVICE, account(host, t)).getPassword();
305
+ if (v) return { token: v, tool: t };
306
+ } catch {
307
+ }
308
+ }
309
+ return void 0;
310
+ }
311
+ function defaultHost() {
312
+ const c = readCredentials();
313
+ return c.default_host && c.hosts[c.default_host] ? c.default_host : Object.keys(c.hosts)[0];
314
+ }
315
+ async function deleteCredentials(host, tool) {
316
+ const creds = readCredentials();
317
+ const kr = await loadKeyring();
318
+ let n = 0;
319
+ for (const [h, hc] of Object.entries(creds.hosts)) {
320
+ if (host && h !== host) continue;
321
+ for (const [t, c] of Object.entries(hc.tools)) {
322
+ if (tool && t !== tool) continue;
323
+ if (c.keychain && kr) {
324
+ try {
325
+ const e = new kr.Entry(SERVICE, account(h, t));
326
+ if (e.deletePassword) e.deletePassword();
327
+ else e.deleteCredential?.();
328
+ } catch {
329
+ }
330
+ }
331
+ delete hc.tools[t];
332
+ n += 1;
333
+ }
334
+ if (hc.last_tool && !hc.tools[hc.last_tool]) hc.last_tool = Object.keys(hc.tools)[0];
335
+ if (Object.keys(hc.tools).length === 0) delete creds.hosts[h];
336
+ }
337
+ if (creds.default_host && !creds.hosts[creds.default_host]) creds.default_host = Object.keys(creds.hosts)[0];
338
+ if (n > 0) {
339
+ if (Object.keys(creds.hosts).length === 0) fs.rmSync(credentialsFile(), { force: true });
340
+ else writeCredentials(creds);
341
+ }
342
+ return n;
343
+ }
344
+
345
+ export {
346
+ SCHTASKS_TASK,
347
+ windowsUser,
348
+ installService,
349
+ windowsServiceStatus,
350
+ uninstallService,
351
+ readCredentials,
352
+ saveToken,
353
+ loadToken,
354
+ defaultHost,
355
+ deleteCredentials
356
+ };