@hasna/machines 0.0.37 → 0.0.39
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/README.md +54 -0
- package/dist/agent/index.js +10554 -166
- package/dist/agent/runtime.d.ts +33 -3
- package/dist/agent/runtime.d.ts.map +1 -1
- package/dist/cli/index.js +875 -53
- package/dist/commands/daemon.d.ts +76 -0
- package/dist/commands/daemon.d.ts.map +1 -0
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/consumer.js +227 -8
- package/dist/db.d.ts +29 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1332 -236
- package/dist/manifests.d.ts +6 -6
- package/dist/mcp/index.js +1744 -1078
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/pg-migrations.d.ts.map +1 -1
- package/dist/redaction.d.ts +9 -0
- package/dist/redaction.d.ts.map +1 -1
- package/dist/remote-storage.d.ts +3 -0
- package/dist/remote-storage.d.ts.map +1 -1
- package/dist/storage.js +116 -7
- package/dist/topology.d.ts +21 -0
- package/dist/topology.d.ts.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2135,9 +2135,23 @@ function createTables(db) {
|
|
|
2135
2135
|
pid INTEGER NOT NULL,
|
|
2136
2136
|
status TEXT NOT NULL,
|
|
2137
2137
|
updated_at TEXT NOT NULL,
|
|
2138
|
+
daemon_version TEXT,
|
|
2139
|
+
agent_mode TEXT,
|
|
2140
|
+
platform TEXT,
|
|
2141
|
+
os_version TEXT,
|
|
2142
|
+
os_build TEXT,
|
|
2143
|
+
arch TEXT,
|
|
2144
|
+
uptime_seconds INTEGER,
|
|
2145
|
+
tool_versions_json TEXT,
|
|
2146
|
+
tailscale_json TEXT,
|
|
2147
|
+
storage_sync_status TEXT,
|
|
2148
|
+
storage_sync_last_error TEXT,
|
|
2149
|
+
doctor_summary_json TEXT,
|
|
2150
|
+
private_metadata INTEGER NOT NULL DEFAULT 0,
|
|
2138
2151
|
PRIMARY KEY (machine_id, pid)
|
|
2139
2152
|
)
|
|
2140
2153
|
`);
|
|
2154
|
+
migrateAgentHeartbeats(db);
|
|
2141
2155
|
db.exec(`
|
|
2142
2156
|
CREATE TABLE IF NOT EXISTS setup_runs (
|
|
2143
2157
|
id TEXT PRIMARY KEY,
|
|
@@ -2159,6 +2173,15 @@ function createTables(db) {
|
|
|
2159
2173
|
)
|
|
2160
2174
|
`);
|
|
2161
2175
|
}
|
|
2176
|
+
function migrateAgentHeartbeats(db) {
|
|
2177
|
+
const columns = db.query("PRAGMA table_info(agent_heartbeats)").all();
|
|
2178
|
+
const existing = new Set(columns.map((column) => column.name));
|
|
2179
|
+
for (const column of AGENT_HEARTBEAT_COLUMNS) {
|
|
2180
|
+
if (existing.has(column.name))
|
|
2181
|
+
continue;
|
|
2182
|
+
db.exec(`ALTER TABLE agent_heartbeats ADD COLUMN ${column.name} ${column.definition}`);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2162
2185
|
function getAdapter(path = getDbPath()) {
|
|
2163
2186
|
if (path === ":memory:") {
|
|
2164
2187
|
const memoryAdapter = new SqliteAdapter(path);
|
|
@@ -2187,12 +2210,12 @@ function getLocalMachineId() {
|
|
|
2187
2210
|
function listHeartbeats(machineId) {
|
|
2188
2211
|
const db = getDb();
|
|
2189
2212
|
if (machineId) {
|
|
2190
|
-
return db.query(`SELECT
|
|
2213
|
+
return db.query(`SELECT *
|
|
2191
2214
|
FROM agent_heartbeats
|
|
2192
2215
|
WHERE machine_id = ?
|
|
2193
2216
|
ORDER BY updated_at DESC`).all(machineId);
|
|
2194
2217
|
}
|
|
2195
|
-
return db.query(`SELECT
|
|
2218
|
+
return db.query(`SELECT *
|
|
2196
2219
|
FROM agent_heartbeats
|
|
2197
2220
|
ORDER BY updated_at DESC`).all();
|
|
2198
2221
|
}
|
|
@@ -2213,9 +2236,24 @@ function recordSyncRun(machineId, status, actions) {
|
|
|
2213
2236
|
db.query(`INSERT INTO sync_runs (id, machine_id, status, actions_json, created_at, updated_at)
|
|
2214
2237
|
VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(actions), now, now);
|
|
2215
2238
|
}
|
|
2216
|
-
var adapter = null;
|
|
2239
|
+
var adapter = null, AGENT_HEARTBEAT_COLUMNS;
|
|
2217
2240
|
var init_db = __esm(() => {
|
|
2218
2241
|
init_paths();
|
|
2242
|
+
AGENT_HEARTBEAT_COLUMNS = [
|
|
2243
|
+
{ name: "daemon_version", definition: "TEXT" },
|
|
2244
|
+
{ name: "agent_mode", definition: "TEXT" },
|
|
2245
|
+
{ name: "platform", definition: "TEXT" },
|
|
2246
|
+
{ name: "os_version", definition: "TEXT" },
|
|
2247
|
+
{ name: "os_build", definition: "TEXT" },
|
|
2248
|
+
{ name: "arch", definition: "TEXT" },
|
|
2249
|
+
{ name: "uptime_seconds", definition: "INTEGER" },
|
|
2250
|
+
{ name: "tool_versions_json", definition: "TEXT" },
|
|
2251
|
+
{ name: "tailscale_json", definition: "TEXT" },
|
|
2252
|
+
{ name: "storage_sync_status", definition: "TEXT" },
|
|
2253
|
+
{ name: "storage_sync_last_error", definition: "TEXT" },
|
|
2254
|
+
{ name: "doctor_summary_json", definition: "TEXT" },
|
|
2255
|
+
{ name: "private_metadata", definition: "INTEGER NOT NULL DEFAULT 0" }
|
|
2256
|
+
];
|
|
2219
2257
|
});
|
|
2220
2258
|
|
|
2221
2259
|
// src/pg-migrations.ts
|
|
@@ -2228,9 +2266,36 @@ var init_pg_migrations = __esm(() => {
|
|
|
2228
2266
|
pid INTEGER NOT NULL,
|
|
2229
2267
|
status TEXT NOT NULL,
|
|
2230
2268
|
updated_at TIMESTAMPTZ NOT NULL,
|
|
2269
|
+
daemon_version TEXT,
|
|
2270
|
+
agent_mode TEXT,
|
|
2271
|
+
platform TEXT,
|
|
2272
|
+
os_version TEXT,
|
|
2273
|
+
os_build TEXT,
|
|
2274
|
+
arch TEXT,
|
|
2275
|
+
uptime_seconds INTEGER,
|
|
2276
|
+
tool_versions_json TEXT,
|
|
2277
|
+
tailscale_json TEXT,
|
|
2278
|
+
storage_sync_status TEXT,
|
|
2279
|
+
storage_sync_last_error TEXT,
|
|
2280
|
+
doctor_summary_json TEXT,
|
|
2281
|
+
private_metadata INTEGER NOT NULL DEFAULT 0,
|
|
2231
2282
|
PRIMARY KEY (machine_id, pid)
|
|
2232
2283
|
);
|
|
2233
2284
|
|
|
2285
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS daemon_version TEXT;
|
|
2286
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS agent_mode TEXT;
|
|
2287
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS platform TEXT;
|
|
2288
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS os_version TEXT;
|
|
2289
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS os_build TEXT;
|
|
2290
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS arch TEXT;
|
|
2291
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS uptime_seconds INTEGER;
|
|
2292
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS tool_versions_json TEXT;
|
|
2293
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS tailscale_json TEXT;
|
|
2294
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS storage_sync_status TEXT;
|
|
2295
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS storage_sync_last_error TEXT;
|
|
2296
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS doctor_summary_json TEXT;
|
|
2297
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS private_metadata INTEGER NOT NULL DEFAULT 0;
|
|
2298
|
+
|
|
2234
2299
|
CREATE TABLE IF NOT EXISTS setup_runs (
|
|
2235
2300
|
id TEXT PRIMARY KEY,
|
|
2236
2301
|
machine_id TEXT NOT NULL,
|
|
@@ -2263,7 +2328,20 @@ function normalizeParams(params) {
|
|
|
2263
2328
|
return flat.map((value) => value === undefined ? null : value);
|
|
2264
2329
|
}
|
|
2265
2330
|
function sslConfigFor(connectionString) {
|
|
2266
|
-
|
|
2331
|
+
let url;
|
|
2332
|
+
try {
|
|
2333
|
+
url = new URL(connectionString);
|
|
2334
|
+
} catch {
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
const sslMode = url.searchParams.get("sslmode")?.toLowerCase();
|
|
2338
|
+
const ssl = url.searchParams.get("ssl")?.toLowerCase();
|
|
2339
|
+
if (sslMode === "disable" || ssl === "false")
|
|
2340
|
+
return;
|
|
2341
|
+
if (sslMode === "no-verify" || process.env["HASNA_MACHINES_DATABASE_SSL_REJECT_UNAUTHORIZED"] === "0") {
|
|
2342
|
+
return { rejectUnauthorized: false };
|
|
2343
|
+
}
|
|
2344
|
+
return sslMode || ssl === "true" ? { rejectUnauthorized: true } : undefined;
|
|
2267
2345
|
}
|
|
2268
2346
|
|
|
2269
2347
|
class PgAdapterAsync {
|
|
@@ -2603,7 +2681,7 @@ var {
|
|
|
2603
2681
|
|
|
2604
2682
|
// src/cli/index.ts
|
|
2605
2683
|
import { registerEventCommands, registerWebhookCommands } from "@hasna/events/commander";
|
|
2606
|
-
import { execFileSync } from "child_process";
|
|
2684
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
2607
2685
|
|
|
2608
2686
|
// node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
2609
2687
|
var ANSI_BACKGROUND_OFFSET = 10;
|
|
@@ -3095,12 +3173,12 @@ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
|
|
|
3095
3173
|
var source_default = chalk;
|
|
3096
3174
|
|
|
3097
3175
|
// src/version.ts
|
|
3098
|
-
import { existsSync, readFileSync } from "fs";
|
|
3176
|
+
import { existsSync, readFileSync, realpathSync } from "fs";
|
|
3099
3177
|
import { dirname, join } from "path";
|
|
3100
3178
|
import { fileURLToPath } from "url";
|
|
3101
3179
|
function getPackageVersion() {
|
|
3102
3180
|
try {
|
|
3103
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
3181
|
+
const here = dirname(realpathSync(fileURLToPath(import.meta.url)));
|
|
3104
3182
|
const candidates = [join(here, "..", "package.json"), join(here, "..", "..", "package.json")];
|
|
3105
3183
|
const pkgPath = candidates.find((candidate) => existsSync(candidate));
|
|
3106
3184
|
if (!pkgPath) {
|
|
@@ -7095,6 +7173,9 @@ init_paths();
|
|
|
7095
7173
|
|
|
7096
7174
|
// src/redaction.ts
|
|
7097
7175
|
var REDACTED_VALUE = "[redacted]";
|
|
7176
|
+
var PRIVATE_OUTPUT_ENV = "HASNA_MACHINES_ALLOW_PRIVATE_OUTPUT";
|
|
7177
|
+
var PRIVATE_OUTPUT_FALLBACK_ENV = "MACHINES_ALLOW_PRIVATE_OUTPUT";
|
|
7178
|
+
var PRIVATE_OUTPUT_DENIED_WARNING = `private_output_denied:set ${PRIVATE_OUTPUT_ENV}=1 to allow private metadata output`;
|
|
7098
7179
|
var SENSITIVE_KEY_PATTERN = /(password|passwd|token|credential|private[_-]?key|privateKey|api[_-]?key|github.*key|pem|secret)/i;
|
|
7099
7180
|
var SECRET_REFERENCE_KEY_PATTERN = /(secret(ref(erence)?|key)?|secretRef|secretKey)$/i;
|
|
7100
7181
|
var SENSITIVE_VALUE_PATTERNS = [
|
|
@@ -7105,9 +7186,17 @@ var SENSITIVE_VALUE_PATTERNS = [
|
|
|
7105
7186
|
/\bAKIA[0-9A-Z]{16}\b/,
|
|
7106
7187
|
/\bsk-[A-Za-z0-9_-]{20,}\b/
|
|
7107
7188
|
];
|
|
7189
|
+
var IPV4_PATTERN = /\b(?:10|127|169\.254|172\.(?:1[6-9]|2\d|3[0-1])|192\.168|100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7]))(?:\.\d{1,3}){2}\b/g;
|
|
7190
|
+
var IPV6_PATTERN = /\b(?:fc|fd|fe80)[0-9a-f:]*:[0-9a-f:]+\b/gi;
|
|
7191
|
+
var DATABASE_URL_PATTERN = /\b(?:postgres(?:ql)?|mysql|mariadb|redis|mongodb|s3):\/\/[^\s"'<>]+/gi;
|
|
7192
|
+
var PRIVATE_HOST_PATTERN = /\b(?:[A-Za-z0-9._%+-]+@)?[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*(?:\.tailnet(?:\.[A-Za-z0-9-]+)*|\.ts\.net|\.private(?:\.[A-Za-z0-9-]+)*|\.internal|\.local)\b/gi;
|
|
7108
7193
|
function isSensitiveKey(key) {
|
|
7109
7194
|
return SENSITIVE_KEY_PATTERN.test(key);
|
|
7110
7195
|
}
|
|
7196
|
+
function isPrivateOutputEnabled(env2 = process.env) {
|
|
7197
|
+
const value = env2[PRIVATE_OUTPUT_ENV] ?? env2[PRIVATE_OUTPUT_FALLBACK_ENV];
|
|
7198
|
+
return ["1", "true", "yes", "on", "private"].includes(String(value ?? "").trim().toLowerCase());
|
|
7199
|
+
}
|
|
7111
7200
|
function isSecretReferenceKey(key) {
|
|
7112
7201
|
return SECRET_REFERENCE_KEY_PATTERN.test(key);
|
|
7113
7202
|
}
|
|
@@ -7120,6 +7209,12 @@ function isRecord(value) {
|
|
|
7120
7209
|
function redactPath(value) {
|
|
7121
7210
|
return value.replace(/\/home\/[^/\s]+/g, "/home/<user>").replace(/\/Users\/[^/\s]+/g, "/Users/<user>").replace(/[A-Za-z]:\\Users\\[^\\\s]+/g, "C:\\Users\\<user>");
|
|
7122
7211
|
}
|
|
7212
|
+
function redactErrorMessage(value) {
|
|
7213
|
+
return redactPath(value).replace(DATABASE_URL_PATTERN, (match) => {
|
|
7214
|
+
const scheme = match.match(/^([a-z][a-z0-9+.-]*:\/\/)/i)?.[1] ?? "";
|
|
7215
|
+
return `${scheme}${REDACTED_VALUE}`;
|
|
7216
|
+
}).replace(IPV4_PATTERN, REDACTED_VALUE).replace(IPV6_PATTERN, REDACTED_VALUE).replace(PRIVATE_HOST_PATTERN, REDACTED_VALUE);
|
|
7217
|
+
}
|
|
7123
7218
|
function redactPrivateRef(value) {
|
|
7124
7219
|
const trimmed = value.trim();
|
|
7125
7220
|
const scheme = trimmed.match(/^([a-z][a-z0-9+.-]*:\/\/)/i);
|
|
@@ -7589,6 +7684,16 @@ function routeRank(hint) {
|
|
|
7589
7684
|
function selectRouteHint(hints) {
|
|
7590
7685
|
return [...hints].sort((left, right) => routeRank(left) - routeRank(right))[0] ?? null;
|
|
7591
7686
|
}
|
|
7687
|
+
function parseHeartbeatJson(value) {
|
|
7688
|
+
if (!value)
|
|
7689
|
+
return null;
|
|
7690
|
+
try {
|
|
7691
|
+
const parsed = JSON.parse(value);
|
|
7692
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
7693
|
+
} catch {
|
|
7694
|
+
return null;
|
|
7695
|
+
}
|
|
7696
|
+
}
|
|
7592
7697
|
function buildEntry(input) {
|
|
7593
7698
|
const manifest = input.manifest;
|
|
7594
7699
|
const peer = input.peer;
|
|
@@ -7611,6 +7716,22 @@ function buildEntry(input) {
|
|
|
7611
7716
|
manifest_declared: Boolean(manifest),
|
|
7612
7717
|
heartbeat_status: input.heartbeat?.status ?? "unknown",
|
|
7613
7718
|
last_heartbeat_at: input.heartbeat?.updated_at ?? null,
|
|
7719
|
+
agent: {
|
|
7720
|
+
pid: input.heartbeat?.pid ?? null,
|
|
7721
|
+
daemon_version: input.heartbeat?.daemon_version ?? null,
|
|
7722
|
+
mode: input.heartbeat?.agent_mode ?? null,
|
|
7723
|
+
private_metadata: Boolean(input.heartbeat?.private_metadata),
|
|
7724
|
+
platform: input.heartbeat?.platform ?? null,
|
|
7725
|
+
os_version: input.heartbeat?.os_version ?? null,
|
|
7726
|
+
os_build: input.heartbeat?.os_build ?? null,
|
|
7727
|
+
arch: input.heartbeat?.arch ?? null,
|
|
7728
|
+
uptime_seconds: input.heartbeat?.uptime_seconds ?? null,
|
|
7729
|
+
tool_versions: parseHeartbeatJson(input.heartbeat?.tool_versions_json),
|
|
7730
|
+
tailscale: parseHeartbeatJson(input.heartbeat?.tailscale_json),
|
|
7731
|
+
storage_sync_status: input.heartbeat?.storage_sync_status ?? null,
|
|
7732
|
+
storage_sync_last_error: input.heartbeat?.storage_sync_last_error ?? null,
|
|
7733
|
+
doctor_summary: parseHeartbeatJson(input.heartbeat?.doctor_summary_json)
|
|
7734
|
+
},
|
|
7614
7735
|
tailscale: {
|
|
7615
7736
|
dns_name: manifest?.tailscaleName ?? peer?.DNSName?.replace(/\.$/, "") ?? null,
|
|
7616
7737
|
ips: peer?.TailscaleIPs ?? [],
|
|
@@ -7670,6 +7791,72 @@ function discoverMachineTopology(options = {}) {
|
|
|
7670
7791
|
warnings
|
|
7671
7792
|
};
|
|
7672
7793
|
}
|
|
7794
|
+
function redactFleetString(value) {
|
|
7795
|
+
if (!value)
|
|
7796
|
+
return value;
|
|
7797
|
+
return redactErrorMessage(value);
|
|
7798
|
+
}
|
|
7799
|
+
function redactPublicRecord(value) {
|
|
7800
|
+
if (!value)
|
|
7801
|
+
return null;
|
|
7802
|
+
const redacted = {};
|
|
7803
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
7804
|
+
if (/(host|hostname|dns|ip|ips|user|username|serial|address|target|url|token|secret|password|credential)/i.test(key)) {
|
|
7805
|
+
redacted[key] = REDACTED_VALUE;
|
|
7806
|
+
continue;
|
|
7807
|
+
}
|
|
7808
|
+
if (typeof entry === "string") {
|
|
7809
|
+
redacted[key] = redactFleetString(entry);
|
|
7810
|
+
} else if (Array.isArray(entry)) {
|
|
7811
|
+
redacted[key] = entry.map((item) => {
|
|
7812
|
+
if (typeof item === "string")
|
|
7813
|
+
return redactFleetString(item);
|
|
7814
|
+
if (item && typeof item === "object")
|
|
7815
|
+
return redactPublicRecord(item);
|
|
7816
|
+
return item;
|
|
7817
|
+
});
|
|
7818
|
+
} else if (entry && typeof entry === "object") {
|
|
7819
|
+
redacted[key] = redactPublicRecord(entry);
|
|
7820
|
+
} else {
|
|
7821
|
+
redacted[key] = entry;
|
|
7822
|
+
}
|
|
7823
|
+
}
|
|
7824
|
+
return redactSensitiveValue(redacted);
|
|
7825
|
+
}
|
|
7826
|
+
function redactTopologyForOutput(topology, options = {}) {
|
|
7827
|
+
if (options.privateMetadata)
|
|
7828
|
+
return topology;
|
|
7829
|
+
return {
|
|
7830
|
+
...topology,
|
|
7831
|
+
local_hostname: REDACTED_VALUE,
|
|
7832
|
+
warnings: topology.warnings.map(redactFleetString),
|
|
7833
|
+
machines: topology.machines.map((machine) => ({
|
|
7834
|
+
...machine,
|
|
7835
|
+
hostname: machine.hostname ? REDACTED_VALUE : null,
|
|
7836
|
+
user: machine.user ? REDACTED_VALUE : null,
|
|
7837
|
+
tailscale: {
|
|
7838
|
+
...machine.tailscale,
|
|
7839
|
+
dns_name: machine.tailscale.dns_name ? REDACTED_VALUE : null,
|
|
7840
|
+
ips: machine.tailscale.ips.map(() => REDACTED_VALUE)
|
|
7841
|
+
},
|
|
7842
|
+
ssh: {
|
|
7843
|
+
...machine.ssh,
|
|
7844
|
+
address: machine.ssh.address ? REDACTED_VALUE : null,
|
|
7845
|
+
command_target: machine.ssh.command_target ? REDACTED_VALUE : null
|
|
7846
|
+
},
|
|
7847
|
+
route_hints: machine.route_hints.map((hint) => ({
|
|
7848
|
+
...hint,
|
|
7849
|
+
target: REDACTED_VALUE
|
|
7850
|
+
})),
|
|
7851
|
+
agent: {
|
|
7852
|
+
...machine.agent,
|
|
7853
|
+
tailscale: redactPublicRecord(machine.agent.tailscale),
|
|
7854
|
+
storage_sync_last_error: machine.agent.storage_sync_last_error ? redactFleetString(machine.agent.storage_sync_last_error) : null,
|
|
7855
|
+
doctor_summary: redactPublicRecord(machine.agent.doctor_summary)
|
|
7856
|
+
}
|
|
7857
|
+
}))
|
|
7858
|
+
};
|
|
7859
|
+
}
|
|
7673
7860
|
function normalizeMachineAlias(value) {
|
|
7674
7861
|
return value.trim().replace(/\.$/, "").toLowerCase();
|
|
7675
7862
|
}
|
|
@@ -7863,6 +8050,20 @@ function resolveMachineRoute(machineId, options = {}) {
|
|
|
7863
8050
|
warnings
|
|
7864
8051
|
};
|
|
7865
8052
|
}
|
|
8053
|
+
function redactRouteForOutput(route, options = {}) {
|
|
8054
|
+
if (options.privateMetadata)
|
|
8055
|
+
return route;
|
|
8056
|
+
return {
|
|
8057
|
+
...route,
|
|
8058
|
+
target: route.target ? REDACTED_VALUE : null,
|
|
8059
|
+
command_target: route.command_target ? REDACTED_VALUE : null,
|
|
8060
|
+
warnings: route.warnings.map(redactFleetString),
|
|
8061
|
+
evidence: {
|
|
8062
|
+
...route.evidence,
|
|
8063
|
+
selected_hint: route.evidence.selected_hint ? { ...route.evidence.selected_hint, target: REDACTED_VALUE } : null
|
|
8064
|
+
}
|
|
8065
|
+
};
|
|
8066
|
+
}
|
|
7866
8067
|
function isRecord2(value) {
|
|
7867
8068
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
7868
8069
|
}
|
|
@@ -9752,6 +9953,16 @@ function runSync(machineId, options = {}, runner = runMachineCommand) {
|
|
|
9752
9953
|
// src/commands/status.ts
|
|
9753
9954
|
init_db();
|
|
9754
9955
|
init_paths();
|
|
9956
|
+
function parseJsonObject(value) {
|
|
9957
|
+
if (!value)
|
|
9958
|
+
return null;
|
|
9959
|
+
try {
|
|
9960
|
+
const parsed = JSON.parse(value);
|
|
9961
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
9962
|
+
} catch {
|
|
9963
|
+
return null;
|
|
9964
|
+
}
|
|
9965
|
+
}
|
|
9755
9966
|
function getStatus() {
|
|
9756
9967
|
const manifest = readManifest();
|
|
9757
9968
|
const heartbeats = listHeartbeats();
|
|
@@ -9775,7 +9986,12 @@ function getStatus() {
|
|
|
9775
9986
|
platform: declared?.platform,
|
|
9776
9987
|
manifestDeclared: Boolean(declared),
|
|
9777
9988
|
heartbeatStatus: heartbeat?.status || "unknown",
|
|
9778
|
-
lastHeartbeatAt: heartbeat?.updated_at
|
|
9989
|
+
lastHeartbeatAt: heartbeat?.updated_at,
|
|
9990
|
+
daemonVersion: heartbeat?.daemon_version ?? null,
|
|
9991
|
+
agentMode: heartbeat?.agent_mode ?? null,
|
|
9992
|
+
storageSyncStatus: heartbeat?.storage_sync_status ?? null,
|
|
9993
|
+
doctorSummary: parseJsonObject(heartbeat?.doctor_summary_json),
|
|
9994
|
+
privateMetadata: Boolean(heartbeat?.private_metadata)
|
|
9779
9995
|
};
|
|
9780
9996
|
}),
|
|
9781
9997
|
recentSetupRuns: countRuns("setup_runs"),
|
|
@@ -10361,11 +10577,514 @@ function runDoctor(machineId, options = {}) {
|
|
|
10361
10577
|
};
|
|
10362
10578
|
}
|
|
10363
10579
|
|
|
10580
|
+
// src/commands/daemon.ts
|
|
10581
|
+
import { execFileSync } from "child_process";
|
|
10582
|
+
import { chmodSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
10583
|
+
import { dirname as dirname4 } from "path";
|
|
10584
|
+
import { platform as osPlatform } from "os";
|
|
10585
|
+
var DEFAULT_SERVICE_NAME = "machines-agent";
|
|
10586
|
+
var DEFAULT_EXECUTABLE = "/usr/local/bin/machines-agent";
|
|
10587
|
+
var DEFAULT_INTERVAL_MS = 30000;
|
|
10588
|
+
var ENV_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
|
|
10589
|
+
var SERVICE_NAME_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
10590
|
+
function buildDaemonServicePlan(options) {
|
|
10591
|
+
const resolved = resolveDaemonServiceOptions(options);
|
|
10592
|
+
const files = resolved.action === "install" ? [buildServiceFile(resolved)] : [];
|
|
10593
|
+
return {
|
|
10594
|
+
platform: resolved.platform,
|
|
10595
|
+
mode: resolved.mode,
|
|
10596
|
+
action: resolved.action,
|
|
10597
|
+
serviceName: resolved.serviceName,
|
|
10598
|
+
serviceId: resolved.serviceId,
|
|
10599
|
+
executable: resolved.executable,
|
|
10600
|
+
intervalMs: resolved.intervalMs,
|
|
10601
|
+
commands: buildActionCommands(resolved),
|
|
10602
|
+
files,
|
|
10603
|
+
warnings: resolved.warnings,
|
|
10604
|
+
manualSteps: buildManualSteps(resolved, files)
|
|
10605
|
+
};
|
|
10606
|
+
}
|
|
10607
|
+
function runDaemonServicePlan(plan, options = {}) {
|
|
10608
|
+
const apply = options.apply === true;
|
|
10609
|
+
const allowed = apply && options.yes === true;
|
|
10610
|
+
const warnings = [...plan.warnings];
|
|
10611
|
+
const filesWritten = [];
|
|
10612
|
+
const commands = [];
|
|
10613
|
+
if (apply && !allowed) {
|
|
10614
|
+
warnings.push("apply_requires_yes");
|
|
10615
|
+
}
|
|
10616
|
+
if (allowed) {
|
|
10617
|
+
for (const file of plan.files) {
|
|
10618
|
+
const path = expandShellPath(file.path);
|
|
10619
|
+
let content;
|
|
10620
|
+
try {
|
|
10621
|
+
content = materializePlaceholders(file.content);
|
|
10622
|
+
} catch (error) {
|
|
10623
|
+
warnings.push(error instanceof Error ? error.message : String(error));
|
|
10624
|
+
return {
|
|
10625
|
+
mode: "plan",
|
|
10626
|
+
applied: false,
|
|
10627
|
+
plan,
|
|
10628
|
+
filesWritten,
|
|
10629
|
+
commands: plan.commands.map((commandSpec) => ({
|
|
10630
|
+
id: commandSpec.id,
|
|
10631
|
+
command: renderCommand(commandSpec),
|
|
10632
|
+
skipped: true,
|
|
10633
|
+
exitCode: null,
|
|
10634
|
+
stdout: "",
|
|
10635
|
+
stderr: ""
|
|
10636
|
+
})),
|
|
10637
|
+
warnings
|
|
10638
|
+
};
|
|
10639
|
+
}
|
|
10640
|
+
mkdirSync2(dirname4(path), { recursive: true });
|
|
10641
|
+
writeFileSync4(path, content, "utf8");
|
|
10642
|
+
chmodSync(path, Number.parseInt(file.mode, 8));
|
|
10643
|
+
filesWritten.push(path);
|
|
10644
|
+
}
|
|
10645
|
+
}
|
|
10646
|
+
for (const commandSpec of plan.commands) {
|
|
10647
|
+
const commandLine = renderCommand(commandSpec);
|
|
10648
|
+
if (!allowed) {
|
|
10649
|
+
commands.push({
|
|
10650
|
+
id: commandSpec.id,
|
|
10651
|
+
command: commandLine,
|
|
10652
|
+
skipped: true,
|
|
10653
|
+
exitCode: null,
|
|
10654
|
+
stdout: "",
|
|
10655
|
+
stderr: ""
|
|
10656
|
+
});
|
|
10657
|
+
continue;
|
|
10658
|
+
}
|
|
10659
|
+
const program2 = commandLine[0];
|
|
10660
|
+
const args = commandLine.slice(1);
|
|
10661
|
+
try {
|
|
10662
|
+
const result = execFileSync(program2, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
10663
|
+
commands.push({
|
|
10664
|
+
id: commandSpec.id,
|
|
10665
|
+
command: commandLine,
|
|
10666
|
+
skipped: false,
|
|
10667
|
+
exitCode: 0,
|
|
10668
|
+
stdout: result,
|
|
10669
|
+
stderr: ""
|
|
10670
|
+
});
|
|
10671
|
+
} catch (error) {
|
|
10672
|
+
const maybe = error;
|
|
10673
|
+
if (commandSpec.allowFailure) {
|
|
10674
|
+
commands.push({
|
|
10675
|
+
id: commandSpec.id,
|
|
10676
|
+
command: commandLine,
|
|
10677
|
+
skipped: false,
|
|
10678
|
+
exitCode: typeof maybe.status === "number" ? maybe.status : 1,
|
|
10679
|
+
stdout: String(maybe.stdout ?? ""),
|
|
10680
|
+
stderr: String(maybe.stderr ?? ""),
|
|
10681
|
+
error: maybe.message ?? String(error)
|
|
10682
|
+
});
|
|
10683
|
+
continue;
|
|
10684
|
+
}
|
|
10685
|
+
commands.push({
|
|
10686
|
+
id: commandSpec.id,
|
|
10687
|
+
command: commandLine,
|
|
10688
|
+
skipped: false,
|
|
10689
|
+
exitCode: typeof maybe.status === "number" ? maybe.status : 1,
|
|
10690
|
+
stdout: String(maybe.stdout ?? ""),
|
|
10691
|
+
stderr: String(maybe.stderr ?? ""),
|
|
10692
|
+
error: maybe.message ?? String(error)
|
|
10693
|
+
});
|
|
10694
|
+
break;
|
|
10695
|
+
}
|
|
10696
|
+
}
|
|
10697
|
+
return {
|
|
10698
|
+
mode: allowed ? "apply" : "plan",
|
|
10699
|
+
applied: allowed,
|
|
10700
|
+
plan,
|
|
10701
|
+
filesWritten,
|
|
10702
|
+
commands,
|
|
10703
|
+
warnings
|
|
10704
|
+
};
|
|
10705
|
+
}
|
|
10706
|
+
function resolveDaemonServiceOptions(options) {
|
|
10707
|
+
const warnings = [];
|
|
10708
|
+
const serviceName = normalizeServiceName(options.serviceName, warnings);
|
|
10709
|
+
const platform4 = normalizePlatform3(options.platform, warnings);
|
|
10710
|
+
const mode = options.mode ?? "user";
|
|
10711
|
+
const intervalMs = normalizeIntervalMs(options.intervalMs, warnings);
|
|
10712
|
+
const executable = options.executable?.trim() || DEFAULT_EXECUTABLE;
|
|
10713
|
+
if (platform4 === "linux" && !executable.startsWith("/")) {
|
|
10714
|
+
warnings.push("systemd units should use an absolute executable path; install plan keeps the provided path unchanged.");
|
|
10715
|
+
}
|
|
10716
|
+
return {
|
|
10717
|
+
action: options.action,
|
|
10718
|
+
platform: platform4,
|
|
10719
|
+
mode,
|
|
10720
|
+
serviceName,
|
|
10721
|
+
serviceId: serviceName,
|
|
10722
|
+
executable,
|
|
10723
|
+
intervalMs,
|
|
10724
|
+
env: buildEnvironment(serviceName, options, warnings),
|
|
10725
|
+
warnings
|
|
10726
|
+
};
|
|
10727
|
+
}
|
|
10728
|
+
function normalizeServiceName(value, warnings) {
|
|
10729
|
+
const serviceName = value?.trim() || DEFAULT_SERVICE_NAME;
|
|
10730
|
+
if (SERVICE_NAME_PATTERN.test(serviceName))
|
|
10731
|
+
return serviceName;
|
|
10732
|
+
warnings.push(`Invalid serviceName "${serviceName}"; using ${DEFAULT_SERVICE_NAME}.`);
|
|
10733
|
+
return DEFAULT_SERVICE_NAME;
|
|
10734
|
+
}
|
|
10735
|
+
function normalizePlatform3(value, warnings) {
|
|
10736
|
+
const raw = value ?? osPlatform();
|
|
10737
|
+
if (raw === "darwin" || raw === "macos")
|
|
10738
|
+
return "macos";
|
|
10739
|
+
if (raw === "linux")
|
|
10740
|
+
return "linux";
|
|
10741
|
+
warnings.push(`Unsupported platform "${raw}"; using linux service planning.`);
|
|
10742
|
+
return "linux";
|
|
10743
|
+
}
|
|
10744
|
+
function normalizeIntervalMs(value, warnings) {
|
|
10745
|
+
if (value === undefined)
|
|
10746
|
+
return DEFAULT_INTERVAL_MS;
|
|
10747
|
+
if (Number.isInteger(value) && value > 0)
|
|
10748
|
+
return value;
|
|
10749
|
+
warnings.push(`Invalid intervalMs "${String(value)}"; using ${DEFAULT_INTERVAL_MS}.`);
|
|
10750
|
+
return DEFAULT_INTERVAL_MS;
|
|
10751
|
+
}
|
|
10752
|
+
function buildEnvironment(serviceName, options, warnings) {
|
|
10753
|
+
const env2 = {
|
|
10754
|
+
HASNA_MACHINES_AGENT_MODE: "daemon",
|
|
10755
|
+
HASNA_MACHINES_AGENT_SERVICE: serviceName
|
|
10756
|
+
};
|
|
10757
|
+
if (options.storagePush) {
|
|
10758
|
+
env2["HASNA_MACHINES_AGENT_STORAGE_PUSH"] = "1";
|
|
10759
|
+
env2["HASNA_MACHINES_AGENT_STORAGE_PUSH_BACKOFF_MS"] = "250";
|
|
10760
|
+
env2["HASNA_MACHINES_AGENT_STORAGE_PUSH_RETRIES"] = "2";
|
|
10761
|
+
env2["HASNA_MACHINES_STORAGE_MODE"] = "hybrid";
|
|
10762
|
+
env2["HASNA_MACHINES_DATABASE_URL"] = placeholderForEnv("HASNA_MACHINES_DATABASE_URL");
|
|
10763
|
+
warnings.push("storagePush is represented with env placeholders; no database URL is embedded in the plan.");
|
|
10764
|
+
}
|
|
10765
|
+
if (options.doctorSummary) {
|
|
10766
|
+
env2["HASNA_MACHINES_AGENT_DOCTOR_SUMMARY"] = "1";
|
|
10767
|
+
}
|
|
10768
|
+
if (options.privateMetadata === true) {
|
|
10769
|
+
env2["HASNA_MACHINES_PRIVATE_METADATA"] = "1";
|
|
10770
|
+
warnings.push("privateMetadata=true enables private host/network facts in heartbeat rows; do not share private-mode output publicly.");
|
|
10771
|
+
} else if (Array.isArray(options.privateMetadata)) {
|
|
10772
|
+
addEnvPlaceholders(env2, options.privateMetadata, warnings);
|
|
10773
|
+
}
|
|
10774
|
+
addEnvPlaceholders(env2, options.env ?? [], warnings);
|
|
10775
|
+
return Object.fromEntries(Object.entries(env2).sort(([left], [right]) => left.localeCompare(right)));
|
|
10776
|
+
}
|
|
10777
|
+
function addEnvPlaceholders(env2, names, warnings) {
|
|
10778
|
+
for (const rawName of names) {
|
|
10779
|
+
const name = rawName.trim();
|
|
10780
|
+
if (!ENV_NAME_PATTERN.test(name)) {
|
|
10781
|
+
warnings.push(`Invalid environment variable name "${rawName}"; skipped.`);
|
|
10782
|
+
continue;
|
|
10783
|
+
}
|
|
10784
|
+
env2[name] = placeholderForEnv(name);
|
|
10785
|
+
}
|
|
10786
|
+
}
|
|
10787
|
+
function placeholderForEnv(name) {
|
|
10788
|
+
return `<set:${name}>`;
|
|
10789
|
+
}
|
|
10790
|
+
function buildServiceFile(options) {
|
|
10791
|
+
if (options.platform === "macos") {
|
|
10792
|
+
return {
|
|
10793
|
+
id: "launchd-plist",
|
|
10794
|
+
description: "launchd property list for machines-agent",
|
|
10795
|
+
path: launchdPlistPath(options),
|
|
10796
|
+
mode: "0644",
|
|
10797
|
+
content: launchdPlist(options)
|
|
10798
|
+
};
|
|
10799
|
+
}
|
|
10800
|
+
return {
|
|
10801
|
+
id: "systemd-unit",
|
|
10802
|
+
description: "systemd unit for machines-agent",
|
|
10803
|
+
path: systemdUnitPath(options),
|
|
10804
|
+
mode: "0644",
|
|
10805
|
+
content: systemdUnit(options)
|
|
10806
|
+
};
|
|
10807
|
+
}
|
|
10808
|
+
function buildActionCommands(options) {
|
|
10809
|
+
if (options.platform === "macos")
|
|
10810
|
+
return buildLaunchdCommands(options);
|
|
10811
|
+
return buildSystemdCommands(options);
|
|
10812
|
+
}
|
|
10813
|
+
function buildLaunchdCommands(options) {
|
|
10814
|
+
const domain = launchdDomain(options);
|
|
10815
|
+
const serviceTarget = `${domain}/${options.serviceId}`;
|
|
10816
|
+
const plistPath = launchdPlistPath(options);
|
|
10817
|
+
const sudo = options.mode === "system";
|
|
10818
|
+
if (options.action === "install") {
|
|
10819
|
+
return [
|
|
10820
|
+
command("launchd-bootout-existing", "Unload any existing launchd job before bootstrap.", "launchctl", ["bootout", domain, plistPath], sudo, true, true),
|
|
10821
|
+
command("launchd-bootstrap", "Load the planned launchd plist.", "launchctl", ["bootstrap", domain, plistPath], sudo, true),
|
|
10822
|
+
command("launchd-enable", "Enable the launchd service.", "launchctl", ["enable", serviceTarget], sudo, true),
|
|
10823
|
+
command("launchd-kickstart", "Start or restart the launchd service.", "launchctl", ["kickstart", "-k", serviceTarget], sudo, true)
|
|
10824
|
+
];
|
|
10825
|
+
}
|
|
10826
|
+
if (options.action === "uninstall") {
|
|
10827
|
+
return [
|
|
10828
|
+
command("launchd-bootout", "Unload the launchd job.", "launchctl", ["bootout", domain, plistPath], sudo, true),
|
|
10829
|
+
command("remove-launchd-plist", "Remove the planned launchd plist file.", "rm", ["-f", plistPath], sudo, true)
|
|
10830
|
+
];
|
|
10831
|
+
}
|
|
10832
|
+
if (options.action === "restart") {
|
|
10833
|
+
return [command("launchd-kickstart", "Restart the launchd service.", "launchctl", ["kickstart", "-k", serviceTarget], sudo, true)];
|
|
10834
|
+
}
|
|
10835
|
+
if (options.action === "status") {
|
|
10836
|
+
return [command("launchd-print", "Print launchd service status.", "launchctl", ["print", serviceTarget], sudo, false)];
|
|
10837
|
+
}
|
|
10838
|
+
return [
|
|
10839
|
+
command("launchd-logs", "Stream logs for machines-agent.", "log", ["stream", "--style", "compact", "--predicate", `process == "${basename(options.executable)}" OR eventMessage CONTAINS "${options.serviceId}"`], false, false)
|
|
10840
|
+
];
|
|
10841
|
+
}
|
|
10842
|
+
function buildSystemdCommands(options) {
|
|
10843
|
+
const userFlag = options.mode === "user" ? ["--user"] : [];
|
|
10844
|
+
const sudo = options.mode === "system";
|
|
10845
|
+
const unitName = systemdUnitName(options);
|
|
10846
|
+
const daemonReload = command("systemd-daemon-reload", "Reload systemd unit metadata.", "systemctl", [...userFlag, "daemon-reload"], sudo, true);
|
|
10847
|
+
if (options.action === "install") {
|
|
10848
|
+
return [
|
|
10849
|
+
daemonReload,
|
|
10850
|
+
command("systemd-enable-now", "Enable and start the systemd service.", "systemctl", [...userFlag, "enable", "--now", unitName], sudo, true)
|
|
10851
|
+
];
|
|
10852
|
+
}
|
|
10853
|
+
if (options.action === "uninstall") {
|
|
10854
|
+
return [
|
|
10855
|
+
command("systemd-disable-now", "Stop and disable the systemd service.", "systemctl", [...userFlag, "disable", "--now", unitName], sudo, true),
|
|
10856
|
+
command("remove-systemd-unit", "Remove the planned systemd unit file.", "rm", ["-f", systemdUnitPath(options)], sudo, true),
|
|
10857
|
+
daemonReload
|
|
10858
|
+
];
|
|
10859
|
+
}
|
|
10860
|
+
if (options.action === "restart") {
|
|
10861
|
+
return [command("systemd-restart", "Restart the systemd service.", "systemctl", [...userFlag, "restart", unitName], sudo, true)];
|
|
10862
|
+
}
|
|
10863
|
+
if (options.action === "status") {
|
|
10864
|
+
return [command("systemd-status", "Show systemd service status.", "systemctl", [...userFlag, "status", unitName, "--no-pager"], sudo, false)];
|
|
10865
|
+
}
|
|
10866
|
+
return [
|
|
10867
|
+
command("systemd-logs", "Follow journal logs for the service.", "journalctl", [...userFlag, "-u", unitName, "-f", "--no-pager"], sudo, false)
|
|
10868
|
+
];
|
|
10869
|
+
}
|
|
10870
|
+
function buildManualSteps(options, files) {
|
|
10871
|
+
const steps = [];
|
|
10872
|
+
if (files[0])
|
|
10873
|
+
steps.push(`Write ${files[0].id} content to ${files[0].path} with mode ${files[0].mode}.`);
|
|
10874
|
+
if (options.mode === "system")
|
|
10875
|
+
steps.push("Run commands marked sudo with root privileges.");
|
|
10876
|
+
if (options.platform === "linux" && options.mode === "user") {
|
|
10877
|
+
steps.push("Run commands as the target user; enable lingering separately if the service must survive logout.");
|
|
10878
|
+
}
|
|
10879
|
+
if (options.action === "logs")
|
|
10880
|
+
steps.push("Stop the log command manually when finished.");
|
|
10881
|
+
return steps;
|
|
10882
|
+
}
|
|
10883
|
+
function command(id, description, program2, args, sudo, mutates, allowFailure = false) {
|
|
10884
|
+
return { id, description, program: program2, args, sudo, mutates, ...allowFailure ? { allowFailure: true } : {} };
|
|
10885
|
+
}
|
|
10886
|
+
function renderCommand(commandSpec) {
|
|
10887
|
+
const expanded = [commandSpec.program, ...commandSpec.args].map(expandShellPath);
|
|
10888
|
+
if (commandSpec.sudo)
|
|
10889
|
+
return ["sudo", ...expanded];
|
|
10890
|
+
return expanded;
|
|
10891
|
+
}
|
|
10892
|
+
function expandShellPath(path) {
|
|
10893
|
+
if (path.startsWith("$HOME/"))
|
|
10894
|
+
return `${process.env["HOME"] ?? ""}/${path.slice("$HOME/".length)}`;
|
|
10895
|
+
return path.replaceAll("$HOME", process.env["HOME"] ?? "").replaceAll("$UID", String(process.getuid ? process.getuid() : ""));
|
|
10896
|
+
}
|
|
10897
|
+
function materializePlaceholders(content) {
|
|
10898
|
+
return content.replace(/(?:<set:([A-Z_][A-Z0-9_]*)>|<set:([A-Z_][A-Z0-9_]*)>)/g, (_match, rawName, escapedName) => {
|
|
10899
|
+
const name = rawName ?? escapedName ?? "";
|
|
10900
|
+
const value = process.env[name];
|
|
10901
|
+
if (value === undefined || value === "") {
|
|
10902
|
+
throw new Error(`Missing environment variable required for service apply: ${name}`);
|
|
10903
|
+
}
|
|
10904
|
+
if (/[\u0000-\u001f\u007f]/.test(value)) {
|
|
10905
|
+
throw new Error(`Environment variable ${name} contains control characters; refusing to write service file.`);
|
|
10906
|
+
}
|
|
10907
|
+
return escapedName ? xmlEscape(value) : escapeSystemdEnvironmentValue(value);
|
|
10908
|
+
});
|
|
10909
|
+
}
|
|
10910
|
+
function escapeSystemdEnvironmentValue(value) {
|
|
10911
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/%/g, "%%");
|
|
10912
|
+
}
|
|
10913
|
+
function launchdPlist(options) {
|
|
10914
|
+
const env2 = Object.entries(options.env).map(([name, value]) => ` <key>${xmlEscape(name)}</key>
|
|
10915
|
+
<string>${xmlEscape(value)}</string>`).join(`
|
|
10916
|
+
`);
|
|
10917
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
10918
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
10919
|
+
<plist version="1.0">
|
|
10920
|
+
<dict>
|
|
10921
|
+
<key>Label</key>
|
|
10922
|
+
<string>${xmlEscape(options.serviceId)}</string>
|
|
10923
|
+
<key>ProgramArguments</key>
|
|
10924
|
+
<array>
|
|
10925
|
+
<string>${xmlEscape(options.executable)}</string>
|
|
10926
|
+
<string>--interval-ms</string>
|
|
10927
|
+
<string>${options.intervalMs}</string>
|
|
10928
|
+
</array>
|
|
10929
|
+
<key>EnvironmentVariables</key>
|
|
10930
|
+
<dict>
|
|
10931
|
+
${env2}
|
|
10932
|
+
</dict>
|
|
10933
|
+
<key>KeepAlive</key>
|
|
10934
|
+
<true/>
|
|
10935
|
+
<key>RunAtLoad</key>
|
|
10936
|
+
<true/>
|
|
10937
|
+
<key>StandardOutPath</key>
|
|
10938
|
+
<string>${xmlEscape(launchdLogPath(options, "out"))}</string>
|
|
10939
|
+
<key>StandardErrorPath</key>
|
|
10940
|
+
<string>${xmlEscape(launchdLogPath(options, "err"))}</string>
|
|
10941
|
+
</dict>
|
|
10942
|
+
</plist>
|
|
10943
|
+
`;
|
|
10944
|
+
}
|
|
10945
|
+
function systemdUnit(options) {
|
|
10946
|
+
const env2 = Object.entries(options.env).map(([name, value]) => `Environment=${quoteSystemdEnvironment(name, value)}`).join(`
|
|
10947
|
+
`);
|
|
10948
|
+
return `[Unit]
|
|
10949
|
+
Description=Hasna machines agent
|
|
10950
|
+
After=network-online.target
|
|
10951
|
+
Wants=network-online.target
|
|
10952
|
+
|
|
10953
|
+
[Service]
|
|
10954
|
+
Type=simple
|
|
10955
|
+
ExecStart=${quoteSystemdExecArg(options.executable)} --interval-ms ${options.intervalMs}
|
|
10956
|
+
Restart=always
|
|
10957
|
+
RestartSec=10
|
|
10958
|
+
${env2}
|
|
10959
|
+
|
|
10960
|
+
[Install]
|
|
10961
|
+
WantedBy=${options.mode === "system" ? "multi-user.target" : "default.target"}
|
|
10962
|
+
`;
|
|
10963
|
+
}
|
|
10964
|
+
function launchdDomain(options) {
|
|
10965
|
+
return options.mode === "system" ? "system" : "gui/$UID";
|
|
10966
|
+
}
|
|
10967
|
+
function launchdPlistPath(options) {
|
|
10968
|
+
if (options.mode === "system")
|
|
10969
|
+
return `/Library/LaunchDaemons/${options.serviceId}.plist`;
|
|
10970
|
+
return `$HOME/Library/LaunchAgents/${options.serviceId}.plist`;
|
|
10971
|
+
}
|
|
10972
|
+
function launchdLogPath(options, stream) {
|
|
10973
|
+
const fileName = `${options.serviceId}.${stream}.log`;
|
|
10974
|
+
if (options.mode === "system")
|
|
10975
|
+
return `/var/log/${fileName}`;
|
|
10976
|
+
return `$HOME/Library/Logs/${fileName}`;
|
|
10977
|
+
}
|
|
10978
|
+
function systemdUnitName(options) {
|
|
10979
|
+
return `${options.serviceId}.service`;
|
|
10980
|
+
}
|
|
10981
|
+
function systemdUnitPath(options) {
|
|
10982
|
+
if (options.mode === "system")
|
|
10983
|
+
return `/etc/systemd/system/${systemdUnitName(options)}`;
|
|
10984
|
+
return `$HOME/.config/systemd/user/${systemdUnitName(options)}`;
|
|
10985
|
+
}
|
|
10986
|
+
function xmlEscape(value) {
|
|
10987
|
+
return String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
10988
|
+
}
|
|
10989
|
+
function quoteSystemdExecArg(value) {
|
|
10990
|
+
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value) && !value.includes("%"))
|
|
10991
|
+
return value;
|
|
10992
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/%/g, "%%")}"`;
|
|
10993
|
+
}
|
|
10994
|
+
function quoteSystemdEnvironment(name, value) {
|
|
10995
|
+
return `"${name}=${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/%/g, "%%")}"`;
|
|
10996
|
+
}
|
|
10997
|
+
function basename(path) {
|
|
10998
|
+
return path.split("/").filter(Boolean).at(-1) || path;
|
|
10999
|
+
}
|
|
11000
|
+
|
|
10364
11001
|
// src/commands/self-test.ts
|
|
10365
11002
|
init_db();
|
|
10366
11003
|
|
|
10367
11004
|
// src/commands/serve.ts
|
|
10368
11005
|
import { EventsClient as EventsClient2, sanitizeChannelsForOutput } from "@hasna/events";
|
|
11006
|
+
|
|
11007
|
+
// src/agent/runtime.ts
|
|
11008
|
+
import { arch as arch3, hostname as hostname6, platform as platform4, release, uptime, version as osVersion } from "os";
|
|
11009
|
+
init_db();
|
|
11010
|
+
init_storage_sync();
|
|
11011
|
+
function parseJsonObject2(value) {
|
|
11012
|
+
if (!value)
|
|
11013
|
+
return null;
|
|
11014
|
+
try {
|
|
11015
|
+
const parsed = JSON.parse(value);
|
|
11016
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
11017
|
+
} catch {
|
|
11018
|
+
return null;
|
|
11019
|
+
}
|
|
11020
|
+
}
|
|
11021
|
+
function heartbeatToStatus(heartbeat, options = {}) {
|
|
11022
|
+
const privateMetadata = options.privateMetadata === true;
|
|
11023
|
+
const tailscale = parseJsonObject2(heartbeat.tailscale_json);
|
|
11024
|
+
const doctorSummary = parseJsonObject2(heartbeat.doctor_summary_json);
|
|
11025
|
+
return {
|
|
11026
|
+
machineId: heartbeat.machine_id,
|
|
11027
|
+
pid: heartbeat.pid,
|
|
11028
|
+
status: heartbeat.status,
|
|
11029
|
+
updatedAt: heartbeat.updated_at,
|
|
11030
|
+
daemonVersion: heartbeat.daemon_version,
|
|
11031
|
+
agentMode: heartbeat.agent_mode,
|
|
11032
|
+
platform: heartbeat.platform,
|
|
11033
|
+
osVersion: heartbeat.os_version,
|
|
11034
|
+
osBuild: heartbeat.os_build,
|
|
11035
|
+
arch: heartbeat.arch,
|
|
11036
|
+
uptimeSeconds: heartbeat.uptime_seconds,
|
|
11037
|
+
toolVersions: sanitizeRecord(parseJsonObject2(heartbeat.tool_versions_json) ?? {}, privateMetadata),
|
|
11038
|
+
tailscale: tailscale ? sanitizeRecord(tailscale, privateMetadata) : null,
|
|
11039
|
+
storageSyncStatus: heartbeat.storage_sync_status,
|
|
11040
|
+
storageSyncLastError: heartbeat.storage_sync_last_error ? sanitizeStorageError(heartbeat.storage_sync_last_error, privateMetadata) : null,
|
|
11041
|
+
doctorSummary: doctorSummary ? sanitizeRecord(doctorSummary, privateMetadata) : null,
|
|
11042
|
+
privateMetadata: Boolean(heartbeat.private_metadata)
|
|
11043
|
+
};
|
|
11044
|
+
}
|
|
11045
|
+
function sanitizePublicString(value, privateMetadata = false) {
|
|
11046
|
+
if (privateMetadata)
|
|
11047
|
+
return value;
|
|
11048
|
+
let redacted = value;
|
|
11049
|
+
const localHostname = hostname6();
|
|
11050
|
+
const localUser = process.env["USER"] || process.env["LOGNAME"] || process.env["USERNAME"];
|
|
11051
|
+
if (localHostname)
|
|
11052
|
+
redacted = redacted.replaceAll(localHostname, "[redacted-host]");
|
|
11053
|
+
if (localUser)
|
|
11054
|
+
redacted = redacted.replaceAll(localUser, "[redacted-user]");
|
|
11055
|
+
return redactErrorMessage(redacted.replace(/[a-z][a-z0-9+.-]*:\/\/[^\s'")]+/gi, (match) => {
|
|
11056
|
+
const scheme = match.match(/^([a-z][a-z0-9+.-]*:\/\/)/i)?.[1] ?? "";
|
|
11057
|
+
return `${scheme}[redacted]`;
|
|
11058
|
+
}).replace(/\b10(?:\.\d{1,3}){3}\b/g, "[redacted-ip]").replace(/\b172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2}\b/g, "[redacted-ip]").replace(/\b192\.168(?:\.\d{1,3}){2}\b/g, "[redacted-ip]").replace(/\b100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7])(?:\.\d{1,3}){2}\b/g, "[redacted-ip]").replace(/\b(password|passwd|token|secret|api[_-]?key)=([^&\s]+)/gi, "$1=[redacted]"));
|
|
11059
|
+
}
|
|
11060
|
+
function sanitizeValue(value, privateMetadata) {
|
|
11061
|
+
if (typeof value === "string")
|
|
11062
|
+
return sanitizePublicString(value, privateMetadata);
|
|
11063
|
+
if (Array.isArray(value))
|
|
11064
|
+
return value.map((entry) => sanitizeValue(entry, privateMetadata));
|
|
11065
|
+
if (value && typeof value === "object")
|
|
11066
|
+
return sanitizeRecord(value, privateMetadata);
|
|
11067
|
+
return value;
|
|
11068
|
+
}
|
|
11069
|
+
function sanitizeRecord(value, privateMetadata) {
|
|
11070
|
+
const sanitized = {};
|
|
11071
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
11072
|
+
if (!privateMetadata && /(hostname|hostName|user|username|serial|dnsName|ip|ips|databaseUrl|url|token|secret|password|credential)/i.test(key)) {
|
|
11073
|
+
sanitized[key] = "[redacted]";
|
|
11074
|
+
continue;
|
|
11075
|
+
}
|
|
11076
|
+
sanitized[key] = sanitizeValue(entry, privateMetadata);
|
|
11077
|
+
}
|
|
11078
|
+
return sanitized;
|
|
11079
|
+
}
|
|
11080
|
+
function sanitizeStorageError(message, privateMetadata) {
|
|
11081
|
+
return privateMetadata ? message : redactErrorMessage(message);
|
|
11082
|
+
}
|
|
11083
|
+
function getAgentStatus(machineId, options = {}) {
|
|
11084
|
+
return listHeartbeats(machineId).map((heartbeat) => heartbeatToStatus(heartbeat, options));
|
|
11085
|
+
}
|
|
11086
|
+
|
|
11087
|
+
// src/commands/serve.ts
|
|
10369
11088
|
function escapeHtml(value) {
|
|
10370
11089
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
10371
11090
|
}
|
|
@@ -10380,6 +11099,9 @@ function getServeInfo(options = {}) {
|
|
|
10380
11099
|
"/",
|
|
10381
11100
|
"/health",
|
|
10382
11101
|
"/api/status",
|
|
11102
|
+
"/api/topology",
|
|
11103
|
+
"/api/routes",
|
|
11104
|
+
"/api/daemon/status",
|
|
10383
11105
|
"/api/manifest",
|
|
10384
11106
|
"/api/notifications",
|
|
10385
11107
|
"/api/webhooks",
|
|
@@ -10397,6 +11119,7 @@ function getServeInfo(options = {}) {
|
|
|
10397
11119
|
}
|
|
10398
11120
|
function renderDashboardHtml() {
|
|
10399
11121
|
const status = getStatus();
|
|
11122
|
+
const topology = discoverMachineTopology();
|
|
10400
11123
|
const manifest = manifestList();
|
|
10401
11124
|
const notifications = listNotificationChannels();
|
|
10402
11125
|
const doctor = runDoctor();
|
|
@@ -10435,17 +11158,20 @@ function renderDashboardHtml() {
|
|
|
10435
11158
|
<section class="card"><div>Heartbeats</div><div class="stat">${status.heartbeatCount}</div></section>
|
|
10436
11159
|
<section class="card"><div>Notification channels</div><div class="stat">${notifications.channels.length}</div></section>
|
|
10437
11160
|
<section class="card"><div>Doctor warnings</div><div class="stat">${doctor.checks.filter((entry) => entry.status !== "ok").length}</div></section>
|
|
11161
|
+
<section class="card"><div>Tailscale routes</div><div class="stat">${topology.machines.filter((machine) => machine.ssh.route === "tailscale").length}</div></section>
|
|
10438
11162
|
</div>
|
|
10439
11163
|
|
|
10440
11164
|
<section class="card" style="margin-top:16px">
|
|
10441
11165
|
<h2>Machines</h2>
|
|
10442
11166
|
<table>
|
|
10443
|
-
<thead><tr><th>ID</th><th>Platform</th><th>Status</th><th>Last heartbeat</th></tr></thead>
|
|
11167
|
+
<thead><tr><th>ID</th><th>Platform</th><th>Status</th><th>Agent</th><th>Storage</th><th>Last heartbeat</th></tr></thead>
|
|
10444
11168
|
<tbody>
|
|
10445
11169
|
${status.machines.map((machine) => `<tr>
|
|
10446
11170
|
<td><code>${escapeHtml(machine.machineId)}</code></td>
|
|
10447
11171
|
<td>${escapeHtml(machine.platform || "unknown")}</td>
|
|
10448
11172
|
<td><span class="badge ${escapeHtml(machine.heartbeatStatus)}">${escapeHtml(machine.heartbeatStatus)}</span></td>
|
|
11173
|
+
<td>${escapeHtml(machine.agentMode || "unknown")} ${escapeHtml(machine.daemonVersion || "")}</td>
|
|
11174
|
+
<td>${escapeHtml(machine.storageSyncStatus || "unknown")}</td>
|
|
10449
11175
|
<td>${escapeHtml(machine.lastHeartbeatAt || "\u2014")}</td>
|
|
10450
11176
|
</tr>`).join("")}
|
|
10451
11177
|
</tbody>
|
|
@@ -10489,6 +11215,14 @@ function renderDashboardHtml() {
|
|
|
10489
11215
|
<script>
|
|
10490
11216
|
// Auto-refresh dashboard data every 15s
|
|
10491
11217
|
const REFRESH_INTERVAL = 15000;
|
|
11218
|
+
function escapeHtml(value) {
|
|
11219
|
+
return String(value ?? "")
|
|
11220
|
+
.replaceAll("&", "&")
|
|
11221
|
+
.replaceAll("<", "<")
|
|
11222
|
+
.replaceAll(">", ">")
|
|
11223
|
+
.replaceAll('"', """)
|
|
11224
|
+
.replaceAll("'", "'");
|
|
11225
|
+
}
|
|
10492
11226
|
async function refreshData() {
|
|
10493
11227
|
try {
|
|
10494
11228
|
const [statusRes, doctorRes] = await Promise.all([
|
|
@@ -10509,10 +11243,12 @@ function renderDashboardHtml() {
|
|
|
10509
11243
|
tbody.innerHTML = status.machines
|
|
10510
11244
|
.map((m) =>
|
|
10511
11245
|
"<tr>" +
|
|
10512
|
-
"<td><code>" + m.machineId + "</code></td>" +
|
|
10513
|
-
"<td>" + (m.platform || "unknown") + "</td>" +
|
|
10514
|
-
'<td><span class="badge ' + m.heartbeatStatus + '">' + m.heartbeatStatus + '</span></td>' +
|
|
10515
|
-
"<td>" + (m.
|
|
11246
|
+
"<td><code>" + escapeHtml(m.machineId) + "</code></td>" +
|
|
11247
|
+
"<td>" + escapeHtml(m.platform || "unknown") + "</td>" +
|
|
11248
|
+
'<td><span class="badge ' + escapeHtml(m.heartbeatStatus) + '">' + escapeHtml(m.heartbeatStatus) + '</span></td>' +
|
|
11249
|
+
"<td>" + escapeHtml(m.agentMode || "unknown") + " " + escapeHtml(m.daemonVersion || "") + "</td>" +
|
|
11250
|
+
"<td>" + escapeHtml(m.storageSyncStatus || "unknown") + "</td>" +
|
|
11251
|
+
"<td>" + escapeHtml(m.lastHeartbeatAt || "\\u2014") + "</td>" +
|
|
10516
11252
|
"</tr>"
|
|
10517
11253
|
)
|
|
10518
11254
|
.join("");
|
|
@@ -10524,9 +11260,9 @@ function renderDashboardHtml() {
|
|
|
10524
11260
|
doctorTbody.innerHTML = doctor.checks
|
|
10525
11261
|
.map((c) =>
|
|
10526
11262
|
"<tr>" +
|
|
10527
|
-
"<td>" + c.summary + "</td>" +
|
|
10528
|
-
'<td><span class="badge ' + c.status + '">' + c.status + '</span></td>' +
|
|
10529
|
-
'<td class="muted">' + c.detail + "</td>" +
|
|
11263
|
+
"<td>" + escapeHtml(c.summary) + "</td>" +
|
|
11264
|
+
'<td><span class="badge ' + escapeHtml(c.status) + '">' + escapeHtml(c.status) + '</span></td>' +
|
|
11265
|
+
'<td class="muted">' + escapeHtml(c.detail) + "</td>" +
|
|
10530
11266
|
"</tr>"
|
|
10531
11267
|
)
|
|
10532
11268
|
.join("");
|
|
@@ -10556,6 +11292,14 @@ async function parseJsonBody(request) {
|
|
|
10556
11292
|
function jsonError(message, status = 400) {
|
|
10557
11293
|
return Response.json({ error: message }, { status });
|
|
10558
11294
|
}
|
|
11295
|
+
function privateOutputWarnings(requested, allowed) {
|
|
11296
|
+
return requested && !allowed ? [PRIVATE_OUTPUT_DENIED_WARNING] : [];
|
|
11297
|
+
}
|
|
11298
|
+
function appendWarnings(payload, warnings) {
|
|
11299
|
+
if (warnings.length === 0)
|
|
11300
|
+
return payload;
|
|
11301
|
+
return { ...payload, warnings: [...payload.warnings ?? [], ...warnings] };
|
|
11302
|
+
}
|
|
10559
11303
|
function startDashboardServer(options = {}) {
|
|
10560
11304
|
const info = getServeInfo(options);
|
|
10561
11305
|
const events = new EventsClient2;
|
|
@@ -10566,12 +11310,34 @@ function startDashboardServer(options = {}) {
|
|
|
10566
11310
|
const url = new URL(request.url);
|
|
10567
11311
|
const machineId = url.searchParams.get("machine") || undefined;
|
|
10568
11312
|
const tools = url.searchParams.get("tools")?.split(",").map((value) => value.trim()).filter(Boolean);
|
|
11313
|
+
const privateMetadataRequested = url.searchParams.get("privateMetadata") === "true" || url.searchParams.get("private_metadata") === "true";
|
|
11314
|
+
const privateMetadata = privateMetadataRequested && isPrivateOutputEnabled();
|
|
11315
|
+
const privateWarnings = privateOutputWarnings(privateMetadataRequested, privateMetadata);
|
|
10569
11316
|
if (url.pathname === "/health") {
|
|
10570
11317
|
return Response.json({ ok: true, ...getServeInfo(options) });
|
|
10571
11318
|
}
|
|
10572
11319
|
if (url.pathname === "/api/status") {
|
|
10573
11320
|
return Response.json(getStatus());
|
|
10574
11321
|
}
|
|
11322
|
+
if (url.pathname === "/api/topology") {
|
|
11323
|
+
const topology = discoverMachineTopology({ includeTailscale: url.searchParams.get("tailscale") !== "false" });
|
|
11324
|
+
return Response.json(appendWarnings(redactTopologyForOutput(topology, { privateMetadata }), privateWarnings));
|
|
11325
|
+
}
|
|
11326
|
+
if (url.pathname === "/api/routes") {
|
|
11327
|
+
const topology = discoverMachineTopology({ includeTailscale: url.searchParams.get("tailscale") !== "false" });
|
|
11328
|
+
return Response.json({
|
|
11329
|
+
generated_at: topology.generated_at,
|
|
11330
|
+
routes: topology.machines.map((machine) => redactRouteForOutput(resolveMachineRoute(machine.machine_id, { topology }), { privateMetadata })),
|
|
11331
|
+
...privateWarnings.length > 0 ? { warnings: privateWarnings } : {}
|
|
11332
|
+
});
|
|
11333
|
+
}
|
|
11334
|
+
if (url.pathname === "/api/daemon/status") {
|
|
11335
|
+
return Response.json({
|
|
11336
|
+
generated_at: new Date().toISOString(),
|
|
11337
|
+
agents: getAgentStatus(machineId, { privateMetadata }),
|
|
11338
|
+
...privateWarnings.length > 0 ? { warnings: privateWarnings } : {}
|
|
11339
|
+
});
|
|
11340
|
+
}
|
|
10575
11341
|
if (url.pathname === "/api/manifest") {
|
|
10576
11342
|
return Response.json(manifestList());
|
|
10577
11343
|
}
|
|
@@ -10715,7 +11481,7 @@ function runSelfTest() {
|
|
|
10715
11481
|
// src/commands/clipboard.ts
|
|
10716
11482
|
init_paths();
|
|
10717
11483
|
import { createHash } from "crypto";
|
|
10718
|
-
import { existsSync as existsSync8, readFileSync as readFileSync6, rmSync, writeFileSync as
|
|
11484
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync5 } from "fs";
|
|
10719
11485
|
import { join as join6 } from "path";
|
|
10720
11486
|
var DEFAULT_CONFIG = {
|
|
10721
11487
|
version: 1,
|
|
@@ -10755,7 +11521,7 @@ function readConfig(configPath) {
|
|
|
10755
11521
|
function writeConfig(config, configPath) {
|
|
10756
11522
|
const path = resolveConfigPath(configPath);
|
|
10757
11523
|
ensureParentDir(path);
|
|
10758
|
-
|
|
11524
|
+
writeFileSync5(path, `${JSON.stringify(config, null, 2)}
|
|
10759
11525
|
`, "utf8");
|
|
10760
11526
|
}
|
|
10761
11527
|
function readHistory(historyPath) {
|
|
@@ -10772,7 +11538,7 @@ function readHistory(historyPath) {
|
|
|
10772
11538
|
function writeHistory(entries, historyPath) {
|
|
10773
11539
|
const path = resolveHistoryPath(historyPath);
|
|
10774
11540
|
ensureParentDir(path);
|
|
10775
|
-
|
|
11541
|
+
writeFileSync5(path, `${JSON.stringify(entries, null, 2)}
|
|
10776
11542
|
`, "utf8");
|
|
10777
11543
|
}
|
|
10778
11544
|
function computeHash(content) {
|
|
@@ -10798,7 +11564,7 @@ function getOrCreateClipboardKey() {
|
|
|
10798
11564
|
}
|
|
10799
11565
|
const key = createHash("sha256").update(crypto.randomUUID()).digest("hex").slice(0, 32);
|
|
10800
11566
|
ensureParentDir(keyPath);
|
|
10801
|
-
|
|
11567
|
+
writeFileSync5(keyPath, `${key}
|
|
10802
11568
|
`, "utf8");
|
|
10803
11569
|
return key;
|
|
10804
11570
|
}
|
|
@@ -10849,7 +11615,7 @@ function getClipboardStatus(historyPath) {
|
|
|
10849
11615
|
|
|
10850
11616
|
// src/commands/clipboard-daemon.ts
|
|
10851
11617
|
init_paths();
|
|
10852
|
-
import { readFileSync as readFileSync8, writeFileSync as
|
|
11618
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
|
|
10853
11619
|
import { join as join7 } from "path";
|
|
10854
11620
|
import { createHash as createHash3 } from "crypto";
|
|
10855
11621
|
|
|
@@ -10859,12 +11625,12 @@ import { createServer } from "http";
|
|
|
10859
11625
|
import { createHash as createHash2 } from "crypto";
|
|
10860
11626
|
import { readFileSync as readFileSync7 } from "fs";
|
|
10861
11627
|
function readLocalClipboardSync() {
|
|
10862
|
-
const
|
|
10863
|
-
if (
|
|
11628
|
+
const platform5 = process.platform;
|
|
11629
|
+
if (platform5 === "darwin") {
|
|
10864
11630
|
const result = Bun.spawnSync(["pbpaste"], { stdout: "pipe", stderr: "pipe" });
|
|
10865
11631
|
return result.exitCode === 0 ? result.stdout.toString("utf8").trim() : "";
|
|
10866
11632
|
}
|
|
10867
|
-
if (
|
|
11633
|
+
if (platform5 === "linux") {
|
|
10868
11634
|
if (hasCommand3("wl-paste")) {
|
|
10869
11635
|
const result = Bun.spawnSync(["wl-paste"], { stdout: "pipe", stderr: "pipe" });
|
|
10870
11636
|
return result.exitCode === 0 ? result.stdout.toString("utf8").trim() : "";
|
|
@@ -10878,12 +11644,12 @@ function readLocalClipboardSync() {
|
|
|
10878
11644
|
return "";
|
|
10879
11645
|
}
|
|
10880
11646
|
function writeLocalClipboardSync(content) {
|
|
10881
|
-
const
|
|
10882
|
-
if (
|
|
11647
|
+
const platform5 = process.platform;
|
|
11648
|
+
if (platform5 === "darwin") {
|
|
10883
11649
|
const result = Bun.spawnSync(["pbcopy"], { stdin: new TextEncoder().encode(content), stdout: "ignore", stderr: "ignore" });
|
|
10884
11650
|
return result.exitCode === 0;
|
|
10885
11651
|
}
|
|
10886
|
-
if (
|
|
11652
|
+
if (platform5 === "linux") {
|
|
10887
11653
|
if (hasCommand3("wl-copy")) {
|
|
10888
11654
|
const result = Bun.spawnSync(["wl-copy"], { stdin: new TextEncoder().encode(content), stdout: "ignore", stderr: "ignore" });
|
|
10889
11655
|
return result.exitCode === 0;
|
|
@@ -11010,12 +11776,12 @@ function handleGetClipboard(response, config) {
|
|
|
11010
11776
|
// src/commands/clipboard-daemon.ts
|
|
11011
11777
|
var DAEMON_PID_PATH = join7(getDataDir(), "clipboard-daemon.pid");
|
|
11012
11778
|
function readLocalClipboardSync2() {
|
|
11013
|
-
const
|
|
11014
|
-
if (
|
|
11779
|
+
const platform5 = process.platform;
|
|
11780
|
+
if (platform5 === "darwin") {
|
|
11015
11781
|
const result = Bun.spawnSync(["pbpaste"], { stdout: "pipe", stderr: "pipe" });
|
|
11016
11782
|
return result.exitCode === 0 ? result.stdout.toString("utf8").trim() : "";
|
|
11017
11783
|
}
|
|
11018
|
-
if (
|
|
11784
|
+
if (platform5 === "linux") {
|
|
11019
11785
|
if (hasCommand4("wl-paste")) {
|
|
11020
11786
|
const result = Bun.spawnSync(["wl-paste"], { stdout: "pipe", stderr: "pipe" });
|
|
11021
11787
|
return result.exitCode === 0 ? result.stdout.toString("utf8").trim() : "";
|
|
@@ -11076,7 +11842,7 @@ function loadSharedSecret2() {
|
|
|
11076
11842
|
}
|
|
11077
11843
|
}
|
|
11078
11844
|
function writePid(pid) {
|
|
11079
|
-
|
|
11845
|
+
writeFileSync6(DAEMON_PID_PATH, `${pid}
|
|
11080
11846
|
`);
|
|
11081
11847
|
}
|
|
11082
11848
|
function readPid() {
|
|
@@ -11171,7 +11937,7 @@ async function discoverPeers() {
|
|
|
11171
11937
|
}
|
|
11172
11938
|
}
|
|
11173
11939
|
} catch {}
|
|
11174
|
-
const knownPeers = ["
|
|
11940
|
+
const knownPeers = (process.env["HASNA_MACHINES_CLIPBOARD_PEERS"] || "").split(",").map((peer) => peer.trim()).filter(Boolean);
|
|
11175
11941
|
for (const ip of knownPeers) {
|
|
11176
11942
|
if (!peers.some((p) => p.host === ip)) {
|
|
11177
11943
|
peers.push({ host: ip, port: config.port });
|
|
@@ -11182,7 +11948,7 @@ async function discoverPeers() {
|
|
|
11182
11948
|
|
|
11183
11949
|
// src/commands/heal.ts
|
|
11184
11950
|
init_paths();
|
|
11185
|
-
import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as
|
|
11951
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
|
|
11186
11952
|
import { join as join8 } from "path";
|
|
11187
11953
|
var DEFAULT_THRESHOLDS = {
|
|
11188
11954
|
reconnect: 3,
|
|
@@ -11247,7 +12013,7 @@ function readHealConfig(path) {
|
|
|
11247
12013
|
function writeHealConfig(config, path) {
|
|
11248
12014
|
const p = path || getHealConfigPath();
|
|
11249
12015
|
ensureParentDir(p);
|
|
11250
|
-
|
|
12016
|
+
writeFileSync7(p, `${JSON.stringify(config, null, 2)}
|
|
11251
12017
|
`, "utf8");
|
|
11252
12018
|
}
|
|
11253
12019
|
function readHealState(path) {
|
|
@@ -11263,7 +12029,7 @@ function readHealState(path) {
|
|
|
11263
12029
|
function writeHealState(state, path) {
|
|
11264
12030
|
const p = path || getHealStatePath();
|
|
11265
12031
|
ensureParentDir(p);
|
|
11266
|
-
|
|
12032
|
+
writeFileSync7(p, `${JSON.stringify(state, null, 2)}
|
|
11267
12033
|
`, "utf8");
|
|
11268
12034
|
}
|
|
11269
12035
|
function evaluateHealth(probe, config, state) {
|
|
@@ -11468,7 +12234,7 @@ function executeAction(action, config) {
|
|
|
11468
12234
|
|
|
11469
12235
|
// src/commands/heal-daemon.ts
|
|
11470
12236
|
init_paths();
|
|
11471
|
-
import { existsSync as existsSync10, readFileSync as readFileSync10, writeFileSync as
|
|
12237
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
|
|
11472
12238
|
import { join as join9 } from "path";
|
|
11473
12239
|
var DAEMON_PID_PATH2 = join9(getDataDir(), "heal-daemon.pid");
|
|
11474
12240
|
var SERVICE_PATH = "/etc/systemd/system/machines-heal.service";
|
|
@@ -11511,7 +12277,7 @@ function runHealOnce(config, opts = {}) {
|
|
|
11511
12277
|
return result;
|
|
11512
12278
|
}
|
|
11513
12279
|
function writePid2(pid) {
|
|
11514
|
-
|
|
12280
|
+
writeFileSync8(DAEMON_PID_PATH2, `${pid}
|
|
11515
12281
|
`);
|
|
11516
12282
|
}
|
|
11517
12283
|
function readPid2() {
|
|
@@ -11602,7 +12368,7 @@ ${key}=${value}
|
|
|
11602
12368
|
};
|
|
11603
12369
|
set("RuntimeWatchdogSec", "20s");
|
|
11604
12370
|
set("RebootWatchdogSec", "2min");
|
|
11605
|
-
|
|
12371
|
+
writeFileSync8(SYSTEM_CONF, conf);
|
|
11606
12372
|
sh2("systemctl daemon-reexec");
|
|
11607
12373
|
log2.push("hardware watchdog: RuntimeWatchdogSec=20s RebootWatchdogSec=2min");
|
|
11608
12374
|
return log2;
|
|
@@ -11647,7 +12413,7 @@ Environment=PATH=${binDir}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sb
|
|
|
11647
12413
|
[Install]
|
|
11648
12414
|
WantedBy=multi-user.target
|
|
11649
12415
|
`;
|
|
11650
|
-
|
|
12416
|
+
writeFileSync8(SERVICE_PATH, unit);
|
|
11651
12417
|
sh2("systemctl daemon-reload");
|
|
11652
12418
|
sh2("systemctl enable --now machines-heal.service");
|
|
11653
12419
|
log2.push(`installed + enabled ${SERVICE_PATH} (ExecStart=${exec} heal daemon)`);
|
|
@@ -11828,18 +12594,18 @@ function checkSecretPresence(secretsCommand, key) {
|
|
|
11828
12594
|
};
|
|
11829
12595
|
}
|
|
11830
12596
|
function parseCommandSpec(value) {
|
|
11831
|
-
const [
|
|
12597
|
+
const [command2, expectedVersion] = value.split(":");
|
|
11832
12598
|
return {
|
|
11833
|
-
command,
|
|
12599
|
+
command: command2,
|
|
11834
12600
|
expectedVersion: expectedVersion || undefined,
|
|
11835
12601
|
required: true
|
|
11836
12602
|
};
|
|
11837
12603
|
}
|
|
11838
12604
|
function parsePackageSpec(value) {
|
|
11839
|
-
const [name,
|
|
12605
|
+
const [name, command2, expectedVersion] = value.split(":");
|
|
11840
12606
|
return {
|
|
11841
12607
|
name,
|
|
11842
|
-
command:
|
|
12608
|
+
command: command2 || undefined,
|
|
11843
12609
|
expectedVersion: expectedVersion || undefined,
|
|
11844
12610
|
required: true
|
|
11845
12611
|
};
|
|
@@ -11930,17 +12696,54 @@ function renderFleetStatus(status) {
|
|
|
11930
12696
|
["sync runs", String(status.recentSyncRuns)]
|
|
11931
12697
|
]),
|
|
11932
12698
|
"",
|
|
11933
|
-
...status.machines.map((machine) => `${machine.machineId.padEnd(18)} ${machine.platform || "unknown"} ${machine.heartbeatStatus} ${machine.lastHeartbeatAt || "\u2014"}`)
|
|
12699
|
+
...status.machines.map((machine) => `${machine.machineId.padEnd(18)} ${machine.platform || "unknown"} ${machine.heartbeatStatus} ${machine.agentMode || "agent:unknown"} ${machine.storageSyncStatus || "storage:unknown"} ${machine.lastHeartbeatAt || "\u2014"}`)
|
|
12700
|
+
].join(`
|
|
12701
|
+
`);
|
|
12702
|
+
}
|
|
12703
|
+
function renderShellCommand(command2) {
|
|
12704
|
+
const parts = command2.sudo ? ["sudo", command2.program, ...command2.args] : [command2.program, ...command2.args];
|
|
12705
|
+
return parts.map((part) => /^[A-Za-z0-9_@%+=:,./$-]+$/.test(part) ? part : JSON.stringify(part)).join(" ");
|
|
12706
|
+
}
|
|
12707
|
+
function renderDaemonPlan(plan) {
|
|
12708
|
+
const files = plan.files.map((file) => `${file.path} (${file.mode})`);
|
|
12709
|
+
const commands = plan.commands.map((command2) => `${command2.mutates ? "apply" : "read"} ${command2.id}: ${renderShellCommand(command2)}`);
|
|
12710
|
+
return [
|
|
12711
|
+
renderKeyValueTable([
|
|
12712
|
+
["action", plan.action],
|
|
12713
|
+
["platform", plan.platform],
|
|
12714
|
+
["mode", plan.mode],
|
|
12715
|
+
["service", plan.serviceName],
|
|
12716
|
+
["executable", plan.executable],
|
|
12717
|
+
["interval", `${plan.intervalMs}ms`],
|
|
12718
|
+
["warnings", plan.warnings.join(", ") || "none"]
|
|
12719
|
+
]),
|
|
12720
|
+
renderList("files", files),
|
|
12721
|
+
renderList("commands", commands),
|
|
12722
|
+
renderList("manual steps", plan.manualSteps)
|
|
11934
12723
|
].join(`
|
|
11935
12724
|
`);
|
|
11936
12725
|
}
|
|
12726
|
+
function parseDaemonOptions(action, options) {
|
|
12727
|
+
return {
|
|
12728
|
+
action,
|
|
12729
|
+
platform: options.platform,
|
|
12730
|
+
mode: options.mode,
|
|
12731
|
+
serviceName: options.serviceName,
|
|
12732
|
+
executable: options.executable,
|
|
12733
|
+
intervalMs: options.intervalMs ? parseIntegerOption(options.intervalMs, "interval-ms", { min: 1 }) : undefined,
|
|
12734
|
+
storagePush: options.storagePush,
|
|
12735
|
+
doctorSummary: options.doctorSummary,
|
|
12736
|
+
privateMetadata: options.privateMetadata,
|
|
12737
|
+
env: options.env
|
|
12738
|
+
};
|
|
12739
|
+
}
|
|
11937
12740
|
program2.name("machines").description("Machine fleet management CLI + MCP for developers").version(getPackageVersion()).option("-q, --quiet", "Suppress non-essential output");
|
|
11938
12741
|
var manifestCommand = program2.command("manifest").description("Manage the fleet manifest");
|
|
11939
12742
|
var appsCommand = program2.command("apps").description("Manage installed applications per machine");
|
|
11940
12743
|
var notificationsCommand = program2.command("notifications").description("Manage fleet alert delivery channels");
|
|
11941
12744
|
var eventWebhooksCommand = registerWebhookCommands(program2, { source: "machines" });
|
|
11942
12745
|
eventWebhooksCommand.description("Manage shared event webhook subscriptions");
|
|
11943
|
-
var webhookTestCommand = eventWebhooksCommand.commands.find((
|
|
12746
|
+
var webhookTestCommand = eventWebhooksCommand.commands.find((command2) => command2.name() === "test");
|
|
11944
12747
|
var webhookOptions = webhookTestCommand?.options ?? [];
|
|
11945
12748
|
var webhookMessageOption = webhookOptions.find((option) => option.long === "--message");
|
|
11946
12749
|
if (webhookMessageOption) {
|
|
@@ -11951,6 +12754,23 @@ eventsCommand.description("Emit, list, and replay shared events");
|
|
|
11951
12754
|
var runtimeCommand = program2.command("runtime").description("Watch runtime conditions and emit shared events");
|
|
11952
12755
|
var clipboardCommand = program2.command("clipboard").description("Real-time clipboard sync across fleet machines");
|
|
11953
12756
|
var installClaudeCommand = program2.command("install-claude").description("Install or inspect Claude, Codex, and Gemini CLIs");
|
|
12757
|
+
var daemonCommand = program2.command("daemon").description("Install and inspect the machines-agent fleet daemon service");
|
|
12758
|
+
function addDaemonLifecycleCommand(action, description) {
|
|
12759
|
+
daemonCommand.command(action).description(description).option("--platform <platform>", "Service platform to plan for (macos, linux)").option("--mode <mode>", "Service mode (user, system)", "user").option("--service-name <name>", "Service name/label", "machines-agent").option("--executable <path>", "Absolute machines-agent executable path").option("--interval-ms <ms>", "Heartbeat interval in milliseconds").option("--storage-push", "Configure daemon to push heartbeat rows to storage", false).option("--doctor-summary", "Configure daemon to include lightweight doctor summaries", false).option("--private-metadata", "Opt in to private host/network metadata in heartbeat rows", false).option("--env <name...>", "Environment variable names to include as placeholders").option("--apply", "Write service files and run planned commands", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action((options) => {
|
|
12760
|
+
const plan = buildDaemonServicePlan(parseDaemonOptions(action, options));
|
|
12761
|
+
const result = runDaemonServicePlan(plan, { apply: options.apply, yes: options.yes });
|
|
12762
|
+
if (options.json || options.apply) {
|
|
12763
|
+
console.log(JSON.stringify(result, null, 2));
|
|
12764
|
+
return;
|
|
12765
|
+
}
|
|
12766
|
+
console.log(renderDaemonPlan(plan));
|
|
12767
|
+
});
|
|
12768
|
+
}
|
|
12769
|
+
addDaemonLifecycleCommand("install", "Plan or install the machines-agent daemon service");
|
|
12770
|
+
addDaemonLifecycleCommand("uninstall", "Plan or uninstall the machines-agent daemon service");
|
|
12771
|
+
addDaemonLifecycleCommand("restart", "Plan or restart the machines-agent daemon service");
|
|
12772
|
+
addDaemonLifecycleCommand("status", "Plan a daemon service status command");
|
|
12773
|
+
addDaemonLifecycleCommand("logs", "Plan a daemon service log command");
|
|
11954
12774
|
manifestCommand.command("init").description("Create an empty fleet manifest").action(() => {
|
|
11955
12775
|
console.log(manifestInit());
|
|
11956
12776
|
});
|
|
@@ -12055,8 +12875,9 @@ program2.command("sync").description("Reconcile a machine against the fleet mani
|
|
|
12055
12875
|
const result = options.apply ? runSync(options.machine, { apply: true, yes: options.yes }) : buildSyncPlan(options.machine);
|
|
12056
12876
|
console.log(JSON.stringify(result, null, 2));
|
|
12057
12877
|
});
|
|
12058
|
-
program2.command("topology").description("Discover local, manifest, heartbeat, SSH, and Tailscale machine topology").option("--no-tailscale", "Skip tailscale status probing").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
12059
|
-
const
|
|
12878
|
+
program2.command("topology").description("Discover local, manifest, heartbeat, SSH, and Tailscale machine topology").option("--no-tailscale", "Skip tailscale status probing").option("--private-metadata", "Print private host/network route fields", false).option("-j, --json", "Print JSON output", false).action((options) => {
|
|
12879
|
+
const rawTopology = discoverMachineTopology({ includeTailscale: options.tailscale !== false });
|
|
12880
|
+
const topology = redactTopologyForOutput(rawTopology, { privateMetadata: options.privateMetadata });
|
|
12060
12881
|
if (options.json) {
|
|
12061
12882
|
console.log(JSON.stringify(topology, null, 2));
|
|
12062
12883
|
return;
|
|
@@ -12296,11 +13117,12 @@ program2.command("install-tailscale").description("Install Tailscale on a machin
|
|
|
12296
13117
|
const result = options.apply ? runTailscaleInstall(options.machine, { apply: true, yes: options.yes }) : buildTailscaleInstallPlan(options.machine);
|
|
12297
13118
|
console.log(JSON.stringify(result, null, 2));
|
|
12298
13119
|
});
|
|
12299
|
-
program2.command("route").description("Resolve the best route for a machine").requiredOption("--machine <id>", "Machine identifier").option("--no-tailscale", "Skip tailscale status probing").option("--cmd <command>", "Remote command to run").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
13120
|
+
program2.command("route").description("Resolve the best route for a machine").requiredOption("--machine <id>", "Machine identifier").option("--no-tailscale", "Skip tailscale status probing").option("--private-metadata", "Print private route targets", false).option("--cmd <command>", "Remote command to run").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
12300
13121
|
const topology = discoverMachineTopology({ includeTailscale: options.tailscale !== false });
|
|
12301
13122
|
const resolved = resolveMachineRoute(options.machine, { topology });
|
|
12302
|
-
const
|
|
12303
|
-
const
|
|
13123
|
+
const publicResolved = redactRouteForOutput(resolved, { privateMetadata: options.privateMetadata });
|
|
13124
|
+
const command2 = resolved.ok && resolved.target ? resolved.route === "local" ? options.cmd ?? null : buildSshCommand(options.machine, options.cmd, { topology }) : null;
|
|
13125
|
+
const payload = { ...publicResolved, command: options.privateMetadata ? command2 : command2 ? "[redacted]" : null };
|
|
12304
13126
|
if (options.json) {
|
|
12305
13127
|
console.log(JSON.stringify(payload, null, 2));
|
|
12306
13128
|
return;
|
|
@@ -12310,7 +13132,7 @@ program2.command("route").description("Resolve the best route for a machine").re
|
|
|
12310
13132
|
process.exitCode = 1;
|
|
12311
13133
|
return;
|
|
12312
13134
|
}
|
|
12313
|
-
console.log(
|
|
13135
|
+
console.log(options.privateMetadata ? command2 ?? `${resolved.route}:${resolved.target}` : `${publicResolved.route}:${publicResolved.target ?? "unresolved"}`);
|
|
12314
13136
|
});
|
|
12315
13137
|
program2.command("ssh").description("Choose the best SSH route for a machine").requiredOption("--machine <id>", "Machine identifier").option("--cmd <command>", "Remote command to run").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
12316
13138
|
if (options.json) {
|
|
@@ -12338,7 +13160,7 @@ program2.command("screen").description("Open Screen Sharing (VNC) to a machine u
|
|
|
12338
13160
|
for (const r of results) {
|
|
12339
13161
|
if (r.ok && r.url) {
|
|
12340
13162
|
if (!options.print)
|
|
12341
|
-
|
|
13163
|
+
execFileSync2("open", [r.url], { stdio: "ignore" });
|
|
12342
13164
|
console.log(`${r.ok ? "\u2713" : "\u2717"} ${r.machine.padEnd(14)} ${r.url ?? r.error}`);
|
|
12343
13165
|
} else {
|
|
12344
13166
|
console.log(`\u2717 ${r.machine.padEnd(14)} ${r.error}`);
|
|
@@ -12361,7 +13183,7 @@ program2.command("screen").description("Open Screen Sharing (VNC) to a machine u
|
|
|
12361
13183
|
console.log(resolved.url);
|
|
12362
13184
|
return;
|
|
12363
13185
|
}
|
|
12364
|
-
|
|
13186
|
+
execFileSync2("open", [resolved.url], { stdio: "ignore" });
|
|
12365
13187
|
console.log(`Opening Screen Sharing \u2192 ${resolved.url} (route: ${resolved.route})`);
|
|
12366
13188
|
});
|
|
12367
13189
|
program2.command("screen-credentials").description("Inspect screen-sharing user and password secret references without printing secrets").option("--machine <id>", "Machine identifier").option("--all", "Inspect every discovered machine", false).option("--check-secret", "Check whether the password secret exists in the local secrets vault", false).option("--secrets-command <command>", "Secrets CLI command to inspect", "secrets").option("--no-tailscale", "Skip tailscale status probing").option("-j, --json", "Print JSON output", false).action((options) => {
|