@hasna/machines 0.0.32 → 0.0.34
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 +48 -47
- package/dist/cli/index.js +154 -1323
- package/dist/commands/heal-daemon.d.ts.map +1 -1
- package/dist/commands/screen.d.ts +2 -1
- package/dist/commands/screen.d.ts.map +1 -1
- package/dist/consumer.js +18 -3
- package/dist/index.js +50 -547
- package/dist/mcp/index.js +103 -604
- package/dist/topology.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -2082,34 +2082,34 @@ var require_commander = __commonJS((exports) => {
|
|
|
2082
2082
|
});
|
|
2083
2083
|
|
|
2084
2084
|
// src/paths.ts
|
|
2085
|
-
import { existsSync as
|
|
2086
|
-
import { dirname as dirname2, join as
|
|
2085
|
+
import { existsSync as existsSync2, mkdirSync } from "fs";
|
|
2086
|
+
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
2087
2087
|
function homeDir() {
|
|
2088
2088
|
return process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
2089
2089
|
}
|
|
2090
2090
|
function getDataDir() {
|
|
2091
|
-
return process.env["HASNA_MACHINES_DIR"] ||
|
|
2091
|
+
return process.env["HASNA_MACHINES_DIR"] || join2(homeDir(), ".hasna", "machines");
|
|
2092
2092
|
}
|
|
2093
2093
|
function getDbPath() {
|
|
2094
|
-
return process.env["HASNA_MACHINES_DB_PATH"] ||
|
|
2094
|
+
return process.env["HASNA_MACHINES_DB_PATH"] || join2(getDataDir(), "machines.db");
|
|
2095
2095
|
}
|
|
2096
2096
|
function getManifestPath() {
|
|
2097
|
-
return process.env["HASNA_MACHINES_MANIFEST_PATH"] ||
|
|
2097
|
+
return process.env["HASNA_MACHINES_MANIFEST_PATH"] || join2(getDataDir(), "machines.json");
|
|
2098
2098
|
}
|
|
2099
2099
|
function getNotificationsPath() {
|
|
2100
|
-
return process.env["HASNA_MACHINES_NOTIFICATIONS_PATH"] ||
|
|
2100
|
+
return process.env["HASNA_MACHINES_NOTIFICATIONS_PATH"] || join2(getDataDir(), "notifications.json");
|
|
2101
2101
|
}
|
|
2102
2102
|
function getClipboardKeyPath() {
|
|
2103
|
-
return process.env["HASNA_MACHINES_CLIPBOARD_KEY_PATH"] ||
|
|
2103
|
+
return process.env["HASNA_MACHINES_CLIPBOARD_KEY_PATH"] || join2(getDataDir(), "clipboard.key");
|
|
2104
2104
|
}
|
|
2105
2105
|
function getClipboardHistoryPath() {
|
|
2106
|
-
return process.env["HASNA_MACHINES_CLIPBOARD_HISTORY_PATH"] ||
|
|
2106
|
+
return process.env["HASNA_MACHINES_CLIPBOARD_HISTORY_PATH"] || join2(getDataDir(), "clipboard-history.json");
|
|
2107
2107
|
}
|
|
2108
2108
|
function ensureParentDir(filePath) {
|
|
2109
2109
|
if (filePath === ":memory:")
|
|
2110
2110
|
return;
|
|
2111
2111
|
const dir = dirname2(resolve(filePath));
|
|
2112
|
-
if (!
|
|
2112
|
+
if (!existsSync2(dir)) {
|
|
2113
2113
|
mkdirSync(dir, { recursive: true });
|
|
2114
2114
|
}
|
|
2115
2115
|
}
|
|
@@ -2203,15 +2203,15 @@ function countRuns(table) {
|
|
|
2203
2203
|
}
|
|
2204
2204
|
function recordSetupRun(machineId, status, details) {
|
|
2205
2205
|
const db = getDb();
|
|
2206
|
-
const
|
|
2206
|
+
const now = new Date().toISOString();
|
|
2207
2207
|
db.query(`INSERT INTO setup_runs (id, machine_id, status, details_json, created_at, updated_at)
|
|
2208
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(details),
|
|
2208
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(details), now, now);
|
|
2209
2209
|
}
|
|
2210
2210
|
function recordSyncRun(machineId, status, actions) {
|
|
2211
2211
|
const db = getDb();
|
|
2212
|
-
const
|
|
2212
|
+
const now = new Date().toISOString();
|
|
2213
2213
|
db.query(`INSERT INTO sync_runs (id, machine_id, status, actions_json, created_at, updated_at)
|
|
2214
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(actions),
|
|
2214
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(actions), now, now);
|
|
2215
2215
|
}
|
|
2216
2216
|
var adapter = null;
|
|
2217
2217
|
var init_db = __esm(() => {
|
|
@@ -2479,7 +2479,7 @@ function upsertSqlite(db, table, columns, rows) {
|
|
|
2479
2479
|
}
|
|
2480
2480
|
function recordSyncMeta(db, direction, results) {
|
|
2481
2481
|
ensureSyncMetaTable(db);
|
|
2482
|
-
const
|
|
2482
|
+
const now = new Date().toISOString();
|
|
2483
2483
|
const statement = db.query(`
|
|
2484
2484
|
INSERT INTO _machines_sync_meta (table_name, last_synced_at, direction)
|
|
2485
2485
|
VALUES (?, ?, ?)
|
|
@@ -2488,7 +2488,7 @@ function recordSyncMeta(db, direction, results) {
|
|
|
2488
2488
|
for (const result of results) {
|
|
2489
2489
|
if (result.errors.length > 0)
|
|
2490
2490
|
continue;
|
|
2491
|
-
statement.run(result.table,
|
|
2491
|
+
statement.run(result.table, now, direction);
|
|
2492
2492
|
}
|
|
2493
2493
|
}
|
|
2494
2494
|
function ensureSyncMetaTable(db) {
|
|
@@ -2601,685 +2601,8 @@ var {
|
|
|
2601
2601
|
Help
|
|
2602
2602
|
} = import__.default;
|
|
2603
2603
|
|
|
2604
|
-
// node_modules/@hasna/events/dist/commander.js
|
|
2605
|
-
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
2606
|
-
import { existsSync } from "fs";
|
|
2607
|
-
import { homedir } from "os";
|
|
2608
|
-
import { join } from "path";
|
|
2609
|
-
import { createHmac, timingSafeEqual } from "crypto";
|
|
2610
|
-
import { randomUUID } from "crypto";
|
|
2611
|
-
import { spawn } from "child_process";
|
|
2612
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
2613
|
-
function getPathValue(input, path) {
|
|
2614
|
-
return path.split(".").reduce((value, part) => {
|
|
2615
|
-
if (value && typeof value === "object" && part in value) {
|
|
2616
|
-
return value[part];
|
|
2617
|
-
}
|
|
2618
|
-
return;
|
|
2619
|
-
}, input);
|
|
2620
|
-
}
|
|
2621
|
-
function wildcardToRegExp(pattern) {
|
|
2622
|
-
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
|
|
2623
|
-
return new RegExp(`^${escaped}$`);
|
|
2624
|
-
}
|
|
2625
|
-
function matchString(value, matcher) {
|
|
2626
|
-
if (matcher === undefined)
|
|
2627
|
-
return true;
|
|
2628
|
-
if (value === undefined)
|
|
2629
|
-
return false;
|
|
2630
|
-
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
2631
|
-
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
2632
|
-
}
|
|
2633
|
-
function matchRecord(input, matcher) {
|
|
2634
|
-
if (!matcher)
|
|
2635
|
-
return true;
|
|
2636
|
-
return Object.entries(matcher).every(([path, expected]) => {
|
|
2637
|
-
const actual = getPathValue(input, path);
|
|
2638
|
-
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
2639
|
-
return matchString(actual === undefined ? undefined : String(actual), expected);
|
|
2640
|
-
}
|
|
2641
|
-
return actual === expected;
|
|
2642
|
-
});
|
|
2643
|
-
}
|
|
2644
|
-
function eventMatchesFilter(event, filter) {
|
|
2645
|
-
return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
|
|
2646
|
-
}
|
|
2647
|
-
function channelMatchesEvent(channel, event) {
|
|
2648
|
-
if (!channel.enabled)
|
|
2649
|
-
return false;
|
|
2650
|
-
if (!channel.filters || channel.filters.length === 0)
|
|
2651
|
-
return true;
|
|
2652
|
-
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
2653
|
-
}
|
|
2654
|
-
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
2655
|
-
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
2656
|
-
function getEventsDataDir(override) {
|
|
2657
|
-
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
|
|
2658
|
-
}
|
|
2659
|
-
|
|
2660
|
-
class JsonEventsStore {
|
|
2661
|
-
dataDir;
|
|
2662
|
-
channelsPath;
|
|
2663
|
-
eventsPath;
|
|
2664
|
-
deliveriesPath;
|
|
2665
|
-
constructor(dataDir = getEventsDataDir()) {
|
|
2666
|
-
this.dataDir = dataDir;
|
|
2667
|
-
this.channelsPath = join(dataDir, "channels.json");
|
|
2668
|
-
this.eventsPath = join(dataDir, "events.json");
|
|
2669
|
-
this.deliveriesPath = join(dataDir, "deliveries.json");
|
|
2670
|
-
}
|
|
2671
|
-
async init() {
|
|
2672
|
-
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
2673
|
-
await chmod(this.dataDir, 448).catch(() => {
|
|
2674
|
-
return;
|
|
2675
|
-
});
|
|
2676
|
-
await this.ensureArrayFile(this.channelsPath);
|
|
2677
|
-
await this.ensureArrayFile(this.eventsPath);
|
|
2678
|
-
await this.ensureArrayFile(this.deliveriesPath);
|
|
2679
|
-
}
|
|
2680
|
-
async addChannel(channel) {
|
|
2681
|
-
await this.init();
|
|
2682
|
-
const channels = await this.readJson(this.channelsPath, []);
|
|
2683
|
-
const index = channels.findIndex((item) => item.id === channel.id);
|
|
2684
|
-
if (index >= 0) {
|
|
2685
|
-
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
2686
|
-
} else {
|
|
2687
|
-
channels.push(channel);
|
|
2688
|
-
}
|
|
2689
|
-
await this.writeJson(this.channelsPath, channels);
|
|
2690
|
-
return index >= 0 ? channels[index] : channel;
|
|
2691
|
-
}
|
|
2692
|
-
async listChannels() {
|
|
2693
|
-
await this.init();
|
|
2694
|
-
return this.readJson(this.channelsPath, []);
|
|
2695
|
-
}
|
|
2696
|
-
async getChannel(id) {
|
|
2697
|
-
const channels = await this.listChannels();
|
|
2698
|
-
return channels.find((channel) => channel.id === id);
|
|
2699
|
-
}
|
|
2700
|
-
async removeChannel(id) {
|
|
2701
|
-
await this.init();
|
|
2702
|
-
const channels = await this.readJson(this.channelsPath, []);
|
|
2703
|
-
const next = channels.filter((channel) => channel.id !== id);
|
|
2704
|
-
await this.writeJson(this.channelsPath, next);
|
|
2705
|
-
return next.length !== channels.length;
|
|
2706
|
-
}
|
|
2707
|
-
async appendEvent(event) {
|
|
2708
|
-
await this.init();
|
|
2709
|
-
const events = await this.readJson(this.eventsPath, []);
|
|
2710
|
-
events.push(event);
|
|
2711
|
-
await this.writeJson(this.eventsPath, events);
|
|
2712
|
-
return event;
|
|
2713
|
-
}
|
|
2714
|
-
async listEvents() {
|
|
2715
|
-
await this.init();
|
|
2716
|
-
return this.readJson(this.eventsPath, []);
|
|
2717
|
-
}
|
|
2718
|
-
async findEventByIdentity(identity) {
|
|
2719
|
-
const events = await this.listEvents();
|
|
2720
|
-
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
2721
|
-
}
|
|
2722
|
-
async appendDelivery(result) {
|
|
2723
|
-
await this.init();
|
|
2724
|
-
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
2725
|
-
deliveries.push(result);
|
|
2726
|
-
await this.writeJson(this.deliveriesPath, deliveries);
|
|
2727
|
-
return result;
|
|
2728
|
-
}
|
|
2729
|
-
async listDeliveries() {
|
|
2730
|
-
await this.init();
|
|
2731
|
-
return this.readJson(this.deliveriesPath, []);
|
|
2732
|
-
}
|
|
2733
|
-
async exportData() {
|
|
2734
|
-
return {
|
|
2735
|
-
channels: await this.listChannels(),
|
|
2736
|
-
events: await this.listEvents(),
|
|
2737
|
-
deliveries: await this.listDeliveries()
|
|
2738
|
-
};
|
|
2739
|
-
}
|
|
2740
|
-
async ensureArrayFile(path) {
|
|
2741
|
-
if (!existsSync(path)) {
|
|
2742
|
-
await writeFile(path, `[]
|
|
2743
|
-
`, { encoding: "utf-8", mode: 384 });
|
|
2744
|
-
}
|
|
2745
|
-
await chmod(path, 384).catch(() => {
|
|
2746
|
-
return;
|
|
2747
|
-
});
|
|
2748
|
-
}
|
|
2749
|
-
async readJson(path, fallback) {
|
|
2750
|
-
try {
|
|
2751
|
-
const raw = await readFile(path, "utf-8");
|
|
2752
|
-
if (!raw.trim())
|
|
2753
|
-
return fallback;
|
|
2754
|
-
return JSON.parse(raw);
|
|
2755
|
-
} catch (error) {
|
|
2756
|
-
if (error.code === "ENOENT")
|
|
2757
|
-
return fallback;
|
|
2758
|
-
throw error;
|
|
2759
|
-
}
|
|
2760
|
-
}
|
|
2761
|
-
async writeJson(path, value) {
|
|
2762
|
-
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
2763
|
-
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
|
|
2764
|
-
`, { encoding: "utf-8", mode: 384 });
|
|
2765
|
-
await rename(tempPath, path);
|
|
2766
|
-
await chmod(path, 384).catch(() => {
|
|
2767
|
-
return;
|
|
2768
|
-
});
|
|
2769
|
-
}
|
|
2770
|
-
}
|
|
2771
|
-
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
2772
|
-
function buildSignatureBase(timestamp, body) {
|
|
2773
|
-
return `${timestamp}.${body}`;
|
|
2774
|
-
}
|
|
2775
|
-
function signPayload(secret, timestamp, body) {
|
|
2776
|
-
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
|
|
2777
|
-
return `sha256=${digest}`;
|
|
2778
|
-
}
|
|
2779
|
-
function now() {
|
|
2780
|
-
return new Date().toISOString();
|
|
2781
|
-
}
|
|
2782
|
-
function truncate(value, max = 4096) {
|
|
2783
|
-
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
2784
|
-
}
|
|
2785
|
-
function buildWebhookRequest(event, channel) {
|
|
2786
|
-
if (!channel.webhook)
|
|
2787
|
-
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
2788
|
-
const body = JSON.stringify(event);
|
|
2789
|
-
const timestamp = event.time;
|
|
2790
|
-
const headers = {
|
|
2791
|
-
"Content-Type": "application/json",
|
|
2792
|
-
"User-Agent": "@hasna/events",
|
|
2793
|
-
"X-Hasna-Event-Id": event.id,
|
|
2794
|
-
"X-Hasna-Event-Type": event.type,
|
|
2795
|
-
"X-Hasna-Timestamp": timestamp,
|
|
2796
|
-
...channel.webhook.headers
|
|
2797
|
-
};
|
|
2798
|
-
if (channel.webhook.secret) {
|
|
2799
|
-
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
|
|
2800
|
-
}
|
|
2801
|
-
return { body, headers };
|
|
2802
|
-
}
|
|
2803
|
-
async function dispatchWebhook(event, channel, options = {}) {
|
|
2804
|
-
if (!channel.webhook)
|
|
2805
|
-
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
2806
|
-
const startedAt = now();
|
|
2807
|
-
const { body, headers } = buildWebhookRequest(event, channel);
|
|
2808
|
-
const controller = new AbortController;
|
|
2809
|
-
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
2810
|
-
try {
|
|
2811
|
-
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
2812
|
-
method: "POST",
|
|
2813
|
-
headers,
|
|
2814
|
-
body,
|
|
2815
|
-
signal: controller.signal
|
|
2816
|
-
});
|
|
2817
|
-
const responseBody = truncate(await response.text());
|
|
2818
|
-
return {
|
|
2819
|
-
attempt: 1,
|
|
2820
|
-
status: response.ok ? "success" : "failed",
|
|
2821
|
-
startedAt,
|
|
2822
|
-
completedAt: now(),
|
|
2823
|
-
responseStatus: response.status,
|
|
2824
|
-
responseBody,
|
|
2825
|
-
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
2826
|
-
};
|
|
2827
|
-
} catch (error) {
|
|
2828
|
-
return {
|
|
2829
|
-
attempt: 1,
|
|
2830
|
-
status: "failed",
|
|
2831
|
-
startedAt,
|
|
2832
|
-
completedAt: now(),
|
|
2833
|
-
error: error instanceof Error ? error.message : String(error)
|
|
2834
|
-
};
|
|
2835
|
-
} finally {
|
|
2836
|
-
clearTimeout(timeout);
|
|
2837
|
-
}
|
|
2838
|
-
}
|
|
2839
|
-
async function dispatchCommand(event, channel) {
|
|
2840
|
-
if (!channel.command)
|
|
2841
|
-
throw new Error(`Channel ${channel.id} has no command config`);
|
|
2842
|
-
const startedAt = now();
|
|
2843
|
-
const eventJson = JSON.stringify(event);
|
|
2844
|
-
const env = {
|
|
2845
|
-
...process.env,
|
|
2846
|
-
...channel.command.env,
|
|
2847
|
-
HASNA_CHANNEL_ID: channel.id,
|
|
2848
|
-
HASNA_EVENT_ID: event.id,
|
|
2849
|
-
HASNA_EVENT_TYPE: event.type,
|
|
2850
|
-
HASNA_EVENT_SOURCE: event.source,
|
|
2851
|
-
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
2852
|
-
HASNA_EVENT_SEVERITY: event.severity,
|
|
2853
|
-
HASNA_EVENT_TIME: event.time,
|
|
2854
|
-
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
2855
|
-
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
2856
|
-
HASNA_EVENT_JSON: eventJson
|
|
2857
|
-
};
|
|
2858
|
-
return new Promise((resolve) => {
|
|
2859
|
-
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
2860
|
-
cwd: channel.command.cwd,
|
|
2861
|
-
env,
|
|
2862
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
2863
|
-
});
|
|
2864
|
-
let stdout = "";
|
|
2865
|
-
let stderr = "";
|
|
2866
|
-
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
2867
|
-
child.stdin.end(eventJson);
|
|
2868
|
-
child.stdout.on("data", (chunk) => {
|
|
2869
|
-
stdout += chunk.toString();
|
|
2870
|
-
});
|
|
2871
|
-
child.stderr.on("data", (chunk) => {
|
|
2872
|
-
stderr += chunk.toString();
|
|
2873
|
-
});
|
|
2874
|
-
child.on("error", (error) => {
|
|
2875
|
-
clearTimeout(timeout);
|
|
2876
|
-
resolve({
|
|
2877
|
-
attempt: 1,
|
|
2878
|
-
status: "failed",
|
|
2879
|
-
startedAt,
|
|
2880
|
-
completedAt: now(),
|
|
2881
|
-
stdout: truncate(stdout),
|
|
2882
|
-
stderr: truncate(stderr),
|
|
2883
|
-
error: error.message
|
|
2884
|
-
});
|
|
2885
|
-
});
|
|
2886
|
-
child.on("close", (code, signal) => {
|
|
2887
|
-
clearTimeout(timeout);
|
|
2888
|
-
const success = code === 0;
|
|
2889
|
-
resolve({
|
|
2890
|
-
attempt: 1,
|
|
2891
|
-
status: success ? "success" : "failed",
|
|
2892
|
-
startedAt,
|
|
2893
|
-
completedAt: now(),
|
|
2894
|
-
stdout: truncate(stdout),
|
|
2895
|
-
stderr: truncate(stderr),
|
|
2896
|
-
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
2897
|
-
});
|
|
2898
|
-
});
|
|
2899
|
-
});
|
|
2900
|
-
}
|
|
2901
|
-
async function dispatchChannel(event, channel, options = {}) {
|
|
2902
|
-
if (channel.transport === "webhook")
|
|
2903
|
-
return dispatchWebhook(event, channel, options);
|
|
2904
|
-
if (channel.transport === "command")
|
|
2905
|
-
return dispatchCommand(event, channel);
|
|
2906
|
-
return {
|
|
2907
|
-
attempt: 1,
|
|
2908
|
-
status: "skipped",
|
|
2909
|
-
startedAt: now(),
|
|
2910
|
-
completedAt: now(),
|
|
2911
|
-
error: `Unsupported transport: ${channel.transport}`
|
|
2912
|
-
};
|
|
2913
|
-
}
|
|
2914
|
-
function createDeliveryResult(event, channel, attempts) {
|
|
2915
|
-
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
2916
|
-
return {
|
|
2917
|
-
id: randomUUID(),
|
|
2918
|
-
eventId: event.id,
|
|
2919
|
-
channelId: channel.id,
|
|
2920
|
-
transport: channel.transport,
|
|
2921
|
-
status,
|
|
2922
|
-
attempts,
|
|
2923
|
-
createdAt: attempts[0]?.startedAt ?? now(),
|
|
2924
|
-
completedAt: attempts.at(-1)?.completedAt ?? now()
|
|
2925
|
-
};
|
|
2926
|
-
}
|
|
2927
|
-
function createEvent(input) {
|
|
2928
|
-
return {
|
|
2929
|
-
id: input.id ?? randomUUID2(),
|
|
2930
|
-
source: input.source,
|
|
2931
|
-
type: input.type,
|
|
2932
|
-
time: normalizeTime(input.time),
|
|
2933
|
-
subject: input.subject,
|
|
2934
|
-
severity: input.severity ?? "info",
|
|
2935
|
-
data: input.data ?? {},
|
|
2936
|
-
message: input.message,
|
|
2937
|
-
dedupeKey: input.dedupeKey,
|
|
2938
|
-
schemaVersion: input.schemaVersion ?? "1.0",
|
|
2939
|
-
metadata: input.metadata ?? {}
|
|
2940
|
-
};
|
|
2941
|
-
}
|
|
2942
|
-
|
|
2943
|
-
class EventsClient {
|
|
2944
|
-
store;
|
|
2945
|
-
redactors;
|
|
2946
|
-
transportOptions;
|
|
2947
|
-
constructor(options = {}) {
|
|
2948
|
-
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
2949
|
-
this.redactors = options.redactors ?? [];
|
|
2950
|
-
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
2951
|
-
}
|
|
2952
|
-
async addChannel(input) {
|
|
2953
|
-
const timestamp = new Date().toISOString();
|
|
2954
|
-
return this.store.addChannel({
|
|
2955
|
-
...input,
|
|
2956
|
-
createdAt: input.createdAt ?? timestamp,
|
|
2957
|
-
updatedAt: input.updatedAt ?? timestamp
|
|
2958
|
-
});
|
|
2959
|
-
}
|
|
2960
|
-
async listChannels() {
|
|
2961
|
-
return this.store.listChannels();
|
|
2962
|
-
}
|
|
2963
|
-
async removeChannel(id) {
|
|
2964
|
-
return this.store.removeChannel(id);
|
|
2965
|
-
}
|
|
2966
|
-
async emit(input, options = {}) {
|
|
2967
|
-
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
2968
|
-
if (options.dedupe !== false) {
|
|
2969
|
-
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
2970
|
-
if (existing) {
|
|
2971
|
-
return { event: existing, deliveries: [], deduped: true };
|
|
2972
|
-
}
|
|
2973
|
-
}
|
|
2974
|
-
await this.store.appendEvent(event);
|
|
2975
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
2976
|
-
return { event, deliveries, deduped: false };
|
|
2977
|
-
}
|
|
2978
|
-
async listEvents() {
|
|
2979
|
-
return this.store.listEvents();
|
|
2980
|
-
}
|
|
2981
|
-
async listDeliveries() {
|
|
2982
|
-
return this.store.listDeliveries();
|
|
2983
|
-
}
|
|
2984
|
-
async deliver(event) {
|
|
2985
|
-
const channels = await this.store.listChannels();
|
|
2986
|
-
const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
|
|
2987
|
-
const deliveries = [];
|
|
2988
|
-
for (const channel of selected) {
|
|
2989
|
-
const eventForChannel = await this.applyRedaction(event, channel);
|
|
2990
|
-
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
2991
|
-
await this.store.appendDelivery(result);
|
|
2992
|
-
deliveries.push(result);
|
|
2993
|
-
}
|
|
2994
|
-
return deliveries;
|
|
2995
|
-
}
|
|
2996
|
-
async testChannel(id, input = {}) {
|
|
2997
|
-
const channel = await this.store.getChannel(id);
|
|
2998
|
-
if (!channel)
|
|
2999
|
-
throw new Error(`Channel not found: ${id}`);
|
|
3000
|
-
const event = createEvent({
|
|
3001
|
-
source: input.source ?? "hasna.events",
|
|
3002
|
-
type: input.type ?? "events.test",
|
|
3003
|
-
subject: input.subject ?? id,
|
|
3004
|
-
severity: input.severity ?? "info",
|
|
3005
|
-
data: input.data ?? { test: true },
|
|
3006
|
-
message: input.message ?? "Hasna events test delivery",
|
|
3007
|
-
dedupeKey: input.dedupeKey,
|
|
3008
|
-
schemaVersion: input.schemaVersion,
|
|
3009
|
-
metadata: input.metadata,
|
|
3010
|
-
time: input.time,
|
|
3011
|
-
id: input.id
|
|
3012
|
-
});
|
|
3013
|
-
const eventForChannel = await this.applyRedaction(event, channel);
|
|
3014
|
-
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
3015
|
-
await this.store.appendDelivery(result);
|
|
3016
|
-
return result;
|
|
3017
|
-
}
|
|
3018
|
-
async replay(options = {}) {
|
|
3019
|
-
const events = (await this.store.listEvents()).filter((event) => {
|
|
3020
|
-
if (options.eventId && event.id !== options.eventId)
|
|
3021
|
-
return false;
|
|
3022
|
-
if (options.source && event.source !== options.source)
|
|
3023
|
-
return false;
|
|
3024
|
-
if (options.type && event.type !== options.type)
|
|
3025
|
-
return false;
|
|
3026
|
-
return true;
|
|
3027
|
-
});
|
|
3028
|
-
if (options.dryRun)
|
|
3029
|
-
return { events, deliveries: [] };
|
|
3030
|
-
const deliveries = [];
|
|
3031
|
-
for (const event of events) {
|
|
3032
|
-
deliveries.push(...await this.deliver(event));
|
|
3033
|
-
}
|
|
3034
|
-
return { events, deliveries };
|
|
3035
|
-
}
|
|
3036
|
-
async applyRedaction(event, channel) {
|
|
3037
|
-
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
3038
|
-
for (const redactor of this.redactors) {
|
|
3039
|
-
next = await redactor(next, channel);
|
|
3040
|
-
}
|
|
3041
|
-
return next;
|
|
3042
|
-
}
|
|
3043
|
-
async deliverWithRetry(event, channel) {
|
|
3044
|
-
const policy = normalizeRetryPolicy(channel.retry);
|
|
3045
|
-
const attempts = [];
|
|
3046
|
-
for (let index = 0;index < policy.maxAttempts; index += 1) {
|
|
3047
|
-
const attempt = await dispatchChannel(event, channel, this.transportOptions);
|
|
3048
|
-
attempt.attempt = index + 1;
|
|
3049
|
-
if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
|
|
3050
|
-
attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
|
|
3051
|
-
}
|
|
3052
|
-
attempts.push(attempt);
|
|
3053
|
-
if (attempt.status !== "failed")
|
|
3054
|
-
break;
|
|
3055
|
-
if (attempt.nextBackoffMs)
|
|
3056
|
-
await Bun.sleep(attempt.nextBackoffMs);
|
|
3057
|
-
}
|
|
3058
|
-
return createDeliveryResult(event, channel, attempts);
|
|
3059
|
-
}
|
|
3060
|
-
}
|
|
3061
|
-
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
3062
|
-
if (paths.length === 0)
|
|
3063
|
-
return event;
|
|
3064
|
-
const copy = structuredClone(event);
|
|
3065
|
-
for (const path of paths) {
|
|
3066
|
-
setPath(copy, path, replacement);
|
|
3067
|
-
}
|
|
3068
|
-
return copy;
|
|
3069
|
-
}
|
|
3070
|
-
function sanitizeChannelForOutput(channel) {
|
|
3071
|
-
const copy = structuredClone(channel);
|
|
3072
|
-
if (copy.webhook?.secret)
|
|
3073
|
-
copy.webhook.secret = "[REDACTED]";
|
|
3074
|
-
if (copy.command?.env) {
|
|
3075
|
-
copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey(key) ? "[REDACTED]" : value]));
|
|
3076
|
-
}
|
|
3077
|
-
return copy;
|
|
3078
|
-
}
|
|
3079
|
-
function sanitizeChannelsForOutput(channels) {
|
|
3080
|
-
return channels.map(sanitizeChannelForOutput);
|
|
3081
|
-
}
|
|
3082
|
-
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
3083
|
-
return redactValue(event, replacement);
|
|
3084
|
-
}
|
|
3085
|
-
function shouldRedactKey(key) {
|
|
3086
|
-
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
3087
|
-
}
|
|
3088
|
-
function redactValue(value, replacement) {
|
|
3089
|
-
if (Array.isArray(value))
|
|
3090
|
-
return value.map((item) => redactValue(item, replacement));
|
|
3091
|
-
if (!value || typeof value !== "object")
|
|
3092
|
-
return value;
|
|
3093
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
3094
|
-
key,
|
|
3095
|
-
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
3096
|
-
]));
|
|
3097
|
-
}
|
|
3098
|
-
function setPath(input, path, replacement) {
|
|
3099
|
-
const parts = path.split(".");
|
|
3100
|
-
let cursor = input;
|
|
3101
|
-
for (const part of parts.slice(0, -1)) {
|
|
3102
|
-
const next = cursor[part];
|
|
3103
|
-
if (!next || typeof next !== "object")
|
|
3104
|
-
return;
|
|
3105
|
-
cursor = next;
|
|
3106
|
-
}
|
|
3107
|
-
const last = parts.at(-1);
|
|
3108
|
-
if (last && last in cursor)
|
|
3109
|
-
cursor[last] = replacement;
|
|
3110
|
-
}
|
|
3111
|
-
function normalizeTime(value) {
|
|
3112
|
-
if (!value)
|
|
3113
|
-
return new Date().toISOString();
|
|
3114
|
-
return value instanceof Date ? value.toISOString() : value;
|
|
3115
|
-
}
|
|
3116
|
-
function normalizeRetryPolicy(policy) {
|
|
3117
|
-
return {
|
|
3118
|
-
maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
|
|
3119
|
-
backoffMs: Math.max(0, policy?.backoffMs ?? 250),
|
|
3120
|
-
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
3121
|
-
};
|
|
3122
|
-
}
|
|
3123
|
-
function parseJsonObject(value, fallback) {
|
|
3124
|
-
if (!value)
|
|
3125
|
-
return fallback;
|
|
3126
|
-
const parsed = JSON.parse(value);
|
|
3127
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3128
|
-
throw new Error("Expected a JSON object");
|
|
3129
|
-
}
|
|
3130
|
-
return parsed;
|
|
3131
|
-
}
|
|
3132
|
-
function parseHeaders(values) {
|
|
3133
|
-
if (!values?.length)
|
|
3134
|
-
return;
|
|
3135
|
-
const headers = {};
|
|
3136
|
-
for (const value of values) {
|
|
3137
|
-
const separator = value.indexOf("=");
|
|
3138
|
-
if (separator === -1)
|
|
3139
|
-
throw new Error(`Invalid header, expected name=value: ${value}`);
|
|
3140
|
-
headers[value.slice(0, separator)] = value.slice(separator + 1);
|
|
3141
|
-
}
|
|
3142
|
-
return headers;
|
|
3143
|
-
}
|
|
3144
|
-
function parseFilter(options) {
|
|
3145
|
-
const filter2 = {};
|
|
3146
|
-
if (options.source)
|
|
3147
|
-
filter2.source = options.source;
|
|
3148
|
-
if (options.type)
|
|
3149
|
-
filter2.type = options.type;
|
|
3150
|
-
if (options.subject)
|
|
3151
|
-
filter2.subject = options.subject;
|
|
3152
|
-
if (options.severity)
|
|
3153
|
-
filter2.severity = options.severity;
|
|
3154
|
-
return Object.keys(filter2).length > 0 ? [filter2] : undefined;
|
|
3155
|
-
}
|
|
3156
|
-
function createClient(options) {
|
|
3157
|
-
if (options.createClient)
|
|
3158
|
-
return options.createClient();
|
|
3159
|
-
return new EventsClient({ store: new JsonEventsStore(options.dataDir) });
|
|
3160
|
-
}
|
|
3161
|
-
function print(value, json, text) {
|
|
3162
|
-
if (json)
|
|
3163
|
-
console.log(JSON.stringify(value, null, 2));
|
|
3164
|
-
else
|
|
3165
|
-
console.log(text);
|
|
3166
|
-
}
|
|
3167
|
-
function registerWebhookCommands(program2, options) {
|
|
3168
|
-
const webhooks = program2.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
|
|
3169
|
-
webhooks.command("add").description("Add or replace a webhook or command subscription").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Subscription/channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions) => {
|
|
3170
|
-
const timestamp = new Date().toISOString();
|
|
3171
|
-
const channel = {
|
|
3172
|
-
id: actionOptions.id,
|
|
3173
|
-
name: actionOptions.name,
|
|
3174
|
-
enabled: !actionOptions.disabled,
|
|
3175
|
-
transport: actionOptions.transport,
|
|
3176
|
-
filters: parseFilter(actionOptions),
|
|
3177
|
-
retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
|
|
3178
|
-
redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
|
|
3179
|
-
createdAt: timestamp,
|
|
3180
|
-
updatedAt: timestamp
|
|
3181
|
-
};
|
|
3182
|
-
if (actionOptions.transport === "webhook") {
|
|
3183
|
-
channel.webhook = { url: target, secret: actionOptions.secret, headers: parseHeaders(actionOptions.header), timeoutMs: actionOptions.timeoutMs };
|
|
3184
|
-
} else if (actionOptions.transport === "command") {
|
|
3185
|
-
channel.command = { command: target, args: actionOptions.arg ?? [], timeoutMs: actionOptions.timeoutMs };
|
|
3186
|
-
} else {
|
|
3187
|
-
throw new Error(`Transport ${actionOptions.transport} is reserved for future use and cannot be added yet`);
|
|
3188
|
-
}
|
|
3189
|
-
const saved = await createClient(options).addChannel(channel);
|
|
3190
|
-
print(sanitizeChannelForOutput(saved), Boolean(actionOptions.json), `Added ${saved.transport} channel ${saved.id}`);
|
|
3191
|
-
});
|
|
3192
|
-
webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions) => {
|
|
3193
|
-
const channels = await createClient(options).listChannels();
|
|
3194
|
-
if (actionOptions.json) {
|
|
3195
|
-
console.log(JSON.stringify(sanitizeChannelsForOutput(channels), null, 2));
|
|
3196
|
-
return;
|
|
3197
|
-
}
|
|
3198
|
-
if (!channels.length) {
|
|
3199
|
-
console.log("No channels configured.");
|
|
3200
|
-
return;
|
|
3201
|
-
}
|
|
3202
|
-
for (const channel of channels) {
|
|
3203
|
-
console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
|
|
3204
|
-
}
|
|
3205
|
-
});
|
|
3206
|
-
webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions) => {
|
|
3207
|
-
const removed = await createClient(options).removeChannel(id);
|
|
3208
|
-
print({ removed }, Boolean(actionOptions.json), removed ? `Removed ${id}` : `Channel not found: ${id}`);
|
|
3209
|
-
});
|
|
3210
|
-
webhooks.command("test").description("Send a test event to one subscription").argument("<id>", "Subscription/channel identifier").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions) => {
|
|
3211
|
-
const result = await createClient(options).testChannel(id, {
|
|
3212
|
-
source: options.source,
|
|
3213
|
-
type: actionOptions.type,
|
|
3214
|
-
subject: actionOptions.subject ?? id,
|
|
3215
|
-
message: actionOptions.message,
|
|
3216
|
-
data: parseJsonObject(actionOptions.data, { test: true })
|
|
3217
|
-
});
|
|
3218
|
-
print(result, Boolean(actionOptions.json), `${result.status}: ${result.channelId}`);
|
|
3219
|
-
});
|
|
3220
|
-
return webhooks;
|
|
3221
|
-
}
|
|
3222
|
-
function registerEventCommands(program2, options) {
|
|
3223
|
-
const events = program2.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
|
|
3224
|
-
events.command("emit").description("Emit an event from this app").argument("<type>", "Event type").option("--source <source>", "Event source override").option("--subject <subject>", "Event subject").option("--severity <severity>", "Event severity", "info").option("--message <message>", "Event message").option("--dedupe-key <key>", "Dedupe key").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--no-deliver", "Record without delivering").option("--no-dedupe", "Allow duplicate id/dedupeKey events").option("-j, --json", "Print JSON output", false).action(async (type, actionOptions) => {
|
|
3225
|
-
const result = await createClient(options).emit({
|
|
3226
|
-
source: actionOptions.source ?? options.source,
|
|
3227
|
-
type,
|
|
3228
|
-
subject: actionOptions.subject,
|
|
3229
|
-
severity: actionOptions.severity,
|
|
3230
|
-
message: actionOptions.message,
|
|
3231
|
-
dedupeKey: actionOptions.dedupeKey,
|
|
3232
|
-
data: parseJsonObject(actionOptions.data, {}),
|
|
3233
|
-
metadata: parseJsonObject(actionOptions.metadata, {})
|
|
3234
|
-
}, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
|
|
3235
|
-
print(result, Boolean(actionOptions.json), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
|
|
3236
|
-
});
|
|
3237
|
-
events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", "Limit results", parseNumber).option("-j, --json", "Print JSON output", false).action(async (actionOptions) => {
|
|
3238
|
-
let rows = await createClient(options).listEvents();
|
|
3239
|
-
if (actionOptions.source)
|
|
3240
|
-
rows = rows.filter((event) => event.source === actionOptions.source);
|
|
3241
|
-
if (actionOptions.type)
|
|
3242
|
-
rows = rows.filter((event) => event.type === actionOptions.type);
|
|
3243
|
-
if (actionOptions.limit)
|
|
3244
|
-
rows = rows.slice(-actionOptions.limit);
|
|
3245
|
-
if (actionOptions.json) {
|
|
3246
|
-
console.log(JSON.stringify(rows, null, 2));
|
|
3247
|
-
return;
|
|
3248
|
-
}
|
|
3249
|
-
if (!rows.length) {
|
|
3250
|
-
console.log("No events recorded.");
|
|
3251
|
-
return;
|
|
3252
|
-
}
|
|
3253
|
-
for (const event of rows)
|
|
3254
|
-
console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
|
|
3255
|
-
});
|
|
3256
|
-
events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions) => {
|
|
3257
|
-
const result = await createClient(options).replay({
|
|
3258
|
-
eventId: actionOptions.id,
|
|
3259
|
-
source: actionOptions.source,
|
|
3260
|
-
type: actionOptions.type,
|
|
3261
|
-
dryRun: actionOptions.dryRun
|
|
3262
|
-
});
|
|
3263
|
-
print(result, Boolean(actionOptions.json), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
|
|
3264
|
-
});
|
|
3265
|
-
return events;
|
|
3266
|
-
}
|
|
3267
|
-
function registerEventsCommands(program2, options) {
|
|
3268
|
-
registerWebhookCommands(program2, options);
|
|
3269
|
-
registerEventCommands(program2, options);
|
|
3270
|
-
}
|
|
3271
|
-
function parseNumber(value) {
|
|
3272
|
-
const parsed = Number(value);
|
|
3273
|
-
if (!Number.isFinite(parsed))
|
|
3274
|
-
throw new Error(`Expected a number, got ${value}`);
|
|
3275
|
-
return parsed;
|
|
3276
|
-
}
|
|
3277
|
-
function collectValues(value, previous) {
|
|
3278
|
-
previous.push(value);
|
|
3279
|
-
return previous;
|
|
3280
|
-
}
|
|
3281
|
-
|
|
3282
2604
|
// src/cli/index.ts
|
|
2605
|
+
import { registerEventCommands, registerWebhookCommands } from "@hasna/events/commander";
|
|
3283
2606
|
import { execFileSync } from "child_process";
|
|
3284
2607
|
|
|
3285
2608
|
// node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
@@ -3772,14 +3095,14 @@ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
|
|
|
3772
3095
|
var source_default = chalk;
|
|
3773
3096
|
|
|
3774
3097
|
// src/version.ts
|
|
3775
|
-
import { existsSync
|
|
3776
|
-
import { dirname, join
|
|
3098
|
+
import { existsSync, readFileSync } from "fs";
|
|
3099
|
+
import { dirname, join } from "path";
|
|
3777
3100
|
import { fileURLToPath } from "url";
|
|
3778
3101
|
function getPackageVersion() {
|
|
3779
3102
|
try {
|
|
3780
3103
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
3781
|
-
const candidates = [
|
|
3782
|
-
const pkgPath = candidates.find((candidate) =>
|
|
3104
|
+
const candidates = [join(here, "..", "package.json"), join(here, "..", "..", "package.json")];
|
|
3105
|
+
const pkgPath = candidates.find((candidate) => existsSync(candidate));
|
|
3783
3106
|
if (!pkgPath) {
|
|
3784
3107
|
return "0.0.0";
|
|
3785
3108
|
}
|
|
@@ -3790,8 +3113,8 @@ function getPackageVersion() {
|
|
|
3790
3113
|
}
|
|
3791
3114
|
|
|
3792
3115
|
// src/manifests.ts
|
|
3793
|
-
import { existsSync as
|
|
3794
|
-
import { arch, homedir
|
|
3116
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
3117
|
+
import { arch, homedir, hostname, platform, userInfo } from "os";
|
|
3795
3118
|
import { dirname as dirname3 } from "path";
|
|
3796
3119
|
|
|
3797
3120
|
// node_modules/zod/v3/external.js
|
|
@@ -7805,7 +7128,7 @@ var fleetSchema = exports_external.object({
|
|
|
7805
7128
|
machines: exports_external.array(machineSchema)
|
|
7806
7129
|
});
|
|
7807
7130
|
function detectWorkspacePath() {
|
|
7808
|
-
const home =
|
|
7131
|
+
const home = homedir();
|
|
7809
7132
|
if (platform() === "darwin") {
|
|
7810
7133
|
return `${home}/Workspace`;
|
|
7811
7134
|
}
|
|
@@ -7829,7 +7152,7 @@ function getDefaultManifest() {
|
|
|
7829
7152
|
};
|
|
7830
7153
|
}
|
|
7831
7154
|
function readManifest(path = getManifestPath()) {
|
|
7832
|
-
if (!
|
|
7155
|
+
if (!existsSync3(path)) {
|
|
7833
7156
|
return getDefaultManifest();
|
|
7834
7157
|
}
|
|
7835
7158
|
const raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
@@ -7910,7 +7233,7 @@ function manifestValidate() {
|
|
|
7910
7233
|
}
|
|
7911
7234
|
|
|
7912
7235
|
// src/commands/setup.ts
|
|
7913
|
-
import { homedir as
|
|
7236
|
+
import { homedir as homedir2 } from "os";
|
|
7914
7237
|
init_db();
|
|
7915
7238
|
|
|
7916
7239
|
// src/remote.ts
|
|
@@ -7920,7 +7243,7 @@ import { hostname as hostname4 } from "os";
|
|
|
7920
7243
|
|
|
7921
7244
|
// src/topology.ts
|
|
7922
7245
|
init_db();
|
|
7923
|
-
import { existsSync as
|
|
7246
|
+
import { existsSync as existsSync4 } from "fs";
|
|
7924
7247
|
import { arch as arch2, hostname as hostname3, platform as platform2, userInfo as userInfo2 } from "os";
|
|
7925
7248
|
import { spawnSync } from "child_process";
|
|
7926
7249
|
init_paths();
|
|
@@ -8033,6 +7356,19 @@ function manifestHostReachable(target) {
|
|
|
8033
7356
|
return null;
|
|
8034
7357
|
return overrides.has(target);
|
|
8035
7358
|
}
|
|
7359
|
+
function userFromSshAddress(address) {
|
|
7360
|
+
if (!address)
|
|
7361
|
+
return null;
|
|
7362
|
+
const at = address.indexOf("@");
|
|
7363
|
+
if (at <= 0)
|
|
7364
|
+
return null;
|
|
7365
|
+
return address.slice(0, at);
|
|
7366
|
+
}
|
|
7367
|
+
function commandTargetForRoute(target, user) {
|
|
7368
|
+
if (target.kind === "local" || target.target.includes("@") || !user)
|
|
7369
|
+
return target.target;
|
|
7370
|
+
return `${user}@${target.target}`;
|
|
7371
|
+
}
|
|
8036
7372
|
function routeHints(input) {
|
|
8037
7373
|
const hints = [];
|
|
8038
7374
|
if (input.machineId === input.localMachineId) {
|
|
@@ -8083,6 +7419,7 @@ function buildEntry(input) {
|
|
|
8083
7419
|
});
|
|
8084
7420
|
const selectedRoute = selectRouteHint(hints);
|
|
8085
7421
|
const route = selectedRoute?.kind === "ssh" ? "ssh" : selectedRoute?.kind ?? "unknown";
|
|
7422
|
+
const routeUser = userFromSshAddress(manifest?.sshAddress) ?? (typeof manifest?.metadata?.user === "string" ? manifest.metadata.user : null);
|
|
8086
7423
|
return {
|
|
8087
7424
|
machine_id: input.machineId,
|
|
8088
7425
|
hostname: manifest?.hostname ?? peer?.HostName ?? null,
|
|
@@ -8103,7 +7440,7 @@ function buildEntry(input) {
|
|
|
8103
7440
|
ssh: {
|
|
8104
7441
|
address: manifest?.sshAddress ?? null,
|
|
8105
7442
|
route,
|
|
8106
|
-
command_target: selectedRoute
|
|
7443
|
+
command_target: selectedRoute ? commandTargetForRoute(selectedRoute, routeUser) : null
|
|
8107
7444
|
},
|
|
8108
7445
|
route_hints: hints,
|
|
8109
7446
|
tags: manifest?.tags ?? [],
|
|
@@ -8111,7 +7448,7 @@ function buildEntry(input) {
|
|
|
8111
7448
|
};
|
|
8112
7449
|
}
|
|
8113
7450
|
function discoverMachineTopology(options = {}) {
|
|
8114
|
-
const
|
|
7451
|
+
const now = options.now ?? new Date;
|
|
8115
7452
|
const runner = options.runner ?? defaultRunner;
|
|
8116
7453
|
const warnings = [];
|
|
8117
7454
|
const manifest = readManifest();
|
|
@@ -8143,11 +7480,11 @@ function discoverMachineTopology(options = {}) {
|
|
|
8143
7480
|
version: getPackageVersion()
|
|
8144
7481
|
},
|
|
8145
7482
|
capabilities: getMachinesConsumerCapabilities(),
|
|
8146
|
-
generated_at:
|
|
7483
|
+
generated_at: now.toISOString(),
|
|
8147
7484
|
local_machine_id: localMachineId,
|
|
8148
7485
|
local_hostname: hostname3(),
|
|
8149
7486
|
current_platform: normalizePlatform2(),
|
|
8150
|
-
manifest_path_known:
|
|
7487
|
+
manifest_path_known: existsSync4(getManifestPath()),
|
|
8151
7488
|
machines,
|
|
8152
7489
|
warnings
|
|
8153
7490
|
};
|
|
@@ -8266,11 +7603,11 @@ function cacheability(input) {
|
|
|
8266
7603
|
};
|
|
8267
7604
|
}
|
|
8268
7605
|
function resolveMachineRoute(machineId, options = {}) {
|
|
8269
|
-
const
|
|
7606
|
+
const now = options.now ?? new Date;
|
|
8270
7607
|
const topology = options.topology ?? discoverMachineTopology(options);
|
|
8271
7608
|
const warnings = [...topology.warnings];
|
|
8272
7609
|
const { machine, matchedBy } = findRouteMachine(topology, machineId);
|
|
8273
|
-
const generatedAt =
|
|
7610
|
+
const generatedAt = now.toISOString();
|
|
8274
7611
|
if (!machine) {
|
|
8275
7612
|
warnings.push(`machine_not_found:${machineId}`);
|
|
8276
7613
|
return {
|
|
@@ -8296,8 +7633,8 @@ function resolveMachineRoute(machineId, options = {}) {
|
|
|
8296
7633
|
},
|
|
8297
7634
|
cacheability: cacheability({
|
|
8298
7635
|
ok: false,
|
|
8299
|
-
observedAt:
|
|
8300
|
-
now
|
|
7636
|
+
observedAt: now,
|
|
7637
|
+
now,
|
|
8301
7638
|
ttlMs: options.resolverTtlMs,
|
|
8302
7639
|
authority: "unresolved",
|
|
8303
7640
|
confidence: "none",
|
|
@@ -8311,6 +7648,7 @@ function resolveMachineRoute(machineId, options = {}) {
|
|
|
8311
7648
|
const local = route === "local" || machine.machine_id === topology.local_machine_id;
|
|
8312
7649
|
const confidence = routeConfidence({ machine, hint: selectedHint, matchedBy });
|
|
8313
7650
|
const ok = Boolean(selectedHint?.target);
|
|
7651
|
+
const commandTarget = selectedHint ? commandTargetForRoute(selectedHint, userFromSshAddress(machine.ssh.address) ?? machine.user) : null;
|
|
8314
7652
|
return {
|
|
8315
7653
|
schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
|
|
8316
7654
|
package: topology.package,
|
|
@@ -8321,7 +7659,7 @@ function resolveMachineRoute(machineId, options = {}) {
|
|
|
8321
7659
|
route,
|
|
8322
7660
|
source: route,
|
|
8323
7661
|
target: selectedHint?.target ?? null,
|
|
8324
|
-
command_target:
|
|
7662
|
+
command_target: commandTarget,
|
|
8325
7663
|
confidence,
|
|
8326
7664
|
local,
|
|
8327
7665
|
evidence: {
|
|
@@ -8334,8 +7672,8 @@ function resolveMachineRoute(machineId, options = {}) {
|
|
|
8334
7672
|
},
|
|
8335
7673
|
cacheability: cacheability({
|
|
8336
7674
|
ok,
|
|
8337
|
-
observedAt:
|
|
8338
|
-
now
|
|
7675
|
+
observedAt: now,
|
|
7676
|
+
now,
|
|
8339
7677
|
ttlMs: options.resolverTtlMs,
|
|
8340
7678
|
authority: routeAuthority({ machine, selectedHint, matchedBy }),
|
|
8341
7679
|
confidence,
|
|
@@ -8422,7 +7760,7 @@ function canCheckPathForMachine(machine, localMachineId) {
|
|
|
8422
7760
|
function checkedPathExists(path, check) {
|
|
8423
7761
|
if (!path || !check)
|
|
8424
7762
|
return null;
|
|
8425
|
-
return
|
|
7763
|
+
return existsSync4(path);
|
|
8426
7764
|
}
|
|
8427
7765
|
function repairHint(input) {
|
|
8428
7766
|
const command = [
|
|
@@ -8637,11 +7975,11 @@ function metadataKeysForDiagnostics(metadata) {
|
|
|
8637
7975
|
return Object.keys(metadata).filter((key) => !/(secret|token|key|password|credential)/i.test(key)).sort();
|
|
8638
7976
|
}
|
|
8639
7977
|
function resolveMachineWorkspace(options) {
|
|
8640
|
-
const
|
|
7978
|
+
const now = options.now ?? new Date;
|
|
8641
7979
|
const topology = options.topology ?? discoverMachineTopology(options);
|
|
8642
7980
|
const warnings = [...topology.warnings];
|
|
8643
7981
|
const { machine, matchedBy } = findRouteMachine(topology, options.machineId);
|
|
8644
|
-
const generatedAt =
|
|
7982
|
+
const generatedAt = now.toISOString();
|
|
8645
7983
|
const repoName = options.repoName ?? options.projectId;
|
|
8646
7984
|
const openFilesRepoName = options.openFilesRepoName ?? "open-files";
|
|
8647
7985
|
if (!machine) {
|
|
@@ -8670,8 +8008,8 @@ function resolveMachineWorkspace(options) {
|
|
|
8670
8008
|
},
|
|
8671
8009
|
cacheability: cacheability({
|
|
8672
8010
|
ok: false,
|
|
8673
|
-
observedAt:
|
|
8674
|
-
now
|
|
8011
|
+
observedAt: now,
|
|
8012
|
+
now,
|
|
8675
8013
|
ttlMs: options.resolverTtlMs,
|
|
8676
8014
|
authority: "unresolved",
|
|
8677
8015
|
confidence: "none",
|
|
@@ -8743,8 +8081,8 @@ function resolveMachineWorkspace(options) {
|
|
|
8743
8081
|
},
|
|
8744
8082
|
cacheability: cacheability({
|
|
8745
8083
|
ok: workspaceOk,
|
|
8746
|
-
observedAt:
|
|
8747
|
-
now
|
|
8084
|
+
observedAt: now,
|
|
8085
|
+
now,
|
|
8748
8086
|
ttlMs: options.resolverTtlMs,
|
|
8749
8087
|
authority: workspaceAuthority(workspacePaths),
|
|
8750
8088
|
confidence: workspaceOk ? "medium" : "none",
|
|
@@ -8779,7 +8117,7 @@ function resolveSshTarget(machineId, options = {}) {
|
|
|
8779
8117
|
}
|
|
8780
8118
|
return {
|
|
8781
8119
|
machineId: resolved.machine_id ?? machineId,
|
|
8782
|
-
target: resolved.target,
|
|
8120
|
+
target: resolved.command_target ?? resolved.target,
|
|
8783
8121
|
route: resolved.route,
|
|
8784
8122
|
confidence: resolved.confidence,
|
|
8785
8123
|
warnings: resolved.warnings
|
|
@@ -8913,7 +8251,7 @@ function buildSetupPlan(machineId) {
|
|
|
8913
8251
|
const target = selected || {
|
|
8914
8252
|
id: currentMachineId,
|
|
8915
8253
|
platform: "linux",
|
|
8916
|
-
workspacePath: `${
|
|
8254
|
+
workspacePath: `${homedir2()}/workspace`
|
|
8917
8255
|
};
|
|
8918
8256
|
const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
|
|
8919
8257
|
return {
|
|
@@ -8958,8 +8296,8 @@ function runSetup(machineId, options = {}, runner = runMachineCommand) {
|
|
|
8958
8296
|
}
|
|
8959
8297
|
|
|
8960
8298
|
// src/commands/backup.ts
|
|
8961
|
-
import { homedir as
|
|
8962
|
-
import { join as
|
|
8299
|
+
import { homedir as homedir3, hostname as hostname5 } from "os";
|
|
8300
|
+
import { join as join3 } from "path";
|
|
8963
8301
|
var MACHINES_BACKUP_BUCKET_ENV = "HASNA_MACHINES_S3_BUCKET";
|
|
8964
8302
|
var MACHINES_BACKUP_BUCKET_FALLBACK_ENV = "MACHINES_S3_BUCKET";
|
|
8965
8303
|
var MACHINES_BACKUP_PREFIX_ENV = "HASNA_MACHINES_S3_PREFIX";
|
|
@@ -9007,16 +8345,16 @@ function resolveBackupTarget(options = {}) {
|
|
|
9007
8345
|
};
|
|
9008
8346
|
}
|
|
9009
8347
|
function defaultBackupSources() {
|
|
9010
|
-
const home =
|
|
8348
|
+
const home = homedir3();
|
|
9011
8349
|
return [
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
8350
|
+
join3(home, ".hasna"),
|
|
8351
|
+
join3(home, ".ssh"),
|
|
8352
|
+
join3(home, ".secrets")
|
|
9015
8353
|
];
|
|
9016
8354
|
}
|
|
9017
8355
|
function buildBackupPlan(bucket, prefix) {
|
|
9018
8356
|
const target = resolveBackupTarget({ bucket, prefix });
|
|
9019
|
-
const archivePath =
|
|
8357
|
+
const archivePath = join3(homedir3(), ".hasna", "machines", "backup.tgz");
|
|
9020
8358
|
const sources = defaultBackupSources();
|
|
9021
8359
|
const steps = [
|
|
9022
8360
|
{
|
|
@@ -9067,21 +8405,21 @@ function runBackup(bucket, prefix, options = {}) {
|
|
|
9067
8405
|
}
|
|
9068
8406
|
|
|
9069
8407
|
// src/commands/cert.ts
|
|
9070
|
-
import { homedir as
|
|
9071
|
-
import { join as
|
|
8408
|
+
import { homedir as homedir4, platform as platform3 } from "os";
|
|
8409
|
+
import { join as join4 } from "path";
|
|
9072
8410
|
function quote3(value) {
|
|
9073
8411
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
9074
8412
|
}
|
|
9075
8413
|
function certDir() {
|
|
9076
|
-
return
|
|
8414
|
+
return join4(homedir4(), ".hasna", "machines", "certs");
|
|
9077
8415
|
}
|
|
9078
8416
|
function buildCertPlan(domains) {
|
|
9079
8417
|
if (domains.length === 0) {
|
|
9080
8418
|
throw new Error("At least one domain is required.");
|
|
9081
8419
|
}
|
|
9082
8420
|
const primary = domains[0];
|
|
9083
|
-
const certPath =
|
|
9084
|
-
const keyPath =
|
|
8421
|
+
const certPath = join4(certDir(), `${primary}.pem`);
|
|
8422
|
+
const keyPath = join4(certDir(), `${primary}-key.pem`);
|
|
9085
8423
|
const steps = [];
|
|
9086
8424
|
if (platform3() === "darwin") {
|
|
9087
8425
|
steps.push({
|
|
@@ -9146,14 +8484,14 @@ function runCertPlan(domains, options = {}) {
|
|
|
9146
8484
|
|
|
9147
8485
|
// src/commands/dns.ts
|
|
9148
8486
|
init_paths();
|
|
9149
|
-
import { existsSync as
|
|
9150
|
-
import { join as
|
|
8487
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
8488
|
+
import { join as join5 } from "path";
|
|
9151
8489
|
function getDnsPath() {
|
|
9152
|
-
return
|
|
8490
|
+
return join5(getDataDir(), "dns.json");
|
|
9153
8491
|
}
|
|
9154
8492
|
function readMappings() {
|
|
9155
8493
|
const path = getDnsPath();
|
|
9156
|
-
if (!
|
|
8494
|
+
if (!existsSync5(path))
|
|
9157
8495
|
return [];
|
|
9158
8496
|
return JSON.parse(readFileSync3(path, "utf8"));
|
|
9159
8497
|
}
|
|
@@ -9182,10 +8520,10 @@ function renderDomainMapping(domain) {
|
|
|
9182
8520
|
hostsEntry: `${entry.targetHost} ${entry.domain}`,
|
|
9183
8521
|
caddySnippet: `${entry.domain} {
|
|
9184
8522
|
reverse_proxy 127.0.0.1:${entry.port}
|
|
9185
|
-
tls ${
|
|
8523
|
+
tls ${join5(getDataDir(), "certs", `${entry.domain}.pem`)} ${join5(getDataDir(), "certs", `${entry.domain}-key.pem`)}
|
|
9186
8524
|
}`,
|
|
9187
|
-
certPath:
|
|
9188
|
-
keyPath:
|
|
8525
|
+
certPath: join5(getDataDir(), "certs", `${entry.domain}.pem`),
|
|
8526
|
+
keyPath: join5(getDataDir(), "certs", `${entry.domain}-key.pem`)
|
|
9189
8527
|
};
|
|
9190
8528
|
}
|
|
9191
8529
|
|
|
@@ -9542,7 +8880,7 @@ function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand
|
|
|
9542
8880
|
}
|
|
9543
8881
|
|
|
9544
8882
|
// src/commands/notifications.ts
|
|
9545
|
-
import { existsSync as
|
|
8883
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
9546
8884
|
init_paths();
|
|
9547
8885
|
var notificationChannelSchema = exports_external.object({
|
|
9548
8886
|
id: exports_external.string(),
|
|
@@ -9625,7 +8963,7 @@ ${message}
|
|
|
9625
8963
|
}
|
|
9626
8964
|
throw new Error("No local email transport available. Install sendmail or mail.");
|
|
9627
8965
|
}
|
|
9628
|
-
async function
|
|
8966
|
+
async function dispatchWebhook(channel, event, message) {
|
|
9629
8967
|
const response = await fetch(channel.target, {
|
|
9630
8968
|
method: "POST",
|
|
9631
8969
|
headers: {
|
|
@@ -9650,7 +8988,7 @@ async function dispatchWebhook2(channel, event, message) {
|
|
|
9650
8988
|
detail: `Webhook accepted with HTTP ${response.status}`
|
|
9651
8989
|
};
|
|
9652
8990
|
}
|
|
9653
|
-
async function
|
|
8991
|
+
async function dispatchCommand(channel, event, message) {
|
|
9654
8992
|
const result = Bun.spawnSync(["bash", "-lc", channel.target], {
|
|
9655
8993
|
stdout: "pipe",
|
|
9656
8994
|
stderr: "pipe",
|
|
@@ -9673,7 +9011,7 @@ async function dispatchCommand2(channel, event, message) {
|
|
|
9673
9011
|
detail: stdout || "Command completed successfully"
|
|
9674
9012
|
};
|
|
9675
9013
|
}
|
|
9676
|
-
async function
|
|
9014
|
+
async function dispatchChannel(channel, event, message) {
|
|
9677
9015
|
if (!channel.enabled) {
|
|
9678
9016
|
return {
|
|
9679
9017
|
channelId: channel.id,
|
|
@@ -9687,9 +9025,9 @@ async function dispatchChannel2(channel, event, message) {
|
|
|
9687
9025
|
return dispatchEmail(channel, event, message);
|
|
9688
9026
|
}
|
|
9689
9027
|
if (channel.type === "webhook") {
|
|
9690
|
-
return
|
|
9028
|
+
return dispatchWebhook(channel, event, message);
|
|
9691
9029
|
}
|
|
9692
|
-
return
|
|
9030
|
+
return dispatchCommand(channel, event, message);
|
|
9693
9031
|
}
|
|
9694
9032
|
function getDefaultNotificationConfig() {
|
|
9695
9033
|
return {
|
|
@@ -9699,7 +9037,7 @@ function getDefaultNotificationConfig() {
|
|
|
9699
9037
|
};
|
|
9700
9038
|
}
|
|
9701
9039
|
function readNotificationConfig(path = getNotificationsPath()) {
|
|
9702
|
-
if (!
|
|
9040
|
+
if (!existsSync6(path)) {
|
|
9703
9041
|
return getDefaultNotificationConfig();
|
|
9704
9042
|
}
|
|
9705
9043
|
return notificationConfigSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
|
|
@@ -9744,7 +9082,7 @@ async function dispatchNotificationEvent(event, message, options = {}) {
|
|
|
9744
9082
|
const deliveries = [];
|
|
9745
9083
|
for (const channel of channels) {
|
|
9746
9084
|
try {
|
|
9747
|
-
deliveries.push(await
|
|
9085
|
+
deliveries.push(await dispatchChannel(channel, event, message));
|
|
9748
9086
|
} catch (error) {
|
|
9749
9087
|
deliveries.push({
|
|
9750
9088
|
channelId: channel.id,
|
|
@@ -9779,7 +9117,7 @@ async function testNotificationChannel(channelId, event = "manual.test", message
|
|
|
9779
9117
|
if (!options.yes) {
|
|
9780
9118
|
throw new Error("Notification test execution requires --yes.");
|
|
9781
9119
|
}
|
|
9782
|
-
const delivery = await
|
|
9120
|
+
const delivery = await dispatchChannel(channel, event, message);
|
|
9783
9121
|
return {
|
|
9784
9122
|
channelId,
|
|
9785
9123
|
mode: "apply",
|
|
@@ -9847,528 +9185,7 @@ function listPorts(machineId) {
|
|
|
9847
9185
|
// src/commands/runtime.ts
|
|
9848
9186
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
9849
9187
|
import { setTimeout as sleep } from "timers/promises";
|
|
9850
|
-
|
|
9851
|
-
// node_modules/@hasna/events/dist/index.js
|
|
9852
|
-
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
|
|
9853
|
-
import { existsSync as existsSync8 } from "fs";
|
|
9854
|
-
import { homedir as homedir6 } from "os";
|
|
9855
|
-
import { join as join7 } from "path";
|
|
9856
|
-
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
9857
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
9858
|
-
import { spawn as spawn2 } from "child_process";
|
|
9859
|
-
import { randomUUID as randomUUID22 } from "crypto";
|
|
9860
|
-
function getPathValue2(input, path) {
|
|
9861
|
-
return path.split(".").reduce((value, part) => {
|
|
9862
|
-
if (value && typeof value === "object" && part in value) {
|
|
9863
|
-
return value[part];
|
|
9864
|
-
}
|
|
9865
|
-
return;
|
|
9866
|
-
}, input);
|
|
9867
|
-
}
|
|
9868
|
-
function wildcardToRegExp2(pattern) {
|
|
9869
|
-
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
|
|
9870
|
-
return new RegExp(`^${escaped}$`);
|
|
9871
|
-
}
|
|
9872
|
-
function matchString2(value, matcher) {
|
|
9873
|
-
if (matcher === undefined)
|
|
9874
|
-
return true;
|
|
9875
|
-
if (value === undefined)
|
|
9876
|
-
return false;
|
|
9877
|
-
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
9878
|
-
return matchers.some((item) => wildcardToRegExp2(item).test(value));
|
|
9879
|
-
}
|
|
9880
|
-
function matchRecord2(input, matcher) {
|
|
9881
|
-
if (!matcher)
|
|
9882
|
-
return true;
|
|
9883
|
-
return Object.entries(matcher).every(([path, expected]) => {
|
|
9884
|
-
const actual = getPathValue2(input, path);
|
|
9885
|
-
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
9886
|
-
return matchString2(actual === undefined ? undefined : String(actual), expected);
|
|
9887
|
-
}
|
|
9888
|
-
return actual === expected;
|
|
9889
|
-
});
|
|
9890
|
-
}
|
|
9891
|
-
function eventMatchesFilter2(event, filter) {
|
|
9892
|
-
return matchString2(event.source, filter.source) && matchString2(event.type, filter.type) && matchString2(event.subject, filter.subject) && matchString2(event.severity, filter.severity) && matchRecord2(event.data, filter.data) && matchRecord2(event.metadata, filter.metadata);
|
|
9893
|
-
}
|
|
9894
|
-
function channelMatchesEvent2(channel, event) {
|
|
9895
|
-
if (!channel.enabled)
|
|
9896
|
-
return false;
|
|
9897
|
-
if (!channel.filters || channel.filters.length === 0)
|
|
9898
|
-
return true;
|
|
9899
|
-
return channel.filters.some((filter) => eventMatchesFilter2(event, filter));
|
|
9900
|
-
}
|
|
9901
|
-
var HASNA_EVENTS_DIR_ENV2 = "HASNA_EVENTS_DIR";
|
|
9902
|
-
var HASNA_EVENTS_HOME_ENV2 = "HASNA_EVENTS_HOME";
|
|
9903
|
-
function getEventsDataDir2(override) {
|
|
9904
|
-
return override || process.env[HASNA_EVENTS_DIR_ENV2] || process.env[HASNA_EVENTS_HOME_ENV2] || join7(homedir6(), ".hasna", "events");
|
|
9905
|
-
}
|
|
9906
|
-
|
|
9907
|
-
class JsonEventsStore2 {
|
|
9908
|
-
dataDir;
|
|
9909
|
-
channelsPath;
|
|
9910
|
-
eventsPath;
|
|
9911
|
-
deliveriesPath;
|
|
9912
|
-
constructor(dataDir = getEventsDataDir2()) {
|
|
9913
|
-
this.dataDir = dataDir;
|
|
9914
|
-
this.channelsPath = join7(dataDir, "channels.json");
|
|
9915
|
-
this.eventsPath = join7(dataDir, "events.json");
|
|
9916
|
-
this.deliveriesPath = join7(dataDir, "deliveries.json");
|
|
9917
|
-
}
|
|
9918
|
-
async init() {
|
|
9919
|
-
await mkdir2(this.dataDir, { recursive: true, mode: 448 });
|
|
9920
|
-
await chmod2(this.dataDir, 448).catch(() => {
|
|
9921
|
-
return;
|
|
9922
|
-
});
|
|
9923
|
-
await this.ensureArrayFile(this.channelsPath);
|
|
9924
|
-
await this.ensureArrayFile(this.eventsPath);
|
|
9925
|
-
await this.ensureArrayFile(this.deliveriesPath);
|
|
9926
|
-
}
|
|
9927
|
-
async addChannel(channel) {
|
|
9928
|
-
await this.init();
|
|
9929
|
-
const channels = await this.readJson(this.channelsPath, []);
|
|
9930
|
-
const index = channels.findIndex((item) => item.id === channel.id);
|
|
9931
|
-
if (index >= 0) {
|
|
9932
|
-
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
9933
|
-
} else {
|
|
9934
|
-
channels.push(channel);
|
|
9935
|
-
}
|
|
9936
|
-
await this.writeJson(this.channelsPath, channels);
|
|
9937
|
-
return index >= 0 ? channels[index] : channel;
|
|
9938
|
-
}
|
|
9939
|
-
async listChannels() {
|
|
9940
|
-
await this.init();
|
|
9941
|
-
return this.readJson(this.channelsPath, []);
|
|
9942
|
-
}
|
|
9943
|
-
async getChannel(id) {
|
|
9944
|
-
const channels = await this.listChannels();
|
|
9945
|
-
return channels.find((channel) => channel.id === id);
|
|
9946
|
-
}
|
|
9947
|
-
async removeChannel(id) {
|
|
9948
|
-
await this.init();
|
|
9949
|
-
const channels = await this.readJson(this.channelsPath, []);
|
|
9950
|
-
const next = channels.filter((channel) => channel.id !== id);
|
|
9951
|
-
await this.writeJson(this.channelsPath, next);
|
|
9952
|
-
return next.length !== channels.length;
|
|
9953
|
-
}
|
|
9954
|
-
async appendEvent(event) {
|
|
9955
|
-
await this.init();
|
|
9956
|
-
const events = await this.readJson(this.eventsPath, []);
|
|
9957
|
-
events.push(event);
|
|
9958
|
-
await this.writeJson(this.eventsPath, events);
|
|
9959
|
-
return event;
|
|
9960
|
-
}
|
|
9961
|
-
async listEvents() {
|
|
9962
|
-
await this.init();
|
|
9963
|
-
return this.readJson(this.eventsPath, []);
|
|
9964
|
-
}
|
|
9965
|
-
async findEventByIdentity(identity) {
|
|
9966
|
-
const events = await this.listEvents();
|
|
9967
|
-
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
9968
|
-
}
|
|
9969
|
-
async appendDelivery(result) {
|
|
9970
|
-
await this.init();
|
|
9971
|
-
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
9972
|
-
deliveries.push(result);
|
|
9973
|
-
await this.writeJson(this.deliveriesPath, deliveries);
|
|
9974
|
-
return result;
|
|
9975
|
-
}
|
|
9976
|
-
async listDeliveries() {
|
|
9977
|
-
await this.init();
|
|
9978
|
-
return this.readJson(this.deliveriesPath, []);
|
|
9979
|
-
}
|
|
9980
|
-
async exportData() {
|
|
9981
|
-
return {
|
|
9982
|
-
channels: await this.listChannels(),
|
|
9983
|
-
events: await this.listEvents(),
|
|
9984
|
-
deliveries: await this.listDeliveries()
|
|
9985
|
-
};
|
|
9986
|
-
}
|
|
9987
|
-
async ensureArrayFile(path) {
|
|
9988
|
-
if (!existsSync8(path)) {
|
|
9989
|
-
await writeFile2(path, `[]
|
|
9990
|
-
`, { encoding: "utf-8", mode: 384 });
|
|
9991
|
-
}
|
|
9992
|
-
await chmod2(path, 384).catch(() => {
|
|
9993
|
-
return;
|
|
9994
|
-
});
|
|
9995
|
-
}
|
|
9996
|
-
async readJson(path, fallback) {
|
|
9997
|
-
try {
|
|
9998
|
-
const raw = await readFile2(path, "utf-8");
|
|
9999
|
-
if (!raw.trim())
|
|
10000
|
-
return fallback;
|
|
10001
|
-
return JSON.parse(raw);
|
|
10002
|
-
} catch (error) {
|
|
10003
|
-
if (error.code === "ENOENT")
|
|
10004
|
-
return fallback;
|
|
10005
|
-
throw error;
|
|
10006
|
-
}
|
|
10007
|
-
}
|
|
10008
|
-
async writeJson(path, value) {
|
|
10009
|
-
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
10010
|
-
await writeFile2(tempPath, `${JSON.stringify(value, null, 2)}
|
|
10011
|
-
`, { encoding: "utf-8", mode: 384 });
|
|
10012
|
-
await rename2(tempPath, path);
|
|
10013
|
-
await chmod2(path, 384).catch(() => {
|
|
10014
|
-
return;
|
|
10015
|
-
});
|
|
10016
|
-
}
|
|
10017
|
-
}
|
|
10018
|
-
var DEFAULT_SIGNATURE_TOLERANCE_MS2 = 5 * 60 * 1000;
|
|
10019
|
-
function buildSignatureBase2(timestamp, body) {
|
|
10020
|
-
return `${timestamp}.${body}`;
|
|
10021
|
-
}
|
|
10022
|
-
function signPayload2(secret, timestamp, body) {
|
|
10023
|
-
const digest = createHmac2("sha256", secret).update(buildSignatureBase2(timestamp, body)).digest("hex");
|
|
10024
|
-
return `sha256=${digest}`;
|
|
10025
|
-
}
|
|
10026
|
-
function now2() {
|
|
10027
|
-
return new Date().toISOString();
|
|
10028
|
-
}
|
|
10029
|
-
function truncate2(value, max = 4096) {
|
|
10030
|
-
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
10031
|
-
}
|
|
10032
|
-
function buildWebhookRequest2(event, channel) {
|
|
10033
|
-
if (!channel.webhook)
|
|
10034
|
-
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
10035
|
-
const body = JSON.stringify(event);
|
|
10036
|
-
const timestamp = event.time;
|
|
10037
|
-
const headers = {
|
|
10038
|
-
"Content-Type": "application/json",
|
|
10039
|
-
"User-Agent": "@hasna/events",
|
|
10040
|
-
"X-Hasna-Event-Id": event.id,
|
|
10041
|
-
"X-Hasna-Event-Type": event.type,
|
|
10042
|
-
"X-Hasna-Timestamp": timestamp,
|
|
10043
|
-
...channel.webhook.headers
|
|
10044
|
-
};
|
|
10045
|
-
if (channel.webhook.secret) {
|
|
10046
|
-
headers["X-Hasna-Signature"] = signPayload2(channel.webhook.secret, timestamp, body);
|
|
10047
|
-
}
|
|
10048
|
-
return { body, headers };
|
|
10049
|
-
}
|
|
10050
|
-
async function dispatchWebhook3(event, channel, options = {}) {
|
|
10051
|
-
if (!channel.webhook)
|
|
10052
|
-
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
10053
|
-
const startedAt = now2();
|
|
10054
|
-
const { body, headers } = buildWebhookRequest2(event, channel);
|
|
10055
|
-
const controller = new AbortController;
|
|
10056
|
-
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
10057
|
-
try {
|
|
10058
|
-
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
10059
|
-
method: "POST",
|
|
10060
|
-
headers,
|
|
10061
|
-
body,
|
|
10062
|
-
signal: controller.signal
|
|
10063
|
-
});
|
|
10064
|
-
const responseBody = truncate2(await response.text());
|
|
10065
|
-
return {
|
|
10066
|
-
attempt: 1,
|
|
10067
|
-
status: response.ok ? "success" : "failed",
|
|
10068
|
-
startedAt,
|
|
10069
|
-
completedAt: now2(),
|
|
10070
|
-
responseStatus: response.status,
|
|
10071
|
-
responseBody,
|
|
10072
|
-
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
10073
|
-
};
|
|
10074
|
-
} catch (error) {
|
|
10075
|
-
return {
|
|
10076
|
-
attempt: 1,
|
|
10077
|
-
status: "failed",
|
|
10078
|
-
startedAt,
|
|
10079
|
-
completedAt: now2(),
|
|
10080
|
-
error: error instanceof Error ? error.message : String(error)
|
|
10081
|
-
};
|
|
10082
|
-
} finally {
|
|
10083
|
-
clearTimeout(timeout);
|
|
10084
|
-
}
|
|
10085
|
-
}
|
|
10086
|
-
async function dispatchCommand3(event, channel) {
|
|
10087
|
-
if (!channel.command)
|
|
10088
|
-
throw new Error(`Channel ${channel.id} has no command config`);
|
|
10089
|
-
const startedAt = now2();
|
|
10090
|
-
const eventJson = JSON.stringify(event);
|
|
10091
|
-
const env2 = {
|
|
10092
|
-
...process.env,
|
|
10093
|
-
...channel.command.env,
|
|
10094
|
-
HASNA_CHANNEL_ID: channel.id,
|
|
10095
|
-
HASNA_EVENT_ID: event.id,
|
|
10096
|
-
HASNA_EVENT_TYPE: event.type,
|
|
10097
|
-
HASNA_EVENT_SOURCE: event.source,
|
|
10098
|
-
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
10099
|
-
HASNA_EVENT_SEVERITY: event.severity,
|
|
10100
|
-
HASNA_EVENT_TIME: event.time,
|
|
10101
|
-
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
10102
|
-
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
10103
|
-
HASNA_EVENT_JSON: eventJson
|
|
10104
|
-
};
|
|
10105
|
-
return new Promise((resolve2) => {
|
|
10106
|
-
const child = spawn2(channel.command.command, channel.command.args ?? [], {
|
|
10107
|
-
cwd: channel.command.cwd,
|
|
10108
|
-
env: env2,
|
|
10109
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
10110
|
-
});
|
|
10111
|
-
let stdout = "";
|
|
10112
|
-
let stderr = "";
|
|
10113
|
-
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
10114
|
-
child.stdin.end(eventJson);
|
|
10115
|
-
child.stdout.on("data", (chunk) => {
|
|
10116
|
-
stdout += chunk.toString();
|
|
10117
|
-
});
|
|
10118
|
-
child.stderr.on("data", (chunk) => {
|
|
10119
|
-
stderr += chunk.toString();
|
|
10120
|
-
});
|
|
10121
|
-
child.on("error", (error) => {
|
|
10122
|
-
clearTimeout(timeout);
|
|
10123
|
-
resolve2({
|
|
10124
|
-
attempt: 1,
|
|
10125
|
-
status: "failed",
|
|
10126
|
-
startedAt,
|
|
10127
|
-
completedAt: now2(),
|
|
10128
|
-
stdout: truncate2(stdout),
|
|
10129
|
-
stderr: truncate2(stderr),
|
|
10130
|
-
error: error.message
|
|
10131
|
-
});
|
|
10132
|
-
});
|
|
10133
|
-
child.on("close", (code, signal) => {
|
|
10134
|
-
clearTimeout(timeout);
|
|
10135
|
-
const success = code === 0;
|
|
10136
|
-
resolve2({
|
|
10137
|
-
attempt: 1,
|
|
10138
|
-
status: success ? "success" : "failed",
|
|
10139
|
-
startedAt,
|
|
10140
|
-
completedAt: now2(),
|
|
10141
|
-
stdout: truncate2(stdout),
|
|
10142
|
-
stderr: truncate2(stderr),
|
|
10143
|
-
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
10144
|
-
});
|
|
10145
|
-
});
|
|
10146
|
-
});
|
|
10147
|
-
}
|
|
10148
|
-
async function dispatchChannel3(event, channel, options = {}) {
|
|
10149
|
-
if (channel.transport === "webhook")
|
|
10150
|
-
return dispatchWebhook3(event, channel, options);
|
|
10151
|
-
if (channel.transport === "command")
|
|
10152
|
-
return dispatchCommand3(event, channel);
|
|
10153
|
-
return {
|
|
10154
|
-
attempt: 1,
|
|
10155
|
-
status: "skipped",
|
|
10156
|
-
startedAt: now2(),
|
|
10157
|
-
completedAt: now2(),
|
|
10158
|
-
error: `Unsupported transport: ${channel.transport}`
|
|
10159
|
-
};
|
|
10160
|
-
}
|
|
10161
|
-
function createDeliveryResult2(event, channel, attempts) {
|
|
10162
|
-
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
10163
|
-
return {
|
|
10164
|
-
id: randomUUID3(),
|
|
10165
|
-
eventId: event.id,
|
|
10166
|
-
channelId: channel.id,
|
|
10167
|
-
transport: channel.transport,
|
|
10168
|
-
status,
|
|
10169
|
-
attempts,
|
|
10170
|
-
createdAt: attempts[0]?.startedAt ?? now2(),
|
|
10171
|
-
completedAt: attempts.at(-1)?.completedAt ?? now2()
|
|
10172
|
-
};
|
|
10173
|
-
}
|
|
10174
|
-
function createEvent2(input) {
|
|
10175
|
-
return {
|
|
10176
|
-
id: input.id ?? randomUUID22(),
|
|
10177
|
-
source: input.source,
|
|
10178
|
-
type: input.type,
|
|
10179
|
-
time: normalizeTime2(input.time),
|
|
10180
|
-
subject: input.subject,
|
|
10181
|
-
severity: input.severity ?? "info",
|
|
10182
|
-
data: input.data ?? {},
|
|
10183
|
-
message: input.message,
|
|
10184
|
-
dedupeKey: input.dedupeKey,
|
|
10185
|
-
schemaVersion: input.schemaVersion ?? "1.0",
|
|
10186
|
-
metadata: input.metadata ?? {}
|
|
10187
|
-
};
|
|
10188
|
-
}
|
|
10189
|
-
|
|
10190
|
-
class EventsClient2 {
|
|
10191
|
-
store;
|
|
10192
|
-
redactors;
|
|
10193
|
-
transportOptions;
|
|
10194
|
-
constructor(options = {}) {
|
|
10195
|
-
this.store = options.store ?? new JsonEventsStore2(options.dataDir);
|
|
10196
|
-
this.redactors = options.redactors ?? [];
|
|
10197
|
-
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
10198
|
-
}
|
|
10199
|
-
async addChannel(input) {
|
|
10200
|
-
const timestamp = new Date().toISOString();
|
|
10201
|
-
return this.store.addChannel({
|
|
10202
|
-
...input,
|
|
10203
|
-
createdAt: input.createdAt ?? timestamp,
|
|
10204
|
-
updatedAt: input.updatedAt ?? timestamp
|
|
10205
|
-
});
|
|
10206
|
-
}
|
|
10207
|
-
async listChannels() {
|
|
10208
|
-
return this.store.listChannels();
|
|
10209
|
-
}
|
|
10210
|
-
async removeChannel(id) {
|
|
10211
|
-
return this.store.removeChannel(id);
|
|
10212
|
-
}
|
|
10213
|
-
async emit(input, options = {}) {
|
|
10214
|
-
const event = options.redactSensitiveData === false ? createEvent2(input) : redactSensitiveKeys2(createEvent2(input));
|
|
10215
|
-
if (options.dedupe !== false) {
|
|
10216
|
-
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
10217
|
-
if (existing) {
|
|
10218
|
-
return { event: existing, deliveries: [], deduped: true };
|
|
10219
|
-
}
|
|
10220
|
-
}
|
|
10221
|
-
await this.store.appendEvent(event);
|
|
10222
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
10223
|
-
return { event, deliveries, deduped: false };
|
|
10224
|
-
}
|
|
10225
|
-
async listEvents() {
|
|
10226
|
-
return this.store.listEvents();
|
|
10227
|
-
}
|
|
10228
|
-
async listDeliveries() {
|
|
10229
|
-
return this.store.listDeliveries();
|
|
10230
|
-
}
|
|
10231
|
-
async deliver(event) {
|
|
10232
|
-
const channels = await this.store.listChannels();
|
|
10233
|
-
const selected = channels.filter((channel) => channelMatchesEvent2(channel, event));
|
|
10234
|
-
const deliveries = [];
|
|
10235
|
-
for (const channel of selected) {
|
|
10236
|
-
const eventForChannel = await this.applyRedaction(event, channel);
|
|
10237
|
-
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
10238
|
-
await this.store.appendDelivery(result);
|
|
10239
|
-
deliveries.push(result);
|
|
10240
|
-
}
|
|
10241
|
-
return deliveries;
|
|
10242
|
-
}
|
|
10243
|
-
async testChannel(id, input = {}) {
|
|
10244
|
-
const channel = await this.store.getChannel(id);
|
|
10245
|
-
if (!channel)
|
|
10246
|
-
throw new Error(`Channel not found: ${id}`);
|
|
10247
|
-
const event = createEvent2({
|
|
10248
|
-
source: input.source ?? "hasna.events",
|
|
10249
|
-
type: input.type ?? "events.test",
|
|
10250
|
-
subject: input.subject ?? id,
|
|
10251
|
-
severity: input.severity ?? "info",
|
|
10252
|
-
data: input.data ?? { test: true },
|
|
10253
|
-
message: input.message ?? "Hasna events test delivery",
|
|
10254
|
-
dedupeKey: input.dedupeKey,
|
|
10255
|
-
schemaVersion: input.schemaVersion,
|
|
10256
|
-
metadata: input.metadata,
|
|
10257
|
-
time: input.time,
|
|
10258
|
-
id: input.id
|
|
10259
|
-
});
|
|
10260
|
-
const eventForChannel = await this.applyRedaction(event, channel);
|
|
10261
|
-
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
10262
|
-
await this.store.appendDelivery(result);
|
|
10263
|
-
return result;
|
|
10264
|
-
}
|
|
10265
|
-
async replay(options = {}) {
|
|
10266
|
-
const events = (await this.store.listEvents()).filter((event) => {
|
|
10267
|
-
if (options.eventId && event.id !== options.eventId)
|
|
10268
|
-
return false;
|
|
10269
|
-
if (options.source && event.source !== options.source)
|
|
10270
|
-
return false;
|
|
10271
|
-
if (options.type && event.type !== options.type)
|
|
10272
|
-
return false;
|
|
10273
|
-
return true;
|
|
10274
|
-
});
|
|
10275
|
-
if (options.dryRun)
|
|
10276
|
-
return { events, deliveries: [] };
|
|
10277
|
-
const deliveries = [];
|
|
10278
|
-
for (const event of events) {
|
|
10279
|
-
deliveries.push(...await this.deliver(event));
|
|
10280
|
-
}
|
|
10281
|
-
return { events, deliveries };
|
|
10282
|
-
}
|
|
10283
|
-
async applyRedaction(event, channel) {
|
|
10284
|
-
let next = redactPaths2(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
10285
|
-
for (const redactor of this.redactors) {
|
|
10286
|
-
next = await redactor(next, channel);
|
|
10287
|
-
}
|
|
10288
|
-
return next;
|
|
10289
|
-
}
|
|
10290
|
-
async deliverWithRetry(event, channel) {
|
|
10291
|
-
const policy = normalizeRetryPolicy2(channel.retry);
|
|
10292
|
-
const attempts = [];
|
|
10293
|
-
for (let index = 0;index < policy.maxAttempts; index += 1) {
|
|
10294
|
-
const attempt = await dispatchChannel3(event, channel, this.transportOptions);
|
|
10295
|
-
attempt.attempt = index + 1;
|
|
10296
|
-
if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
|
|
10297
|
-
attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
|
|
10298
|
-
}
|
|
10299
|
-
attempts.push(attempt);
|
|
10300
|
-
if (attempt.status !== "failed")
|
|
10301
|
-
break;
|
|
10302
|
-
if (attempt.nextBackoffMs)
|
|
10303
|
-
await Bun.sleep(attempt.nextBackoffMs);
|
|
10304
|
-
}
|
|
10305
|
-
return createDeliveryResult2(event, channel, attempts);
|
|
10306
|
-
}
|
|
10307
|
-
}
|
|
10308
|
-
function redactPaths2(event, paths, replacement = "[REDACTED]") {
|
|
10309
|
-
if (paths.length === 0)
|
|
10310
|
-
return event;
|
|
10311
|
-
const copy = structuredClone(event);
|
|
10312
|
-
for (const path of paths) {
|
|
10313
|
-
setPath2(copy, path, replacement);
|
|
10314
|
-
}
|
|
10315
|
-
return copy;
|
|
10316
|
-
}
|
|
10317
|
-
function sanitizeChannelForOutput2(channel) {
|
|
10318
|
-
const copy = structuredClone(channel);
|
|
10319
|
-
if (copy.webhook?.secret)
|
|
10320
|
-
copy.webhook.secret = "[REDACTED]";
|
|
10321
|
-
if (copy.command?.env) {
|
|
10322
|
-
copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey2(key) ? "[REDACTED]" : value]));
|
|
10323
|
-
}
|
|
10324
|
-
return copy;
|
|
10325
|
-
}
|
|
10326
|
-
function sanitizeChannelsForOutput2(channels) {
|
|
10327
|
-
return channels.map(sanitizeChannelForOutput2);
|
|
10328
|
-
}
|
|
10329
|
-
function redactSensitiveKeys2(event, replacement = "[REDACTED]") {
|
|
10330
|
-
return redactValue2(event, replacement);
|
|
10331
|
-
}
|
|
10332
|
-
function shouldRedactKey2(key) {
|
|
10333
|
-
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
10334
|
-
}
|
|
10335
|
-
function redactValue2(value, replacement) {
|
|
10336
|
-
if (Array.isArray(value))
|
|
10337
|
-
return value.map((item) => redactValue2(item, replacement));
|
|
10338
|
-
if (!value || typeof value !== "object")
|
|
10339
|
-
return value;
|
|
10340
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
10341
|
-
key,
|
|
10342
|
-
shouldRedactKey2(key) ? replacement : redactValue2(item, replacement)
|
|
10343
|
-
]));
|
|
10344
|
-
}
|
|
10345
|
-
function setPath2(input, path, replacement) {
|
|
10346
|
-
const parts = path.split(".");
|
|
10347
|
-
let cursor = input;
|
|
10348
|
-
for (const part of parts.slice(0, -1)) {
|
|
10349
|
-
const next = cursor[part];
|
|
10350
|
-
if (!next || typeof next !== "object")
|
|
10351
|
-
return;
|
|
10352
|
-
cursor = next;
|
|
10353
|
-
}
|
|
10354
|
-
const last = parts.at(-1);
|
|
10355
|
-
if (last && last in cursor)
|
|
10356
|
-
cursor[last] = replacement;
|
|
10357
|
-
}
|
|
10358
|
-
function normalizeTime2(value) {
|
|
10359
|
-
if (!value)
|
|
10360
|
-
return new Date().toISOString();
|
|
10361
|
-
return value instanceof Date ? value.toISOString() : value;
|
|
10362
|
-
}
|
|
10363
|
-
function normalizeRetryPolicy2(policy) {
|
|
10364
|
-
return {
|
|
10365
|
-
maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
|
|
10366
|
-
backoffMs: Math.max(0, policy?.backoffMs ?? 250),
|
|
10367
|
-
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
10368
|
-
};
|
|
10369
|
-
}
|
|
10370
|
-
|
|
10371
|
-
// src/commands/runtime.ts
|
|
9188
|
+
import { EventsClient } from "@hasna/events";
|
|
10372
9189
|
function probeTmuxPane(target, tmuxCommand = process.env["HASNA_MACHINES_TMUX_BIN"] || "tmux") {
|
|
10373
9190
|
const checkedAt = new Date().toISOString();
|
|
10374
9191
|
const result = spawnSync4(tmuxCommand, ["display-message", "-p", "-t", target, "#{pane_id}"], {
|
|
@@ -10394,7 +9211,7 @@ async function watchTmuxPane(options) {
|
|
|
10394
9211
|
throw new Error("tmux pane target is required");
|
|
10395
9212
|
const intervalMs = Math.max(0, options.intervalMs ?? 5000);
|
|
10396
9213
|
const maxChecks = options.maxChecks ?? Number.POSITIVE_INFINITY;
|
|
10397
|
-
const client = options.client ?? new
|
|
9214
|
+
const client = options.client ?? new EventsClient;
|
|
10398
9215
|
const probe = options.probe ?? ((paneTarget) => probeTmuxPane(paneTarget, options.tmuxCommand));
|
|
10399
9216
|
const wait = options.sleep ?? sleep;
|
|
10400
9217
|
let lastPresent;
|
|
@@ -10455,7 +9272,8 @@ async function emitTmuxEvent(client, type, probe, lastPresent, deliver) {
|
|
|
10455
9272
|
}
|
|
10456
9273
|
|
|
10457
9274
|
// src/commands/screen.ts
|
|
10458
|
-
var
|
|
9275
|
+
var SCREEN_SECRET_NAMESPACE_ENV = "HASNA_MACHINES_SCREEN_SECRET_NAMESPACE";
|
|
9276
|
+
var DEFAULT_SCREEN_SECRET_NAMESPACE = "machines/screen-sharing";
|
|
10459
9277
|
function shellQuote6(value) {
|
|
10460
9278
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
10461
9279
|
}
|
|
@@ -10479,7 +9297,8 @@ function splitTarget(target) {
|
|
|
10479
9297
|
return [target.slice(0, at), target.slice(at + 1)];
|
|
10480
9298
|
}
|
|
10481
9299
|
function defaultScreenPasswordSecretKey(machineId) {
|
|
10482
|
-
|
|
9300
|
+
const namespace = process.env[SCREEN_SECRET_NAMESPACE_ENV]?.trim() || DEFAULT_SCREEN_SECRET_NAMESPACE;
|
|
9301
|
+
return `${namespace}/screen-${machineId}-vnc-password`;
|
|
10483
9302
|
}
|
|
10484
9303
|
function resolveScreenTarget(machineId, options = {}) {
|
|
10485
9304
|
const resolved = resolveMachineRoute(machineId, options);
|
|
@@ -10563,8 +9382,8 @@ function buildScreenEnableCommand(machineId, options = {}) {
|
|
|
10563
9382
|
}
|
|
10564
9383
|
|
|
10565
9384
|
// src/commands/sync.ts
|
|
10566
|
-
import { existsSync as
|
|
10567
|
-
import { homedir as
|
|
9385
|
+
import { existsSync as existsSync7, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
|
|
9386
|
+
import { homedir as homedir5 } from "os";
|
|
10568
9387
|
init_paths();
|
|
10569
9388
|
init_db();
|
|
10570
9389
|
function quote4(value) {
|
|
@@ -10618,8 +9437,8 @@ function detectFileActions(machine) {
|
|
|
10618
9437
|
throw new Error(`Remote file sync planning is not supported for ${machine.id}; refusing to inspect or apply local paths as remote state.`);
|
|
10619
9438
|
}
|
|
10620
9439
|
return (machine.files || []).map((file, index) => {
|
|
10621
|
-
const sourceExists =
|
|
10622
|
-
const targetExists =
|
|
9440
|
+
const sourceExists = existsSync7(file.source);
|
|
9441
|
+
const targetExists = existsSync7(file.target);
|
|
10623
9442
|
let status = "missing";
|
|
10624
9443
|
if (sourceExists && targetExists) {
|
|
10625
9444
|
if (file.mode === "symlink") {
|
|
@@ -10650,7 +9469,7 @@ function buildSyncPlan(machineId, runner = runMachineCommand) {
|
|
|
10650
9469
|
const target = selected || {
|
|
10651
9470
|
id: currentMachineId,
|
|
10652
9471
|
platform: "linux",
|
|
10653
|
-
workspacePath: `${
|
|
9472
|
+
workspacePath: `${homedir5()}/workspace`
|
|
10654
9473
|
};
|
|
10655
9474
|
const actions = [
|
|
10656
9475
|
...detectPackageActions(target, runner),
|
|
@@ -11207,6 +10026,7 @@ function runDoctor(machineId = getLocalMachineId()) {
|
|
|
11207
10026
|
init_db();
|
|
11208
10027
|
|
|
11209
10028
|
// src/commands/serve.ts
|
|
10029
|
+
import { EventsClient as EventsClient2, sanitizeChannelsForOutput } from "@hasna/events";
|
|
11210
10030
|
function escapeHtml(value) {
|
|
11211
10031
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
11212
10032
|
}
|
|
@@ -11423,7 +10243,7 @@ function startDashboardServer(options = {}) {
|
|
|
11423
10243
|
if (request.method !== "GET") {
|
|
11424
10244
|
return jsonError("Use GET for webhook channel listing.", 405);
|
|
11425
10245
|
}
|
|
11426
|
-
return Response.json(
|
|
10246
|
+
return Response.json(sanitizeChannelsForOutput(await events.listChannels()));
|
|
11427
10247
|
}
|
|
11428
10248
|
if (url.pathname === "/api/events") {
|
|
11429
10249
|
if (request.method === "GET") {
|
|
@@ -11556,8 +10376,8 @@ function runSelfTest() {
|
|
|
11556
10376
|
// src/commands/clipboard.ts
|
|
11557
10377
|
init_paths();
|
|
11558
10378
|
import { createHash } from "crypto";
|
|
11559
|
-
import { existsSync as
|
|
11560
|
-
import { join as
|
|
10379
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "fs";
|
|
10380
|
+
import { join as join6 } from "path";
|
|
11561
10381
|
var DEFAULT_CONFIG = {
|
|
11562
10382
|
version: 1,
|
|
11563
10383
|
enabled: true,
|
|
@@ -11575,7 +10395,7 @@ var DEFAULT_CONFIG = {
|
|
|
11575
10395
|
function resolveConfigPath(configPath) {
|
|
11576
10396
|
if (configPath)
|
|
11577
10397
|
return configPath;
|
|
11578
|
-
return
|
|
10398
|
+
return join6(getDataDir(), "clipboard-config.json");
|
|
11579
10399
|
}
|
|
11580
10400
|
function resolveHistoryPath(historyPath) {
|
|
11581
10401
|
if (historyPath)
|
|
@@ -11587,7 +10407,7 @@ function getDefaultConfig() {
|
|
|
11587
10407
|
}
|
|
11588
10408
|
function readConfig(configPath) {
|
|
11589
10409
|
const path = resolveConfigPath(configPath);
|
|
11590
|
-
if (!
|
|
10410
|
+
if (!existsSync8(path)) {
|
|
11591
10411
|
return getDefaultConfig();
|
|
11592
10412
|
}
|
|
11593
10413
|
const parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
@@ -11601,7 +10421,7 @@ function writeConfig(config, configPath) {
|
|
|
11601
10421
|
}
|
|
11602
10422
|
function readHistory(historyPath) {
|
|
11603
10423
|
const path = resolveHistoryPath(historyPath);
|
|
11604
|
-
if (!
|
|
10424
|
+
if (!existsSync8(path)) {
|
|
11605
10425
|
return [];
|
|
11606
10426
|
}
|
|
11607
10427
|
try {
|
|
@@ -11634,7 +10454,7 @@ function sanitizeClipboardForRead(content, maxSizeBytes, skipPatterns) {
|
|
|
11634
10454
|
}
|
|
11635
10455
|
function getOrCreateClipboardKey() {
|
|
11636
10456
|
const keyPath = getClipboardKeyPath();
|
|
11637
|
-
if (
|
|
10457
|
+
if (existsSync8(keyPath)) {
|
|
11638
10458
|
return readFileSync6(keyPath, "utf8").trim();
|
|
11639
10459
|
}
|
|
11640
10460
|
const key = createHash("sha256").update(crypto.randomUUID()).digest("hex").slice(0, 32);
|
|
@@ -11674,7 +10494,7 @@ function addClipboardEntry(entry, historyPath) {
|
|
|
11674
10494
|
}
|
|
11675
10495
|
function clearClipboardHistory(historyPath) {
|
|
11676
10496
|
const path = resolveHistoryPath(historyPath);
|
|
11677
|
-
if (
|
|
10497
|
+
if (existsSync8(path)) {
|
|
11678
10498
|
rmSync(path);
|
|
11679
10499
|
}
|
|
11680
10500
|
}
|
|
@@ -11691,7 +10511,7 @@ function getClipboardStatus(historyPath) {
|
|
|
11691
10511
|
// src/commands/clipboard-daemon.ts
|
|
11692
10512
|
init_paths();
|
|
11693
10513
|
import { readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
11694
|
-
import { join as
|
|
10514
|
+
import { join as join7 } from "path";
|
|
11695
10515
|
import { createHash as createHash3 } from "crypto";
|
|
11696
10516
|
|
|
11697
10517
|
// src/commands/clipboard-server.ts
|
|
@@ -11849,7 +10669,7 @@ function handleGetClipboard(response, config) {
|
|
|
11849
10669
|
}
|
|
11850
10670
|
|
|
11851
10671
|
// src/commands/clipboard-daemon.ts
|
|
11852
|
-
var DAEMON_PID_PATH =
|
|
10672
|
+
var DAEMON_PID_PATH = join7(getDataDir(), "clipboard-daemon.pid");
|
|
11853
10673
|
function readLocalClipboardSync2() {
|
|
11854
10674
|
const platform4 = process.platform;
|
|
11855
10675
|
if (platform4 === "darwin") {
|
|
@@ -12023,8 +10843,8 @@ async function discoverPeers() {
|
|
|
12023
10843
|
|
|
12024
10844
|
// src/commands/heal.ts
|
|
12025
10845
|
init_paths();
|
|
12026
|
-
import { existsSync as
|
|
12027
|
-
import { join as
|
|
10846
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
|
|
10847
|
+
import { join as join8 } from "path";
|
|
12028
10848
|
var DEFAULT_THRESHOLDS = {
|
|
12029
10849
|
reconnect: 3,
|
|
12030
10850
|
nmRestart: 7,
|
|
@@ -12068,14 +10888,14 @@ function defaultHealState() {
|
|
|
12068
10888
|
};
|
|
12069
10889
|
}
|
|
12070
10890
|
function getHealConfigPath() {
|
|
12071
|
-
return process.env["HASNA_MACHINES_HEAL_CONFIG_PATH"] ||
|
|
10891
|
+
return process.env["HASNA_MACHINES_HEAL_CONFIG_PATH"] || join8(getDataDir(), "heal-config.json");
|
|
12072
10892
|
}
|
|
12073
10893
|
function getHealStatePath() {
|
|
12074
|
-
return process.env["HASNA_MACHINES_HEAL_STATE_PATH"] ||
|
|
10894
|
+
return process.env["HASNA_MACHINES_HEAL_STATE_PATH"] || join8(getDataDir(), "heal-state.json");
|
|
12075
10895
|
}
|
|
12076
10896
|
function readHealConfig(path) {
|
|
12077
10897
|
const p = path || getHealConfigPath();
|
|
12078
|
-
if (!
|
|
10898
|
+
if (!existsSync9(p))
|
|
12079
10899
|
return { ...DEFAULT_HEAL_CONFIG, thresholds: { ...DEFAULT_THRESHOLDS } };
|
|
12080
10900
|
const parsed = JSON.parse(readFileSync9(p, "utf8"));
|
|
12081
10901
|
return {
|
|
@@ -12093,7 +10913,7 @@ function writeHealConfig(config, path) {
|
|
|
12093
10913
|
}
|
|
12094
10914
|
function readHealState(path) {
|
|
12095
10915
|
const p = path || getHealStatePath();
|
|
12096
|
-
if (!
|
|
10916
|
+
if (!existsSync9(p))
|
|
12097
10917
|
return defaultHealState();
|
|
12098
10918
|
try {
|
|
12099
10919
|
return { ...defaultHealState(), ...JSON.parse(readFileSync9(p, "utf8")) };
|
|
@@ -12133,7 +10953,7 @@ function evaluateHealth(probe, config, state) {
|
|
|
12133
10953
|
return { healthy: localOk && quorumOk, remoteScore, reasons };
|
|
12134
10954
|
}
|
|
12135
10955
|
function decideAction(input) {
|
|
12136
|
-
const { healthy, now
|
|
10956
|
+
const { healthy, now, gpuBusy, config, currentBootId } = input;
|
|
12137
10957
|
const s = { ...input.state };
|
|
12138
10958
|
const t = config.thresholds;
|
|
12139
10959
|
if (s.bootId !== currentBootId) {
|
|
@@ -12144,13 +10964,13 @@ function decideAction(input) {
|
|
|
12144
10964
|
if (healthy) {
|
|
12145
10965
|
s.failCount = 0;
|
|
12146
10966
|
if (s.bootHealthySince === null)
|
|
12147
|
-
s.bootHealthySince =
|
|
12148
|
-
if (
|
|
10967
|
+
s.bootHealthySince = now;
|
|
10968
|
+
if (now - s.bootHealthySince >= config.healthyWindowSec) {
|
|
12149
10969
|
s.failedBootRecoveries = 0;
|
|
12150
10970
|
s.rebootSuppressUntil = 0;
|
|
12151
10971
|
s.pendingRebootRecovery = false;
|
|
12152
10972
|
}
|
|
12153
|
-
if (s.degradedUntil > 0 &&
|
|
10973
|
+
if (s.degradedUntil > 0 && now >= s.degradedUntil) {
|
|
12154
10974
|
s.degradedUntil = 0;
|
|
12155
10975
|
return { action: "restore_preferred", state: s };
|
|
12156
10976
|
}
|
|
@@ -12168,8 +10988,8 @@ function decideAction(input) {
|
|
|
12168
10988
|
else if (s.failCount >= t.reconnect)
|
|
12169
10989
|
tier = "reconnect";
|
|
12170
10990
|
const tryReconnect = (reason) => {
|
|
12171
|
-
if (
|
|
12172
|
-
s.lastReconnect =
|
|
10991
|
+
if (now - s.lastReconnect >= config.reconnectMinIntervalSec) {
|
|
10992
|
+
s.lastReconnect = now;
|
|
12173
10993
|
return { action: "reconnect_wifi", suppressedReason: reason, state: s };
|
|
12174
10994
|
}
|
|
12175
10995
|
return { action: "none", suppressedReason: reason, state: s };
|
|
@@ -12178,15 +10998,15 @@ function decideAction(input) {
|
|
|
12178
10998
|
case "reconnect":
|
|
12179
10999
|
return tryReconnect();
|
|
12180
11000
|
case "nmRestart":
|
|
12181
|
-
if (
|
|
12182
|
-
s.lastNmRestart =
|
|
11001
|
+
if (now - s.lastNmRestart >= config.nmRestartMinIntervalSec) {
|
|
11002
|
+
s.lastNmRestart = now;
|
|
12183
11003
|
return { action: "restart_nm", state: s };
|
|
12184
11004
|
}
|
|
12185
11005
|
return tryReconnect();
|
|
12186
11006
|
case "fallback":
|
|
12187
|
-
if (
|
|
12188
|
-
s.lastFallback =
|
|
12189
|
-
s.degradedUntil =
|
|
11007
|
+
if (now - s.lastFallback >= config.fallbackWindowSec) {
|
|
11008
|
+
s.lastFallback = now;
|
|
11009
|
+
s.degradedUntil = now + config.fallbackWindowSec;
|
|
12190
11010
|
return { action: "fallback_ssid", state: s };
|
|
12191
11011
|
}
|
|
12192
11012
|
return tryReconnect();
|
|
@@ -12194,22 +11014,22 @@ function decideAction(input) {
|
|
|
12194
11014
|
let reason = null;
|
|
12195
11015
|
if (!config.allowReboot)
|
|
12196
11016
|
reason = "disabled";
|
|
12197
|
-
else if (
|
|
11017
|
+
else if (now < s.rebootSuppressUntil)
|
|
12198
11018
|
reason = "loop";
|
|
12199
11019
|
else if (config.gpuJobGuard && gpuBusy)
|
|
12200
11020
|
reason = "gpu";
|
|
12201
|
-
else if (
|
|
11021
|
+
else if (now - s.lastRebootAttempt < config.rebootMinIntervalSec)
|
|
12202
11022
|
reason = "rate";
|
|
12203
11023
|
if (reason)
|
|
12204
11024
|
return tryReconnect(reason);
|
|
12205
11025
|
if (s.pendingRebootRecovery) {
|
|
12206
11026
|
s.failedBootRecoveries += 1;
|
|
12207
11027
|
if (s.failedBootRecoveries >= config.maxFailedBootRecoveries) {
|
|
12208
|
-
s.rebootSuppressUntil =
|
|
11028
|
+
s.rebootSuppressUntil = now + config.bootBackoffSec;
|
|
12209
11029
|
return tryReconnect("loop");
|
|
12210
11030
|
}
|
|
12211
11031
|
}
|
|
12212
|
-
s.lastRebootAttempt =
|
|
11032
|
+
s.lastRebootAttempt = now;
|
|
12213
11033
|
s.pendingRebootRecovery = true;
|
|
12214
11034
|
return { action: "reboot", state: s };
|
|
12215
11035
|
}
|
|
@@ -12309,9 +11129,9 @@ function executeAction(action, config) {
|
|
|
12309
11129
|
|
|
12310
11130
|
// src/commands/heal-daemon.ts
|
|
12311
11131
|
init_paths();
|
|
12312
|
-
import { existsSync as
|
|
12313
|
-
import { join as
|
|
12314
|
-
var DAEMON_PID_PATH2 =
|
|
11132
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
|
|
11133
|
+
import { join as join9 } from "path";
|
|
11134
|
+
var DAEMON_PID_PATH2 = join9(getDataDir(), "heal-daemon.pid");
|
|
12315
11135
|
var SERVICE_PATH = "/etc/systemd/system/machines-heal.service";
|
|
12316
11136
|
var SYSTEM_CONF = "/etc/systemd/system.conf";
|
|
12317
11137
|
function log(msg) {
|
|
@@ -12429,7 +11249,7 @@ function applyDeterminism(config) {
|
|
|
12429
11249
|
}
|
|
12430
11250
|
function enableHardwareWatchdog() {
|
|
12431
11251
|
const log2 = [];
|
|
12432
|
-
if (!
|
|
11252
|
+
if (!existsSync10(SYSTEM_CONF))
|
|
12433
11253
|
return ["/etc/systemd/system.conf not found; skipping hardware watchdog"];
|
|
12434
11254
|
let conf = readFileSync10(SYSTEM_CONF, "utf8");
|
|
12435
11255
|
const set = (key, value) => {
|
|
@@ -12456,10 +11276,12 @@ function binPath() {
|
|
|
12456
11276
|
candidates.push(which);
|
|
12457
11277
|
if (process.argv[1])
|
|
12458
11278
|
candidates.push(process.argv[1]);
|
|
12459
|
-
const home = process.env["HOME"]
|
|
12460
|
-
|
|
11279
|
+
const home = process.env["HOME"];
|
|
11280
|
+
if (home)
|
|
11281
|
+
candidates.push(`${home}/.bun/bin/machines`);
|
|
11282
|
+
candidates.push("/root/.bun/bin/machines", "/usr/local/bin/machines");
|
|
12461
11283
|
for (const c of candidates) {
|
|
12462
|
-
if (c &&
|
|
11284
|
+
if (c && existsSync10(c))
|
|
12463
11285
|
return c;
|
|
12464
11286
|
}
|
|
12465
11287
|
return "machines";
|
|
@@ -12495,7 +11317,7 @@ WantedBy=multi-user.target
|
|
|
12495
11317
|
function uninstallHealService() {
|
|
12496
11318
|
const log2 = [];
|
|
12497
11319
|
sh2("systemctl disable --now machines-heal.service 2>/dev/null || true");
|
|
12498
|
-
if (
|
|
11320
|
+
if (existsSync10(SERVICE_PATH)) {
|
|
12499
11321
|
sh2(`rm -f ${SERVICE_PATH}`);
|
|
12500
11322
|
sh2("systemctl daemon-reload");
|
|
12501
11323
|
log2.push(`removed ${SERVICE_PATH}`);
|
|
@@ -12506,7 +11328,7 @@ function uninstallHealService() {
|
|
|
12506
11328
|
}
|
|
12507
11329
|
function healServiceStatus() {
|
|
12508
11330
|
return {
|
|
12509
|
-
installed:
|
|
11331
|
+
installed: existsSync10(SERVICE_PATH),
|
|
12510
11332
|
active: sh2("systemctl is-active machines-heal.service").out === "active",
|
|
12511
11333
|
enabled: sh2("systemctl is-enabled machines-heal.service 2>/dev/null").out === "enabled"
|
|
12512
11334
|
};
|
|
@@ -12777,8 +11599,17 @@ program2.name("machines").description("Machine fleet management CLI + MCP for de
|
|
|
12777
11599
|
var manifestCommand = program2.command("manifest").description("Manage the fleet manifest");
|
|
12778
11600
|
var appsCommand = program2.command("apps").description("Manage installed applications per machine");
|
|
12779
11601
|
var notificationsCommand = program2.command("notifications").description("Manage fleet alert delivery channels");
|
|
12780
|
-
|
|
12781
|
-
|
|
11602
|
+
var eventWebhooksCommand = registerWebhookCommands(program2, { source: "machines" });
|
|
11603
|
+
eventWebhooksCommand.description("Manage shared event webhook subscriptions");
|
|
11604
|
+
var webhookTestCommand = eventWebhooksCommand.commands.find((command) => command.name() === "test");
|
|
11605
|
+
var webhookOptions = webhookTestCommand?.options ?? [];
|
|
11606
|
+
var webhookMessageOption = webhookOptions.find((option) => option.long === "--message");
|
|
11607
|
+
if (webhookMessageOption) {
|
|
11608
|
+
webhookMessageOption.defaultValue = "Shared events test delivery";
|
|
11609
|
+
}
|
|
11610
|
+
var eventsCommand = registerEventCommands(program2, { source: "machines" });
|
|
11611
|
+
eventsCommand.description("Emit, list, and replay shared events");
|
|
11612
|
+
var runtimeCommand = program2.command("runtime").description("Watch runtime conditions and emit shared events");
|
|
12782
11613
|
var clipboardCommand = program2.command("clipboard").description("Real-time clipboard sync across fleet machines");
|
|
12783
11614
|
var installClaudeCommand = program2.command("install-claude").description("Install or inspect Claude, Codex, and Gemini CLIs");
|
|
12784
11615
|
manifestCommand.command("init").description("Create an empty fleet manifest").action(() => {
|