@hasna/machines 0.0.32 → 0.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 existsSync3, mkdirSync } from "fs";
2086
- import { dirname as dirname2, join as join3, resolve } from "path";
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"] || join3(homeDir(), ".hasna", "machines");
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"] || join3(getDataDir(), "machines.db");
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"] || join3(getDataDir(), "machines.json");
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"] || join3(getDataDir(), "notifications.json");
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"] || join3(getDataDir(), "clipboard.key");
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"] || join3(getDataDir(), "clipboard-history.json");
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 (!existsSync3(dir)) {
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 now2 = new Date().toISOString();
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), now2, now2);
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 now2 = new Date().toISOString();
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), now2, now2);
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 now3 = new Date().toISOString();
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, now3, direction);
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 as existsSync2, readFileSync } from "fs";
3776
- import { dirname, join as join2 } from "path";
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 = [join2(here, "..", "package.json"), join2(here, "..", "..", "package.json")];
3782
- const pkgPath = candidates.find((candidate) => existsSync2(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 existsSync4, readFileSync as readFileSync2, writeFileSync } from "fs";
3794
- import { arch, homedir as homedir2, hostname, platform, userInfo } from "os";
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 = homedir2();
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 (!existsSync4(path)) {
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 homedir3 } from "os";
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 existsSync5 } from "fs";
7246
+ import { existsSync as existsSync4 } from "fs";
7924
7247
  import { arch as arch2, hostname as hostname3, platform as platform2, userInfo as userInfo2 } from "os";
7925
7248
  import { spawnSync } from "child_process";
7926
7249
  init_paths();
@@ -8033,6 +7356,19 @@ function manifestHostReachable(target) {
8033
7356
  return null;
8034
7357
  return overrides.has(target);
8035
7358
  }
