@hivefloor/cli 0.0.0-stage → 0.1.1
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 +21 -0
- package/README.md +76 -2
- package/dist/chunk-2IMJECYD.js +71 -0
- package/dist/chunk-3IMV5EHP.js +94 -0
- package/dist/chunk-76HDATOB.js +182 -0
- package/dist/chunk-CWEEIRW2.js +38 -0
- package/dist/chunk-JCLWCMEM.js +8 -0
- package/dist/chunk-K3NQKI34.js +10 -0
- package/dist/chunk-MMWX62ZU.js +30 -0
- package/dist/chunk-NCRQGIJY.js +143 -0
- package/dist/chunk-PY74LT2W.js +41 -0
- package/dist/chunk-Q2TS3GT3.js +356 -0
- package/dist/chunk-SGDM7MAT.js +237 -0
- package/dist/chunk-TKMIV7BG.js +46 -0
- package/dist/chunk-U7PMOZGI.js +182 -0
- package/dist/chunk-WHHV2NZA.js +138 -0
- package/dist/chunk-WLHRUZTR.js +71 -0
- package/dist/chunk-WNDUGQDX.js +135 -0
- package/dist/chunk-WO7NALQO.js +74 -0
- package/dist/chunk-YCZ7XJJF.js +594 -0
- package/dist/chunk-ZYRPQPGL.js +21279 -0
- package/dist/cli-FFAZRKLR.js +100 -0
- package/dist/connect-FM4BXTQQ.js +23 -0
- package/dist/disconnect-VWUOUZP6.js +140 -0
- package/dist/hook-WSJXG2XU.js +265 -0
- package/dist/index.js +22 -0
- package/dist/link-TFITMC6H.js +78 -0
- package/dist/machine-connect-I3OZCTUZ.js +139 -0
- package/dist/mcp-47WTNQNN.js +121 -0
- package/dist/run-JO4NEHWG.js +629 -0
- package/dist/status-GXZ267BD.js +113 -0
- package/dist/token-C4K23KXH.js +32 -0
- package/package.json +46 -4
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/api.ts
|
|
4
|
+
var ApiError = class extends Error {
|
|
5
|
+
constructor(status, code, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
status;
|
|
11
|
+
code;
|
|
12
|
+
};
|
|
13
|
+
async function apiRequest(ctx, host, path, opts = {}) {
|
|
14
|
+
const headers = { accept: "application/json", ...opts.headers };
|
|
15
|
+
if (opts.token) headers.authorization = `Bearer ${opts.token}`;
|
|
16
|
+
if (opts.body !== void 0) headers["content-type"] = "application/json";
|
|
17
|
+
const res = await ctx.fetch(`${host}${path}`, {
|
|
18
|
+
method: opts.method ?? (opts.body !== void 0 ? "POST" : "GET"),
|
|
19
|
+
headers,
|
|
20
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
|
|
21
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 15e3)
|
|
22
|
+
});
|
|
23
|
+
const text = await res.text();
|
|
24
|
+
let json = null;
|
|
25
|
+
try {
|
|
26
|
+
json = text ? JSON.parse(text) : null;
|
|
27
|
+
} catch {
|
|
28
|
+
json = null;
|
|
29
|
+
}
|
|
30
|
+
return { status: res.status, json };
|
|
31
|
+
}
|
|
32
|
+
function errorOf(r) {
|
|
33
|
+
const j = r.json ?? {};
|
|
34
|
+
return new ApiError(r.status, typeof j.error === "string" ? j.error : "error", typeof j.message === "string" ? j.message : `HTTP ${r.status}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
ApiError,
|
|
39
|
+
apiRequest,
|
|
40
|
+
errorOf
|
|
41
|
+
};
|
|
@@ -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-ZYRPQPGL.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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
+
};
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
configDir,
|
|
4
|
+
readText,
|
|
5
|
+
removeFile,
|
|
6
|
+
serviceLogFile,
|
|
7
|
+
spoolFile
|
|
8
|
+
} from "./chunk-WNDUGQDX.js";
|
|
9
|
+
|
|
10
|
+
// src/spool.ts
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
var SPOOL_MAX_BYTES = 1024 * 1024;
|
|
14
|
+
var LOCK_STALE_MS = 1e4;
|
|
15
|
+
var CLAIM_STALE_MS = 6e4;
|
|
16
|
+
var LOCK_WAIT_MS = 250;
|
|
17
|
+
var CAP_SLACK = 64 * 1024;
|
|
18
|
+
var counter = 0;
|
|
19
|
+
var uniq = () => `${Date.now()}-${process.pid}-${counter++}`;
|
|
20
|
+
function parseSpool(text) {
|
|
21
|
+
const out = [];
|
|
22
|
+
for (const line of text.split("\n")) {
|
|
23
|
+
if (!line.trim()) continue;
|
|
24
|
+
try {
|
|
25
|
+
out.push(JSON.parse(line));
|
|
26
|
+
} catch {
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
function capped(events, maxBytes = SPOOL_MAX_BYTES) {
|
|
32
|
+
const lines = events.map((e) => JSON.stringify(e));
|
|
33
|
+
let total = lines.reduce((n, l) => n + Buffer.byteLength(l) + 1, 0);
|
|
34
|
+
let start = 0;
|
|
35
|
+
while (total > maxBytes && start < lines.length) {
|
|
36
|
+
total -= Buffer.byteLength(lines[start]) + 1;
|
|
37
|
+
start += 1;
|
|
38
|
+
}
|
|
39
|
+
return { events: events.slice(start), dropped: start };
|
|
40
|
+
}
|
|
41
|
+
function droppedEvents(file = spoolFile()) {
|
|
42
|
+
try {
|
|
43
|
+
return Number(fs.readFileSync(`${file}.dropped`, "utf8").trim()) || 0;
|
|
44
|
+
} catch {
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function noteDropped(spool, dropped) {
|
|
49
|
+
if (dropped <= 0) return;
|
|
50
|
+
try {
|
|
51
|
+
fs.writeFileSync(`${spool}.dropped`, String(droppedEvents(spool) + dropped), { mode: 384 });
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
fs.appendFileSync(serviceLogFile(), `${(/* @__PURE__ */ new Date()).toISOString()} spool full: dropped ${dropped} event(s), the cap is ${SPOOL_MAX_BYTES} bytes
|
|
56
|
+
`, { mode: 384 });
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function pause(ms) {
|
|
61
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
62
|
+
}
|
|
63
|
+
var lockPath = (file) => `${file}.lock`;
|
|
64
|
+
function acquireLock(file) {
|
|
65
|
+
const lp = lockPath(file);
|
|
66
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
67
|
+
for (; ; ) {
|
|
68
|
+
try {
|
|
69
|
+
fs.closeSync(fs.openSync(lp, "wx", 384));
|
|
70
|
+
return true;
|
|
71
|
+
} catch (e) {
|
|
72
|
+
if (e.code !== "EEXIST") return false;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
if (Date.now() - fs.statSync(lp).mtimeMs > LOCK_STALE_MS) {
|
|
76
|
+
fs.rmSync(lp, { force: true });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (Date.now() >= deadline) return false;
|
|
83
|
+
pause(5);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function releaseLock(file) {
|
|
87
|
+
removeFile(lockPath(file));
|
|
88
|
+
}
|
|
89
|
+
var claimPath = (file) => `${file}.claim-${uniq()}`;
|
|
90
|
+
var pendingPath = (file) => `${file}.pending-${uniq()}`;
|
|
91
|
+
function sideFiles(dir, file, kind) {
|
|
92
|
+
const prefix = `${path.basename(file)}.${kind}-`;
|
|
93
|
+
let names;
|
|
94
|
+
try {
|
|
95
|
+
names = fs.readdirSync(dir);
|
|
96
|
+
} catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
return names.filter((n) => n.startsWith(prefix)).sort().map((n) => path.join(dir, n));
|
|
100
|
+
}
|
|
101
|
+
function owner(file) {
|
|
102
|
+
const m = /\.(?:claim|pending)-(\d+)-(\d+)-\d+$/.exec(path.basename(file));
|
|
103
|
+
if (!m) return void 0;
|
|
104
|
+
return { ts: Number(m[1]), pid: Number(m[2]) };
|
|
105
|
+
}
|
|
106
|
+
function alive(pid) {
|
|
107
|
+
if (pid === process.pid) return true;
|
|
108
|
+
try {
|
|
109
|
+
process.kill(pid, 0);
|
|
110
|
+
return true;
|
|
111
|
+
} catch (e) {
|
|
112
|
+
return e.code === "EPERM";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function abandoned(file) {
|
|
116
|
+
const o = owner(file);
|
|
117
|
+
if (!o) return false;
|
|
118
|
+
if (!alive(o.pid)) return true;
|
|
119
|
+
return Date.now() - o.ts > CLAIM_STALE_MS;
|
|
120
|
+
}
|
|
121
|
+
function writeEvents(file, events) {
|
|
122
|
+
const tmp = `${file}.tmp-${process.pid}-${counter++}`;
|
|
123
|
+
try {
|
|
124
|
+
fs.writeFileSync(tmp, events.map((e) => JSON.stringify(e)).join("\n") + (events.length ? "\n" : ""), { mode: 384 });
|
|
125
|
+
fs.renameSync(tmp, file);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
removeFile(tmp);
|
|
128
|
+
throw e;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function writeSpool(file, events, maxBytes = SPOOL_MAX_BYTES) {
|
|
132
|
+
const { events: kept, dropped } = capped(events, maxBytes);
|
|
133
|
+
if (dropped > 0) noteDropped(file, dropped);
|
|
134
|
+
writeEvents(file, kept);
|
|
135
|
+
}
|
|
136
|
+
function recover(file) {
|
|
137
|
+
const dir = path.dirname(file);
|
|
138
|
+
const stale = [...sideFiles(dir, file, "claim").filter(abandoned), ...sideFiles(dir, file, "pending")].sort();
|
|
139
|
+
if (stale.length === 0) return;
|
|
140
|
+
const events = [];
|
|
141
|
+
for (const f of stale) events.push(...parseSpool(readText(f) ?? ""));
|
|
142
|
+
if (events.length > 0) {
|
|
143
|
+
const existing = parseSpool(readText(file) ?? "");
|
|
144
|
+
writeSpool(file, [...events, ...existing]);
|
|
145
|
+
}
|
|
146
|
+
for (const f of stale) removeFile(f);
|
|
147
|
+
}
|
|
148
|
+
function takeSpool(file = spoolFile()) {
|
|
149
|
+
fs.mkdirSync(configDir(), { recursive: true, mode: 448 });
|
|
150
|
+
if (!acquireLock(file)) return void 0;
|
|
151
|
+
try {
|
|
152
|
+
recover(file);
|
|
153
|
+
const claim = claimPath(file);
|
|
154
|
+
try {
|
|
155
|
+
fs.renameSync(file, claim);
|
|
156
|
+
} catch {
|
|
157
|
+
return void 0;
|
|
158
|
+
}
|
|
159
|
+
let events;
|
|
160
|
+
try {
|
|
161
|
+
events = parseSpool(fs.readFileSync(claim, "utf8"));
|
|
162
|
+
} catch {
|
|
163
|
+
return void 0;
|
|
164
|
+
}
|
|
165
|
+
if (events.length === 0) {
|
|
166
|
+
removeFile(claim);
|
|
167
|
+
return void 0;
|
|
168
|
+
}
|
|
169
|
+
return { spool: file, file: claim, events };
|
|
170
|
+
} finally {
|
|
171
|
+
releaseLock(file);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function releaseSpool(claim, keep, spool = spoolFile()) {
|
|
175
|
+
if (keep.length === 0) {
|
|
176
|
+
if (claim) removeFile(claim.file);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const target = claim?.spool ?? spool;
|
|
180
|
+
try {
|
|
181
|
+
returnToSpool(keep, target);
|
|
182
|
+
} catch {
|
|
183
|
+
if (claim) {
|
|
184
|
+
try {
|
|
185
|
+
writeEvents(claim.file, keep);
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
writeEvents(pendingPath(target), keep);
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (claim) removeFile(claim.file);
|
|
197
|
+
}
|
|
198
|
+
function returnToSpool(events, file = spoolFile()) {
|
|
199
|
+
if (events.length === 0) return;
|
|
200
|
+
fs.mkdirSync(configDir(), { recursive: true, mode: 448 });
|
|
201
|
+
if (!acquireLock(file)) {
|
|
202
|
+
writeEvents(pendingPath(file), events);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const existing = parseSpool(readText(file) ?? "");
|
|
207
|
+
writeSpool(file, [...events, ...existing]);
|
|
208
|
+
} finally {
|
|
209
|
+
releaseLock(file);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function appendToSpool(event, file = spoolFile()) {
|
|
213
|
+
fs.mkdirSync(configDir(), { recursive: true, mode: 448 });
|
|
214
|
+
const line = JSON.stringify(event) + "\n";
|
|
215
|
+
if (!acquireLock(file)) {
|
|
216
|
+
fs.appendFileSync(file, line, { mode: 384 });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
fs.appendFileSync(file, line, { mode: 384 });
|
|
221
|
+
let size = 0;
|
|
222
|
+
try {
|
|
223
|
+
size = fs.statSync(file).size;
|
|
224
|
+
} catch {
|
|
225
|
+
size = 0;
|
|
226
|
+
}
|
|
227
|
+
if (size > SPOOL_MAX_BYTES) writeSpool(file, parseSpool(readText(file) ?? ""), SPOOL_MAX_BYTES - CAP_SLACK);
|
|
228
|
+
} finally {
|
|
229
|
+
releaseLock(file);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export {
|
|
234
|
+
takeSpool,
|
|
235
|
+
releaseSpool,
|
|
236
|
+
appendToSpool
|
|
237
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/args.ts
|
|
4
|
+
var BOOLEAN = /* @__PURE__ */ new Set(["no-open", "yes", "y", "reveal", "help", "h", "json", "no-cli", "agent", "no-service"]);
|
|
5
|
+
function parseArgs(argv) {
|
|
6
|
+
const positionals = [];
|
|
7
|
+
const flags = {};
|
|
8
|
+
for (let k = 0; k < argv.length; k++) {
|
|
9
|
+
const a = argv[k];
|
|
10
|
+
if (a === "--") {
|
|
11
|
+
positionals.push(...argv.slice(k + 1));
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
if (a.startsWith("--") && a.length > 2) {
|
|
15
|
+
const eq = a.indexOf("=");
|
|
16
|
+
if (eq > 0) {
|
|
17
|
+
flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
18
|
+
} else {
|
|
19
|
+
const name = a.slice(2);
|
|
20
|
+
const next = argv[k + 1];
|
|
21
|
+
if (BOOLEAN.has(name) || next === void 0 || next.startsWith("--") || next === "-y" || next === "-h") {
|
|
22
|
+
flags[name] = true;
|
|
23
|
+
} else {
|
|
24
|
+
flags[name] = next;
|
|
25
|
+
k += 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
} else if (a === "-y") flags.yes = true;
|
|
29
|
+
else if (a === "-h") flags.help = true;
|
|
30
|
+
else positionals.push(a);
|
|
31
|
+
}
|
|
32
|
+
return { positionals, flags };
|
|
33
|
+
}
|
|
34
|
+
var flagStr = (p, name) => typeof p.flags[name] === "string" ? p.flags[name] : void 0;
|
|
35
|
+
function checkUnknownFlags(p, allowed) {
|
|
36
|
+
for (const f of Object.keys(p.flags)) {
|
|
37
|
+
if (!allowed.has(f)) return f;
|
|
38
|
+
}
|
|
39
|
+
return void 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
parseArgs,
|
|
44
|
+
flagStr,
|
|
45
|
+
checkUnknownFlags
|
|
46
|
+
};
|