@hasna/machines 0.0.31 → 0.0.33
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 +14 -13
- package/dist/cli/index.js +157 -1321
- package/dist/commands/apps.d.ts.map +1 -1
- package/dist/commands/install-claude.d.ts.map +1 -1
- package/dist/commands/install-tailscale.d.ts.map +1 -1
- package/dist/commands/screen.d.ts +2 -1
- package/dist/commands/screen.d.ts.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/index.js +57 -547
- package/dist/mcp/index.js +110 -604
- 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();
|
|
@@ -8111,7 +7434,7 @@ function buildEntry(input) {
|
|
|
8111
7434
|
};
|
|
8112
7435
|
}
|
|
8113
7436
|
function discoverMachineTopology(options = {}) {
|
|
8114
|
-
const
|
|
7437
|
+
const now = options.now ?? new Date;
|
|
8115
7438
|
const runner = options.runner ?? defaultRunner;
|
|
8116
7439
|
const warnings = [];
|
|
8117
7440
|
const manifest = readManifest();
|
|
@@ -8143,11 +7466,11 @@ function discoverMachineTopology(options = {}) {
|
|
|
8143
7466
|
version: getPackageVersion()
|
|
8144
7467
|
},
|
|
8145
7468
|
capabilities: getMachinesConsumerCapabilities(),
|
|
8146
|
-
generated_at:
|
|
7469
|
+
generated_at: now.toISOString(),
|
|
8147
7470
|
local_machine_id: localMachineId,
|
|
8148
7471
|
local_hostname: hostname3(),
|
|
8149
7472
|
current_platform: normalizePlatform2(),
|
|
8150
|
-
manifest_path_known:
|
|
7473
|
+
manifest_path_known: existsSync4(getManifestPath()),
|
|
8151
7474
|
machines,
|
|
8152
7475
|
warnings
|
|
8153
7476
|
};
|
|
@@ -8266,11 +7589,11 @@ function cacheability(input) {
|
|
|
8266
7589
|
};
|
|
8267
7590
|
}
|
|
8268
7591
|
function resolveMachineRoute(machineId, options = {}) {
|
|
8269
|
-
const
|
|
7592
|
+
const now = options.now ?? new Date;
|
|
8270
7593
|
const topology = options.topology ?? discoverMachineTopology(options);
|
|
8271
7594
|
const warnings = [...topology.warnings];
|
|
8272
7595
|
const { machine, matchedBy } = findRouteMachine(topology, machineId);
|
|
8273
|
-
const generatedAt =
|
|
7596
|
+
const generatedAt = now.toISOString();
|
|
8274
7597
|
if (!machine) {
|
|
8275
7598
|
warnings.push(`machine_not_found:${machineId}`);
|
|
8276
7599
|
return {
|
|
@@ -8296,8 +7619,8 @@ function resolveMachineRoute(machineId, options = {}) {
|
|
|
8296
7619
|
},
|
|
8297
7620
|
cacheability: cacheability({
|
|
8298
7621
|
ok: false,
|
|
8299
|
-
observedAt:
|
|
8300
|
-
now
|
|
7622
|
+
observedAt: now,
|
|
7623
|
+
now,
|
|
8301
7624
|
ttlMs: options.resolverTtlMs,
|
|
8302
7625
|
authority: "unresolved",
|
|
8303
7626
|
confidence: "none",
|
|
@@ -8334,8 +7657,8 @@ function resolveMachineRoute(machineId, options = {}) {
|
|
|
8334
7657
|
},
|
|
8335
7658
|
cacheability: cacheability({
|
|
8336
7659
|
ok,
|
|
8337
|
-
observedAt:
|
|
8338
|
-
now
|
|
7660
|
+
observedAt: now,
|
|
7661
|
+
now,
|
|
8339
7662
|
ttlMs: options.resolverTtlMs,
|
|
8340
7663
|
authority: routeAuthority({ machine, selectedHint, matchedBy }),
|
|
8341
7664
|
confidence,
|
|
@@ -8422,7 +7745,7 @@ function canCheckPathForMachine(machine, localMachineId) {
|
|
|
8422
7745
|
function checkedPathExists(path, check) {
|
|
8423
7746
|
if (!path || !check)
|
|
8424
7747
|
return null;
|
|
8425
|
-
return
|
|
7748
|
+
return existsSync4(path);
|
|
8426
7749
|
}
|
|
8427
7750
|
function repairHint(input) {
|
|
8428
7751
|
const command = [
|
|
@@ -8637,11 +7960,11 @@ function metadataKeysForDiagnostics(metadata) {
|
|
|
8637
7960
|
return Object.keys(metadata).filter((key) => !/(secret|token|key|password|credential)/i.test(key)).sort();
|
|
8638
7961
|
}
|
|
8639
7962
|
function resolveMachineWorkspace(options) {
|
|
8640
|
-
const
|
|
7963
|
+
const now = options.now ?? new Date;
|
|
8641
7964
|
const topology = options.topology ?? discoverMachineTopology(options);
|
|
8642
7965
|
const warnings = [...topology.warnings];
|
|
8643
7966
|
const { machine, matchedBy } = findRouteMachine(topology, options.machineId);
|
|
8644
|
-
const generatedAt =
|
|
7967
|
+
const generatedAt = now.toISOString();
|
|
8645
7968
|
const repoName = options.repoName ?? options.projectId;
|
|
8646
7969
|
const openFilesRepoName = options.openFilesRepoName ?? "open-files";
|
|
8647
7970
|
if (!machine) {
|
|
@@ -8670,8 +7993,8 @@ function resolveMachineWorkspace(options) {
|
|
|
8670
7993
|
},
|
|
8671
7994
|
cacheability: cacheability({
|
|
8672
7995
|
ok: false,
|
|
8673
|
-
observedAt:
|
|
8674
|
-
now
|
|
7996
|
+
observedAt: now,
|
|
7997
|
+
now,
|
|
8675
7998
|
ttlMs: options.resolverTtlMs,
|
|
8676
7999
|
authority: "unresolved",
|
|
8677
8000
|
confidence: "none",
|
|
@@ -8743,8 +8066,8 @@ function resolveMachineWorkspace(options) {
|
|
|
8743
8066
|
},
|
|
8744
8067
|
cacheability: cacheability({
|
|
8745
8068
|
ok: workspaceOk,
|
|
8746
|
-
observedAt:
|
|
8747
|
-
now
|
|
8069
|
+
observedAt: now,
|
|
8070
|
+
now,
|
|
8748
8071
|
ttlMs: options.resolverTtlMs,
|
|
8749
8072
|
authority: workspaceAuthority(workspacePaths),
|
|
8750
8073
|
confidence: workspaceOk ? "medium" : "none",
|
|
@@ -8907,10 +8230,13 @@ function buildSetupPlan(machineId) {
|
|
|
8907
8230
|
const manifest = readManifest();
|
|
8908
8231
|
const currentMachineId = getLocalMachineId();
|
|
8909
8232
|
const selected = machineId ? manifest.machines.find((machine) => machine.id === machineId) : manifest.machines.find((machine) => machine.id === currentMachineId);
|
|
8233
|
+
if (machineId && !selected) {
|
|
8234
|
+
throw new Error(`Machine not found in manifest: ${machineId}`);
|
|
8235
|
+
}
|
|
8910
8236
|
const target = selected || {
|
|
8911
8237
|
id: currentMachineId,
|
|
8912
8238
|
platform: "linux",
|
|
8913
|
-
workspacePath: `${
|
|
8239
|
+
workspacePath: `${homedir2()}/workspace`
|
|
8914
8240
|
};
|
|
8915
8241
|
const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
|
|
8916
8242
|
return {
|
|
@@ -8955,8 +8281,8 @@ function runSetup(machineId, options = {}, runner = runMachineCommand) {
|
|
|
8955
8281
|
}
|
|
8956
8282
|
|
|
8957
8283
|
// src/commands/backup.ts
|
|
8958
|
-
import { homedir as
|
|
8959
|
-
import { join as
|
|
8284
|
+
import { homedir as homedir3, hostname as hostname5 } from "os";
|
|
8285
|
+
import { join as join3 } from "path";
|
|
8960
8286
|
var MACHINES_BACKUP_BUCKET_ENV = "HASNA_MACHINES_S3_BUCKET";
|
|
8961
8287
|
var MACHINES_BACKUP_BUCKET_FALLBACK_ENV = "MACHINES_S3_BUCKET";
|
|
8962
8288
|
var MACHINES_BACKUP_PREFIX_ENV = "HASNA_MACHINES_S3_PREFIX";
|
|
@@ -9004,16 +8330,16 @@ function resolveBackupTarget(options = {}) {
|
|
|
9004
8330
|
};
|
|
9005
8331
|
}
|
|
9006
8332
|
function defaultBackupSources() {
|
|
9007
|
-
const home =
|
|
8333
|
+
const home = homedir3();
|
|
9008
8334
|
return [
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
8335
|
+
join3(home, ".hasna"),
|
|
8336
|
+
join3(home, ".ssh"),
|
|
8337
|
+
join3(home, ".secrets")
|
|
9012
8338
|
];
|
|
9013
8339
|
}
|
|
9014
8340
|
function buildBackupPlan(bucket, prefix) {
|
|
9015
8341
|
const target = resolveBackupTarget({ bucket, prefix });
|
|
9016
|
-
const archivePath =
|
|
8342
|
+
const archivePath = join3(homedir3(), ".hasna", "machines", "backup.tgz");
|
|
9017
8343
|
const sources = defaultBackupSources();
|
|
9018
8344
|
const steps = [
|
|
9019
8345
|
{
|
|
@@ -9064,21 +8390,21 @@ function runBackup(bucket, prefix, options = {}) {
|
|
|
9064
8390
|
}
|
|
9065
8391
|
|
|
9066
8392
|
// src/commands/cert.ts
|
|
9067
|
-
import { homedir as
|
|
9068
|
-
import { join as
|
|
8393
|
+
import { homedir as homedir4, platform as platform3 } from "os";
|
|
8394
|
+
import { join as join4 } from "path";
|
|
9069
8395
|
function quote3(value) {
|
|
9070
8396
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
9071
8397
|
}
|
|
9072
8398
|
function certDir() {
|
|
9073
|
-
return
|
|
8399
|
+
return join4(homedir4(), ".hasna", "machines", "certs");
|
|
9074
8400
|
}
|
|
9075
8401
|
function buildCertPlan(domains) {
|
|
9076
8402
|
if (domains.length === 0) {
|
|
9077
8403
|
throw new Error("At least one domain is required.");
|
|
9078
8404
|
}
|
|
9079
8405
|
const primary = domains[0];
|
|
9080
|
-
const certPath =
|
|
9081
|
-
const keyPath =
|
|
8406
|
+
const certPath = join4(certDir(), `${primary}.pem`);
|
|
8407
|
+
const keyPath = join4(certDir(), `${primary}-key.pem`);
|
|
9082
8408
|
const steps = [];
|
|
9083
8409
|
if (platform3() === "darwin") {
|
|
9084
8410
|
steps.push({
|
|
@@ -9143,14 +8469,14 @@ function runCertPlan(domains, options = {}) {
|
|
|
9143
8469
|
|
|
9144
8470
|
// src/commands/dns.ts
|
|
9145
8471
|
init_paths();
|
|
9146
|
-
import { existsSync as
|
|
9147
|
-
import { join as
|
|
8472
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
8473
|
+
import { join as join5 } from "path";
|
|
9148
8474
|
function getDnsPath() {
|
|
9149
|
-
return
|
|
8475
|
+
return join5(getDataDir(), "dns.json");
|
|
9150
8476
|
}
|
|
9151
8477
|
function readMappings() {
|
|
9152
8478
|
const path = getDnsPath();
|
|
9153
|
-
if (!
|
|
8479
|
+
if (!existsSync5(path))
|
|
9154
8480
|
return [];
|
|
9155
8481
|
return JSON.parse(readFileSync3(path, "utf8"));
|
|
9156
8482
|
}
|
|
@@ -9179,10 +8505,10 @@ function renderDomainMapping(domain) {
|
|
|
9179
8505
|
hostsEntry: `${entry.targetHost} ${entry.domain}`,
|
|
9180
8506
|
caddySnippet: `${entry.domain} {
|
|
9181
8507
|
reverse_proxy 127.0.0.1:${entry.port}
|
|
9182
|
-
tls ${
|
|
8508
|
+
tls ${join5(getDataDir(), "certs", `${entry.domain}.pem`)} ${join5(getDataDir(), "certs", `${entry.domain}-key.pem`)}
|
|
9183
8509
|
}`,
|
|
9184
|
-
certPath:
|
|
9185
|
-
keyPath:
|
|
8510
|
+
certPath: join5(getDataDir(), "certs", `${entry.domain}.pem`),
|
|
8511
|
+
keyPath: join5(getDataDir(), "certs", `${entry.domain}-key.pem`)
|
|
9186
8512
|
};
|
|
9187
8513
|
}
|
|
9188
8514
|
|
|
@@ -9288,7 +8614,14 @@ function buildAppSteps(machine) {
|
|
|
9288
8614
|
}));
|
|
9289
8615
|
}
|
|
9290
8616
|
function resolveMachine(machineId) {
|
|
9291
|
-
|
|
8617
|
+
if (!machineId)
|
|
8618
|
+
return detectCurrentMachineManifest();
|
|
8619
|
+
return getManifestMachine(machineId) || {
|
|
8620
|
+
id: machineId,
|
|
8621
|
+
platform: "linux",
|
|
8622
|
+
workspacePath: "",
|
|
8623
|
+
apps: []
|
|
8624
|
+
};
|
|
9292
8625
|
}
|
|
9293
8626
|
function parseProbeOutput(app, machine, stdout) {
|
|
9294
8627
|
const lines = stdout.trim().split(`
|
|
@@ -9393,7 +8726,13 @@ function buildInstallSteps(machine, tools) {
|
|
|
9393
8726
|
}));
|
|
9394
8727
|
}
|
|
9395
8728
|
function resolveMachine2(machineId) {
|
|
9396
|
-
|
|
8729
|
+
if (!machineId)
|
|
8730
|
+
return detectCurrentMachineManifest();
|
|
8731
|
+
return getManifestMachine(machineId) || {
|
|
8732
|
+
id: machineId,
|
|
8733
|
+
platform: "linux",
|
|
8734
|
+
workspacePath: ""
|
|
8735
|
+
};
|
|
9397
8736
|
}
|
|
9398
8737
|
function buildProbeCommand(tool) {
|
|
9399
8738
|
const binary = getToolBinary(tool);
|
|
@@ -9494,7 +8833,10 @@ function buildInstallSteps2(machine) {
|
|
|
9494
8833
|
];
|
|
9495
8834
|
}
|
|
9496
8835
|
function buildTailscaleInstallPlan(machineId) {
|
|
9497
|
-
const machine =
|
|
8836
|
+
const machine = machineId ? getManifestMachine(machineId) : detectCurrentMachineManifest();
|
|
8837
|
+
if (!machine) {
|
|
8838
|
+
throw new Error(`Machine not found in manifest: ${machineId}`);
|
|
8839
|
+
}
|
|
9498
8840
|
return {
|
|
9499
8841
|
machineId: machine.id,
|
|
9500
8842
|
mode: "plan",
|
|
@@ -9523,7 +8865,7 @@ function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand
|
|
|
9523
8865
|
}
|
|
9524
8866
|
|
|
9525
8867
|
// src/commands/notifications.ts
|
|
9526
|
-
import { existsSync as
|
|
8868
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
9527
8869
|
init_paths();
|
|
9528
8870
|
var notificationChannelSchema = exports_external.object({
|
|
9529
8871
|
id: exports_external.string(),
|
|
@@ -9606,7 +8948,7 @@ ${message}
|
|
|
9606
8948
|
}
|
|
9607
8949
|
throw new Error("No local email transport available. Install sendmail or mail.");
|
|
9608
8950
|
}
|
|
9609
|
-
async function
|
|
8951
|
+
async function dispatchWebhook(channel, event, message) {
|
|
9610
8952
|
const response = await fetch(channel.target, {
|
|
9611
8953
|
method: "POST",
|
|
9612
8954
|
headers: {
|
|
@@ -9631,7 +8973,7 @@ async function dispatchWebhook2(channel, event, message) {
|
|
|
9631
8973
|
detail: `Webhook accepted with HTTP ${response.status}`
|
|
9632
8974
|
};
|
|
9633
8975
|
}
|
|
9634
|
-
async function
|
|
8976
|
+
async function dispatchCommand(channel, event, message) {
|
|
9635
8977
|
const result = Bun.spawnSync(["bash", "-lc", channel.target], {
|
|
9636
8978
|
stdout: "pipe",
|
|
9637
8979
|
stderr: "pipe",
|
|
@@ -9654,7 +8996,7 @@ async function dispatchCommand2(channel, event, message) {
|
|
|
9654
8996
|
detail: stdout || "Command completed successfully"
|
|
9655
8997
|
};
|
|
9656
8998
|
}
|
|
9657
|
-
async function
|
|
8999
|
+
async function dispatchChannel(channel, event, message) {
|
|
9658
9000
|
if (!channel.enabled) {
|
|
9659
9001
|
return {
|
|
9660
9002
|
channelId: channel.id,
|
|
@@ -9668,9 +9010,9 @@ async function dispatchChannel2(channel, event, message) {
|
|
|
9668
9010
|
return dispatchEmail(channel, event, message);
|
|
9669
9011
|
}
|
|
9670
9012
|
if (channel.type === "webhook") {
|
|
9671
|
-
return
|
|
9013
|
+
return dispatchWebhook(channel, event, message);
|
|
9672
9014
|
}
|
|
9673
|
-
return
|
|
9015
|
+
return dispatchCommand(channel, event, message);
|
|
9674
9016
|
}
|
|
9675
9017
|
function getDefaultNotificationConfig() {
|
|
9676
9018
|
return {
|
|
@@ -9680,7 +9022,7 @@ function getDefaultNotificationConfig() {
|
|
|
9680
9022
|
};
|
|
9681
9023
|
}
|
|
9682
9024
|
function readNotificationConfig(path = getNotificationsPath()) {
|
|
9683
|
-
if (!
|
|
9025
|
+
if (!existsSync6(path)) {
|
|
9684
9026
|
return getDefaultNotificationConfig();
|
|
9685
9027
|
}
|
|
9686
9028
|
return notificationConfigSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
|
|
@@ -9725,7 +9067,7 @@ async function dispatchNotificationEvent(event, message, options = {}) {
|
|
|
9725
9067
|
const deliveries = [];
|
|
9726
9068
|
for (const channel of channels) {
|
|
9727
9069
|
try {
|
|
9728
|
-
deliveries.push(await
|
|
9070
|
+
deliveries.push(await dispatchChannel(channel, event, message));
|
|
9729
9071
|
} catch (error) {
|
|
9730
9072
|
deliveries.push({
|
|
9731
9073
|
channelId: channel.id,
|
|
@@ -9760,7 +9102,7 @@ async function testNotificationChannel(channelId, event = "manual.test", message
|
|
|
9760
9102
|
if (!options.yes) {
|
|
9761
9103
|
throw new Error("Notification test execution requires --yes.");
|
|
9762
9104
|
}
|
|
9763
|
-
const delivery = await
|
|
9105
|
+
const delivery = await dispatchChannel(channel, event, message);
|
|
9764
9106
|
return {
|
|
9765
9107
|
channelId,
|
|
9766
9108
|
mode: "apply",
|
|
@@ -9828,528 +9170,7 @@ function listPorts(machineId) {
|
|
|
9828
9170
|
// src/commands/runtime.ts
|
|
9829
9171
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
9830
9172
|
import { setTimeout as sleep } from "timers/promises";
|
|
9831
|
-
|
|
9832
|
-
// node_modules/@hasna/events/dist/index.js
|
|
9833
|
-
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
|
|
9834
|
-
import { existsSync as existsSync8 } from "fs";
|
|
9835
|
-
import { homedir as homedir6 } from "os";
|
|
9836
|
-
import { join as join7 } from "path";
|
|
9837
|
-
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
9838
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
9839
|
-
import { spawn as spawn2 } from "child_process";
|
|
9840
|
-
import { randomUUID as randomUUID22 } from "crypto";
|
|
9841
|
-
function getPathValue2(input, path) {
|
|
9842
|
-
return path.split(".").reduce((value, part) => {
|
|
9843
|
-
if (value && typeof value === "object" && part in value) {
|
|
9844
|
-
return value[part];
|
|
9845
|
-
}
|
|
9846
|
-
return;
|
|
9847
|
-
}, input);
|
|
9848
|
-
}
|
|
9849
|
-
function wildcardToRegExp2(pattern) {
|
|
9850
|
-
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
|
|
9851
|
-
return new RegExp(`^${escaped}$`);
|
|
9852
|
-
}
|
|
9853
|
-
function matchString2(value, matcher) {
|
|
9854
|
-
if (matcher === undefined)
|
|
9855
|
-
return true;
|
|
9856
|
-
if (value === undefined)
|
|
9857
|
-
return false;
|
|
9858
|
-
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
9859
|
-
return matchers.some((item) => wildcardToRegExp2(item).test(value));
|
|
9860
|
-
}
|
|
9861
|
-
function matchRecord2(input, matcher) {
|
|
9862
|
-
if (!matcher)
|
|
9863
|
-
return true;
|
|
9864
|
-
return Object.entries(matcher).every(([path, expected]) => {
|
|
9865
|
-
const actual = getPathValue2(input, path);
|
|
9866
|
-
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
9867
|
-
return matchString2(actual === undefined ? undefined : String(actual), expected);
|
|
9868
|
-
}
|
|
9869
|
-
return actual === expected;
|
|
9870
|
-
});
|
|
9871
|
-
}
|
|
9872
|
-
function eventMatchesFilter2(event, filter) {
|
|
9873
|
-
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);
|
|
9874
|
-
}
|
|
9875
|
-
function channelMatchesEvent2(channel, event) {
|
|
9876
|
-
if (!channel.enabled)
|
|
9877
|
-
return false;
|
|
9878
|
-
if (!channel.filters || channel.filters.length === 0)
|
|
9879
|
-
return true;
|
|
9880
|
-
return channel.filters.some((filter) => eventMatchesFilter2(event, filter));
|
|
9881
|
-
}
|
|
9882
|
-
var HASNA_EVENTS_DIR_ENV2 = "HASNA_EVENTS_DIR";
|
|
9883
|
-
var HASNA_EVENTS_HOME_ENV2 = "HASNA_EVENTS_HOME";
|
|
9884
|
-
function getEventsDataDir2(override) {
|
|
9885
|
-
return override || process.env[HASNA_EVENTS_DIR_ENV2] || process.env[HASNA_EVENTS_HOME_ENV2] || join7(homedir6(), ".hasna", "events");
|
|
9886
|
-
}
|
|
9887
|
-
|
|
9888
|
-
class JsonEventsStore2 {
|
|
9889
|
-
dataDir;
|
|
9890
|
-
channelsPath;
|
|
9891
|
-
eventsPath;
|
|
9892
|
-
deliveriesPath;
|
|
9893
|
-
constructor(dataDir = getEventsDataDir2()) {
|
|
9894
|
-
this.dataDir = dataDir;
|
|
9895
|
-
this.channelsPath = join7(dataDir, "channels.json");
|
|
9896
|
-
this.eventsPath = join7(dataDir, "events.json");
|
|
9897
|
-
this.deliveriesPath = join7(dataDir, "deliveries.json");
|
|
9898
|
-
}
|
|
9899
|
-
async init() {
|
|
9900
|
-
await mkdir2(this.dataDir, { recursive: true, mode: 448 });
|
|
9901
|
-
await chmod2(this.dataDir, 448).catch(() => {
|
|
9902
|
-
return;
|
|
9903
|
-
});
|
|
9904
|
-
await this.ensureArrayFile(this.channelsPath);
|
|
9905
|
-
await this.ensureArrayFile(this.eventsPath);
|
|
9906
|
-
await this.ensureArrayFile(this.deliveriesPath);
|
|
9907
|
-
}
|
|
9908
|
-
async addChannel(channel) {
|
|
9909
|
-
await this.init();
|
|
9910
|
-
const channels = await this.readJson(this.channelsPath, []);
|
|
9911
|
-
const index = channels.findIndex((item) => item.id === channel.id);
|
|
9912
|
-
if (index >= 0) {
|
|
9913
|
-
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
9914
|
-
} else {
|
|
9915
|
-
channels.push(channel);
|
|
9916
|
-
}
|
|
9917
|
-
await this.writeJson(this.channelsPath, channels);
|
|
9918
|
-
return index >= 0 ? channels[index] : channel;
|
|
9919
|
-
}
|
|
9920
|
-
async listChannels() {
|
|
9921
|
-
await this.init();
|
|
9922
|
-
return this.readJson(this.channelsPath, []);
|
|
9923
|
-
}
|
|
9924
|
-
async getChannel(id) {
|
|
9925
|
-
const channels = await this.listChannels();
|
|
9926
|
-
return channels.find((channel) => channel.id === id);
|
|
9927
|
-
}
|
|
9928
|
-
async removeChannel(id) {
|
|
9929
|
-
await this.init();
|
|
9930
|
-
const channels = await this.readJson(this.channelsPath, []);
|
|
9931
|
-
const next = channels.filter((channel) => channel.id !== id);
|
|
9932
|
-
await this.writeJson(this.channelsPath, next);
|
|
9933
|
-
return next.length !== channels.length;
|
|
9934
|
-
}
|
|
9935
|
-
async appendEvent(event) {
|
|
9936
|
-
await this.init();
|
|
9937
|
-
const events = await this.readJson(this.eventsPath, []);
|
|
9938
|
-
events.push(event);
|
|
9939
|
-
await this.writeJson(this.eventsPath, events);
|
|
9940
|
-
return event;
|
|
9941
|
-
}
|
|
9942
|
-
async listEvents() {
|
|
9943
|
-
await this.init();
|
|
9944
|
-
return this.readJson(this.eventsPath, []);
|
|
9945
|
-
}
|
|
9946
|
-
async findEventByIdentity(identity) {
|
|
9947
|
-
const events = await this.listEvents();
|
|
9948
|
-
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
9949
|
-
}
|
|
9950
|
-
async appendDelivery(result) {
|
|
9951
|
-
await this.init();
|
|
9952
|
-
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
9953
|
-
deliveries.push(result);
|
|
9954
|
-
await this.writeJson(this.deliveriesPath, deliveries);
|
|
9955
|
-
return result;
|
|
9956
|
-
}
|
|
9957
|
-
async listDeliveries() {
|
|
9958
|
-
await this.init();
|
|
9959
|
-
return this.readJson(this.deliveriesPath, []);
|
|
9960
|
-
}
|
|
9961
|
-
async exportData() {
|
|
9962
|
-
return {
|
|
9963
|
-
channels: await this.listChannels(),
|
|
9964
|
-
events: await this.listEvents(),
|
|
9965
|
-
deliveries: await this.listDeliveries()
|
|
9966
|
-
};
|
|
9967
|
-
}
|
|
9968
|
-
async ensureArrayFile(path) {
|
|
9969
|
-
if (!existsSync8(path)) {
|
|
9970
|
-
await writeFile2(path, `[]
|
|
9971
|
-
`, { encoding: "utf-8", mode: 384 });
|
|
9972
|
-
}
|
|
9973
|
-
await chmod2(path, 384).catch(() => {
|
|
9974
|
-
return;
|
|
9975
|
-
});
|
|
9976
|
-
}
|
|
9977
|
-
async readJson(path, fallback) {
|
|
9978
|
-
try {
|
|
9979
|
-
const raw = await readFile2(path, "utf-8");
|
|
9980
|
-
if (!raw.trim())
|
|
9981
|
-
return fallback;
|
|
9982
|
-
return JSON.parse(raw);
|
|
9983
|
-
} catch (error) {
|
|
9984
|
-
if (error.code === "ENOENT")
|
|
9985
|
-
return fallback;
|
|
9986
|
-
throw error;
|
|
9987
|
-
}
|
|
9988
|
-
}
|
|
9989
|
-
async writeJson(path, value) {
|
|
9990
|
-
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
9991
|
-
await writeFile2(tempPath, `${JSON.stringify(value, null, 2)}
|
|
9992
|
-
`, { encoding: "utf-8", mode: 384 });
|
|
9993
|
-
await rename2(tempPath, path);
|
|
9994
|
-
await chmod2(path, 384).catch(() => {
|
|
9995
|
-
return;
|
|
9996
|
-
});
|
|
9997
|
-
}
|
|
9998
|
-
}
|
|
9999
|
-
var DEFAULT_SIGNATURE_TOLERANCE_MS2 = 5 * 60 * 1000;
|
|
10000
|
-
function buildSignatureBase2(timestamp, body) {
|
|
10001
|
-
return `${timestamp}.${body}`;
|
|
10002
|
-
}
|
|
10003
|
-
function signPayload2(secret, timestamp, body) {
|
|
10004
|
-
const digest = createHmac2("sha256", secret).update(buildSignatureBase2(timestamp, body)).digest("hex");
|
|
10005
|
-
return `sha256=${digest}`;
|
|
10006
|
-
}
|
|
10007
|
-
function now2() {
|
|
10008
|
-
return new Date().toISOString();
|
|
10009
|
-
}
|
|
10010
|
-
function truncate2(value, max = 4096) {
|
|
10011
|
-
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
10012
|
-
}
|
|
10013
|
-
function buildWebhookRequest2(event, channel) {
|
|
10014
|
-
if (!channel.webhook)
|
|
10015
|
-
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
10016
|
-
const body = JSON.stringify(event);
|
|
10017
|
-
const timestamp = event.time;
|
|
10018
|
-
const headers = {
|
|
10019
|
-
"Content-Type": "application/json",
|
|
10020
|
-
"User-Agent": "@hasna/events",
|
|
10021
|
-
"X-Hasna-Event-Id": event.id,
|
|
10022
|
-
"X-Hasna-Event-Type": event.type,
|
|
10023
|
-
"X-Hasna-Timestamp": timestamp,
|
|
10024
|
-
...channel.webhook.headers
|
|
10025
|
-
};
|
|
10026
|
-
if (channel.webhook.secret) {
|
|
10027
|
-
headers["X-Hasna-Signature"] = signPayload2(channel.webhook.secret, timestamp, body);
|
|
10028
|
-
}
|
|
10029
|
-
return { body, headers };
|
|
10030
|
-
}
|
|
10031
|
-
async function dispatchWebhook3(event, channel, options = {}) {
|
|
10032
|
-
if (!channel.webhook)
|
|
10033
|
-
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
10034
|
-
const startedAt = now2();
|
|
10035
|
-
const { body, headers } = buildWebhookRequest2(event, channel);
|
|
10036
|
-
const controller = new AbortController;
|
|
10037
|
-
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
10038
|
-
try {
|
|
10039
|
-
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
10040
|
-
method: "POST",
|
|
10041
|
-
headers,
|
|
10042
|
-
body,
|
|
10043
|
-
signal: controller.signal
|
|
10044
|
-
});
|
|
10045
|
-
const responseBody = truncate2(await response.text());
|
|
10046
|
-
return {
|
|
10047
|
-
attempt: 1,
|
|
10048
|
-
status: response.ok ? "success" : "failed",
|
|
10049
|
-
startedAt,
|
|
10050
|
-
completedAt: now2(),
|
|
10051
|
-
responseStatus: response.status,
|
|
10052
|
-
responseBody,
|
|
10053
|
-
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
10054
|
-
};
|
|
10055
|
-
} catch (error) {
|
|
10056
|
-
return {
|
|
10057
|
-
attempt: 1,
|
|
10058
|
-
status: "failed",
|
|
10059
|
-
startedAt,
|
|
10060
|
-
completedAt: now2(),
|
|
10061
|
-
error: error instanceof Error ? error.message : String(error)
|
|
10062
|
-
};
|
|
10063
|
-
} finally {
|
|
10064
|
-
clearTimeout(timeout);
|
|
10065
|
-
}
|
|
10066
|
-
}
|
|
10067
|
-
async function dispatchCommand3(event, channel) {
|
|
10068
|
-
if (!channel.command)
|
|
10069
|
-
throw new Error(`Channel ${channel.id} has no command config`);
|
|
10070
|
-
const startedAt = now2();
|
|
10071
|
-
const eventJson = JSON.stringify(event);
|
|
10072
|
-
const env2 = {
|
|
10073
|
-
...process.env,
|
|
10074
|
-
...channel.command.env,
|
|
10075
|
-
HASNA_CHANNEL_ID: channel.id,
|
|
10076
|
-
HASNA_EVENT_ID: event.id,
|
|
10077
|
-
HASNA_EVENT_TYPE: event.type,
|
|
10078
|
-
HASNA_EVENT_SOURCE: event.source,
|
|
10079
|
-
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
10080
|
-
HASNA_EVENT_SEVERITY: event.severity,
|
|
10081
|
-
HASNA_EVENT_TIME: event.time,
|
|
10082
|
-
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
10083
|
-
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
10084
|
-
HASNA_EVENT_JSON: eventJson
|
|
10085
|
-
};
|
|
10086
|
-
return new Promise((resolve2) => {
|
|
10087
|
-
const child = spawn2(channel.command.command, channel.command.args ?? [], {
|
|
10088
|
-
cwd: channel.command.cwd,
|
|
10089
|
-
env: env2,
|
|
10090
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
10091
|
-
});
|
|
10092
|
-
let stdout = "";
|
|
10093
|
-
let stderr = "";
|
|
10094
|
-
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
10095
|
-
child.stdin.end(eventJson);
|
|
10096
|
-
child.stdout.on("data", (chunk) => {
|
|
10097
|
-
stdout += chunk.toString();
|
|
10098
|
-
});
|
|
10099
|
-
child.stderr.on("data", (chunk) => {
|
|
10100
|
-
stderr += chunk.toString();
|
|
10101
|
-
});
|
|
10102
|
-
child.on("error", (error) => {
|
|
10103
|
-
clearTimeout(timeout);
|
|
10104
|
-
resolve2({
|
|
10105
|
-
attempt: 1,
|
|
10106
|
-
status: "failed",
|
|
10107
|
-
startedAt,
|
|
10108
|
-
completedAt: now2(),
|
|
10109
|
-
stdout: truncate2(stdout),
|
|
10110
|
-
stderr: truncate2(stderr),
|
|
10111
|
-
error: error.message
|
|
10112
|
-
});
|
|
10113
|
-
});
|
|
10114
|
-
child.on("close", (code, signal) => {
|
|
10115
|
-
clearTimeout(timeout);
|
|
10116
|
-
const success = code === 0;
|
|
10117
|
-
resolve2({
|
|
10118
|
-
attempt: 1,
|
|
10119
|
-
status: success ? "success" : "failed",
|
|
10120
|
-
startedAt,
|
|
10121
|
-
completedAt: now2(),
|
|
10122
|
-
stdout: truncate2(stdout),
|
|
10123
|
-
stderr: truncate2(stderr),
|
|
10124
|
-
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
10125
|
-
});
|
|
10126
|
-
});
|
|
10127
|
-
});
|
|
10128
|
-
}
|
|
10129
|
-
async function dispatchChannel3(event, channel, options = {}) {
|
|
10130
|
-
if (channel.transport === "webhook")
|
|
10131
|
-
return dispatchWebhook3(event, channel, options);
|
|
10132
|
-
if (channel.transport === "command")
|
|
10133
|
-
return dispatchCommand3(event, channel);
|
|
10134
|
-
return {
|
|
10135
|
-
attempt: 1,
|
|
10136
|
-
status: "skipped",
|
|
10137
|
-
startedAt: now2(),
|
|
10138
|
-
completedAt: now2(),
|
|
10139
|
-
error: `Unsupported transport: ${channel.transport}`
|
|
10140
|
-
};
|
|
10141
|
-
}
|
|
10142
|
-
function createDeliveryResult2(event, channel, attempts) {
|
|
10143
|
-
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
10144
|
-
return {
|
|
10145
|
-
id: randomUUID3(),
|
|
10146
|
-
eventId: event.id,
|
|
10147
|
-
channelId: channel.id,
|
|
10148
|
-
transport: channel.transport,
|
|
10149
|
-
status,
|
|
10150
|
-
attempts,
|
|
10151
|
-
createdAt: attempts[0]?.startedAt ?? now2(),
|
|
10152
|
-
completedAt: attempts.at(-1)?.completedAt ?? now2()
|
|
10153
|
-
};
|
|
10154
|
-
}
|
|
10155
|
-
function createEvent2(input) {
|
|
10156
|
-
return {
|
|
10157
|
-
id: input.id ?? randomUUID22(),
|
|
10158
|
-
source: input.source,
|
|
10159
|
-
type: input.type,
|
|
10160
|
-
time: normalizeTime2(input.time),
|
|
10161
|
-
subject: input.subject,
|
|
10162
|
-
severity: input.severity ?? "info",
|
|
10163
|
-
data: input.data ?? {},
|
|
10164
|
-
message: input.message,
|
|
10165
|
-
dedupeKey: input.dedupeKey,
|
|
10166
|
-
schemaVersion: input.schemaVersion ?? "1.0",
|
|
10167
|
-
metadata: input.metadata ?? {}
|
|
10168
|
-
};
|
|
10169
|
-
}
|
|
10170
|
-
|
|
10171
|
-
class EventsClient2 {
|
|
10172
|
-
store;
|
|
10173
|
-
redactors;
|
|
10174
|
-
transportOptions;
|
|
10175
|
-
constructor(options = {}) {
|
|
10176
|
-
this.store = options.store ?? new JsonEventsStore2(options.dataDir);
|
|
10177
|
-
this.redactors = options.redactors ?? [];
|
|
10178
|
-
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
10179
|
-
}
|
|
10180
|
-
async addChannel(input) {
|
|
10181
|
-
const timestamp = new Date().toISOString();
|
|
10182
|
-
return this.store.addChannel({
|
|
10183
|
-
...input,
|
|
10184
|
-
createdAt: input.createdAt ?? timestamp,
|
|
10185
|
-
updatedAt: input.updatedAt ?? timestamp
|
|
10186
|
-
});
|
|
10187
|
-
}
|
|
10188
|
-
async listChannels() {
|
|
10189
|
-
return this.store.listChannels();
|
|
10190
|
-
}
|
|
10191
|
-
async removeChannel(id) {
|
|
10192
|
-
return this.store.removeChannel(id);
|
|
10193
|
-
}
|
|
10194
|
-
async emit(input, options = {}) {
|
|
10195
|
-
const event = options.redactSensitiveData === false ? createEvent2(input) : redactSensitiveKeys2(createEvent2(input));
|
|
10196
|
-
if (options.dedupe !== false) {
|
|
10197
|
-
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
10198
|
-
if (existing) {
|
|
10199
|
-
return { event: existing, deliveries: [], deduped: true };
|
|
10200
|
-
}
|
|
10201
|
-
}
|
|
10202
|
-
await this.store.appendEvent(event);
|
|
10203
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
10204
|
-
return { event, deliveries, deduped: false };
|
|
10205
|
-
}
|
|
10206
|
-
async listEvents() {
|
|
10207
|
-
return this.store.listEvents();
|
|
10208
|
-
}
|
|
10209
|
-
async listDeliveries() {
|
|
10210
|
-
return this.store.listDeliveries();
|
|
10211
|
-
}
|
|
10212
|
-
async deliver(event) {
|
|
10213
|
-
const channels = await this.store.listChannels();
|
|
10214
|
-
const selected = channels.filter((channel) => channelMatchesEvent2(channel, event));
|
|
10215
|
-
const deliveries = [];
|
|
10216
|
-
for (const channel of selected) {
|
|
10217
|
-
const eventForChannel = await this.applyRedaction(event, channel);
|
|
10218
|
-
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
10219
|
-
await this.store.appendDelivery(result);
|
|
10220
|
-
deliveries.push(result);
|
|
10221
|
-
}
|
|
10222
|
-
return deliveries;
|
|
10223
|
-
}
|
|
10224
|
-
async testChannel(id, input = {}) {
|
|
10225
|
-
const channel = await this.store.getChannel(id);
|
|
10226
|
-
if (!channel)
|
|
10227
|
-
throw new Error(`Channel not found: ${id}`);
|
|
10228
|
-
const event = createEvent2({
|
|
10229
|
-
source: input.source ?? "hasna.events",
|
|
10230
|
-
type: input.type ?? "events.test",
|
|
10231
|
-
subject: input.subject ?? id,
|
|
10232
|
-
severity: input.severity ?? "info",
|
|
10233
|
-
data: input.data ?? { test: true },
|
|
10234
|
-
message: input.message ?? "Hasna events test delivery",
|
|
10235
|
-
dedupeKey: input.dedupeKey,
|
|
10236
|
-
schemaVersion: input.schemaVersion,
|
|
10237
|
-
metadata: input.metadata,
|
|
10238
|
-
time: input.time,
|
|
10239
|
-
id: input.id
|
|
10240
|
-
});
|
|
10241
|
-
const eventForChannel = await this.applyRedaction(event, channel);
|
|
10242
|
-
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
10243
|
-
await this.store.appendDelivery(result);
|
|
10244
|
-
return result;
|
|
10245
|
-
}
|
|
10246
|
-
async replay(options = {}) {
|
|
10247
|
-
const events = (await this.store.listEvents()).filter((event) => {
|
|
10248
|
-
if (options.eventId && event.id !== options.eventId)
|
|
10249
|
-
return false;
|
|
10250
|
-
if (options.source && event.source !== options.source)
|
|
10251
|
-
return false;
|
|
10252
|
-
if (options.type && event.type !== options.type)
|
|
10253
|
-
return false;
|
|
10254
|
-
return true;
|
|
10255
|
-
});
|
|
10256
|
-
if (options.dryRun)
|
|
10257
|
-
return { events, deliveries: [] };
|
|
10258
|
-
const deliveries = [];
|
|
10259
|
-
for (const event of events) {
|
|
10260
|
-
deliveries.push(...await this.deliver(event));
|
|
10261
|
-
}
|
|
10262
|
-
return { events, deliveries };
|
|
10263
|
-
}
|
|
10264
|
-
async applyRedaction(event, channel) {
|
|
10265
|
-
let next = redactPaths2(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
10266
|
-
for (const redactor of this.redactors) {
|
|
10267
|
-
next = await redactor(next, channel);
|
|
10268
|
-
}
|
|
10269
|
-
return next;
|
|
10270
|
-
}
|
|
10271
|
-
async deliverWithRetry(event, channel) {
|
|
10272
|
-
const policy = normalizeRetryPolicy2(channel.retry);
|
|
10273
|
-
const attempts = [];
|
|
10274
|
-
for (let index = 0;index < policy.maxAttempts; index += 1) {
|
|
10275
|
-
const attempt = await dispatchChannel3(event, channel, this.transportOptions);
|
|
10276
|
-
attempt.attempt = index + 1;
|
|
10277
|
-
if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
|
|
10278
|
-
attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
|
|
10279
|
-
}
|
|
10280
|
-
attempts.push(attempt);
|
|
10281
|
-
if (attempt.status !== "failed")
|
|
10282
|
-
break;
|
|
10283
|
-
if (attempt.nextBackoffMs)
|
|
10284
|
-
await Bun.sleep(attempt.nextBackoffMs);
|
|
10285
|
-
}
|
|
10286
|
-
return createDeliveryResult2(event, channel, attempts);
|
|
10287
|
-
}
|
|
10288
|
-
}
|
|
10289
|
-
function redactPaths2(event, paths, replacement = "[REDACTED]") {
|
|
10290
|
-
if (paths.length === 0)
|
|
10291
|
-
return event;
|
|
10292
|
-
const copy = structuredClone(event);
|
|
10293
|
-
for (const path of paths) {
|
|
10294
|
-
setPath2(copy, path, replacement);
|
|
10295
|
-
}
|
|
10296
|
-
return copy;
|
|
10297
|
-
}
|
|
10298
|
-
function sanitizeChannelForOutput2(channel) {
|
|
10299
|
-
const copy = structuredClone(channel);
|
|
10300
|
-
if (copy.webhook?.secret)
|
|
10301
|
-
copy.webhook.secret = "[REDACTED]";
|
|
10302
|
-
if (copy.command?.env) {
|
|
10303
|
-
copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey2(key) ? "[REDACTED]" : value]));
|
|
10304
|
-
}
|
|
10305
|
-
return copy;
|
|
10306
|
-
}
|
|
10307
|
-
function sanitizeChannelsForOutput2(channels) {
|
|
10308
|
-
return channels.map(sanitizeChannelForOutput2);
|
|
10309
|
-
}
|
|
10310
|
-
function redactSensitiveKeys2(event, replacement = "[REDACTED]") {
|
|
10311
|
-
return redactValue2(event, replacement);
|
|
10312
|
-
}
|
|
10313
|
-
function shouldRedactKey2(key) {
|
|
10314
|
-
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
10315
|
-
}
|
|
10316
|
-
function redactValue2(value, replacement) {
|
|
10317
|
-
if (Array.isArray(value))
|
|
10318
|
-
return value.map((item) => redactValue2(item, replacement));
|
|
10319
|
-
if (!value || typeof value !== "object")
|
|
10320
|
-
return value;
|
|
10321
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
10322
|
-
key,
|
|
10323
|
-
shouldRedactKey2(key) ? replacement : redactValue2(item, replacement)
|
|
10324
|
-
]));
|
|
10325
|
-
}
|
|
10326
|
-
function setPath2(input, path, replacement) {
|
|
10327
|
-
const parts = path.split(".");
|
|
10328
|
-
let cursor = input;
|
|
10329
|
-
for (const part of parts.slice(0, -1)) {
|
|
10330
|
-
const next = cursor[part];
|
|
10331
|
-
if (!next || typeof next !== "object")
|
|
10332
|
-
return;
|
|
10333
|
-
cursor = next;
|
|
10334
|
-
}
|
|
10335
|
-
const last = parts.at(-1);
|
|
10336
|
-
if (last && last in cursor)
|
|
10337
|
-
cursor[last] = replacement;
|
|
10338
|
-
}
|
|
10339
|
-
function normalizeTime2(value) {
|
|
10340
|
-
if (!value)
|
|
10341
|
-
return new Date().toISOString();
|
|
10342
|
-
return value instanceof Date ? value.toISOString() : value;
|
|
10343
|
-
}
|
|
10344
|
-
function normalizeRetryPolicy2(policy) {
|
|
10345
|
-
return {
|
|
10346
|
-
maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
|
|
10347
|
-
backoffMs: Math.max(0, policy?.backoffMs ?? 250),
|
|
10348
|
-
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
10349
|
-
};
|
|
10350
|
-
}
|
|
10351
|
-
|
|
10352
|
-
// src/commands/runtime.ts
|
|
9173
|
+
import { EventsClient } from "@hasna/events";
|
|
10353
9174
|
function probeTmuxPane(target, tmuxCommand = process.env["HASNA_MACHINES_TMUX_BIN"] || "tmux") {
|
|
10354
9175
|
const checkedAt = new Date().toISOString();
|
|
10355
9176
|
const result = spawnSync4(tmuxCommand, ["display-message", "-p", "-t", target, "#{pane_id}"], {
|
|
@@ -10375,7 +9196,7 @@ async function watchTmuxPane(options) {
|
|
|
10375
9196
|
throw new Error("tmux pane target is required");
|
|
10376
9197
|
const intervalMs = Math.max(0, options.intervalMs ?? 5000);
|
|
10377
9198
|
const maxChecks = options.maxChecks ?? Number.POSITIVE_INFINITY;
|
|
10378
|
-
const client = options.client ?? new
|
|
9199
|
+
const client = options.client ?? new EventsClient;
|
|
10379
9200
|
const probe = options.probe ?? ((paneTarget) => probeTmuxPane(paneTarget, options.tmuxCommand));
|
|
10380
9201
|
const wait = options.sleep ?? sleep;
|
|
10381
9202
|
let lastPresent;
|
|
@@ -10436,7 +9257,8 @@ async function emitTmuxEvent(client, type, probe, lastPresent, deliver) {
|
|
|
10436
9257
|
}
|
|
10437
9258
|
|
|
10438
9259
|
// src/commands/screen.ts
|
|
10439
|
-
var
|
|
9260
|
+
var SCREEN_SECRET_NAMESPACE_ENV = "HASNA_MACHINES_SCREEN_SECRET_NAMESPACE";
|
|
9261
|
+
var DEFAULT_SCREEN_SECRET_NAMESPACE = "machines/screen-sharing";
|
|
10440
9262
|
function shellQuote6(value) {
|
|
10441
9263
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
10442
9264
|
}
|
|
@@ -10460,7 +9282,8 @@ function splitTarget(target) {
|
|
|
10460
9282
|
return [target.slice(0, at), target.slice(at + 1)];
|
|
10461
9283
|
}
|
|
10462
9284
|
function defaultScreenPasswordSecretKey(machineId) {
|
|
10463
|
-
|
|
9285
|
+
const namespace = process.env[SCREEN_SECRET_NAMESPACE_ENV]?.trim() || DEFAULT_SCREEN_SECRET_NAMESPACE;
|
|
9286
|
+
return `${namespace}/screen-${machineId}-vnc-password`;
|
|
10464
9287
|
}
|
|
10465
9288
|
function resolveScreenTarget(machineId, options = {}) {
|
|
10466
9289
|
const resolved = resolveMachineRoute(machineId, options);
|
|
@@ -10544,8 +9367,8 @@ function buildScreenEnableCommand(machineId, options = {}) {
|
|
|
10544
9367
|
}
|
|
10545
9368
|
|
|
10546
9369
|
// src/commands/sync.ts
|
|
10547
|
-
import { existsSync as
|
|
10548
|
-
import { homedir as
|
|
9370
|
+
import { existsSync as existsSync7, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
|
|
9371
|
+
import { homedir as homedir5 } from "os";
|
|
10549
9372
|
init_paths();
|
|
10550
9373
|
init_db();
|
|
10551
9374
|
function quote4(value) {
|
|
@@ -10599,8 +9422,8 @@ function detectFileActions(machine) {
|
|
|
10599
9422
|
throw new Error(`Remote file sync planning is not supported for ${machine.id}; refusing to inspect or apply local paths as remote state.`);
|
|
10600
9423
|
}
|
|
10601
9424
|
return (machine.files || []).map((file, index) => {
|
|
10602
|
-
const sourceExists =
|
|
10603
|
-
const targetExists =
|
|
9425
|
+
const sourceExists = existsSync7(file.source);
|
|
9426
|
+
const targetExists = existsSync7(file.target);
|
|
10604
9427
|
let status = "missing";
|
|
10605
9428
|
if (sourceExists && targetExists) {
|
|
10606
9429
|
if (file.mode === "symlink") {
|
|
@@ -10625,10 +9448,13 @@ function buildSyncPlan(machineId, runner = runMachineCommand) {
|
|
|
10625
9448
|
const manifest = readManifest();
|
|
10626
9449
|
const currentMachineId = getLocalMachineId();
|
|
10627
9450
|
const selected = machineId ? manifest.machines.find((machine) => machine.id === machineId) : manifest.machines.find((machine) => machine.id === currentMachineId);
|
|
9451
|
+
if (machineId && !selected) {
|
|
9452
|
+
throw new Error(`Machine not found in manifest: ${machineId}`);
|
|
9453
|
+
}
|
|
10628
9454
|
const target = selected || {
|
|
10629
9455
|
id: currentMachineId,
|
|
10630
9456
|
platform: "linux",
|
|
10631
|
-
workspacePath: `${
|
|
9457
|
+
workspacePath: `${homedir5()}/workspace`
|
|
10632
9458
|
};
|
|
10633
9459
|
const actions = [
|
|
10634
9460
|
...detectPackageActions(target, runner),
|
|
@@ -11185,6 +10011,7 @@ function runDoctor(machineId = getLocalMachineId()) {
|
|
|
11185
10011
|
init_db();
|
|
11186
10012
|
|
|
11187
10013
|
// src/commands/serve.ts
|
|
10014
|
+
import { EventsClient as EventsClient2, sanitizeChannelsForOutput } from "@hasna/events";
|
|
11188
10015
|
function escapeHtml(value) {
|
|
11189
10016
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
11190
10017
|
}
|
|
@@ -11401,7 +10228,7 @@ function startDashboardServer(options = {}) {
|
|
|
11401
10228
|
if (request.method !== "GET") {
|
|
11402
10229
|
return jsonError("Use GET for webhook channel listing.", 405);
|
|
11403
10230
|
}
|
|
11404
|
-
return Response.json(
|
|
10231
|
+
return Response.json(sanitizeChannelsForOutput(await events.listChannels()));
|
|
11405
10232
|
}
|
|
11406
10233
|
if (url.pathname === "/api/events") {
|
|
11407
10234
|
if (request.method === "GET") {
|
|
@@ -11534,8 +10361,8 @@ function runSelfTest() {
|
|
|
11534
10361
|
// src/commands/clipboard.ts
|
|
11535
10362
|
init_paths();
|
|
11536
10363
|
import { createHash } from "crypto";
|
|
11537
|
-
import { existsSync as
|
|
11538
|
-
import { join as
|
|
10364
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "fs";
|
|
10365
|
+
import { join as join6 } from "path";
|
|
11539
10366
|
var DEFAULT_CONFIG = {
|
|
11540
10367
|
version: 1,
|
|
11541
10368
|
enabled: true,
|
|
@@ -11553,7 +10380,7 @@ var DEFAULT_CONFIG = {
|
|
|
11553
10380
|
function resolveConfigPath(configPath) {
|
|
11554
10381
|
if (configPath)
|
|
11555
10382
|
return configPath;
|
|
11556
|
-
return
|
|
10383
|
+
return join6(getDataDir(), "clipboard-config.json");
|
|
11557
10384
|
}
|
|
11558
10385
|
function resolveHistoryPath(historyPath) {
|
|
11559
10386
|
if (historyPath)
|
|
@@ -11565,7 +10392,7 @@ function getDefaultConfig() {
|
|
|
11565
10392
|
}
|
|
11566
10393
|
function readConfig(configPath) {
|
|
11567
10394
|
const path = resolveConfigPath(configPath);
|
|
11568
|
-
if (!
|
|
10395
|
+
if (!existsSync8(path)) {
|
|
11569
10396
|
return getDefaultConfig();
|
|
11570
10397
|
}
|
|
11571
10398
|
const parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
@@ -11579,7 +10406,7 @@ function writeConfig(config, configPath) {
|
|
|
11579
10406
|
}
|
|
11580
10407
|
function readHistory(historyPath) {
|
|
11581
10408
|
const path = resolveHistoryPath(historyPath);
|
|
11582
|
-
if (!
|
|
10409
|
+
if (!existsSync8(path)) {
|
|
11583
10410
|
return [];
|
|
11584
10411
|
}
|
|
11585
10412
|
try {
|
|
@@ -11612,7 +10439,7 @@ function sanitizeClipboardForRead(content, maxSizeBytes, skipPatterns) {
|
|
|
11612
10439
|
}
|
|
11613
10440
|
function getOrCreateClipboardKey() {
|
|
11614
10441
|
const keyPath = getClipboardKeyPath();
|
|
11615
|
-
if (
|
|
10442
|
+
if (existsSync8(keyPath)) {
|
|
11616
10443
|
return readFileSync6(keyPath, "utf8").trim();
|
|
11617
10444
|
}
|
|
11618
10445
|
const key = createHash("sha256").update(crypto.randomUUID()).digest("hex").slice(0, 32);
|
|
@@ -11652,7 +10479,7 @@ function addClipboardEntry(entry, historyPath) {
|
|
|
11652
10479
|
}
|
|
11653
10480
|
function clearClipboardHistory(historyPath) {
|
|
11654
10481
|
const path = resolveHistoryPath(historyPath);
|
|
11655
|
-
if (
|
|
10482
|
+
if (existsSync8(path)) {
|
|
11656
10483
|
rmSync(path);
|
|
11657
10484
|
}
|
|
11658
10485
|
}
|
|
@@ -11669,7 +10496,7 @@ function getClipboardStatus(historyPath) {
|
|
|
11669
10496
|
// src/commands/clipboard-daemon.ts
|
|
11670
10497
|
init_paths();
|
|
11671
10498
|
import { readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
11672
|
-
import { join as
|
|
10499
|
+
import { join as join7 } from "path";
|
|
11673
10500
|
import { createHash as createHash3 } from "crypto";
|
|
11674
10501
|
|
|
11675
10502
|
// src/commands/clipboard-server.ts
|
|
@@ -11827,7 +10654,7 @@ function handleGetClipboard(response, config) {
|
|
|
11827
10654
|
}
|
|
11828
10655
|
|
|
11829
10656
|
// src/commands/clipboard-daemon.ts
|
|
11830
|
-
var DAEMON_PID_PATH =
|
|
10657
|
+
var DAEMON_PID_PATH = join7(getDataDir(), "clipboard-daemon.pid");
|
|
11831
10658
|
function readLocalClipboardSync2() {
|
|
11832
10659
|
const platform4 = process.platform;
|
|
11833
10660
|
if (platform4 === "darwin") {
|
|
@@ -12001,8 +10828,8 @@ async function discoverPeers() {
|
|
|
12001
10828
|
|
|
12002
10829
|
// src/commands/heal.ts
|
|
12003
10830
|
init_paths();
|
|
12004
|
-
import { existsSync as
|
|
12005
|
-
import { join as
|
|
10831
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
|
|
10832
|
+
import { join as join8 } from "path";
|
|
12006
10833
|
var DEFAULT_THRESHOLDS = {
|
|
12007
10834
|
reconnect: 3,
|
|
12008
10835
|
nmRestart: 7,
|
|
@@ -12046,14 +10873,14 @@ function defaultHealState() {
|
|
|
12046
10873
|
};
|
|
12047
10874
|
}
|
|
12048
10875
|
function getHealConfigPath() {
|
|
12049
|
-
return process.env["HASNA_MACHINES_HEAL_CONFIG_PATH"] ||
|
|
10876
|
+
return process.env["HASNA_MACHINES_HEAL_CONFIG_PATH"] || join8(getDataDir(), "heal-config.json");
|
|
12050
10877
|
}
|
|
12051
10878
|
function getHealStatePath() {
|
|
12052
|
-
return process.env["HASNA_MACHINES_HEAL_STATE_PATH"] ||
|
|
10879
|
+
return process.env["HASNA_MACHINES_HEAL_STATE_PATH"] || join8(getDataDir(), "heal-state.json");
|
|
12053
10880
|
}
|
|
12054
10881
|
function readHealConfig(path) {
|
|
12055
10882
|
const p = path || getHealConfigPath();
|
|
12056
|
-
if (!
|
|
10883
|
+
if (!existsSync9(p))
|
|
12057
10884
|
return { ...DEFAULT_HEAL_CONFIG, thresholds: { ...DEFAULT_THRESHOLDS } };
|
|
12058
10885
|
const parsed = JSON.parse(readFileSync9(p, "utf8"));
|
|
12059
10886
|
return {
|
|
@@ -12071,7 +10898,7 @@ function writeHealConfig(config, path) {
|
|
|
12071
10898
|
}
|
|
12072
10899
|
function readHealState(path) {
|
|
12073
10900
|
const p = path || getHealStatePath();
|
|
12074
|
-
if (!
|
|
10901
|
+
if (!existsSync9(p))
|
|
12075
10902
|
return defaultHealState();
|
|
12076
10903
|
try {
|
|
12077
10904
|
return { ...defaultHealState(), ...JSON.parse(readFileSync9(p, "utf8")) };
|
|
@@ -12111,7 +10938,7 @@ function evaluateHealth(probe, config, state) {
|
|
|
12111
10938
|
return { healthy: localOk && quorumOk, remoteScore, reasons };
|
|
12112
10939
|
}
|
|
12113
10940
|
function decideAction(input) {
|
|
12114
|
-
const { healthy, now
|
|
10941
|
+
const { healthy, now, gpuBusy, config, currentBootId } = input;
|
|
12115
10942
|
const s = { ...input.state };
|
|
12116
10943
|
const t = config.thresholds;
|
|
12117
10944
|
if (s.bootId !== currentBootId) {
|
|
@@ -12122,13 +10949,13 @@ function decideAction(input) {
|
|
|
12122
10949
|
if (healthy) {
|
|
12123
10950
|
s.failCount = 0;
|
|
12124
10951
|
if (s.bootHealthySince === null)
|
|
12125
|
-
s.bootHealthySince =
|
|
12126
|
-
if (
|
|
10952
|
+
s.bootHealthySince = now;
|
|
10953
|
+
if (now - s.bootHealthySince >= config.healthyWindowSec) {
|
|
12127
10954
|
s.failedBootRecoveries = 0;
|
|
12128
10955
|
s.rebootSuppressUntil = 0;
|
|
12129
10956
|
s.pendingRebootRecovery = false;
|
|
12130
10957
|
}
|
|
12131
|
-
if (s.degradedUntil > 0 &&
|
|
10958
|
+
if (s.degradedUntil > 0 && now >= s.degradedUntil) {
|
|
12132
10959
|
s.degradedUntil = 0;
|
|
12133
10960
|
return { action: "restore_preferred", state: s };
|
|
12134
10961
|
}
|
|
@@ -12146,8 +10973,8 @@ function decideAction(input) {
|
|
|
12146
10973
|
else if (s.failCount >= t.reconnect)
|
|
12147
10974
|
tier = "reconnect";
|
|
12148
10975
|
const tryReconnect = (reason) => {
|
|
12149
|
-
if (
|
|
12150
|
-
s.lastReconnect =
|
|
10976
|
+
if (now - s.lastReconnect >= config.reconnectMinIntervalSec) {
|
|
10977
|
+
s.lastReconnect = now;
|
|
12151
10978
|
return { action: "reconnect_wifi", suppressedReason: reason, state: s };
|
|
12152
10979
|
}
|
|
12153
10980
|
return { action: "none", suppressedReason: reason, state: s };
|
|
@@ -12156,15 +10983,15 @@ function decideAction(input) {
|
|
|
12156
10983
|
case "reconnect":
|
|
12157
10984
|
return tryReconnect();
|
|
12158
10985
|
case "nmRestart":
|
|
12159
|
-
if (
|
|
12160
|
-
s.lastNmRestart =
|
|
10986
|
+
if (now - s.lastNmRestart >= config.nmRestartMinIntervalSec) {
|
|
10987
|
+
s.lastNmRestart = now;
|
|
12161
10988
|
return { action: "restart_nm", state: s };
|
|
12162
10989
|
}
|
|
12163
10990
|
return tryReconnect();
|
|
12164
10991
|
case "fallback":
|
|
12165
|
-
if (
|
|
12166
|
-
s.lastFallback =
|
|
12167
|
-
s.degradedUntil =
|
|
10992
|
+
if (now - s.lastFallback >= config.fallbackWindowSec) {
|
|
10993
|
+
s.lastFallback = now;
|
|
10994
|
+
s.degradedUntil = now + config.fallbackWindowSec;
|
|
12168
10995
|
return { action: "fallback_ssid", state: s };
|
|
12169
10996
|
}
|
|
12170
10997
|
return tryReconnect();
|
|
@@ -12172,22 +10999,22 @@ function decideAction(input) {
|
|
|
12172
10999
|
let reason = null;
|
|
12173
11000
|
if (!config.allowReboot)
|
|
12174
11001
|
reason = "disabled";
|
|
12175
|
-
else if (
|
|
11002
|
+
else if (now < s.rebootSuppressUntil)
|
|
12176
11003
|
reason = "loop";
|
|
12177
11004
|
else if (config.gpuJobGuard && gpuBusy)
|
|
12178
11005
|
reason = "gpu";
|
|
12179
|
-
else if (
|
|
11006
|
+
else if (now - s.lastRebootAttempt < config.rebootMinIntervalSec)
|
|
12180
11007
|
reason = "rate";
|
|
12181
11008
|
if (reason)
|
|
12182
11009
|
return tryReconnect(reason);
|
|
12183
11010
|
if (s.pendingRebootRecovery) {
|
|
12184
11011
|
s.failedBootRecoveries += 1;
|
|
12185
11012
|
if (s.failedBootRecoveries >= config.maxFailedBootRecoveries) {
|
|
12186
|
-
s.rebootSuppressUntil =
|
|
11013
|
+
s.rebootSuppressUntil = now + config.bootBackoffSec;
|
|
12187
11014
|
return tryReconnect("loop");
|
|
12188
11015
|
}
|
|
12189
11016
|
}
|
|
12190
|
-
s.lastRebootAttempt =
|
|
11017
|
+
s.lastRebootAttempt = now;
|
|
12191
11018
|
s.pendingRebootRecovery = true;
|
|
12192
11019
|
return { action: "reboot", state: s };
|
|
12193
11020
|
}
|
|
@@ -12287,9 +11114,9 @@ function executeAction(action, config) {
|
|
|
12287
11114
|
|
|
12288
11115
|
// src/commands/heal-daemon.ts
|
|
12289
11116
|
init_paths();
|
|
12290
|
-
import { existsSync as
|
|
12291
|
-
import { join as
|
|
12292
|
-
var DAEMON_PID_PATH2 =
|
|
11117
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
|
|
11118
|
+
import { join as join9 } from "path";
|
|
11119
|
+
var DAEMON_PID_PATH2 = join9(getDataDir(), "heal-daemon.pid");
|
|
12293
11120
|
var SERVICE_PATH = "/etc/systemd/system/machines-heal.service";
|
|
12294
11121
|
var SYSTEM_CONF = "/etc/systemd/system.conf";
|
|
12295
11122
|
function log(msg) {
|
|
@@ -12407,7 +11234,7 @@ function applyDeterminism(config) {
|
|
|
12407
11234
|
}
|
|
12408
11235
|
function enableHardwareWatchdog() {
|
|
12409
11236
|
const log2 = [];
|
|
12410
|
-
if (!
|
|
11237
|
+
if (!existsSync10(SYSTEM_CONF))
|
|
12411
11238
|
return ["/etc/systemd/system.conf not found; skipping hardware watchdog"];
|
|
12412
11239
|
let conf = readFileSync10(SYSTEM_CONF, "utf8");
|
|
12413
11240
|
const set = (key, value) => {
|
|
@@ -12437,7 +11264,7 @@ function binPath() {
|
|
|
12437
11264
|
const home = process.env["HOME"] || "/home/hasna";
|
|
12438
11265
|
candidates.push(`${home}/.bun/bin/machines`, "/home/hasna/.bun/bin/machines", "/root/.bun/bin/machines", "/usr/local/bin/machines");
|
|
12439
11266
|
for (const c of candidates) {
|
|
12440
|
-
if (c &&
|
|
11267
|
+
if (c && existsSync10(c))
|
|
12441
11268
|
return c;
|
|
12442
11269
|
}
|
|
12443
11270
|
return "machines";
|
|
@@ -12473,7 +11300,7 @@ WantedBy=multi-user.target
|
|
|
12473
11300
|
function uninstallHealService() {
|
|
12474
11301
|
const log2 = [];
|
|
12475
11302
|
sh2("systemctl disable --now machines-heal.service 2>/dev/null || true");
|
|
12476
|
-
if (
|
|
11303
|
+
if (existsSync10(SERVICE_PATH)) {
|
|
12477
11304
|
sh2(`rm -f ${SERVICE_PATH}`);
|
|
12478
11305
|
sh2("systemctl daemon-reload");
|
|
12479
11306
|
log2.push(`removed ${SERVICE_PATH}`);
|
|
@@ -12484,7 +11311,7 @@ function uninstallHealService() {
|
|
|
12484
11311
|
}
|
|
12485
11312
|
function healServiceStatus() {
|
|
12486
11313
|
return {
|
|
12487
|
-
installed:
|
|
11314
|
+
installed: existsSync10(SERVICE_PATH),
|
|
12488
11315
|
active: sh2("systemctl is-active machines-heal.service").out === "active",
|
|
12489
11316
|
enabled: sh2("systemctl is-enabled machines-heal.service 2>/dev/null").out === "enabled"
|
|
12490
11317
|
};
|
|
@@ -12755,8 +11582,17 @@ program2.name("machines").description("Machine fleet management CLI + MCP for de
|
|
|
12755
11582
|
var manifestCommand = program2.command("manifest").description("Manage the fleet manifest");
|
|
12756
11583
|
var appsCommand = program2.command("apps").description("Manage installed applications per machine");
|
|
12757
11584
|
var notificationsCommand = program2.command("notifications").description("Manage fleet alert delivery channels");
|
|
12758
|
-
|
|
12759
|
-
|
|
11585
|
+
var eventWebhooksCommand = registerWebhookCommands(program2, { source: "machines" });
|
|
11586
|
+
eventWebhooksCommand.description("Manage shared event webhook subscriptions");
|
|
11587
|
+
var webhookTestCommand = eventWebhooksCommand.commands.find((command) => command.name() === "test");
|
|
11588
|
+
var webhookOptions = webhookTestCommand?.options ?? [];
|
|
11589
|
+
var webhookMessageOption = webhookOptions.find((option) => option.long === "--message");
|
|
11590
|
+
if (webhookMessageOption) {
|
|
11591
|
+
webhookMessageOption.defaultValue = "Shared events test delivery";
|
|
11592
|
+
}
|
|
11593
|
+
var eventsCommand = registerEventCommands(program2, { source: "machines" });
|
|
11594
|
+
eventsCommand.description("Emit, list, and replay shared events");
|
|
11595
|
+
var runtimeCommand = program2.command("runtime").description("Watch runtime conditions and emit shared events");
|
|
12760
11596
|
var clipboardCommand = program2.command("clipboard").description("Real-time clipboard sync across fleet machines");
|
|
12761
11597
|
var installClaudeCommand = program2.command("install-claude").description("Install or inspect Claude, Codex, and Gemini CLIs");
|
|
12762
11598
|
manifestCommand.command("init").description("Create an empty fleet manifest").action(() => {
|