7359
+ function userFromSshAddress(address) {
7360
+ if (!address)
7361
+ return null;
7362
+ const at = address.indexOf("@");
7363
+ if (at <= 0)
7364
+ return null;
7365
+ return address.slice(0, at);
7366
+ }
7367
+ function commandTargetForRoute(target, user) {
7368
+ if (target.kind === "local" || target.target.includes("@") || !user)
7369
+ return target.target;
7370
+ return `${user}@${target.target}`;
7371
+ }
8036
7372
  function routeHints(input) {
8037
7373
  const hints = [];
8038
7374
  if (input.machineId === input.localMachineId) {
@@ -8083,6 +7419,7 @@ function buildEntry(input) {
8083
7419
  });
8084
7420
  const selectedRoute = selectRouteHint(hints);
8085
7421
  const route = selectedRoute?.kind === "ssh" ? "ssh" : selectedRoute?.kind ?? "unknown";
7422
+ const routeUser = userFromSshAddress(manifest?.sshAddress) ?? (typeof manifest?.metadata?.user === "string" ? manifest.metadata.user : null);
8086
7423
  return {
8087
7424
  machine_id: input.machineId,
8088
7425
  hostname: manifest?.hostname ?? peer?.HostName ?? null,
@@ -8103,7 +7440,7 @@ function buildEntry(input) {
8103
7440
  ssh: {
8104
7441
  address: manifest?.sshAddress ?? null,
8105
7442
  route,
8106
- command_target: selectedRoute?.target ?? null
7443
+ command_target: selectedRoute ? commandTargetForRoute(selectedRoute, routeUser) : null
8107
7444
  },
8108
7445
  route_hints: hints,
8109
7446
  tags: manifest?.tags ?? [],
@@ -8111,7 +7448,7 @@ function buildEntry(input) {
8111
7448
  };
8112
7449
  }
8113
7450
  function discoverMachineTopology(options = {}) {
8114
- const now2 = options.now ?? new Date;
7451
+ const now = options.now ?? new Date;
8115
7452
  const runner = options.runner ?? defaultRunner;
8116
7453
  const warnings = [];
8117
7454
  const manifest = readManifest();
@@ -8143,11 +7480,11 @@ function discoverMachineTopology(options = {}) {
8143
7480
  version: getPackageVersion()
8144
7481
  },
8145
7482
  capabilities: getMachinesConsumerCapabilities(),
8146
- generated_at: now2.toISOString(),
7483
+ generated_at: now.toISOString(),
8147
7484
  local_machine_id: localMachineId,
8148
7485
  local_hostname: hostname3(),
8149
7486
  current_platform: normalizePlatform2(),
8150
- manifest_path_known: existsSync5(getManifestPath()),
7487
+ manifest_path_known: existsSync4(getManifestPath()),
8151
7488
  machines,
8152
7489
  warnings
8153
7490
  };
@@ -8266,11 +7603,11 @@ function cacheability(input) {
8266
7603
  };
8267
7604
  }
8268
7605
  function resolveMachineRoute(machineId, options = {}) {
8269
- const now2 = options.now ?? new Date;
7606
+ const now = options.now ?? new Date;
8270
7607
  const topology = options.topology ?? discoverMachineTopology(options);
8271
7608
  const warnings = [...topology.warnings];
8272
7609
  const { machine, matchedBy } = findRouteMachine(topology, machineId);
8273
- const generatedAt = now2.toISOString();
7610
+ const generatedAt = now.toISOString();
8274
7611
  if (!machine) {
8275
7612
  warnings.push(`machine_not_found:${machineId}`);
8276
7613
  return {
@@ -8296,8 +7633,8 @@ function resolveMachineRoute(machineId, options = {}) {
8296
7633
  },
8297
7634
  cacheability: cacheability({
8298
7635
  ok: false,
8299
- observedAt: now2,
8300
- now: now2,
7636
+ observedAt: now,
7637
+ now,
8301
7638
  ttlMs: options.resolverTtlMs,
8302
7639
  authority: "unresolved",
8303
7640
  confidence: "none",
@@ -8311,6 +7648,7 @@ function resolveMachineRoute(machineId, options = {}) {
8311
7648
  const local = route === "local" || machine.machine_id === topology.local_machine_id;
8312
7649
  const confidence = routeConfidence({ machine, hint: selectedHint, matchedBy });
8313
7650
  const ok = Boolean(selectedHint?.target);
7651
+ const commandTarget = selectedHint ? commandTargetForRoute(selectedHint, userFromSshAddress(machine.ssh.address) ?? machine.user) : null;
8314
7652
  return {
8315
7653
  schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
8316
7654
  package: topology.package,
@@ -8321,7 +7659,7 @@ function resolveMachineRoute(machineId, options = {}) {
8321
7659
  route,
8322
7660
  source: route,
8323
7661
  target: selectedHint?.target ?? null,
8324
- command_target: selectedHint?.target ?? null,
7662
+ command_target: commandTarget,
8325
7663
  confidence,
8326
7664
  local,
8327
7665
  evidence: {
@@ -8334,8 +7672,8 @@ function resolveMachineRoute(machineId, options = {}) {
8334
7672
  },
8335
7673
  cacheability: cacheability({
8336
7674
  ok,
8337
- observedAt: now2,
8338
- now: now2,
7675
+ observedAt: now,
7676
+ now,
8339
7677
  ttlMs: options.resolverTtlMs,
8340
7678
  authority: routeAuthority({ machine, selectedHint, matchedBy }),
8341
7679
  confidence,
@@ -8422,7 +7760,7 @@ function canCheckPathForMachine(machine, localMachineId) {
8422
7760
  function checkedPathExists(path, check) {
8423
7761
  if (!path || !check)
8424
7762
  return null;
8425
- return existsSync5(path);
7763
+ return existsSync4(path);
8426
7764
  }
8427
7765
  function repairHint(input) {
8428
7766
  const command = [
@@ -8637,11 +7975,11 @@ function metadataKeysForDiagnostics(metadata) {
8637
7975
  return Object.keys(metadata).filter((key) => !/(secret|token|key|password|credential)/i.test(key)).sort();
8638
7976
  }
8639
7977
  function resolveMachineWorkspace(options) {
8640
- const now2 = options.now ?? new Date;
7978
+ const now = options.now ?? new Date;
8641
7979
  const topology = options.topology ?? discoverMachineTopology(options);
8642
7980
  const warnings = [...topology.warnings];
8643
7981
  const { machine, matchedBy } = findRouteMachine(topology, options.machineId);
8644
- const generatedAt = now2.toISOString();
7982
+ const generatedAt = now.toISOString();
8645
7983
  const repoName = options.repoName ?? options.projectId;
8646
7984
  const openFilesRepoName = options.openFilesRepoName ?? "open-files";
8647
7985
  if (!machine) {
@@ -8670,8 +8008,8 @@ function resolveMachineWorkspace(options) {
8670
8008
  },
8671
8009
  cacheability: cacheability({
8672
8010
  ok: false,
8673
- observedAt: now2,
8674
- now: now2,
8011
+ observedAt: now,
8012
+ now,
8675
8013
  ttlMs: options.resolverTtlMs,
8676
8014
  authority: "unresolved",
8677
8015
  confidence: "none",
@@ -8743,8 +8081,8 @@ function resolveMachineWorkspace(options) {
8743
8081
  },
8744
8082
  cacheability: cacheability({
8745
8083
  ok: workspaceOk,
8746
- observedAt: now2,
8747
- now: now2,
8084
+ observedAt: now,
8085
+ now,
8748
8086
  ttlMs: options.resolverTtlMs,
8749
8087
  authority: workspaceAuthority(workspacePaths),
8750
8088
  confidence: workspaceOk ? "medium" : "none",
@@ -8779,7 +8117,7 @@ function resolveSshTarget(machineId, options = {}) {
8779
8117
  }
8780
8118
  return {
8781
8119
  machineId: resolved.machine_id ?? machineId,
8782
- target: resolved.target,
8120
+ target: resolved.command_target ?? resolved.target,
8783
8121
  route: resolved.route,
8784
8122
  confidence: resolved.confidence,
8785
8123
  warnings: resolved.warnings
@@ -8913,7 +8251,7 @@ function buildSetupPlan(machineId) {
8913
8251
  const target = selected || {
8914
8252
  id: currentMachineId,
8915
8253
  platform: "linux",
8916
- workspacePath: `${homedir3()}/workspace`
8254
+ workspacePath: `${homedir2()}/workspace`
8917
8255
  };
8918
8256
  const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
8919
8257
  return {
@@ -8958,8 +8296,8 @@ function runSetup(machineId, options = {}, runner = runMachineCommand) {
8958
8296
  }
8959
8297
 
8960
8298
  // src/commands/backup.ts
8961
- import { homedir as homedir4, hostname as hostname5 } from "os";
8962
- import { join as join4 } from "path";
8299
+ import { homedir as homedir3, hostname as hostname5 } from "os";
8300
+ import { join as join3 } from "path";
8963
8301
  var MACHINES_BACKUP_BUCKET_ENV = "HASNA_MACHINES_S3_BUCKET";
8964
8302
  var MACHINES_BACKUP_BUCKET_FALLBACK_ENV = "MACHINES_S3_BUCKET";
8965
8303
  var MACHINES_BACKUP_PREFIX_ENV = "HASNA_MACHINES_S3_PREFIX";
@@ -9007,16 +8345,16 @@ function resolveBackupTarget(options = {}) {
9007
8345
  };
9008
8346
  }
9009
8347
  function defaultBackupSources() {
9010
- const home = homedir4();
8348
+ const home = homedir3();
9011
8349
  return [
9012
- join4(home, ".hasna"),
9013
- join4(home, ".ssh"),
9014
- join4(home, ".secrets")
8350
+ join3(home, ".hasna"),
8351
+ join3(home, ".ssh"),
8352
+ join3(home, ".secrets")
9015
8353
  ];
9016
8354
  }
9017
8355
  function buildBackupPlan(bucket, prefix) {
9018
8356
  const target = resolveBackupTarget({ bucket, prefix });
9019
- const archivePath = join4(homedir4(), ".hasna", "machines", "backup.tgz");
8357
+ const archivePath = join3(homedir3(), ".hasna", "machines", "backup.tgz");
9020
8358
  const sources = defaultBackupSources();
9021
8359
  const steps = [
9022
8360
  {
@@ -9067,21 +8405,21 @@ function runBackup(bucket, prefix, options = {}) {
9067
8405
  }
9068
8406
 
9069
8407
  // src/commands/cert.ts
9070
- import { homedir as homedir5, platform as platform3 } from "os";
9071
- import { join as join5 } from "path";
8408
+ import { homedir as homedir4, platform as platform3 } from "os";
8409
+ import { join as join4 } from "path";
9072
8410
  function quote3(value) {
9073
8411
  return `'${value.replace(/'/g, `'\\''`)}'`;
9074
8412
  }
9075
8413
  function certDir() {
9076
- return join5(homedir5(), ".hasna", "machines", "certs");
8414
+ return join4(homedir4(), ".hasna", "machines", "certs");
9077
8415
  }
9078
8416
  function buildCertPlan(domains) {
9079
8417
  if (domains.length === 0) {
9080
8418
  throw new Error("At least one domain is required.");
9081
8419
  }
9082
8420
  const primary = domains[0];
9083
- const certPath = join5(certDir(), `${primary}.pem`);
9084
- const keyPath = join5(certDir(), `${primary}-key.pem`);
8421
+ const certPath = join4(certDir(), `${primary}.pem`);
8422
+ const keyPath = join4(certDir(), `${primary}-key.pem`);
9085
8423
  const steps = [];
9086
8424
  if (platform3() === "darwin") {
9087
8425
  steps.push({
@@ -9146,14 +8484,14 @@ function runCertPlan(domains, options = {}) {
9146
8484
 
9147
8485
  // src/commands/dns.ts
9148
8486
  init_paths();
9149
- import { existsSync as existsSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
9150
- import { join as join6 } from "path";
8487
+ import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
8488
+ import { join as join5 } from "path";
9151
8489
  function getDnsPath() {
9152
- return join6(getDataDir(), "dns.json");
8490
+ return join5(getDataDir(), "dns.json");
9153
8491
  }
9154
8492
  function readMappings() {
9155
8493
  const path = getDnsPath();
9156
- if (!existsSync6(path))
8494
+ if (!existsSync5(path))
9157
8495
  return [];
9158
8496
  return JSON.parse(readFileSync3(path, "utf8"));
9159
8497
  }
@@ -9182,10 +8520,10 @@ function renderDomainMapping(domain) {
9182
8520
  hostsEntry: `${entry.targetHost} ${entry.domain}`,
9183
8521
  caddySnippet: `${entry.domain} {
9184
8522
  reverse_proxy 127.0.0.1:${entry.port}
9185
- tls ${join6(getDataDir(), "certs", `${entry.domain}.pem`)} ${join6(getDataDir(), "certs", `${entry.domain}-key.pem`)}
8523
+ tls ${join5(getDataDir(), "certs", `${entry.domain}.pem`)} ${join5(getDataDir(), "certs", `${entry.domain}-key.pem`)}
9186
8524
  }`,
9187
- certPath: join6(getDataDir(), "certs", `${entry.domain}.pem`),
9188
- keyPath: join6(getDataDir(), "certs", `${entry.domain}-key.pem`)
8525
+ certPath: join5(getDataDir(), "certs", `${entry.domain}.pem`),
8526
+ keyPath: join5(getDataDir(), "certs", `${entry.domain}-key.pem`)
9189
8527
  };
9190
8528
  }
9191
8529
 
@@ -9542,7 +8880,7 @@ function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand
9542
8880
  }
9543
8881
 
9544
8882
  // src/commands/notifications.ts
9545
- import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
8883
+ import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
9546
8884
  init_paths();
9547
8885
  var notificationChannelSchema = exports_external.object({
9548
8886
  id: exports_external.string(),
@@ -9625,7 +8963,7 @@ ${message}
9625
8963
  }
9626
8964
  throw new Error("No local email transport available. Install sendmail or mail.");
9627
8965
  }
9628
- async function dispatchWebhook2(channel, event, message) {
8966
+ async function dispatchWebhook(channel, event, message) {
9629
8967
  const response = await fetch(channel.target, {
9630
8968
  method: "POST",
9631
8969
  headers: {
@@ -9650,7 +8988,7 @@ async function dispatchWebhook2(channel, event, message) {
9650
8988
  detail: `Webhook accepted with HTTP ${response.status}`
9651
8989
  };
9652
8990
  }
9653
- async function dispatchCommand2(channel, event, message) {
8991
+ async function dispatchCommand(channel, event, message) {
9654
8992
  const result = Bun.spawnSync(["bash", "-lc", channel.target], {
9655
8993
  stdout: "pipe",
9656
8994
  stderr: "pipe",
@@ -9673,7 +9011,7 @@ async function dispatchCommand2(channel, event, message) {
9673
9011
  detail: stdout || "Command completed successfully"
9674
9012
  };
9675
9013
  }
9676
- async function dispatchChannel2(channel, event, message) {
9014
+ async function dispatchChannel(channel, event, message) {
9677
9015
  if (!channel.enabled) {
9678
9016
  return {
9679
9017
  channelId: channel.id,
@@ -9687,9 +9025,9 @@ async function dispatchChannel2(channel, event, message) {
9687
9025
  return dispatchEmail(channel, event, message);
9688
9026
  }
9689
9027
  if (channel.type === "webhook") {
9690
- return dispatchWebhook2(channel, event, message);
9028
+ return dispatchWebhook(channel, event, message);
9691
9029
  }
9692
- return dispatchCommand2(channel, event, message);
9030
+ return dispatchCommand(channel, event, message);
9693
9031
  }
9694
9032
  function getDefaultNotificationConfig() {
9695
9033
  return {
@@ -9699,7 +9037,7 @@ function getDefaultNotificationConfig() {
9699
9037
  };
9700
9038
  }
9701
9039
  function readNotificationConfig(path = getNotificationsPath()) {
9702
- if (!existsSync7(path)) {
9040
+ if (!existsSync6(path)) {
9703
9041
  return getDefaultNotificationConfig();
9704
9042
  }
9705
9043
  return notificationConfigSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
@@ -9744,7 +9082,7 @@ async function dispatchNotificationEvent(event, message, options = {}) {
9744
9082
  const deliveries = [];
9745
9083
  for (const channel of channels) {
9746
9084
  try {
9747
- deliveries.push(await dispatchChannel2(channel, event, message));
9085
+ deliveries.push(await dispatchChannel(channel, event, message));
9748
9086
  } catch (error) {
9749
9087
  deliveries.push({
9750
9088
  channelId: channel.id,
@@ -9779,7 +9117,7 @@ async function testNotificationChannel(channelId, event = "manual.test", message
9779
9117
  if (!options.yes) {
9780
9118
  throw new Error("Notification test execution requires --yes.");
9781
9119
  }
9782
- const delivery = await dispatchChannel2(channel, event, message);
9120
+ const delivery = await dispatchChannel(channel, event, message);
9783
9121
  return {
9784
9122
  channelId,
9785
9123
  mode: "apply",
@@ -9847,528 +9185,7 @@ function listPorts(machineId) {
9847
9185
  // src/commands/runtime.ts
9848
9186
  import { spawnSync as spawnSync4 } from "child_process";
9849
9187
  import { setTimeout as sleep } from "timers/promises";
9850
-
9851
- // node_modules/@hasna/events/dist/index.js
9852
- import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
9853
- import { existsSync as existsSync8 } from "fs";
9854
- import { homedir as homedir6 } from "os";
9855
- import { join as join7 } from "path";
9856
- import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
9857
- import { randomUUID as randomUUID3 } from "crypto";
9858
- import { spawn as spawn2 } from "child_process";
9859
- import { randomUUID as randomUUID22 } from "crypto";
9860
- function getPathValue2(input, path) {
9861
- return path.split(".").reduce((value, part) => {
9862
- if (value && typeof value === "object" && part in value) {
9863
- return value[part];
9864
- }
9865
- return;
9866
- }, input);
9867
- }
9868
- function wildcardToRegExp2(pattern) {
9869
- const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
9870
- return new RegExp(`^${escaped}$`);
9871
- }
9872
- function matchString2(value, matcher) {
9873
- if (matcher === undefined)
9874
- return true;
9875
- if (value === undefined)
9876
- return false;
9877
- const matchers = Array.isArray(matcher) ? matcher : [matcher];
9878
- return matchers.some((item) => wildcardToRegExp2(item).test(value));
9879
- }
9880
- function matchRecord2(input, matcher) {
9881
- if (!matcher)
9882
- return true;
9883
- return Object.entries(matcher).every(([path, expected]) => {
9884
- const actual = getPathValue2(input, path);
9885
- if (typeof expected === "string" || Array.isArray(expected)) {
9886
- return matchString2(actual === undefined ? undefined : String(actual), expected);
9887
- }
9888
- return actual === expected;
9889
- });
9890
- }
9891
- function eventMatchesFilter2(event, filter) {
9892
- return matchString2(event.source, filter.source) && matchString2(event.type, filter.type) && matchString2(event.subject, filter.subject) && matchString2(event.severity, filter.severity) && matchRecord2(event.data, filter.data) && matchRecord2(event.metadata, filter.metadata);
9893
- }
9894
- function channelMatchesEvent2(channel, event) {
9895
- if (!channel.enabled)
9896
- return false;
9897
- if (!channel.filters || channel.filters.length === 0)
9898
- return true;
9899
- return channel.filters.some((filter) => eventMatchesFilter2(event, filter));
9900
- }
9901
- var HASNA_EVENTS_DIR_ENV2 = "HASNA_EVENTS_DIR";
9902
- var HASNA_EVENTS_HOME_ENV2 = "HASNA_EVENTS_HOME";
9903
- function getEventsDataDir2(override) {
9904
- return override || process.env[HASNA_EVENTS_DIR_ENV2] || process.env[HASNA_EVENTS_HOME_ENV2] || join7(homedir6(), ".hasna", "events");
9905
- }
9906
-
9907
- class JsonEventsStore2 {
9908
- dataDir;
9909
- channelsPath;
9910
- eventsPath;
9911
- deliveriesPath;
9912
- constructor(dataDir = getEventsDataDir2()) {
9913
- this.dataDir = dataDir;
9914
- this.channelsPath = join7(dataDir, "channels.json");
9915
- this.eventsPath = join7(dataDir, "events.json");
9916
- this.deliveriesPath = join7(dataDir, "deliveries.json");
9917
- }
9918
- async init() {
9919
- await mkdir2(this.dataDir, { recursive: true, mode: 448 });
9920
- await chmod2(this.dataDir, 448).catch(() => {
9921
- return;
9922
- });
9923
- await this.ensureArrayFile(this.channelsPath);
9924
- await this.ensureArrayFile(this.eventsPath);
9925
- await this.ensureArrayFile(this.deliveriesPath);
9926
- }
9927
- async addChannel(channel) {
9928
- await this.init();
9929
- const channels = await this.readJson(this.channelsPath, []);
9930
- const index = channels.findIndex((item) => item.id === channel.id);
9931
- if (index >= 0) {
9932
- channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
9933
- } else {
9934
- channels.push(channel);
9935
- }
9936
- await this.writeJson(this.channelsPath, channels);
9937
- return index >= 0 ? channels[index] : channel;
9938
- }
9939
- async listChannels() {
9940
- await this.init();
9941
- return this.readJson(this.channelsPath, []);
9942
- }
9943
- async getChannel(id) {
9944
- const channels = await this.listChannels();
9945
- return channels.find((channel) => channel.id === id);
9946
- }
9947
- async removeChannel(id) {
9948
- await this.init();
9949
- const channels = await this.readJson(this.channelsPath, []);
9950
- const next = channels.filter((channel) => channel.id !== id);
9951
- await this.writeJson(this.channelsPath, next);
9952
- return next.length !== channels.length;
9953
- }
9954
- async appendEvent(event) {
9955
- await this.init();
9956
- const events = await this.readJson(this.eventsPath, []);
9957
- events.push(event);
9958
- await this.writeJson(this.eventsPath, events);
9959
- return event;
9960
- }
9961
- async listEvents() {
9962
- await this.init();
9963
- return this.readJson(this.eventsPath, []);
9964
- }
9965
- async findEventByIdentity(identity) {
9966
- const events = await this.listEvents();
9967
- return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
9968
- }
9969
- async appendDelivery(result) {
9970
- await this.init();
9971
- const deliveries = await this.readJson(this.deliveriesPath, []);
9972
- deliveries.push(result);
9973
- await this.writeJson(this.deliveriesPath, deliveries);
9974
- return result;
9975
- }
9976
- async listDeliveries() {
9977
- await this.init();
9978
- return this.readJson(this.deliveriesPath, []);
9979
- }
9980
- async exportData() {
9981
- return {
9982
- channels: await this.listChannels(),
9983
- events: await this.listEvents(),
9984
- deliveries: await this.listDeliveries()
9985
- };
9986
- }
9987
- async ensureArrayFile(path) {
9988
- if (!existsSync8(path)) {
9989
- await writeFile2(path, `[]
9990
- `, { encoding: "utf-8", mode: 384 });
9991
- }
9992
- await chmod2(path, 384).catch(() => {
9993
- return;
9994
- });
9995
- }
9996
- async readJson(path, fallback) {
9997
- try {
9998
- const raw = await readFile2(path, "utf-8");
9999
- if (!raw.trim())
10000
- return fallback;
10001
- return JSON.parse(raw);
10002
- } catch (error) {
10003
- if (error.code === "ENOENT")
10004
- return fallback;
10005
- throw error;
10006
- }
10007
- }
10008
- async writeJson(path, value) {
10009
- const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
10010
- await writeFile2(tempPath, `${JSON.stringify(value, null, 2)}
10011
- `, { encoding: "utf-8", mode: 384 });
10012
- await rename2(tempPath, path);
10013
- await chmod2(path, 384).catch(() => {
10014
- return;
10015
- });
10016
- }
10017
- }
10018
- var DEFAULT_SIGNATURE_TOLERANCE_MS2 = 5 * 60 * 1000;
10019
- function buildSignatureBase2(timestamp, body) {
10020
- return `${timestamp}.${body}`;
10021
- }
10022
- function signPayload2(secret, timestamp, body) {
10023
- const digest = createHmac2("sha256", secret).update(buildSignatureBase2(timestamp, body)).digest("hex");
10024
- return `sha256=${digest}`;
10025
- }
10026
- function now2() {
10027
- return new Date().toISOString();
10028
- }
10029
- function truncate2(value, max = 4096) {
10030
- return value.length > max ? `${value.slice(0, max)}...` : value;
10031
- }
10032
- function buildWebhookRequest2(event, channel) {
10033
- if (!channel.webhook)
10034
- throw new Error(`Channel ${channel.id} has no webhook config`);
10035
- const body = JSON.stringify(event);
10036
- const timestamp = event.time;
10037
- const headers = {
10038
- "Content-Type": "application/json",
10039
- "User-Agent": "@hasna/events",
10040
- "X-Hasna-Event-Id": event.id,
10041
- "X-Hasna-Event-Type": event.type,
10042
- "X-Hasna-Timestamp": timestamp,
10043
- ...channel.webhook.headers
10044
- };
10045
- if (channel.webhook.secret) {
10046
- headers["X-Hasna-Signature"] = signPayload2(channel.webhook.secret, timestamp, body);
10047
- }
10048
- return { body, headers };
10049
- }
10050
- async function dispatchWebhook3(event, channel, options = {}) {
10051
- if (!channel.webhook)
10052
- throw new Error(`Channel ${channel.id} has no webhook config`);
10053
- const startedAt = now2();
10054
- const { body, headers } = buildWebhookRequest2(event, channel);
10055
- const controller = new AbortController;
10056
- const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
10057
- try {
10058
- const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
10059
- method: "POST",
10060
- headers,
10061
- body,
10062
- signal: controller.signal
10063
- });
10064
- const responseBody = truncate2(await response.text());
10065
- return {
10066
- attempt: 1,
10067
- status: response.ok ? "success" : "failed",
10068
- startedAt,
10069
- completedAt: now2(),
10070
- responseStatus: response.status,
10071
- responseBody,
10072
- error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
10073
- };
10074
- } catch (error) {
10075
- return {
10076
- attempt: 1,
10077
- status: "failed",
10078
- startedAt,
10079
- completedAt: now2(),
10080
- error: error instanceof Error ? error.message : String(error)
10081
- };
10082
- } finally {
10083
- clearTimeout(timeout);
10084
- }
10085
- }
10086
- async function dispatchCommand3(event, channel) {
10087
- if (!channel.command)
10088
- throw new Error(`Channel ${channel.id} has no command config`);
10089
- const startedAt = now2();
10090
- const eventJson = JSON.stringify(event);
10091
- const env2 = {
10092
- ...process.env,
10093
- ...channel.command.env,
10094
- HASNA_CHANNEL_ID: channel.id,
10095
- HASNA_EVENT_ID: event.id,
10096
- HASNA_EVENT_TYPE: event.type,
10097
- HASNA_EVENT_SOURCE: event.source,
10098
- HASNA_EVENT_SUBJECT: event.subject ?? "",
10099
- HASNA_EVENT_SEVERITY: event.severity,
10100
- HASNA_EVENT_TIME: event.time,
10101
- HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
10102
- HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
10103
- HASNA_EVENT_JSON: eventJson
10104
- };
10105
- return new Promise((resolve2) => {
10106
- const child = spawn2(channel.command.command, channel.command.args ?? [], {
10107
- cwd: channel.command.cwd,
10108
- env: env2,
10109
- stdio: ["pipe", "pipe", "pipe"]
10110
- });
10111
- let stdout = "";
10112
- let stderr = "";
10113
- const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
10114
- child.stdin.end(eventJson);
10115
- child.stdout.on("data", (chunk) => {
10116
- stdout += chunk.toString();
10117
- });
10118
- child.stderr.on("data", (chunk) => {
10119
- stderr += chunk.toString();
10120
- });
10121
- child.on("error", (error) => {
10122
- clearTimeout(timeout);
10123
- resolve2({
10124
- attempt: 1,
10125
- status: "failed",
10126
- startedAt,
10127
- completedAt: now2(),
10128
- stdout: truncate2(stdout),
10129
- stderr: truncate2(stderr),
10130
- error: error.message
10131
- });
10132
- });
10133
- child.on("close", (code, signal) => {
10134
- clearTimeout(timeout);
10135
- const success = code === 0;
10136
- resolve2({
10137
- attempt: 1,
10138
- status: success ? "success" : "failed",
10139
- startedAt,
10140
- completedAt: now2(),
10141
- stdout: truncate2(stdout),
10142
- stderr: truncate2(stderr),
10143
- error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
10144
- });
10145
- });
10146
- });
10147
- }
10148
- async function dispatchChannel3(event, channel, options = {}) {
10149
- if (channel.transport === "webhook")
10150
- return dispatchWebhook3(event, channel, options);
10151
- if (channel.transport === "command")
10152
- return dispatchCommand3(event, channel);
10153
- return {
10154
- attempt: 1,
10155
- status: "skipped",
10156
- startedAt: now2(),
10157
- completedAt: now2(),
10158
- error: `Unsupported transport: ${channel.transport}`
10159
- };
10160
- }
10161
- function createDeliveryResult2(event, channel, attempts) {
10162
- const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
10163
- return {
10164
- id: randomUUID3(),
10165
- eventId: event.id,
10166
- channelId: channel.id,
10167
- transport: channel.transport,
10168
- status,
10169
- attempts,
10170
- createdAt: attempts[0]?.startedAt ?? now2(),
10171
- completedAt: attempts.at(-1)?.completedAt ?? now2()
10172
- };
10173
- }
10174
- function createEvent2(input) {
10175
- return {
10176
- id: input.id ?? randomUUID22(),
10177
- source: input.source,
10178
- type: input.type,
10179
- time: normalizeTime2(input.time),
10180
- subject: input.subject,
10181
- severity: input.severity ?? "info",
10182
- data: input.data ?? {},
10183
- message: input.message,
10184
- dedupeKey: input.dedupeKey,
10185
- schemaVersion: input.schemaVersion ?? "1.0",
10186
- metadata: input.metadata ?? {}
10187
- };
10188
- }
10189
-
10190
- class EventsClient2 {
10191
- store;
10192
- redactors;
10193
- transportOptions;
10194
- constructor(options = {}) {
10195
- this.store = options.store ?? new JsonEventsStore2(options.dataDir);
10196
- this.redactors = options.redactors ?? [];
10197
- this.transportOptions = { fetchImpl: options.fetchImpl };
10198
- }
10199
- async addChannel(input) {
10200
- const timestamp = new Date().toISOString();
10201
- return this.store.addChannel({
10202
- ...input,
10203
- createdAt: input.createdAt ?? timestamp,
10204
- updatedAt: input.updatedAt ?? timestamp
10205
- });
10206
- }
10207
- async listChannels() {
10208
- return this.store.listChannels();
10209
- }
10210
- async removeChannel(id) {
10211
- return this.store.removeChannel(id);
10212
- }
10213
- async emit(input, options = {}) {
10214
- const event = options.redactSensitiveData === false ? createEvent2(input) : redactSensitiveKeys2(createEvent2(input));
10215
- if (options.dedupe !== false) {
10216
- const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
10217
- if (existing) {
10218
- return { event: existing, deliveries: [], deduped: true };
10219
- }
10220
- }
10221
- await this.store.appendEvent(event);
10222
- const deliveries = options.deliver === false ? [] : await this.deliver(event);
10223
- return { event, deliveries, deduped: false };
10224
- }
10225
- async listEvents() {
10226
- return this.store.listEvents();
10227
- }
10228
- async listDeliveries() {
10229
- return this.store.listDeliveries();
10230
- }
10231
- async deliver(event) {
10232
- const channels = await this.store.listChannels();
10233
- const selected = channels.filter((channel) => channelMatchesEvent2(channel, event));
10234
- const deliveries = [];
10235
- for (const channel of selected) {
10236
- const eventForChannel = await this.applyRedaction(event, channel);
10237
- const result = await this.deliverWithRetry(eventForChannel, channel);
10238
- await this.store.appendDelivery(result);
10239
- deliveries.push(result);
10240
- }
10241
- return deliveries;
10242
- }
10243
- async testChannel(id, input = {}) {
10244
- const channel = await this.store.getChannel(id);
10245
- if (!channel)
10246
- throw new Error(`Channel not found: ${id}`);
10247
- const event = createEvent2({
10248
- source: input.source ?? "hasna.events",
10249
- type: input.type ?? "events.test",
10250
- subject: input.subject ?? id,
10251
- severity: input.severity ?? "info",
10252
- data: input.data ?? { test: true },
10253
- message: input.message ?? "Hasna events test delivery",
10254
- dedupeKey: input.dedupeKey,
10255
- schemaVersion: input.schemaVersion,
10256
- metadata: input.metadata,
10257
- time: input.time,
10258
- id: input.id
10259
- });
10260
- const eventForChannel = await this.applyRedaction(event, channel);
10261
- const result = await this.deliverWithRetry(eventForChannel, channel);
10262
- await this.store.appendDelivery(result);
10263
- return result;
10264
- }
10265
- async replay(options = {}) {
10266
- const events = (await this.store.listEvents()).filter((event) => {
10267
- if (options.eventId && event.id !== options.eventId)
10268
- return false;
10269
- if (options.source && event.source !== options.source)
10270
- return false;
10271
- if (options.type && event.type !== options.type)
10272
- return false;
10273
- return true;
10274
- });
10275
- if (options.dryRun)
10276
- return { events, deliveries: [] };
10277
- const deliveries = [];
10278
- for (const event of events) {
10279
- deliveries.push(...await this.deliver(event));
10280
- }
10281
- return { events, deliveries };
10282
- }
10283
- async applyRedaction(event, channel) {
10284
- let next = redactPaths2(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
10285
- for (const redactor of this.redactors) {
10286
- next = await redactor(next, channel);
10287
- }
10288
- return next;
10289
- }
10290
- async deliverWithRetry(event, channel) {
10291
- const policy = normalizeRetryPolicy2(channel.retry);
10292
- const attempts = [];
10293
- for (let index = 0;index < policy.maxAttempts; index += 1) {
10294
- const attempt = await dispatchChannel3(event, channel, this.transportOptions);
10295
- attempt.attempt = index + 1;
10296
- if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
10297
- attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
10298
- }
10299
- attempts.push(attempt);
10300
- if (attempt.status !== "failed")
10301
- break;
10302
- if (attempt.nextBackoffMs)
10303
- await Bun.sleep(attempt.nextBackoffMs);
10304
- }
10305
- return createDeliveryResult2(event, channel, attempts);
10306
- }
10307
- }
10308
- function redactPaths2(event, paths, replacement = "[REDACTED]") {
10309
- if (paths.length === 0)
10310
- return event;
10311
- const copy = structuredClone(event);
10312
- for (const path of paths) {
10313
- setPath2(copy, path, replacement);
10314
- }
10315
- return copy;
10316
- }
10317
- function sanitizeChannelForOutput2(channel) {
10318
- const copy = structuredClone(channel);
10319
- if (copy.webhook?.secret)
10320
- copy.webhook.secret = "[REDACTED]";
10321
- if (copy.command?.env) {
10322
- copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey2(key) ? "[REDACTED]" : value]));
10323
- }
10324
- return copy;
10325
- }
10326
- function sanitizeChannelsForOutput2(channels) {
10327
- return channels.map(sanitizeChannelForOutput2);
10328
- }
10329
- function redactSensitiveKeys2(event, replacement = "[REDACTED]") {
10330
- return redactValue2(event, replacement);
10331
- }
10332
- function shouldRedactKey2(key) {
10333
- return /secret|token|password|api[_-]?key|authorization/i.test(key);
10334
- }
10335
- function redactValue2(value, replacement) {
10336
- if (Array.isArray(value))
10337
- return value.map((item) => redactValue2(item, replacement));
10338
- if (!value || typeof value !== "object")
10339
- return value;
10340
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [
10341
- key,
10342
- shouldRedactKey2(key) ? replacement : redactValue2(item, replacement)
10343
- ]));
10344
- }
10345
- function setPath2(input, path, replacement) {
10346
- const parts = path.split(".");
10347
- let cursor = input;
10348
- for (const part of parts.slice(0, -1)) {
10349
- const next = cursor[part];
10350
- if (!next || typeof next !== "object")
10351
- return;
10352
- cursor = next;
10353
- }
10354
- const last = parts.at(-1);
10355
- if (last && last in cursor)
10356
- cursor[last] = replacement;
10357
- }
10358
- function normalizeTime2(value) {
10359
- if (!value)
10360
- return new Date().toISOString();
10361
- return value instanceof Date ? value.toISOString() : value;
10362
- }
10363
- function normalizeRetryPolicy2(policy) {
10364
- return {
10365
- maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
10366
- backoffMs: Math.max(0, policy?.backoffMs ?? 250),
10367
- multiplier: Math.max(1, policy?.multiplier ?? 2)
10368
- };
10369
- }
10370
-
10371
- // src/commands/runtime.ts
9188
+ import { EventsClient } from "@hasna/events";
10372
9189
  function probeTmuxPane(target, tmuxCommand = process.env["HASNA_MACHINES_TMUX_BIN"] || "tmux") {
10373
9190
  const checkedAt = new Date().toISOString();
10374
9191
  const result = spawnSync4(tmuxCommand, ["display-message", "-p", "-t", target, "#{pane_id}"], {
@@ -10394,7 +9211,7 @@ async function watchTmuxPane(options) {
10394
9211
  throw new Error("tmux pane target is required");
10395
9212
  const intervalMs = Math.max(0, options.intervalMs ?? 5000);
10396
9213
  const maxChecks = options.maxChecks ?? Number.POSITIVE_INFINITY;
10397
- const client = options.client ?? new EventsClient2;
9214
+ const client = options.client ?? new EventsClient;
10398
9215
  const probe = options.probe ?? ((paneTarget) => probeTmuxPane(paneTarget, options.tmuxCommand));
10399
9216
  const wait = options.sleep ?? sleep;
10400
9217
  let lastPresent;
@@ -10455,7 +9272,8 @@ async function emitTmuxEvent(client, type, probe, lastPresent, deliver) {
10455
9272
  }
10456
9273
 
10457
9274
  // src/commands/screen.ts
10458
- var DEFAULT_SCREEN_SECRET_NAMESPACE = "hasna/xyz/opensource/machines/prod";
9275
+ var SCREEN_SECRET_NAMESPACE_ENV = "HASNA_MACHINES_SCREEN_SECRET_NAMESPACE";
9276
+ var DEFAULT_SCREEN_SECRET_NAMESPACE = "machines/screen-sharing";
10459
9277
  function shellQuote6(value) {
10460
9278
  return `'${value.replace(/'/g, "'\\''")}'`;
10461
9279
  }
@@ -10479,7 +9297,8 @@ function splitTarget(target) {
10479
9297
  return [target.slice(0, at), target.slice(at + 1)];
10480
9298
  }
10481
9299
  function defaultScreenPasswordSecretKey(machineId) {
10482
- return `${DEFAULT_SCREEN_SECRET_NAMESPACE}/screen-${machineId}-vnc-password`;
9300
+ const namespace = process.env[SCREEN_SECRET_NAMESPACE_ENV]?.trim() || DEFAULT_SCREEN_SECRET_NAMESPACE;
9301
+ return `${namespace}/screen-${machineId}-vnc-password`;
10483
9302
  }
10484
9303
  function resolveScreenTarget(machineId, options = {}) {
10485
9304
  const resolved = resolveMachineRoute(machineId, options);
@@ -10563,8 +9382,8 @@ function buildScreenEnableCommand(machineId, options = {}) {
10563
9382
  }
10564
9383
 
10565
9384
  // src/commands/sync.ts
10566
- import { existsSync as existsSync9, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
10567
- import { homedir as homedir7 } from "os";
9385
+ import { existsSync as existsSync7, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
9386
+ import { homedir as homedir5 } from "os";
10568
9387
  init_paths();
10569
9388
  init_db();
10570
9389
  function quote4(value) {
@@ -10618,8 +9437,8 @@ function detectFileActions(machine) {
10618
9437
  throw new Error(`Remote file sync planning is not supported for ${machine.id}; refusing to inspect or apply local paths as remote state.`);
10619
9438
  }
10620
9439
  return (machine.files || []).map((file, index) => {
10621
- const sourceExists = existsSync9(file.source);
10622
- const targetExists = existsSync9(file.target);
9440
+ const sourceExists = existsSync7(file.source);
9441
+ const targetExists = existsSync7(file.target);
10623
9442
  let status = "missing";
10624
9443
  if (sourceExists && targetExists) {
10625
9444
  if (file.mode === "symlink") {
@@ -10650,7 +9469,7 @@ function buildSyncPlan(machineId, runner = runMachineCommand) {
10650
9469
  const target = selected || {
10651
9470
  id: currentMachineId,
10652
9471
  platform: "linux",
10653
- workspacePath: `${homedir7()}/workspace`
9472
+ workspacePath: `${homedir5()}/workspace`
10654
9473
  };
10655
9474
  const actions = [
10656
9475
  ...detectPackageActions(target, runner),
@@ -11207,6 +10026,7 @@ function runDoctor(machineId = getLocalMachineId()) {
11207
10026
  init_db();
11208
10027
 
11209
10028
  // src/commands/serve.ts
10029
+ import { EventsClient as EventsClient2, sanitizeChannelsForOutput } from "@hasna/events";
11210
10030
  function escapeHtml(value) {
11211
10031
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11212
10032
  }
@@ -11423,7 +10243,7 @@ function startDashboardServer(options = {}) {
11423
10243
  if (request.method !== "GET") {
11424
10244
  return jsonError("Use GET for webhook channel listing.", 405);
11425
10245
  }
11426
- return Response.json(sanitizeChannelsForOutput2(await events.listChannels()));
10246
+ return Response.json(sanitizeChannelsForOutput(await events.listChannels()));
11427
10247
  }
11428
10248
  if (url.pathname === "/api/events") {
11429
10249
  if (request.method === "GET") {
@@ -11556,8 +10376,8 @@ function runSelfTest() {
11556
10376
  // src/commands/clipboard.ts
11557
10377
  init_paths();
11558
10378
  import { createHash } from "crypto";
11559
- import { existsSync as existsSync10, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "fs";
11560
- import { join as join8 } from "path";
10379
+ import { existsSync as existsSync8, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "fs";
10380
+ import { join as join6 } from "path";
11561
10381
  var DEFAULT_CONFIG = {
11562
10382
  version: 1,
11563
10383
  enabled: true,
@@ -11575,7 +10395,7 @@ var DEFAULT_CONFIG = {
11575
10395
  function resolveConfigPath(configPath) {
11576
10396
  if (configPath)
11577
10397
  return configPath;
11578
- return join8(getDataDir(), "clipboard-config.json");
10398
+ return join6(getDataDir(), "clipboard-config.json");
11579
10399
  }
11580
10400
  function resolveHistoryPath(historyPath) {
11581
10401
  if (historyPath)
@@ -11587,7 +10407,7 @@ function getDefaultConfig() {
11587
10407
  }
11588
10408
  function readConfig(configPath) {
11589
10409
  const path = resolveConfigPath(configPath);
11590
- if (!existsSync10(path)) {
10410
+ if (!existsSync8(path)) {
11591
10411
  return getDefaultConfig();
11592
10412
  }
11593
10413
  const parsed = JSON.parse(readFileSync6(path, "utf8"));
@@ -11601,7 +10421,7 @@ function writeConfig(config, configPath) {
11601
10421
  }
11602
10422
  function readHistory(historyPath) {
11603
10423
  const path = resolveHistoryPath(historyPath);
11604
- if (!existsSync10(path)) {
10424
+ if (!existsSync8(path)) {
11605
10425
  return [];
11606
10426
  }
11607
10427
  try {
@@ -11634,7 +10454,7 @@ function sanitizeClipboardForRead(content, maxSizeBytes, skipPatterns) {
11634
10454
  }
11635
10455
  function getOrCreateClipboardKey() {
11636
10456
  const keyPath = getClipboardKeyPath();
11637
- if (existsSync10(keyPath)) {
10457
+ if (existsSync8(keyPath)) {
11638
10458
  return readFileSync6(keyPath, "utf8").trim();
11639
10459
  }
11640
10460
  const key = createHash("sha256").update(crypto.randomUUID()).digest("hex").slice(0, 32);
@@ -11674,7 +10494,7 @@ function addClipboardEntry(entry, historyPath) {
11674
10494
  }
11675
10495
  function clearClipboardHistory(historyPath) {
11676
10496
  const path = resolveHistoryPath(historyPath);
11677
- if (existsSync10(path)) {
10497
+ if (existsSync8(path)) {
11678
10498
  rmSync(path);
11679
10499
  }
11680
10500
  }
@@ -11691,7 +10511,7 @@ function getClipboardStatus(historyPath) {
11691
10511
  // src/commands/clipboard-daemon.ts
11692
10512
  init_paths();
11693
10513
  import { readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
11694
- import { join as join9 } from "path";
10514
+ import { join as join7 } from "path";
11695
10515
  import { createHash as createHash3 } from "crypto";
11696
10516
 
11697
10517
  // src/commands/clipboard-server.ts
@@ -11849,7 +10669,7 @@ function handleGetClipboard(response, config) {
11849
10669
  }
11850
10670
 
11851
10671
  // src/commands/clipboard-daemon.ts
11852
- var DAEMON_PID_PATH = join9(getDataDir(), "clipboard-daemon.pid");
10672
+ var DAEMON_PID_PATH = join7(getDataDir(), "clipboard-daemon.pid");
11853
10673
  function readLocalClipboardSync2() {
11854
10674
  const platform4 = process.platform;
11855
10675
  if (platform4 === "darwin") {
@@ -12023,8 +10843,8 @@ async function discoverPeers() {
12023
10843
 
12024
10844
  // src/commands/heal.ts
12025
10845
  init_paths();
12026
- import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
12027
- import { join as join10 } from "path";
10846
+ import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
10847
+ import { join as join8 } from "path";
12028
10848
  var DEFAULT_THRESHOLDS = {
12029
10849
  reconnect: 3,
12030
10850
  nmRestart: 7,
@@ -12068,14 +10888,14 @@ function defaultHealState() {
12068
10888
  };
12069
10889
  }
12070
10890
  function getHealConfigPath() {
12071
- return process.env["HASNA_MACHINES_HEAL_CONFIG_PATH"] || join10(getDataDir(), "heal-config.json");
10891
+ return process.env["HASNA_MACHINES_HEAL_CONFIG_PATH"] || join8(getDataDir(), "heal-config.json");
12072
10892
  }
12073
10893
  function getHealStatePath() {
12074
- return process.env["HASNA_MACHINES_HEAL_STATE_PATH"] || join10(getDataDir(), "heal-state.json");
10894
+ return process.env["HASNA_MACHINES_HEAL_STATE_PATH"] || join8(getDataDir(), "heal-state.json");
12075
10895
  }
12076
10896
  function readHealConfig(path) {
12077
10897
  const p = path || getHealConfigPath();
12078
- if (!existsSync11(p))
10898
+ if (!existsSync9(p))
12079
10899
  return { ...DEFAULT_HEAL_CONFIG, thresholds: { ...DEFAULT_THRESHOLDS } };
12080
10900
  const parsed = JSON.parse(readFileSync9(p, "utf8"));
12081
10901
  return {
@@ -12093,7 +10913,7 @@ function writeHealConfig(config, path) {
12093
10913
  }
12094
10914
  function readHealState(path) {
12095
10915
  const p = path || getHealStatePath();
12096
- if (!existsSync11(p))
10916
+ if (!existsSync9(p))
12097
10917
  return defaultHealState();
12098
10918
  try {
12099
10919
  return { ...defaultHealState(), ...JSON.parse(readFileSync9(p, "utf8")) };
@@ -12133,7 +10953,7 @@ function evaluateHealth(probe, config, state) {
12133
10953
  return { healthy: localOk && quorumOk, remoteScore, reasons };
12134
10954
  }
12135
10955
  function decideAction(input) {
12136
- const { healthy, now: now3, gpuBusy, config, currentBootId } = input;
10956
+ const { healthy, now, gpuBusy, config, currentBootId } = input;
12137
10957
  const s = { ...input.state };
12138
10958
  const t = config.thresholds;
12139
10959
  if (s.bootId !== currentBootId) {
@@ -12144,13 +10964,13 @@ function decideAction(input) {
12144
10964
  if (healthy) {
12145
10965
  s.failCount = 0;
12146
10966
  if (s.bootHealthySince === null)
12147
- s.bootHealthySince = now3;
12148
- if (now3 - s.bootHealthySince >= config.healthyWindowSec) {
10967
+ s.bootHealthySince = now;
10968
+ if (now - s.bootHealthySince >= config.healthyWindowSec) {
12149
10969
  s.failedBootRecoveries = 0;
12150
10970
  s.rebootSuppressUntil = 0;
12151
10971
  s.pendingRebootRecovery = false;
12152
10972
  }
12153
- if (s.degradedUntil > 0 && now3 >= s.degradedUntil) {
10973
+ if (s.degradedUntil > 0 && now >= s.degradedUntil) {
12154
10974
  s.degradedUntil = 0;
12155
10975
  return { action: "restore_preferred", state: s };
12156
10976
  }
@@ -12168,8 +10988,8 @@ function decideAction(input) {
12168
10988
  else if (s.failCount >= t.reconnect)
12169
10989
  tier = "reconnect";
12170
10990
  const tryReconnect = (reason) => {
12171
- if (now3 - s.lastReconnect >= config.reconnectMinIntervalSec) {
12172
- s.lastReconnect = now3;
10991
+ if (now - s.lastReconnect >= config.reconnectMinIntervalSec) {
10992
+ s.lastReconnect = now;
12173
10993
  return { action: "reconnect_wifi", suppressedReason: reason, state: s };
12174
10994
  }
12175
10995
  return { action: "none", suppressedReason: reason, state: s };
@@ -12178,15 +10998,15 @@ function decideAction(input) {
12178
10998
  case "reconnect":
12179
10999
  return tryReconnect();
12180
11000
  case "nmRestart":
12181
- if (now3 - s.lastNmRestart >= config.nmRestartMinIntervalSec) {
12182
- s.lastNmRestart = now3;
11001
+ if (now - s.lastNmRestart >= config.nmRestartMinIntervalSec) {
11002
+ s.lastNmRestart = now;
12183
11003
  return { action: "restart_nm", state: s };
12184
11004
  }
12185
11005
  return tryReconnect();
12186
11006
  case "fallback":
12187
- if (now3 - s.lastFallback >= config.fallbackWindowSec) {
12188
- s.lastFallback = now3;
12189
- s.degradedUntil = now3 + config.fallbackWindowSec;
11007
+ if (now - s.lastFallback >= config.fallbackWindowSec) {
11008
+ s.lastFallback = now;
11009
+ s.degradedUntil = now + config.fallbackWindowSec;
12190
11010
  return { action: "fallback_ssid", state: s };
12191
11011
  }
12192
11012
  return tryReconnect();
@@ -12194,22 +11014,22 @@ function decideAction(input) {
12194
11014
  let reason = null;
12195
11015
  if (!config.allowReboot)
12196
11016
  reason = "disabled";
12197
- else if (now3 < s.rebootSuppressUntil)
11017
+ else if (now < s.rebootSuppressUntil)
12198
11018
  reason = "loop";
12199
11019
  else if (config.gpuJobGuard && gpuBusy)
12200
11020
  reason = "gpu";
12201
- else if (now3 - s.lastRebootAttempt < config.rebootMinIntervalSec)
11021
+ else if (now - s.lastRebootAttempt < config.rebootMinIntervalSec)
12202
11022
  reason = "rate";
12203
11023
  if (reason)
12204
11024
  return tryReconnect(reason);
12205
11025
  if (s.pendingRebootRecovery) {
12206
11026
  s.failedBootRecoveries += 1;
12207
11027
  if (s.failedBootRecoveries >= config.maxFailedBootRecoveries) {
12208
- s.rebootSuppressUntil = now3 + config.bootBackoffSec;
11028
+ s.rebootSuppressUntil = now + config.bootBackoffSec;
12209
11029
  return tryReconnect("loop");
12210
11030
  }
12211
11031
  }
12212
- s.lastRebootAttempt = now3;
11032
+ s.lastRebootAttempt = now;
12213
11033
  s.pendingRebootRecovery = true;
12214
11034
  return { action: "reboot", state: s };
12215
11035
  }
@@ -12309,9 +11129,9 @@ function executeAction(action, config) {
12309
11129
 
12310
11130
  // src/commands/heal-daemon.ts
12311
11131
  init_paths();
12312
- import { existsSync as existsSync12, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
12313
- import { join as join11 } from "path";
12314
- var DAEMON_PID_PATH2 = join11(getDataDir(), "heal-daemon.pid");
11132
+ import { existsSync as existsSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
11133
+ import { join as join9 } from "path";
11134
+ var DAEMON_PID_PATH2 = join9(getDataDir(), "heal-daemon.pid");
12315
11135
  var SERVICE_PATH = "/etc/systemd/system/machines-heal.service";
12316
11136
  var SYSTEM_CONF = "/etc/systemd/system.conf";
12317
11137
  function log(msg) {
@@ -12429,7 +11249,7 @@ function applyDeterminism(config) {
12429
11249
  }
12430
11250
  function enableHardwareWatchdog() {
12431
11251
  const log2 = [];
12432
- if (!existsSync12(SYSTEM_CONF))
11252
+ if (!existsSync10(SYSTEM_CONF))
12433
11253
  return ["/etc/systemd/system.conf not found; skipping hardware watchdog"];
12434
11254
  let conf = readFileSync10(SYSTEM_CONF, "utf8");
12435
11255
  const set = (key, value) => {
@@ -12456,10 +11276,12 @@ function binPath() {
12456
11276
  candidates.push(which);
12457
11277
  if (process.argv[1])
12458
11278
  candidates.push(process.argv[1]);
12459
- const home = process.env["HOME"] || "/home/hasna";
12460
- candidates.push(`${home}/.bun/bin/machines`, "/home/hasna/.bun/bin/machines", "/root/.bun/bin/machines", "/usr/local/bin/machines");
11279
+ const home = process.env["HOME"];
11280
+ if (home)
11281
+ candidates.push(`${home}/.bun/bin/machines`);
11282
+ candidates.push("/root/.bun/bin/machines", "/usr/local/bin/machines");
12461
11283
  for (const c of candidates) {
12462
- if (c && existsSync12(c))
11284
+ if (c && existsSync10(c))
12463
11285
  return c;
12464
11286
  }
12465
11287
  return "machines";
@@ -12495,7 +11317,7 @@ WantedBy=multi-user.target
12495
11317
  function uninstallHealService() {
12496
11318
  const log2 = [];
12497
11319
  sh2("systemctl disable --now machines-heal.service 2>/dev/null || true");
12498
- if (existsSync12(SERVICE_PATH)) {
11320
+ if (existsSync10(SERVICE_PATH)) {
12499
11321
  sh2(`rm -f ${SERVICE_PATH}`);
12500
11322
  sh2("systemctl daemon-reload");
12501
11323
  log2.push(`removed ${SERVICE_PATH}`);
@@ -12506,7 +11328,7 @@ function uninstallHealService() {
12506
11328
  }
12507
11329
  function healServiceStatus() {
12508
11330
  return {
12509
- installed: existsSync12(SERVICE_PATH),
11331
+ installed: existsSync10(SERVICE_PATH),
12510
11332
  active: sh2("systemctl is-active machines-heal.service").out === "active",
12511
11333
  enabled: sh2("systemctl is-enabled machines-heal.service 2>/dev/null").out === "enabled"
12512
11334
  };
@@ -12777,8 +11599,17 @@ program2.name("machines").description("Machine fleet management CLI + MCP for de
12777
11599
  var manifestCommand = program2.command("manifest").description("Manage the fleet manifest");
12778
11600
  var appsCommand = program2.command("apps").description("Manage installed applications per machine");
12779
11601
  var notificationsCommand = program2.command("notifications").description("Manage fleet alert delivery channels");
12780
- registerEventsCommands(program2, { source: "machines" });
12781
- var runtimeCommand = program2.command("runtime").description("Watch runtime conditions and emit Hasna events");
11602
+ var eventWebhooksCommand = registerWebhookCommands(program2, { source: "machines" });
11603
+ eventWebhooksCommand.description("Manage shared event webhook subscriptions");
11604
+ var webhookTestCommand = eventWebhooksCommand.commands.find((command) => command.name() === "test");
11605
+ var webhookOptions = webhookTestCommand?.options ?? [];
11606
+ var webhookMessageOption = webhookOptions.find((option) => option.long === "--message");
11607
+ if (webhookMessageOption) {
11608
+ webhookMessageOption.defaultValue = "Shared events test delivery";
11609
+ }
11610
+ var eventsCommand = registerEventCommands(program2, { source: "machines" });
11611
+ eventsCommand.description("Emit, list, and replay shared events");
11612
+ var runtimeCommand = program2.command("runtime").description("Watch runtime conditions and emit shared events");
12782
11613
  var clipboardCommand = program2.command("clipboard").description("Real-time clipboard sync across fleet machines");
12783
11614
  var installClaudeCommand = program2.command("install-claude").description("Install or inspect Claude, Codex, and Gemini CLIs");
12784
11615
  manifestCommand.command("init").description("Create an empty fleet manifest").action(() => {