@hasna/machines 0.0.36 → 0.0.38
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 +886 -58
- package/dist/commands/daemon.d.ts +76 -0
- package/dist/commands/daemon.d.ts.map +1 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/consumer.js +225 -6
- 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 +1330 -228
- package/dist/manifests.d.ts +6 -6
- package/dist/mcp/index.js +1755 -1083
- 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;
|
|
@@ -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"),
|
|
@@ -10266,14 +10482,20 @@ function runOptionalAdapterChecks(context, adapters) {
|
|
|
10266
10482
|
}
|
|
10267
10483
|
return checks;
|
|
10268
10484
|
}
|
|
10269
|
-
function runDoctor(machineId
|
|
10485
|
+
function runDoctor(machineId, options = {}) {
|
|
10486
|
+
const implicitLocalMachine = !machineId;
|
|
10487
|
+
const requestedMachineId = machineId ?? getLocalMachineId();
|
|
10488
|
+
const reportedMachineId = implicitLocalMachine ? "local" : requestedMachineId;
|
|
10270
10489
|
const now = options.now ?? new Date;
|
|
10271
10490
|
const { manifest, info: manifestSource } = readManifestWithSource({ adapter: options.manifestAdapter ?? null });
|
|
10272
|
-
const commandChecks = runMachineCommand(
|
|
10491
|
+
const commandChecks = runMachineCommand(requestedMachineId, buildDoctorCommand());
|
|
10273
10492
|
const details = parseKeyValueOutput(commandChecks.stdout);
|
|
10274
|
-
const machineInManifest = manifest.machines.find((machine) => machine.id ===
|
|
10493
|
+
const machineInManifest = manifest.machines.find((machine) => machine.id === requestedMachineId);
|
|
10494
|
+
const diagnosticMachine = machineInManifest ? redactManifestForDiagnostics(machineInManifest) : null;
|
|
10495
|
+
if (implicitLocalMachine && diagnosticMachine)
|
|
10496
|
+
diagnosticMachine.id = reportedMachineId;
|
|
10275
10497
|
const optionalAdapterChecks = options.includeOptionalAdapters === false ? [] : runOptionalAdapterChecks({
|
|
10276
|
-
machineId,
|
|
10498
|
+
machineId: requestedMachineId,
|
|
10277
10499
|
manifest,
|
|
10278
10500
|
manifestSource,
|
|
10279
10501
|
commandDetails: details,
|
|
@@ -10289,10 +10511,10 @@ function runDoctor(machineId = getLocalMachineId(), options = {}) {
|
|
|
10289
10511
|
},
|
|
10290
10512
|
remediation: manifestSource.warnings.length > 0 ? ["Provide a private manifest adapter or unset the private manifest ref to use the local manifest only."] : undefined
|
|
10291
10513
|
}),
|
|
10292
|
-
makeCheck2("manifest-entry", machineInManifest ? "ok" : "warn", machineInManifest ? "Machine exists in manifest" : "Machine missing from manifest",
|
|
10514
|
+
makeCheck2("manifest-entry", machineInManifest ? "ok" : "warn", machineInManifest ? "Machine exists in manifest" : "Machine missing from manifest", diagnosticMachine ? JSON.stringify(diagnosticMachine) : `No manifest entry for ${reportedMachineId}`, {
|
|
10293
10515
|
data: {
|
|
10294
10516
|
declared: Boolean(machineInManifest),
|
|
10295
|
-
machine:
|
|
10517
|
+
machine: diagnosticMachine
|
|
10296
10518
|
}
|
|
10297
10519
|
}),
|
|
10298
10520
|
makeCheck2("data-dir", details["data_dir_exists"] === "yes" ? "ok" : "warn", "Data directory check", `${redactPath(details["data_dir"] || "unknown")} ${details["data_dir_exists"] === "yes" ? "exists" : "missing"}`, {
|
|
@@ -10343,7 +10565,7 @@ function runDoctor(machineId = getLocalMachineId(), options = {}) {
|
|
|
10343
10565
|
...optionalAdapterChecks
|
|
10344
10566
|
];
|
|
10345
10567
|
return {
|
|
10346
|
-
machineId,
|
|
10568
|
+
machineId: reportedMachineId,
|
|
10347
10569
|
source: commandChecks.source,
|
|
10348
10570
|
schemaVersion: 1,
|
|
10349
10571
|
generatedAt: now.toISOString(),
|
|
@@ -10355,11 +10577,514 @@ function runDoctor(machineId = getLocalMachineId(), options = {}) {
|
|
|
10355
10577
|
};
|
|
10356
10578
|
}
|
|
10357
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
|
+
|
|
10358
11001
|
// src/commands/self-test.ts
|
|
10359
11002
|
init_db();
|
|
10360
11003
|
|
|
10361
11004
|
// src/commands/serve.ts
|
|
10362
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
|
|
10363
11088
|
function escapeHtml(value) {
|
|
10364
11089
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
10365
11090
|
}
|
|
@@ -10374,6 +11099,9 @@ function getServeInfo(options = {}) {
|
|
|
10374
11099
|
"/",
|
|
10375
11100
|
"/health",
|
|
10376
11101
|
"/api/status",
|
|
11102
|
+
"/api/topology",
|
|
11103
|
+
"/api/routes",
|
|
11104
|
+
"/api/daemon/status",
|
|
10377
11105
|
"/api/manifest",
|
|
10378
11106
|
"/api/notifications",
|
|
10379
11107
|
"/api/webhooks",
|
|
@@ -10391,6 +11119,7 @@ function getServeInfo(options = {}) {
|
|
|
10391
11119
|
}
|
|
10392
11120
|
function renderDashboardHtml() {
|
|
10393
11121
|
const status = getStatus();
|
|
11122
|
+
const topology = discoverMachineTopology();
|
|
10394
11123
|
const manifest = manifestList();
|
|
10395
11124
|
const notifications = listNotificationChannels();
|
|
10396
11125
|
const doctor = runDoctor();
|
|
@@ -10429,17 +11158,20 @@ function renderDashboardHtml() {
|
|
|
10429
11158
|
<section class="card"><div>Heartbeats</div><div class="stat">${status.heartbeatCount}</div></section>
|
|
10430
11159
|
<section class="card"><div>Notification channels</div><div class="stat">${notifications.channels.length}</div></section>
|
|
10431
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>
|
|
10432
11162
|
</div>
|
|
10433
11163
|
|
|
10434
11164
|
<section class="card" style="margin-top:16px">
|
|
10435
11165
|
<h2>Machines</h2>
|
|
10436
11166
|
<table>
|
|
10437
|
-
<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>
|
|
10438
11168
|
<tbody>
|
|
10439
11169
|
${status.machines.map((machine) => `<tr>
|
|
10440
11170
|
<td><code>${escapeHtml(machine.machineId)}</code></td>
|
|
10441
11171
|
<td>${escapeHtml(machine.platform || "unknown")}</td>
|
|
10442
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>
|
|
10443
11175
|
<td>${escapeHtml(machine.lastHeartbeatAt || "\u2014")}</td>
|
|
10444
11176
|
</tr>`).join("")}
|
|
10445
11177
|
</tbody>
|
|
@@ -10483,6 +11215,14 @@ function renderDashboardHtml() {
|
|
|
10483
11215
|
<script>
|
|
10484
11216
|
// Auto-refresh dashboard data every 15s
|
|
10485
11217
|
const REFRESH_INTERVAL = 15000;
|
|
11218
|
+
function escapeHtml(value) {
|
|
11219
|
+
return String(value ?? "")
|
|
11220
|
+
.replaceAll("&", "&")
|
|
11221
|
+
.replaceAll("<", "<")
|
|
11222
|
+
.replaceAll(">", ">")
|
|
11223
|
+
.replaceAll('"', """)
|
|
11224
|
+
.replaceAll("'", "'");
|
|
11225
|
+
}
|
|
10486
11226
|
async function refreshData() {
|
|
10487
11227
|
try {
|
|
10488
11228
|
const [statusRes, doctorRes] = await Promise.all([
|
|
@@ -10503,10 +11243,12 @@ function renderDashboardHtml() {
|
|
|
10503
11243
|
tbody.innerHTML = status.machines
|
|
10504
11244
|
.map((m) =>
|
|
10505
11245
|
"<tr>" +
|
|
10506
|
-
"<td><code>" + m.machineId + "</code></td>" +
|
|
10507
|
-
"<td>" + (m.platform || "unknown") + "</td>" +
|
|
10508
|
-
'<td><span class="badge ' + m.heartbeatStatus + '">' + m.heartbeatStatus + '</span></td>' +
|
|
10509
|
-
"<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>" +
|
|
10510
11252
|
"</tr>"
|
|
10511
11253
|
)
|
|
10512
11254
|
.join("");
|
|
@@ -10518,9 +11260,9 @@ function renderDashboardHtml() {
|
|
|
10518
11260
|
doctorTbody.innerHTML = doctor.checks
|
|
10519
11261
|
.map((c) =>
|
|
10520
11262
|
"<tr>" +
|
|
10521
|
-
"<td>" + c.summary + "</td>" +
|
|
10522
|
-
'<td><span class="badge ' + c.status + '">' + c.status + '</span></td>' +
|
|
10523
|
-
'<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>" +
|
|
10524
11266
|
"</tr>"
|
|
10525
11267
|
)
|
|
10526
11268
|
.join("");
|
|
@@ -10550,6 +11292,14 @@ async function parseJsonBody(request) {
|
|
|
10550
11292
|
function jsonError(message, status = 400) {
|
|
10551
11293
|
return Response.json({ error: message }, { status });
|
|
10552
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
|
+
}
|
|
10553
11303
|
function startDashboardServer(options = {}) {
|
|
10554
11304
|
const info = getServeInfo(options);
|
|
10555
11305
|
const events = new EventsClient2;
|
|
@@ -10560,12 +11310,34 @@ function startDashboardServer(options = {}) {
|
|
|
10560
11310
|
const url = new URL(request.url);
|
|
10561
11311
|
const machineId = url.searchParams.get("machine") || undefined;
|
|
10562
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);
|
|
10563
11316
|
if (url.pathname === "/health") {
|
|
10564
11317
|
return Response.json({ ok: true, ...getServeInfo(options) });
|
|
10565
11318
|
}
|
|
10566
11319
|
if (url.pathname === "/api/status") {
|
|
10567
11320
|
return Response.json(getStatus());
|
|
10568
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
|
+
}
|
|
10569
11341
|
if (url.pathname === "/api/manifest") {
|
|
10570
11342
|
return Response.json(manifestList());
|
|
10571
11343
|
}
|
|
@@ -10709,7 +11481,7 @@ function runSelfTest() {
|
|
|
10709
11481
|
// src/commands/clipboard.ts
|
|
10710
11482
|
init_paths();
|
|
10711
11483
|
import { createHash } from "crypto";
|
|
10712
|
-
import { existsSync as existsSync8, readFileSync as readFileSync6, rmSync, writeFileSync as
|
|
11484
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync5 } from "fs";
|
|
10713
11485
|
import { join as join6 } from "path";
|
|
10714
11486
|
var DEFAULT_CONFIG = {
|
|
10715
11487
|
version: 1,
|
|
@@ -10749,7 +11521,7 @@ function readConfig(configPath) {
|
|
|
10749
11521
|
function writeConfig(config, configPath) {
|
|
10750
11522
|
const path = resolveConfigPath(configPath);
|
|
10751
11523
|
ensureParentDir(path);
|
|
10752
|
-
|
|
11524
|
+
writeFileSync5(path, `${JSON.stringify(config, null, 2)}
|
|
10753
11525
|
`, "utf8");
|
|
10754
11526
|
}
|
|
10755
11527
|
function readHistory(historyPath) {
|
|
@@ -10766,7 +11538,7 @@ function readHistory(historyPath) {
|
|
|
10766
11538
|
function writeHistory(entries, historyPath) {
|
|
10767
11539
|
const path = resolveHistoryPath(historyPath);
|
|
10768
11540
|
ensureParentDir(path);
|
|
10769
|
-
|
|
11541
|
+
writeFileSync5(path, `${JSON.stringify(entries, null, 2)}
|
|
10770
11542
|
`, "utf8");
|
|
10771
11543
|
}
|
|
10772
11544
|
function computeHash(content) {
|
|
@@ -10792,7 +11564,7 @@ function getOrCreateClipboardKey() {
|
|
|
10792
11564
|
}
|
|
10793
11565
|
const key = createHash("sha256").update(crypto.randomUUID()).digest("hex").slice(0, 32);
|
|
10794
11566
|
ensureParentDir(keyPath);
|
|
10795
|
-
|
|
11567
|
+
writeFileSync5(keyPath, `${key}
|
|
10796
11568
|
`, "utf8");
|
|
10797
11569
|
return key;
|
|
10798
11570
|
}
|
|
@@ -10843,7 +11615,7 @@ function getClipboardStatus(historyPath) {
|
|
|
10843
11615
|
|
|
10844
11616
|
// src/commands/clipboard-daemon.ts
|
|
10845
11617
|
init_paths();
|
|
10846
|
-
import { readFileSync as readFileSync8, writeFileSync as
|
|
11618
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
|
|
10847
11619
|
import { join as join7 } from "path";
|
|
10848
11620
|
import { createHash as createHash3 } from "crypto";
|
|
10849
11621
|
|
|
@@ -10853,12 +11625,12 @@ import { createServer } from "http";
|
|
|
10853
11625
|
import { createHash as createHash2 } from "crypto";
|
|
10854
11626
|
import { readFileSync as readFileSync7 } from "fs";
|
|
10855
11627
|
function readLocalClipboardSync() {
|
|
10856
|
-
const
|
|
10857
|
-
if (
|
|
11628
|
+
const platform5 = process.platform;
|
|
11629
|
+
if (platform5 === "darwin") {
|
|
10858
11630
|
const result = Bun.spawnSync(["pbpaste"], { stdout: "pipe", stderr: "pipe" });
|
|
10859
11631
|
return result.exitCode === 0 ? result.stdout.toString("utf8").trim() : "";
|
|
10860
11632
|
}
|
|
10861
|
-
if (
|
|
11633
|
+
if (platform5 === "linux") {
|
|
10862
11634
|
if (hasCommand3("wl-paste")) {
|
|
10863
11635
|
const result = Bun.spawnSync(["wl-paste"], { stdout: "pipe", stderr: "pipe" });
|
|
10864
11636
|
return result.exitCode === 0 ? result.stdout.toString("utf8").trim() : "";
|
|
@@ -10872,12 +11644,12 @@ function readLocalClipboardSync() {
|
|
|
10872
11644
|
return "";
|
|
10873
11645
|
}
|
|
10874
11646
|
function writeLocalClipboardSync(content) {
|
|
10875
|
-
const
|
|
10876
|
-
if (
|
|
11647
|
+
const platform5 = process.platform;
|
|
11648
|
+
if (platform5 === "darwin") {
|
|
10877
11649
|
const result = Bun.spawnSync(["pbcopy"], { stdin: new TextEncoder().encode(content), stdout: "ignore", stderr: "ignore" });
|
|
10878
11650
|
return result.exitCode === 0;
|
|
10879
11651
|
}
|
|
10880
|
-
if (
|
|
11652
|
+
if (platform5 === "linux") {
|
|
10881
11653
|
if (hasCommand3("wl-copy")) {
|
|
10882
11654
|
const result = Bun.spawnSync(["wl-copy"], { stdin: new TextEncoder().encode(content), stdout: "ignore", stderr: "ignore" });
|
|
10883
11655
|
return result.exitCode === 0;
|
|
@@ -11004,12 +11776,12 @@ function handleGetClipboard(response, config) {
|
|
|
11004
11776
|
// src/commands/clipboard-daemon.ts
|
|
11005
11777
|
var DAEMON_PID_PATH = join7(getDataDir(), "clipboard-daemon.pid");
|
|
11006
11778
|
function readLocalClipboardSync2() {
|
|
11007
|
-
const
|
|
11008
|
-
if (
|
|
11779
|
+
const platform5 = process.platform;
|
|
11780
|
+
if (platform5 === "darwin") {
|
|
11009
11781
|
const result = Bun.spawnSync(["pbpaste"], { stdout: "pipe", stderr: "pipe" });
|
|
11010
11782
|
return result.exitCode === 0 ? result.stdout.toString("utf8").trim() : "";
|
|
11011
11783
|
}
|
|
11012
|
-
if (
|
|
11784
|
+
if (platform5 === "linux") {
|
|
11013
11785
|
if (hasCommand4("wl-paste")) {
|
|
11014
11786
|
const result = Bun.spawnSync(["wl-paste"], { stdout: "pipe", stderr: "pipe" });
|
|
11015
11787
|
return result.exitCode === 0 ? result.stdout.toString("utf8").trim() : "";
|
|
@@ -11070,7 +11842,7 @@ function loadSharedSecret2() {
|
|
|
11070
11842
|
}
|
|
11071
11843
|
}
|
|
11072
11844
|
function writePid(pid) {
|
|
11073
|
-
|
|
11845
|
+
writeFileSync6(DAEMON_PID_PATH, `${pid}
|
|
11074
11846
|
`);
|
|
11075
11847
|
}
|
|
11076
11848
|
function readPid() {
|
|
@@ -11165,7 +11937,7 @@ async function discoverPeers() {
|
|
|
11165
11937
|
}
|
|
11166
11938
|
}
|
|
11167
11939
|
} catch {}
|
|
11168
|
-
const knownPeers = ["
|
|
11940
|
+
const knownPeers = (process.env["HASNA_MACHINES_CLIPBOARD_PEERS"] || "").split(",").map((peer) => peer.trim()).filter(Boolean);
|
|
11169
11941
|
for (const ip of knownPeers) {
|
|
11170
11942
|
if (!peers.some((p) => p.host === ip)) {
|
|
11171
11943
|
peers.push({ host: ip, port: config.port });
|
|
@@ -11176,7 +11948,7 @@ async function discoverPeers() {
|
|
|
11176
11948
|
|
|
11177
11949
|
// src/commands/heal.ts
|
|
11178
11950
|
init_paths();
|
|
11179
|
-
import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as
|
|
11951
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
|
|
11180
11952
|
import { join as join8 } from "path";
|
|
11181
11953
|
var DEFAULT_THRESHOLDS = {
|
|
11182
11954
|
reconnect: 3,
|
|
@@ -11241,7 +12013,7 @@ function readHealConfig(path) {
|
|
|
11241
12013
|
function writeHealConfig(config, path) {
|
|
11242
12014
|
const p = path || getHealConfigPath();
|
|
11243
12015
|
ensureParentDir(p);
|
|
11244
|
-
|
|
12016
|
+
writeFileSync7(p, `${JSON.stringify(config, null, 2)}
|
|
11245
12017
|
`, "utf8");
|
|
11246
12018
|
}
|
|
11247
12019
|
function readHealState(path) {
|
|
@@ -11257,7 +12029,7 @@ function readHealState(path) {
|
|
|
11257
12029
|
function writeHealState(state, path) {
|
|
11258
12030
|
const p = path || getHealStatePath();
|
|
11259
12031
|
ensureParentDir(p);
|
|
11260
|
-
|
|
12032
|
+
writeFileSync7(p, `${JSON.stringify(state, null, 2)}
|
|
11261
12033
|
`, "utf8");
|
|
11262
12034
|
}
|
|
11263
12035
|
function evaluateHealth(probe, config, state) {
|
|
@@ -11462,7 +12234,7 @@ function executeAction(action, config) {
|
|
|
11462
12234
|
|
|
11463
12235
|
// src/commands/heal-daemon.ts
|
|
11464
12236
|
init_paths();
|
|
11465
|
-
import { existsSync as existsSync10, readFileSync as readFileSync10, writeFileSync as
|
|
12237
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
|
|
11466
12238
|
import { join as join9 } from "path";
|
|
11467
12239
|
var DAEMON_PID_PATH2 = join9(getDataDir(), "heal-daemon.pid");
|
|
11468
12240
|
var SERVICE_PATH = "/etc/systemd/system/machines-heal.service";
|
|
@@ -11505,7 +12277,7 @@ function runHealOnce(config, opts = {}) {
|
|
|
11505
12277
|
return result;
|
|
11506
12278
|
}
|
|
11507
12279
|
function writePid2(pid) {
|
|
11508
|
-
|
|
12280
|
+
writeFileSync8(DAEMON_PID_PATH2, `${pid}
|
|
11509
12281
|
`);
|
|
11510
12282
|
}
|
|
11511
12283
|
function readPid2() {
|
|
@@ -11596,7 +12368,7 @@ ${key}=${value}
|
|
|
11596
12368
|
};
|
|
11597
12369
|
set("RuntimeWatchdogSec", "20s");
|
|
11598
12370
|
set("RebootWatchdogSec", "2min");
|
|
11599
|
-
|
|
12371
|
+
writeFileSync8(SYSTEM_CONF, conf);
|
|
11600
12372
|
sh2("systemctl daemon-reexec");
|
|
11601
12373
|
log2.push("hardware watchdog: RuntimeWatchdogSec=20s RebootWatchdogSec=2min");
|
|
11602
12374
|
return log2;
|
|
@@ -11641,7 +12413,7 @@ Environment=PATH=${binDir}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sb
|
|
|
11641
12413
|
[Install]
|
|
11642
12414
|
WantedBy=multi-user.target
|
|
11643
12415
|
`;
|
|
11644
|
-
|
|
12416
|
+
writeFileSync8(SERVICE_PATH, unit);
|
|
11645
12417
|
sh2("systemctl daemon-reload");
|
|
11646
12418
|
sh2("systemctl enable --now machines-heal.service");
|
|
11647
12419
|
log2.push(`installed + enabled ${SERVICE_PATH} (ExecStart=${exec} heal daemon)`);
|
|
@@ -11822,18 +12594,18 @@ function checkSecretPresence(secretsCommand, key) {
|
|
|
11822
12594
|
};
|
|
11823
12595
|
}
|
|
11824
12596
|
function parseCommandSpec(value) {
|
|
11825
|
-
const [
|
|
12597
|
+
const [command2, expectedVersion] = value.split(":");
|
|
11826
12598
|
return {
|
|
11827
|
-
command,
|
|
12599
|
+
command: command2,
|
|
11828
12600
|
expectedVersion: expectedVersion || undefined,
|
|
11829
12601
|
required: true
|
|
11830
12602
|
};
|
|
11831
12603
|
}
|
|
11832
12604
|
function parsePackageSpec(value) {
|
|
11833
|
-
const [name,
|
|
12605
|
+
const [name, command2, expectedVersion] = value.split(":");
|
|
11834
12606
|
return {
|
|
11835
12607
|
name,
|
|
11836
|
-
command:
|
|
12608
|
+
command: command2 || undefined,
|
|
11837
12609
|
expectedVersion: expectedVersion || undefined,
|
|
11838
12610
|
required: true
|
|
11839
12611
|
};
|
|
@@ -11924,17 +12696,54 @@ function renderFleetStatus(status) {
|
|
|
11924
12696
|
["sync runs", String(status.recentSyncRuns)]
|
|
11925
12697
|
]),
|
|
11926
12698
|
"",
|
|
11927
|
-
...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)
|
|
11928
12723
|
].join(`
|
|
11929
12724
|
`);
|
|
11930
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
|
+
}
|
|
11931
12740
|
program2.name("machines").description("Machine fleet management CLI + MCP for developers").version(getPackageVersion()).option("-q, --quiet", "Suppress non-essential output");
|
|
11932
12741
|
var manifestCommand = program2.command("manifest").description("Manage the fleet manifest");
|
|
11933
12742
|
var appsCommand = program2.command("apps").description("Manage installed applications per machine");
|
|
11934
12743
|
var notificationsCommand = program2.command("notifications").description("Manage fleet alert delivery channels");
|
|
11935
12744
|
var eventWebhooksCommand = registerWebhookCommands(program2, { source: "machines" });
|
|
11936
12745
|
eventWebhooksCommand.description("Manage shared event webhook subscriptions");
|
|
11937
|
-
var webhookTestCommand = eventWebhooksCommand.commands.find((
|
|
12746
|
+
var webhookTestCommand = eventWebhooksCommand.commands.find((command2) => command2.name() === "test");
|
|
11938
12747
|
var webhookOptions = webhookTestCommand?.options ?? [];
|
|
11939
12748
|
var webhookMessageOption = webhookOptions.find((option) => option.long === "--message");
|
|
11940
12749
|
if (webhookMessageOption) {
|
|
@@ -11945,6 +12754,23 @@ eventsCommand.description("Emit, list, and replay shared events");
|
|
|
11945
12754
|
var runtimeCommand = program2.command("runtime").description("Watch runtime conditions and emit shared events");
|
|
11946
12755
|
var clipboardCommand = program2.command("clipboard").description("Real-time clipboard sync across fleet machines");
|
|
11947
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");
|
|
11948
12774
|
manifestCommand.command("init").description("Create an empty fleet manifest").action(() => {
|
|
11949
12775
|
console.log(manifestInit());
|
|
11950
12776
|
});
|
|
@@ -12049,8 +12875,9 @@ program2.command("sync").description("Reconcile a machine against the fleet mani
|
|
|
12049
12875
|
const result = options.apply ? runSync(options.machine, { apply: true, yes: options.yes }) : buildSyncPlan(options.machine);
|
|
12050
12876
|
console.log(JSON.stringify(result, null, 2));
|
|
12051
12877
|
});
|
|
12052
|
-
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) => {
|
|
12053
|
-
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 });
|
|
12054
12881
|
if (options.json) {
|
|
12055
12882
|
console.log(JSON.stringify(topology, null, 2));
|
|
12056
12883
|
return;
|
|
@@ -12290,11 +13117,12 @@ program2.command("install-tailscale").description("Install Tailscale on a machin
|
|
|
12290
13117
|
const result = options.apply ? runTailscaleInstall(options.machine, { apply: true, yes: options.yes }) : buildTailscaleInstallPlan(options.machine);
|
|
12291
13118
|
console.log(JSON.stringify(result, null, 2));
|
|
12292
13119
|
});
|
|
12293
|
-
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) => {
|
|
12294
13121
|
const topology = discoverMachineTopology({ includeTailscale: options.tailscale !== false });
|
|
12295
13122
|
const resolved = resolveMachineRoute(options.machine, { topology });
|
|
12296
|
-
const
|
|
12297
|
-
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 };
|
|
12298
13126
|
if (options.json) {
|
|
12299
13127
|
console.log(JSON.stringify(payload, null, 2));
|
|
12300
13128
|
return;
|
|
@@ -12304,7 +13132,7 @@ program2.command("route").description("Resolve the best route for a machine").re
|
|
|
12304
13132
|
process.exitCode = 1;
|
|
12305
13133
|
return;
|
|
12306
13134
|
}
|
|
12307
|
-
console.log(
|
|
13135
|
+
console.log(options.privateMetadata ? command2 ?? `${resolved.route}:${resolved.target}` : `${publicResolved.route}:${publicResolved.target ?? "unresolved"}`);
|
|
12308
13136
|
});
|
|
12309
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) => {
|
|
12310
13138
|
if (options.json) {
|
|
@@ -12332,7 +13160,7 @@ program2.command("screen").description("Open Screen Sharing (VNC) to a machine u
|
|
|
12332
13160
|
for (const r of results) {
|
|
12333
13161
|
if (r.ok && r.url) {
|
|
12334
13162
|
if (!options.print)
|
|
12335
|
-
|
|
13163
|
+
execFileSync2("open", [r.url], { stdio: "ignore" });
|
|
12336
13164
|
console.log(`${r.ok ? "\u2713" : "\u2717"} ${r.machine.padEnd(14)} ${r.url ?? r.error}`);
|
|
12337
13165
|
} else {
|
|
12338
13166
|
console.log(`\u2717 ${r.machine.padEnd(14)} ${r.error}`);
|
|
@@ -12355,7 +13183,7 @@ program2.command("screen").description("Open Screen Sharing (VNC) to a machine u
|
|
|
12355
13183
|
console.log(resolved.url);
|
|
12356
13184
|
return;
|
|
12357
13185
|
}
|
|
12358
|
-
|
|
13186
|
+
execFileSync2("open", [resolved.url], { stdio: "ignore" });
|
|
12359
13187
|
console.log(`Opening Screen Sharing \u2192 ${resolved.url} (route: ${resolved.route})`);
|
|
12360
13188
|
});
|
|
12361
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) => {
|