@hasna/machines 0.0.26 → 0.0.28

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 existsSync2, mkdirSync } from "fs";
2086
- import { dirname as dirname2, join as join2, resolve } from "path";
2085
+ import { existsSync as existsSync3, mkdirSync } from "fs";
2086
+ import { dirname as dirname2, join as join3, 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"] || join2(homeDir(), ".hasna", "machines");
2091
+ return process.env["HASNA_MACHINES_DIR"] || join3(homeDir(), ".hasna", "machines");
2092
2092
  }
2093
2093
  function getDbPath() {
2094
- return process.env["HASNA_MACHINES_DB_PATH"] || join2(getDataDir(), "machines.db");
2094
+ return process.env["HASNA_MACHINES_DB_PATH"] || join3(getDataDir(), "machines.db");
2095
2095
  }
2096
2096
  function getManifestPath() {
2097
- return process.env["HASNA_MACHINES_MANIFEST_PATH"] || join2(getDataDir(), "machines.json");
2097
+ return process.env["HASNA_MACHINES_MANIFEST_PATH"] || join3(getDataDir(), "machines.json");
2098
2098
  }
2099
2099
  function getNotificationsPath() {
2100
- return process.env["HASNA_MACHINES_NOTIFICATIONS_PATH"] || join2(getDataDir(), "notifications.json");
2100
+ return process.env["HASNA_MACHINES_NOTIFICATIONS_PATH"] || join3(getDataDir(), "notifications.json");
2101
2101
  }
2102
2102
  function getClipboardKeyPath() {
2103
- return process.env["HASNA_MACHINES_CLIPBOARD_KEY_PATH"] || join2(getDataDir(), "clipboard.key");
2103
+ return process.env["HASNA_MACHINES_CLIPBOARD_KEY_PATH"] || join3(getDataDir(), "clipboard.key");
2104
2104
  }
2105
2105
  function getClipboardHistoryPath() {
2106
- return process.env["HASNA_MACHINES_CLIPBOARD_HISTORY_PATH"] || join2(getDataDir(), "clipboard-history.json");
2106
+ return process.env["HASNA_MACHINES_CLIPBOARD_HISTORY_PATH"] || join3(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 (!existsSync2(dir)) {
2112
+ if (!existsSync3(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 now = new Date().toISOString();
2206
+ const now2 = 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), now, now);
2208
+ VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(details), now2, now2);
2209
2209
  }
2210
2210
  function recordSyncRun(machineId, status, actions) {
2211
2211
  const db = getDb();
2212
- const now = new Date().toISOString();
2212
+ const now2 = 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), now, now);
2214
+ VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(actions), now2, now2);
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 now = new Date().toISOString();
2482
+ const now3 = 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, now, direction);
2491
+ statement.run(result.table, now3, direction);
2492
2492
  }
2493
2493
  }
2494
2494
  function ensureSyncMetaTable(db) {
@@ -2601,6 +2601,684 @@ 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
+
2604
3282
  // src/cli/index.ts
2605
3283
  import { execFileSync } from "child_process";
2606
3284
 
@@ -3094,14 +3772,14 @@ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
3094
3772
  var source_default = chalk;
3095
3773
 
3096
3774
  // src/version.ts
3097
- import { existsSync, readFileSync } from "fs";
3098
- import { dirname, join } from "path";
3775
+ import { existsSync as existsSync2, readFileSync } from "fs";
3776
+ import { dirname, join as join2 } from "path";
3099
3777
  import { fileURLToPath } from "url";
3100
3778
  function getPackageVersion() {
3101
3779
  try {
3102
3780
  const here = dirname(fileURLToPath(import.meta.url));
3103
- const candidates = [join(here, "..", "package.json"), join(here, "..", "..", "package.json")];
3104
- const pkgPath = candidates.find((candidate) => existsSync(candidate));
3781
+ const candidates = [join2(here, "..", "package.json"), join2(here, "..", "..", "package.json")];
3782
+ const pkgPath = candidates.find((candidate) => existsSync2(candidate));
3105
3783
  if (!pkgPath) {
3106
3784
  return "0.0.0";
3107
3785
  }
@@ -3112,8 +3790,8 @@ function getPackageVersion() {
3112
3790
  }
3113
3791
 
3114
3792
  // src/manifests.ts
3115
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
3116
- import { arch, homedir, hostname, platform, userInfo } from "os";
3793
+ import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync } from "fs";
3794
+ import { arch, homedir as homedir2, hostname, platform, userInfo } from "os";
3117
3795
  import { dirname as dirname3 } from "path";
3118
3796
 
3119
3797
  // node_modules/zod/v3/external.js
@@ -7127,7 +7805,7 @@ var fleetSchema = exports_external.object({
7127
7805
  machines: exports_external.array(machineSchema)
7128
7806
  });
7129
7807
  function detectWorkspacePath() {
7130
- const home = homedir();
7808
+ const home = homedir2();
7131
7809
  if (platform() === "darwin") {
7132
7810
  return `${home}/Workspace`;
7133
7811
  }
@@ -7151,7 +7829,7 @@ function getDefaultManifest() {
7151
7829
  };
7152
7830
  }
7153
7831
  function readManifest(path = getManifestPath()) {
7154
- if (!existsSync3(path)) {
7832
+ if (!existsSync4(path)) {
7155
7833
  return getDefaultManifest();
7156
7834
  }
7157
7835
  const raw = JSON.parse(readFileSync2(path, "utf8"));
@@ -7232,7 +7910,7 @@ function manifestValidate() {
7232
7910
  }
7233
7911
 
7234
7912
  // src/commands/setup.ts
7235
- import { homedir as homedir2 } from "os";
7913
+ import { homedir as homedir3 } from "os";
7236
7914
  init_db();
7237
7915
  function quote(value) {
7238
7916
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -7303,7 +7981,7 @@ function buildSetupPlan(machineId) {
7303
7981
  const target = selected || {
7304
7982
  id: currentMachineId,
7305
7983
  platform: "linux",
7306
- workspacePath: `${homedir2()}/workspace`
7984
+ workspacePath: `${homedir3()}/workspace`
7307
7985
  };
7308
7986
  const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
7309
7987
  return {
@@ -7349,8 +8027,8 @@ function runSetup(machineId, options = {}) {
7349
8027
  }
7350
8028
 
7351
8029
  // src/commands/backup.ts
7352
- import { homedir as homedir3, hostname as hostname3 } from "os";
7353
- import { join as join3 } from "path";
8030
+ import { homedir as homedir4, hostname as hostname3 } from "os";
8031
+ import { join as join4 } from "path";
7354
8032
  var MACHINES_BACKUP_BUCKET_ENV = "HASNA_MACHINES_S3_BUCKET";
7355
8033
  var MACHINES_BACKUP_BUCKET_FALLBACK_ENV = "MACHINES_S3_BUCKET";
7356
8034
  var MACHINES_BACKUP_PREFIX_ENV = "HASNA_MACHINES_S3_PREFIX";
@@ -7398,16 +8076,16 @@ function resolveBackupTarget(options = {}) {
7398
8076
  };
7399
8077
  }
7400
8078
  function defaultBackupSources() {
7401
- const home = homedir3();
8079
+ const home = homedir4();
7402
8080
  return [
7403
- join3(home, ".hasna"),
7404
- join3(home, ".ssh"),
7405
- join3(home, ".secrets")
8081
+ join4(home, ".hasna"),
8082
+ join4(home, ".ssh"),
8083
+ join4(home, ".secrets")
7406
8084
  ];
7407
8085
  }
7408
8086
  function buildBackupPlan(bucket, prefix) {
7409
8087
  const target = resolveBackupTarget({ bucket, prefix });
7410
- const archivePath = join3(homedir3(), ".hasna", "machines", "backup.tgz");
8088
+ const archivePath = join4(homedir4(), ".hasna", "machines", "backup.tgz");
7411
8089
  const sources = defaultBackupSources();
7412
8090
  const steps = [
7413
8091
  {
@@ -7458,21 +8136,21 @@ function runBackup(bucket, prefix, options = {}) {
7458
8136
  }
7459
8137
 
7460
8138
  // src/commands/cert.ts
7461
- import { homedir as homedir4, platform as platform2 } from "os";
7462
- import { join as join4 } from "path";
8139
+ import { homedir as homedir5, platform as platform2 } from "os";
8140
+ import { join as join5 } from "path";
7463
8141
  function quote3(value) {
7464
8142
  return `'${value.replace(/'/g, `'\\''`)}'`;
7465
8143
  }
7466
8144
  function certDir() {
7467
- return join4(homedir4(), ".hasna", "machines", "certs");
8145
+ return join5(homedir5(), ".hasna", "machines", "certs");
7468
8146
  }
7469
8147
  function buildCertPlan(domains) {
7470
8148
  if (domains.length === 0) {
7471
8149
  throw new Error("At least one domain is required.");
7472
8150
  }
7473
8151
  const primary = domains[0];
7474
- const certPath = join4(certDir(), `${primary}.pem`);
7475
- const keyPath = join4(certDir(), `${primary}-key.pem`);
8152
+ const certPath = join5(certDir(), `${primary}.pem`);
8153
+ const keyPath = join5(certDir(), `${primary}-key.pem`);
7476
8154
  const steps = [];
7477
8155
  if (platform2() === "darwin") {
7478
8156
  steps.push({
@@ -7537,14 +8215,14 @@ function runCertPlan(domains, options = {}) {
7537
8215
 
7538
8216
  // src/commands/dns.ts
7539
8217
  init_paths();
7540
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
7541
- import { join as join5 } from "path";
8218
+ import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
8219
+ import { join as join6 } from "path";
7542
8220
  function getDnsPath() {
7543
- return join5(getDataDir(), "dns.json");
8221
+ return join6(getDataDir(), "dns.json");
7544
8222
  }
7545
8223
  function readMappings() {
7546
8224
  const path = getDnsPath();
7547
- if (!existsSync4(path))
8225
+ if (!existsSync5(path))
7548
8226
  return [];
7549
8227
  return JSON.parse(readFileSync3(path, "utf8"));
7550
8228
  }
@@ -7573,10 +8251,10 @@ function renderDomainMapping(domain) {
7573
8251
  hostsEntry: `${entry.targetHost} ${entry.domain}`,
7574
8252
  caddySnippet: `${entry.domain} {
7575
8253
  reverse_proxy 127.0.0.1:${entry.port}
7576
- tls ${join5(getDataDir(), "certs", `${entry.domain}.pem`)} ${join5(getDataDir(), "certs", `${entry.domain}-key.pem`)}
8254
+ tls ${join6(getDataDir(), "certs", `${entry.domain}.pem`)} ${join6(getDataDir(), "certs", `${entry.domain}-key.pem`)}
7577
8255
  }`,
7578
- certPath: join5(getDataDir(), "certs", `${entry.domain}.pem`),
7579
- keyPath: join5(getDataDir(), "certs", `${entry.domain}-key.pem`)
8256
+ certPath: join6(getDataDir(), "certs", `${entry.domain}.pem`),
8257
+ keyPath: join6(getDataDir(), "certs", `${entry.domain}-key.pem`)
7580
8258
  };
7581
8259
  }
7582
8260
 
@@ -7628,7 +8306,7 @@ import { hostname as hostname5 } from "os";
7628
8306
 
7629
8307
  // src/topology.ts
7630
8308
  init_db();
7631
- import { existsSync as existsSync5 } from "fs";
8309
+ import { existsSync as existsSync6 } from "fs";
7632
8310
  import { arch as arch2, hostname as hostname4, platform as platform3, userInfo as userInfo2 } from "os";
7633
8311
  import { spawnSync } from "child_process";
7634
8312
  init_paths();
@@ -7819,7 +8497,7 @@ function buildEntry(input) {
7819
8497
  };
7820
8498
  }
7821
8499
  function discoverMachineTopology(options = {}) {
7822
- const now = options.now ?? new Date;
8500
+ const now2 = options.now ?? new Date;
7823
8501
  const runner = options.runner ?? defaultRunner;
7824
8502
  const warnings = [];
7825
8503
  const manifest = readManifest();
@@ -7851,11 +8529,11 @@ function discoverMachineTopology(options = {}) {
7851
8529
  version: getPackageVersion()
7852
8530
  },
7853
8531
  capabilities: getMachinesConsumerCapabilities(),
7854
- generated_at: now.toISOString(),
8532
+ generated_at: now2.toISOString(),
7855
8533
  local_machine_id: localMachineId,
7856
8534
  local_hostname: hostname4(),
7857
8535
  current_platform: normalizePlatform2(),
7858
- manifest_path_known: existsSync5(getManifestPath()),
8536
+ manifest_path_known: existsSync6(getManifestPath()),
7859
8537
  machines,
7860
8538
  warnings
7861
8539
  };
@@ -7974,11 +8652,11 @@ function cacheability(input) {
7974
8652
  };
7975
8653
  }
7976
8654
  function resolveMachineRoute(machineId, options = {}) {
7977
- const now = options.now ?? new Date;
8655
+ const now2 = options.now ?? new Date;
7978
8656
  const topology = options.topology ?? discoverMachineTopology(options);
7979
8657
  const warnings = [...topology.warnings];
7980
8658
  const { machine, matchedBy } = findRouteMachine(topology, machineId);
7981
- const generatedAt = now.toISOString();
8659
+ const generatedAt = now2.toISOString();
7982
8660
  if (!machine) {
7983
8661
  warnings.push(`machine_not_found:${machineId}`);
7984
8662
  return {
@@ -8004,8 +8682,8 @@ function resolveMachineRoute(machineId, options = {}) {
8004
8682
  },
8005
8683
  cacheability: cacheability({
8006
8684
  ok: false,
8007
- observedAt: now,
8008
- now,
8685
+ observedAt: now2,
8686
+ now: now2,
8009
8687
  ttlMs: options.resolverTtlMs,
8010
8688
  authority: "unresolved",
8011
8689
  confidence: "none",
@@ -8042,8 +8720,8 @@ function resolveMachineRoute(machineId, options = {}) {
8042
8720
  },
8043
8721
  cacheability: cacheability({
8044
8722
  ok,
8045
- observedAt: now,
8046
- now,
8723
+ observedAt: now2,
8724
+ now: now2,
8047
8725
  ttlMs: options.resolverTtlMs,
8048
8726
  authority: routeAuthority({ machine, selectedHint, matchedBy }),
8049
8727
  confidence,
@@ -8130,7 +8808,7 @@ function canCheckPathForMachine(machine, localMachineId) {
8130
8808
  function checkedPathExists(path, check) {
8131
8809
  if (!path || !check)
8132
8810
  return null;
8133
- return existsSync5(path);
8811
+ return existsSync6(path);
8134
8812
  }
8135
8813
  function repairHint(input) {
8136
8814
  const command = [
@@ -8345,11 +9023,11 @@ function metadataKeysForDiagnostics(metadata) {
8345
9023
  return Object.keys(metadata).filter((key) => !/(secret|token|key|password|credential)/i.test(key)).sort();
8346
9024
  }
8347
9025
  function resolveMachineWorkspace(options) {
8348
- const now = options.now ?? new Date;
9026
+ const now2 = options.now ?? new Date;
8349
9027
  const topology = options.topology ?? discoverMachineTopology(options);
8350
9028
  const warnings = [...topology.warnings];
8351
9029
  const { machine, matchedBy } = findRouteMachine(topology, options.machineId);
8352
- const generatedAt = now.toISOString();
9030
+ const generatedAt = now2.toISOString();
8353
9031
  const repoName = options.repoName ?? options.projectId;
8354
9032
  const openFilesRepoName = options.openFilesRepoName ?? "open-files";
8355
9033
  if (!machine) {
@@ -8378,8 +9056,8 @@ function resolveMachineWorkspace(options) {
8378
9056
  },
8379
9057
  cacheability: cacheability({
8380
9058
  ok: false,
8381
- observedAt: now,
8382
- now,
9059
+ observedAt: now2,
9060
+ now: now2,
8383
9061
  ttlMs: options.resolverTtlMs,
8384
9062
  authority: "unresolved",
8385
9063
  confidence: "none",
@@ -8451,8 +9129,8 @@ function resolveMachineWorkspace(options) {
8451
9129
  },
8452
9130
  cacheability: cacheability({
8453
9131
  ok: workspaceOk,
8454
- observedAt: now,
8455
- now,
9132
+ observedAt: now2,
9133
+ now: now2,
8456
9134
  ttlMs: options.resolverTtlMs,
8457
9135
  authority: workspaceAuthority(workspacePaths),
8458
9136
  confidence: workspaceOk ? "medium" : "none",
@@ -8850,7 +9528,7 @@ function runTailscaleInstall(machineId, options = {}) {
8850
9528
  }
8851
9529
 
8852
9530
  // src/commands/notifications.ts
8853
- import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
9531
+ import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
8854
9532
  init_paths();
8855
9533
  var notificationChannelSchema = exports_external.object({
8856
9534
  id: exports_external.string(),
@@ -8933,7 +9611,7 @@ ${message}
8933
9611
  }
8934
9612
  throw new Error("No local email transport available. Install sendmail or mail.");
8935
9613
  }
8936
- async function dispatchWebhook(channel, event, message) {
9614
+ async function dispatchWebhook2(channel, event, message) {
8937
9615
  const response = await fetch(channel.target, {
8938
9616
  method: "POST",
8939
9617
  headers: {
@@ -8958,7 +9636,7 @@ async function dispatchWebhook(channel, event, message) {
8958
9636
  detail: `Webhook accepted with HTTP ${response.status}`
8959
9637
  };
8960
9638
  }
8961
- async function dispatchCommand(channel, event, message) {
9639
+ async function dispatchCommand2(channel, event, message) {
8962
9640
  const result = Bun.spawnSync(["bash", "-lc", channel.target], {
8963
9641
  stdout: "pipe",
8964
9642
  stderr: "pipe",
@@ -8981,7 +9659,7 @@ async function dispatchCommand(channel, event, message) {
8981
9659
  detail: stdout || "Command completed successfully"
8982
9660
  };
8983
9661
  }
8984
- async function dispatchChannel(channel, event, message) {
9662
+ async function dispatchChannel2(channel, event, message) {
8985
9663
  if (!channel.enabled) {
8986
9664
  return {
8987
9665
  channelId: channel.id,
@@ -8995,9 +9673,9 @@ async function dispatchChannel(channel, event, message) {
8995
9673
  return dispatchEmail(channel, event, message);
8996
9674
  }
8997
9675
  if (channel.type === "webhook") {
8998
- return dispatchWebhook(channel, event, message);
9676
+ return dispatchWebhook2(channel, event, message);
8999
9677
  }
9000
- return dispatchCommand(channel, event, message);
9678
+ return dispatchCommand2(channel, event, message);
9001
9679
  }
9002
9680
  function getDefaultNotificationConfig() {
9003
9681
  return {
@@ -9007,7 +9685,7 @@ function getDefaultNotificationConfig() {
9007
9685
  };
9008
9686
  }
9009
9687
  function readNotificationConfig(path = getNotificationsPath()) {
9010
- if (!existsSync6(path)) {
9688
+ if (!existsSync7(path)) {
9011
9689
  return getDefaultNotificationConfig();
9012
9690
  }
9013
9691
  return notificationConfigSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
@@ -9052,7 +9730,7 @@ async function dispatchNotificationEvent(event, message, options = {}) {
9052
9730
  const deliveries = [];
9053
9731
  for (const channel of channels) {
9054
9732
  try {
9055
- deliveries.push(await dispatchChannel(channel, event, message));
9733
+ deliveries.push(await dispatchChannel2(channel, event, message));
9056
9734
  } catch (error) {
9057
9735
  deliveries.push({
9058
9736
  channelId: channel.id,
@@ -9087,7 +9765,7 @@ async function testNotificationChannel(channelId, event = "manual.test", message
9087
9765
  if (!options.yes) {
9088
9766
  throw new Error("Notification test execution requires --yes.");
9089
9767
  }
9090
- const delivery = await dispatchChannel(channel, event, message);
9768
+ const delivery = await dispatchChannel2(channel, event, message);
9091
9769
  return {
9092
9770
  channelId,
9093
9771
  mode: "apply",
@@ -9153,15 +9831,32 @@ function listPorts(machineId) {
9153
9831
  }
9154
9832
 
9155
9833
  // src/commands/screen.ts
9834
+ var DEFAULT_SCREEN_SECRET_NAMESPACE = "hasna/xyz/opensource/machines/prod";
9156
9835
  function shellQuote6(value) {
9157
9836
  return `'${value.replace(/'/g, "'\\''")}'`;
9158
9837
  }
9838
+ function shellCommand2(command) {
9839
+ return command.map(shellQuote6).join(" ");
9840
+ }
9841
+ function metadataString2(metadata, keys) {
9842
+ if (!metadata)
9843
+ return null;
9844
+ for (const key of keys) {
9845
+ const value = metadata[key];
9846
+ if (typeof value === "string" && value.trim())
9847
+ return value.trim();
9848
+ }
9849
+ return null;
9850
+ }
9159
9851
  function splitTarget(target) {
9160
9852
  const at = target.indexOf("@");
9161
9853
  if (at === -1)
9162
9854
  return [null, target];
9163
9855
  return [target.slice(0, at), target.slice(at + 1)];
9164
9856
  }
9857
+ function defaultScreenPasswordSecretKey(machineId) {
9858
+ return `${DEFAULT_SCREEN_SECRET_NAMESPACE}/screen-${machineId}-vnc-password`;
9859
+ }
9165
9860
  function resolveScreenTarget(machineId, options = {}) {
9166
9861
  const resolved = resolveMachineRoute(machineId, options);
9167
9862
  if (!resolved.ok || !resolved.target) {
@@ -9187,20 +9882,65 @@ function resolveScreenTarget(machineId, options = {}) {
9187
9882
  warnings: resolved.warnings
9188
9883
  };
9189
9884
  }
9190
- function buildScreenEnableRemoteCommand(user, vncPassword) {
9885
+ function resolveScreenCredentials(machineId, options = {}) {
9886
+ const topology = options.topology ?? discoverMachineTopology(options);
9887
+ const screen = resolveScreenTarget(machineId, { ...options, topology });
9888
+ const entry = topology.machines.find((machine) => machine.machine_id === screen.machineId);
9889
+ const metadata = entry?.metadata;
9890
+ const metadataUser = metadataString2(metadata, ["screenUser", "screen_user", "user", "username"]);
9891
+ const metadataPasswordSecret = metadataString2(metadata, [
9892
+ "screenPasswordSecret",
9893
+ "screen_password_secret",
9894
+ "screenVncPasswordSecret",
9895
+ "screen_vnc_password_secret",
9896
+ "vncPasswordSecret",
9897
+ "vnc_password_secret"
9898
+ ]);
9899
+ const user = options.user ?? screen.user ?? metadataUser;
9900
+ const passwordSecretKey = options.passwordSecretKey ?? metadataPasswordSecret ?? defaultScreenPasswordSecretKey(screen.machineId);
9901
+ return {
9902
+ machineId: screen.machineId,
9903
+ user: user ?? null,
9904
+ userSource: options.user ? "option" : screen.user ? "route" : metadataUser ? "metadata" : "missing",
9905
+ passwordSecretKey,
9906
+ passwordSecretSource: options.passwordSecretKey ? "option" : metadataPasswordSecret ? "metadata" : "default"
9907
+ };
9908
+ }
9909
+ function buildScreenEnableRemoteCommandFromStdin(user) {
9191
9910
  const kickstart = "/System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart";
9192
- const lines = [
9193
- `dseditgroup -o edit -a ${shellQuote6(user)} -t user com.apple.access_screensharing 2>/dev/null || true`,
9911
+ const script = [
9912
+ "set -euo pipefail",
9913
+ 'user="$1"',
9914
+ "IFS= read -r vnc_pw",
9915
+ 'if [ -z "$vnc_pw" ]; then echo "missing VNC password on stdin" >&2; exit 1; fi',
9916
+ `kickstart=${shellQuote6(kickstart)}`,
9917
+ 'dseditgroup -o edit -a "$user" -t user com.apple.access_screensharing 2>/dev/null || true',
9194
9918
  "defaults write /Library/Preferences/com.apple.RemoteManagement AllowSRPForNetworkNodes -bool true",
9195
- `${kickstart} -configure -clientopts -setvnclegacy -vnclegacy yes -setvncpw -vncpw ${shellQuote6(vncPassword)}`,
9196
- `${kickstart} -activate -configure -access -on -users ${shellQuote6(user)} -privs -all -restart -agent -menu`
9197
- ];
9198
- return lines.join(" && ");
9919
+ '"$kickstart" -configure -clientopts -setvnclegacy -vnclegacy yes -setvncpw -vncpw "$vnc_pw"',
9920
+ '"$kickstart" -activate -configure -access -on -users "$user" -privs -all -restart -agent -menu'
9921
+ ].join(`
9922
+ `);
9923
+ return `sudo -n -p '' /bin/bash -c ${shellQuote6(script)} -- ${shellQuote6(user)}`;
9924
+ }
9925
+ function buildScreenEnableCommand(machineId, options = {}) {
9926
+ const credentials = resolveScreenCredentials(machineId, options);
9927
+ if (!credentials.user) {
9928
+ throw new Error(`No screen-sharing user known for ${machineId}; pass --user <name> or set metadata.user in the manifest.`);
9929
+ }
9930
+ const secretsCommand = options.secretsCommand || "secrets";
9931
+ const remoteCommand = buildScreenEnableRemoteCommandFromStdin(credentials.user);
9932
+ return {
9933
+ machineId: credentials.machineId,
9934
+ user: credentials.user,
9935
+ passwordSecretKey: credentials.passwordSecretKey,
9936
+ remoteCommand,
9937
+ command: `${shellCommand2([secretsCommand, "get", credentials.passwordSecretKey])} | ${buildSshCommand(machineId, remoteCommand, options)}`
9938
+ };
9199
9939
  }
9200
9940
 
9201
9941
  // src/commands/sync.ts
9202
- import { existsSync as existsSync7, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
9203
- import { homedir as homedir5 } from "os";
9942
+ import { existsSync as existsSync8, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
9943
+ import { homedir as homedir6 } from "os";
9204
9944
  init_paths();
9205
9945
  init_db();
9206
9946
  function quote4(value) {
@@ -9250,8 +9990,8 @@ function detectPackageActions(machine) {
9250
9990
  }
9251
9991
  function detectFileActions(machine) {
9252
9992
  return (machine.files || []).map((file, index) => {
9253
- const sourceExists = existsSync7(file.source);
9254
- const targetExists = existsSync7(file.target);
9993
+ const sourceExists = existsSync8(file.source);
9994
+ const targetExists = existsSync8(file.target);
9255
9995
  let status = "missing";
9256
9996
  if (sourceExists && targetExists) {
9257
9997
  if (file.mode === "symlink") {
@@ -9279,7 +10019,7 @@ function buildSyncPlan(machineId) {
9279
10019
  const target = selected || {
9280
10020
  id: currentMachineId,
9281
10021
  platform: "linux",
9282
- workspacePath: `${homedir5()}/workspace`
10022
+ workspacePath: `${homedir6()}/workspace`
9283
10023
  };
9284
10024
  const actions = [
9285
10025
  ...detectPackageActions(target),
@@ -9836,6 +10576,526 @@ function runDoctor(machineId = getLocalMachineId()) {
9836
10576
  // src/commands/self-test.ts
9837
10577
  init_db();
9838
10578
 
10579
+ // node_modules/@hasna/events/dist/index.js
10580
+ import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
10581
+ import { existsSync as existsSync9 } from "fs";
10582
+ import { homedir as homedir7 } from "os";
10583
+ import { join as join7 } from "path";
10584
+ import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
10585
+ import { randomUUID as randomUUID3 } from "crypto";
10586
+ import { spawn as spawn2 } from "child_process";
10587
+ import { randomUUID as randomUUID22 } from "crypto";
10588
+ function getPathValue2(input, path) {
10589
+ return path.split(".").reduce((value, part) => {
10590
+ if (value && typeof value === "object" && part in value) {
10591
+ return value[part];
10592
+ }
10593
+ return;
10594
+ }, input);
10595
+ }
10596
+ function wildcardToRegExp2(pattern) {
10597
+ const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
10598
+ return new RegExp(`^${escaped}$`);
10599
+ }
10600
+ function matchString2(value, matcher) {
10601
+ if (matcher === undefined)
10602
+ return true;
10603
+ if (value === undefined)
10604
+ return false;
10605
+ const matchers = Array.isArray(matcher) ? matcher : [matcher];
10606
+ return matchers.some((item) => wildcardToRegExp2(item).test(value));
10607
+ }
10608
+ function matchRecord2(input, matcher) {
10609
+ if (!matcher)
10610
+ return true;
10611
+ return Object.entries(matcher).every(([path, expected]) => {
10612
+ const actual = getPathValue2(input, path);
10613
+ if (typeof expected === "string" || Array.isArray(expected)) {
10614
+ return matchString2(actual === undefined ? undefined : String(actual), expected);
10615
+ }
10616
+ return actual === expected;
10617
+ });
10618
+ }
10619
+ function eventMatchesFilter2(event, filter) {
10620
+ 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);
10621
+ }
10622
+ function channelMatchesEvent2(channel, event) {
10623
+ if (!channel.enabled)
10624
+ return false;
10625
+ if (!channel.filters || channel.filters.length === 0)
10626
+ return true;
10627
+ return channel.filters.some((filter) => eventMatchesFilter2(event, filter));
10628
+ }
10629
+ var HASNA_EVENTS_DIR_ENV2 = "HASNA_EVENTS_DIR";
10630
+ var HASNA_EVENTS_HOME_ENV2 = "HASNA_EVENTS_HOME";
10631
+ function getEventsDataDir2(override) {
10632
+ return override || process.env[HASNA_EVENTS_DIR_ENV2] || process.env[HASNA_EVENTS_HOME_ENV2] || join7(homedir7(), ".hasna", "events");
10633
+ }
10634
+
10635
+ class JsonEventsStore2 {
10636
+ dataDir;
10637
+ channelsPath;
10638
+ eventsPath;
10639
+ deliveriesPath;
10640
+ constructor(dataDir = getEventsDataDir2()) {
10641
+ this.dataDir = dataDir;
10642
+ this.channelsPath = join7(dataDir, "channels.json");
10643
+ this.eventsPath = join7(dataDir, "events.json");
10644
+ this.deliveriesPath = join7(dataDir, "deliveries.json");
10645
+ }
10646
+ async init() {
10647
+ await mkdir2(this.dataDir, { recursive: true, mode: 448 });
10648
+ await chmod2(this.dataDir, 448).catch(() => {
10649
+ return;
10650
+ });
10651
+ await this.ensureArrayFile(this.channelsPath);
10652
+ await this.ensureArrayFile(this.eventsPath);
10653
+ await this.ensureArrayFile(this.deliveriesPath);
10654
+ }
10655
+ async addChannel(channel) {
10656
+ await this.init();
10657
+ const channels = await this.readJson(this.channelsPath, []);
10658
+ const index = channels.findIndex((item) => item.id === channel.id);
10659
+ if (index >= 0) {
10660
+ channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
10661
+ } else {
10662
+ channels.push(channel);
10663
+ }
10664
+ await this.writeJson(this.channelsPath, channels);
10665
+ return index >= 0 ? channels[index] : channel;
10666
+ }
10667
+ async listChannels() {
10668
+ await this.init();
10669
+ return this.readJson(this.channelsPath, []);
10670
+ }
10671
+ async getChannel(id) {
10672
+ const channels = await this.listChannels();
10673
+ return channels.find((channel) => channel.id === id);
10674
+ }
10675
+ async removeChannel(id) {
10676
+ await this.init();
10677
+ const channels = await this.readJson(this.channelsPath, []);
10678
+ const next = channels.filter((channel) => channel.id !== id);
10679
+ await this.writeJson(this.channelsPath, next);
10680
+ return next.length !== channels.length;
10681
+ }
10682
+ async appendEvent(event) {
10683
+ await this.init();
10684
+ const events = await this.readJson(this.eventsPath, []);
10685
+ events.push(event);
10686
+ await this.writeJson(this.eventsPath, events);
10687
+ return event;
10688
+ }
10689
+ async listEvents() {
10690
+ await this.init();
10691
+ return this.readJson(this.eventsPath, []);
10692
+ }
10693
+ async findEventByIdentity(identity) {
10694
+ const events = await this.listEvents();
10695
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
10696
+ }
10697
+ async appendDelivery(result) {
10698
+ await this.init();
10699
+ const deliveries = await this.readJson(this.deliveriesPath, []);
10700
+ deliveries.push(result);
10701
+ await this.writeJson(this.deliveriesPath, deliveries);
10702
+ return result;
10703
+ }
10704
+ async listDeliveries() {
10705
+ await this.init();
10706
+ return this.readJson(this.deliveriesPath, []);
10707
+ }
10708
+ async exportData() {
10709
+ return {
10710
+ channels: await this.listChannels(),
10711
+ events: await this.listEvents(),
10712
+ deliveries: await this.listDeliveries()
10713
+ };
10714
+ }
10715
+ async ensureArrayFile(path) {
10716
+ if (!existsSync9(path)) {
10717
+ await writeFile2(path, `[]
10718
+ `, { encoding: "utf-8", mode: 384 });
10719
+ }
10720
+ await chmod2(path, 384).catch(() => {
10721
+ return;
10722
+ });
10723
+ }
10724
+ async readJson(path, fallback) {
10725
+ try {
10726
+ const raw = await readFile2(path, "utf-8");
10727
+ if (!raw.trim())
10728
+ return fallback;
10729
+ return JSON.parse(raw);
10730
+ } catch (error) {
10731
+ if (error.code === "ENOENT")
10732
+ return fallback;
10733
+ throw error;
10734
+ }
10735
+ }
10736
+ async writeJson(path, value) {
10737
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
10738
+ await writeFile2(tempPath, `${JSON.stringify(value, null, 2)}
10739
+ `, { encoding: "utf-8", mode: 384 });
10740
+ await rename2(tempPath, path);
10741
+ await chmod2(path, 384).catch(() => {
10742
+ return;
10743
+ });
10744
+ }
10745
+ }
10746
+ var DEFAULT_SIGNATURE_TOLERANCE_MS2 = 5 * 60 * 1000;
10747
+ function buildSignatureBase2(timestamp, body) {
10748
+ return `${timestamp}.${body}`;
10749
+ }
10750
+ function signPayload2(secret, timestamp, body) {
10751
+ const digest = createHmac2("sha256", secret).update(buildSignatureBase2(timestamp, body)).digest("hex");
10752
+ return `sha256=${digest}`;
10753
+ }
10754
+ function now2() {
10755
+ return new Date().toISOString();
10756
+ }
10757
+ function truncate2(value, max = 4096) {
10758
+ return value.length > max ? `${value.slice(0, max)}...` : value;
10759
+ }
10760
+ function buildWebhookRequest2(event, channel) {
10761
+ if (!channel.webhook)
10762
+ throw new Error(`Channel ${channel.id} has no webhook config`);
10763
+ const body = JSON.stringify(event);
10764
+ const timestamp = event.time;
10765
+ const headers = {
10766
+ "Content-Type": "application/json",
10767
+ "User-Agent": "@hasna/events",
10768
+ "X-Hasna-Event-Id": event.id,
10769
+ "X-Hasna-Event-Type": event.type,
10770
+ "X-Hasna-Timestamp": timestamp,
10771
+ ...channel.webhook.headers
10772
+ };
10773
+ if (channel.webhook.secret) {
10774
+ headers["X-Hasna-Signature"] = signPayload2(channel.webhook.secret, timestamp, body);
10775
+ }
10776
+ return { body, headers };
10777
+ }
10778
+ async function dispatchWebhook3(event, channel, options = {}) {
10779
+ if (!channel.webhook)
10780
+ throw new Error(`Channel ${channel.id} has no webhook config`);
10781
+ const startedAt = now2();
10782
+ const { body, headers } = buildWebhookRequest2(event, channel);
10783
+ const controller = new AbortController;
10784
+ const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
10785
+ try {
10786
+ const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
10787
+ method: "POST",
10788
+ headers,
10789
+ body,
10790
+ signal: controller.signal
10791
+ });
10792
+ const responseBody = truncate2(await response.text());
10793
+ return {
10794
+ attempt: 1,
10795
+ status: response.ok ? "success" : "failed",
10796
+ startedAt,
10797
+ completedAt: now2(),
10798
+ responseStatus: response.status,
10799
+ responseBody,
10800
+ error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
10801
+ };
10802
+ } catch (error) {
10803
+ return {
10804
+ attempt: 1,
10805
+ status: "failed",
10806
+ startedAt,
10807
+ completedAt: now2(),
10808
+ error: error instanceof Error ? error.message : String(error)
10809
+ };
10810
+ } finally {
10811
+ clearTimeout(timeout);
10812
+ }
10813
+ }
10814
+ async function dispatchCommand3(event, channel) {
10815
+ if (!channel.command)
10816
+ throw new Error(`Channel ${channel.id} has no command config`);
10817
+ const startedAt = now2();
10818
+ const eventJson = JSON.stringify(event);
10819
+ const env2 = {
10820
+ ...process.env,
10821
+ ...channel.command.env,
10822
+ HASNA_CHANNEL_ID: channel.id,
10823
+ HASNA_EVENT_ID: event.id,
10824
+ HASNA_EVENT_TYPE: event.type,
10825
+ HASNA_EVENT_SOURCE: event.source,
10826
+ HASNA_EVENT_SUBJECT: event.subject ?? "",
10827
+ HASNA_EVENT_SEVERITY: event.severity,
10828
+ HASNA_EVENT_TIME: event.time,
10829
+ HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
10830
+ HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
10831
+ HASNA_EVENT_JSON: eventJson
10832
+ };
10833
+ return new Promise((resolve2) => {
10834
+ const child = spawn2(channel.command.command, channel.command.args ?? [], {
10835
+ cwd: channel.command.cwd,
10836
+ env: env2,
10837
+ stdio: ["pipe", "pipe", "pipe"]
10838
+ });
10839
+ let stdout = "";
10840
+ let stderr = "";
10841
+ const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
10842
+ child.stdin.end(eventJson);
10843
+ child.stdout.on("data", (chunk) => {
10844
+ stdout += chunk.toString();
10845
+ });
10846
+ child.stderr.on("data", (chunk) => {
10847
+ stderr += chunk.toString();
10848
+ });
10849
+ child.on("error", (error) => {
10850
+ clearTimeout(timeout);
10851
+ resolve2({
10852
+ attempt: 1,
10853
+ status: "failed",
10854
+ startedAt,
10855
+ completedAt: now2(),
10856
+ stdout: truncate2(stdout),
10857
+ stderr: truncate2(stderr),
10858
+ error: error.message
10859
+ });
10860
+ });
10861
+ child.on("close", (code, signal) => {
10862
+ clearTimeout(timeout);
10863
+ const success = code === 0;
10864
+ resolve2({
10865
+ attempt: 1,
10866
+ status: success ? "success" : "failed",
10867
+ startedAt,
10868
+ completedAt: now2(),
10869
+ stdout: truncate2(stdout),
10870
+ stderr: truncate2(stderr),
10871
+ error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
10872
+ });
10873
+ });
10874
+ });
10875
+ }
10876
+ async function dispatchChannel3(event, channel, options = {}) {
10877
+ if (channel.transport === "webhook")
10878
+ return dispatchWebhook3(event, channel, options);
10879
+ if (channel.transport === "command")
10880
+ return dispatchCommand3(event, channel);
10881
+ return {
10882
+ attempt: 1,
10883
+ status: "skipped",
10884
+ startedAt: now2(),
10885
+ completedAt: now2(),
10886
+ error: `Unsupported transport: ${channel.transport}`
10887
+ };
10888
+ }
10889
+ function createDeliveryResult2(event, channel, attempts) {
10890
+ const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
10891
+ return {
10892
+ id: randomUUID3(),
10893
+ eventId: event.id,
10894
+ channelId: channel.id,
10895
+ transport: channel.transport,
10896
+ status,
10897
+ attempts,
10898
+ createdAt: attempts[0]?.startedAt ?? now2(),
10899
+ completedAt: attempts.at(-1)?.completedAt ?? now2()
10900
+ };
10901
+ }
10902
+ function createEvent2(input) {
10903
+ return {
10904
+ id: input.id ?? randomUUID22(),
10905
+ source: input.source,
10906
+ type: input.type,
10907
+ time: normalizeTime2(input.time),
10908
+ subject: input.subject,
10909
+ severity: input.severity ?? "info",
10910
+ data: input.data ?? {},
10911
+ message: input.message,
10912
+ dedupeKey: input.dedupeKey,
10913
+ schemaVersion: input.schemaVersion ?? "1.0",
10914
+ metadata: input.metadata ?? {}
10915
+ };
10916
+ }
10917
+
10918
+ class EventsClient2 {
10919
+ store;
10920
+ redactors;
10921
+ transportOptions;
10922
+ constructor(options = {}) {
10923
+ this.store = options.store ?? new JsonEventsStore2(options.dataDir);
10924
+ this.redactors = options.redactors ?? [];
10925
+ this.transportOptions = { fetchImpl: options.fetchImpl };
10926
+ }
10927
+ async addChannel(input) {
10928
+ const timestamp = new Date().toISOString();
10929
+ return this.store.addChannel({
10930
+ ...input,
10931
+ createdAt: input.createdAt ?? timestamp,
10932
+ updatedAt: input.updatedAt ?? timestamp
10933
+ });
10934
+ }
10935
+ async listChannels() {
10936
+ return this.store.listChannels();
10937
+ }
10938
+ async removeChannel(id) {
10939
+ return this.store.removeChannel(id);
10940
+ }
10941
+ async emit(input, options = {}) {
10942
+ const event = options.redactSensitiveData === false ? createEvent2(input) : redactSensitiveKeys2(createEvent2(input));
10943
+ if (options.dedupe !== false) {
10944
+ const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
10945
+ if (existing) {
10946
+ return { event: existing, deliveries: [], deduped: true };
10947
+ }
10948
+ }
10949
+ await this.store.appendEvent(event);
10950
+ const deliveries = options.deliver === false ? [] : await this.deliver(event);
10951
+ return { event, deliveries, deduped: false };
10952
+ }
10953
+ async listEvents() {
10954
+ return this.store.listEvents();
10955
+ }
10956
+ async listDeliveries() {
10957
+ return this.store.listDeliveries();
10958
+ }
10959
+ async deliver(event) {
10960
+ const channels = await this.store.listChannels();
10961
+ const selected = channels.filter((channel) => channelMatchesEvent2(channel, event));
10962
+ const deliveries = [];
10963
+ for (const channel of selected) {
10964
+ const eventForChannel = await this.applyRedaction(event, channel);
10965
+ const result = await this.deliverWithRetry(eventForChannel, channel);
10966
+ await this.store.appendDelivery(result);
10967
+ deliveries.push(result);
10968
+ }
10969
+ return deliveries;
10970
+ }
10971
+ async testChannel(id, input = {}) {
10972
+ const channel = await this.store.getChannel(id);
10973
+ if (!channel)
10974
+ throw new Error(`Channel not found: ${id}`);
10975
+ const event = createEvent2({
10976
+ source: input.source ?? "hasna.events",
10977
+ type: input.type ?? "events.test",
10978
+ subject: input.subject ?? id,
10979
+ severity: input.severity ?? "info",
10980
+ data: input.data ?? { test: true },
10981
+ message: input.message ?? "Hasna events test delivery",
10982
+ dedupeKey: input.dedupeKey,
10983
+ schemaVersion: input.schemaVersion,
10984
+ metadata: input.metadata,
10985
+ time: input.time,
10986
+ id: input.id
10987
+ });
10988
+ const eventForChannel = await this.applyRedaction(event, channel);
10989
+ const result = await this.deliverWithRetry(eventForChannel, channel);
10990
+ await this.store.appendDelivery(result);
10991
+ return result;
10992
+ }
10993
+ async replay(options = {}) {
10994
+ const events = (await this.store.listEvents()).filter((event) => {
10995
+ if (options.eventId && event.id !== options.eventId)
10996
+ return false;
10997
+ if (options.source && event.source !== options.source)
10998
+ return false;
10999
+ if (options.type && event.type !== options.type)
11000
+ return false;
11001
+ return true;
11002
+ });
11003
+ if (options.dryRun)
11004
+ return { events, deliveries: [] };
11005
+ const deliveries = [];
11006
+ for (const event of events) {
11007
+ deliveries.push(...await this.deliver(event));
11008
+ }
11009
+ return { events, deliveries };
11010
+ }
11011
+ async applyRedaction(event, channel) {
11012
+ let next = redactPaths2(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
11013
+ for (const redactor of this.redactors) {
11014
+ next = await redactor(next, channel);
11015
+ }
11016
+ return next;
11017
+ }
11018
+ async deliverWithRetry(event, channel) {
11019
+ const policy = normalizeRetryPolicy2(channel.retry);
11020
+ const attempts = [];
11021
+ for (let index = 0;index < policy.maxAttempts; index += 1) {
11022
+ const attempt = await dispatchChannel3(event, channel, this.transportOptions);
11023
+ attempt.attempt = index + 1;
11024
+ if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
11025
+ attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
11026
+ }
11027
+ attempts.push(attempt);
11028
+ if (attempt.status !== "failed")
11029
+ break;
11030
+ if (attempt.nextBackoffMs)
11031
+ await Bun.sleep(attempt.nextBackoffMs);
11032
+ }
11033
+ return createDeliveryResult2(event, channel, attempts);
11034
+ }
11035
+ }
11036
+ function redactPaths2(event, paths, replacement = "[REDACTED]") {
11037
+ if (paths.length === 0)
11038
+ return event;
11039
+ const copy = structuredClone(event);
11040
+ for (const path of paths) {
11041
+ setPath2(copy, path, replacement);
11042
+ }
11043
+ return copy;
11044
+ }
11045
+ function sanitizeChannelForOutput2(channel) {
11046
+ const copy = structuredClone(channel);
11047
+ if (copy.webhook?.secret)
11048
+ copy.webhook.secret = "[REDACTED]";
11049
+ if (copy.command?.env) {
11050
+ copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey2(key) ? "[REDACTED]" : value]));
11051
+ }
11052
+ return copy;
11053
+ }
11054
+ function sanitizeChannelsForOutput2(channels) {
11055
+ return channels.map(sanitizeChannelForOutput2);
11056
+ }
11057
+ function redactSensitiveKeys2(event, replacement = "[REDACTED]") {
11058
+ return redactValue2(event, replacement);
11059
+ }
11060
+ function shouldRedactKey2(key) {
11061
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
11062
+ }
11063
+ function redactValue2(value, replacement) {
11064
+ if (Array.isArray(value))
11065
+ return value.map((item) => redactValue2(item, replacement));
11066
+ if (!value || typeof value !== "object")
11067
+ return value;
11068
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
11069
+ key,
11070
+ shouldRedactKey2(key) ? replacement : redactValue2(item, replacement)
11071
+ ]));
11072
+ }
11073
+ function setPath2(input, path, replacement) {
11074
+ const parts = path.split(".");
11075
+ let cursor = input;
11076
+ for (const part of parts.slice(0, -1)) {
11077
+ const next = cursor[part];
11078
+ if (!next || typeof next !== "object")
11079
+ return;
11080
+ cursor = next;
11081
+ }
11082
+ const last = parts.at(-1);
11083
+ if (last && last in cursor)
11084
+ cursor[last] = replacement;
11085
+ }
11086
+ function normalizeTime2(value) {
11087
+ if (!value)
11088
+ return new Date().toISOString();
11089
+ return value instanceof Date ? value.toISOString() : value;
11090
+ }
11091
+ function normalizeRetryPolicy2(policy) {
11092
+ return {
11093
+ maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
11094
+ backoffMs: Math.max(0, policy?.backoffMs ?? 250),
11095
+ multiplier: Math.max(1, policy?.multiplier ?? 2)
11096
+ };
11097
+ }
11098
+
9839
11099
  // src/commands/serve.ts
9840
11100
  function escapeHtml(value) {
9841
11101
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
@@ -9853,13 +11113,16 @@ function getServeInfo(options = {}) {
9853
11113
  "/api/status",
9854
11114
  "/api/manifest",
9855
11115
  "/api/notifications",
11116
+ "/api/webhooks",
11117
+ "/api/events",
9856
11118
  "/api/doctor",
9857
11119
  "/api/self-test",
9858
11120
  "/api/apps/status",
9859
11121
  "/api/apps/diff",
9860
11122
  "/api/install-claude/status",
9861
11123
  "/api/install-claude/diff",
9862
- "/api/notifications/test"
11124
+ "/api/notifications/test",
11125
+ "/api/webhooks/test"
9863
11126
  ]
9864
11127
  };
9865
11128
  }
@@ -10026,6 +11289,7 @@ function jsonError(message, status = 400) {
10026
11289
  }
10027
11290
  function startDashboardServer(options = {}) {
10028
11291
  const info = getServeInfo(options);
11292
+ const events = new EventsClient2;
10029
11293
  return Bun.serve({
10030
11294
  hostname: info.host,
10031
11295
  port: info.port,
@@ -10045,6 +11309,42 @@ function startDashboardServer(options = {}) {
10045
11309
  if (url.pathname === "/api/notifications") {
10046
11310
  return Response.json(listNotificationChannels());
10047
11311
  }
11312
+ if (url.pathname === "/api/webhooks") {
11313
+ if (request.method !== "GET") {
11314
+ return jsonError("Use GET for webhook channel listing.", 405);
11315
+ }
11316
+ return Response.json(sanitizeChannelsForOutput2(await events.listChannels()));
11317
+ }
11318
+ if (url.pathname === "/api/events") {
11319
+ if (request.method === "GET") {
11320
+ return Response.json(await events.listEvents());
11321
+ }
11322
+ if (request.method !== "POST") {
11323
+ return jsonError("Use GET or POST for events.", 405);
11324
+ }
11325
+ const body = await parseJsonBody(request);
11326
+ const type = typeof body["type"] === "string" ? body["type"] : undefined;
11327
+ if (!type) {
11328
+ return jsonError("type is required.");
11329
+ }
11330
+ const source = typeof body["source"] === "string" ? body["source"] : "machines";
11331
+ const subject = typeof body["subject"] === "string" ? body["subject"] : undefined;
11332
+ const severity = typeof body["severity"] === "string" ? body["severity"] : undefined;
11333
+ const message = typeof body["message"] === "string" ? body["message"] : undefined;
11334
+ const dedupeKey = typeof body["dedupeKey"] === "string" ? body["dedupeKey"] : undefined;
11335
+ const data = body["data"] && typeof body["data"] === "object" && !Array.isArray(body["data"]) ? body["data"] : {};
11336
+ const metadata = body["metadata"] && typeof body["metadata"] === "object" && !Array.isArray(body["metadata"]) ? body["metadata"] : {};
11337
+ return Response.json(await events.emit({
11338
+ source,
11339
+ type,
11340
+ subject,
11341
+ severity,
11342
+ message,
11343
+ dedupeKey,
11344
+ data,
11345
+ metadata
11346
+ }));
11347
+ }
10048
11348
  if (url.pathname === "/api/doctor") {
10049
11349
  return Response.json(runDoctor(machineId));
10050
11350
  }
@@ -10082,6 +11382,28 @@ function startDashboardServer(options = {}) {
10082
11382
  return jsonError(error instanceof Error ? error.message : String(error));
10083
11383
  }
10084
11384
  }
11385
+ if (url.pathname === "/api/webhooks/test") {
11386
+ if (request.method !== "POST") {
11387
+ return jsonError("Use POST for webhook tests.", 405);
11388
+ }
11389
+ const body = await parseJsonBody(request);
11390
+ const channelId = typeof body["channelId"] === "string" ? body["channelId"] : undefined;
11391
+ if (!channelId) {
11392
+ return jsonError("channelId is required.");
11393
+ }
11394
+ const type = typeof body["type"] === "string" ? body["type"] : "events.test";
11395
+ const message = typeof body["message"] === "string" ? body["message"] : undefined;
11396
+ try {
11397
+ return Response.json(await events.testChannel(channelId, {
11398
+ source: "machines",
11399
+ type,
11400
+ subject: channelId,
11401
+ message
11402
+ }));
11403
+ } catch (error) {
11404
+ return jsonError(error instanceof Error ? error.message : String(error));
11405
+ }
11406
+ }
10085
11407
  return new Response(renderDashboardHtml(), {
10086
11408
  headers: {
10087
11409
  "content-type": "text/html; charset=utf-8"
@@ -10124,8 +11446,8 @@ function runSelfTest() {
10124
11446
  // src/commands/clipboard.ts
10125
11447
  init_paths();
10126
11448
  import { createHash } from "crypto";
10127
- import { existsSync as existsSync8, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "fs";
10128
- import { join as join6 } from "path";
11449
+ import { existsSync as existsSync10, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "fs";
11450
+ import { join as join8 } from "path";
10129
11451
  var DEFAULT_CONFIG = {
10130
11452
  version: 1,
10131
11453
  enabled: true,
@@ -10143,7 +11465,7 @@ var DEFAULT_CONFIG = {
10143
11465
  function resolveConfigPath(configPath) {
10144
11466
  if (configPath)
10145
11467
  return configPath;
10146
- return join6(getDataDir(), "clipboard-config.json");
11468
+ return join8(getDataDir(), "clipboard-config.json");
10147
11469
  }
10148
11470
  function resolveHistoryPath(historyPath) {
10149
11471
  if (historyPath)
@@ -10155,7 +11477,7 @@ function getDefaultConfig() {
10155
11477
  }
10156
11478
  function readConfig(configPath) {
10157
11479
  const path = resolveConfigPath(configPath);
10158
- if (!existsSync8(path)) {
11480
+ if (!existsSync10(path)) {
10159
11481
  return getDefaultConfig();
10160
11482
  }
10161
11483
  const parsed = JSON.parse(readFileSync6(path, "utf8"));
@@ -10169,7 +11491,7 @@ function writeConfig(config, configPath) {
10169
11491
  }
10170
11492
  function readHistory(historyPath) {
10171
11493
  const path = resolveHistoryPath(historyPath);
10172
- if (!existsSync8(path)) {
11494
+ if (!existsSync10(path)) {
10173
11495
  return [];
10174
11496
  }
10175
11497
  try {
@@ -10202,7 +11524,7 @@ function sanitizeClipboardForRead(content, maxSizeBytes, skipPatterns) {
10202
11524
  }
10203
11525
  function getOrCreateClipboardKey() {
10204
11526
  const keyPath = getClipboardKeyPath();
10205
- if (existsSync8(keyPath)) {
11527
+ if (existsSync10(keyPath)) {
10206
11528
  return readFileSync6(keyPath, "utf8").trim();
10207
11529
  }
10208
11530
  const key = createHash("sha256").update(crypto.randomUUID()).digest("hex").slice(0, 32);
@@ -10242,7 +11564,7 @@ function addClipboardEntry(entry, historyPath) {
10242
11564
  }
10243
11565
  function clearClipboardHistory(historyPath) {
10244
11566
  const path = resolveHistoryPath(historyPath);
10245
- if (existsSync8(path)) {
11567
+ if (existsSync10(path)) {
10246
11568
  rmSync(path);
10247
11569
  }
10248
11570
  }
@@ -10259,7 +11581,7 @@ function getClipboardStatus(historyPath) {
10259
11581
  // src/commands/clipboard-daemon.ts
10260
11582
  init_paths();
10261
11583
  import { readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
10262
- import { join as join7 } from "path";
11584
+ import { join as join9 } from "path";
10263
11585
  import { createHash as createHash3 } from "crypto";
10264
11586
 
10265
11587
  // src/commands/clipboard-server.ts
@@ -10417,7 +11739,7 @@ function handleGetClipboard(response, config) {
10417
11739
  }
10418
11740
 
10419
11741
  // src/commands/clipboard-daemon.ts
10420
- var DAEMON_PID_PATH = join7(getDataDir(), "clipboard-daemon.pid");
11742
+ var DAEMON_PID_PATH = join9(getDataDir(), "clipboard-daemon.pid");
10421
11743
  function readLocalClipboardSync2() {
10422
11744
  const platform4 = process.platform;
10423
11745
  if (platform4 === "darwin") {
@@ -10591,8 +11913,8 @@ async function discoverPeers() {
10591
11913
 
10592
11914
  // src/commands/heal.ts
10593
11915
  init_paths();
10594
- import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
10595
- import { join as join8 } from "path";
11916
+ import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
11917
+ import { join as join10 } from "path";
10596
11918
  var DEFAULT_THRESHOLDS = {
10597
11919
  reconnect: 3,
10598
11920
  nmRestart: 7,
@@ -10636,14 +11958,14 @@ function defaultHealState() {
10636
11958
  };
10637
11959
  }
10638
11960
  function getHealConfigPath() {
10639
- return process.env["HASNA_MACHINES_HEAL_CONFIG_PATH"] || join8(getDataDir(), "heal-config.json");
11961
+ return process.env["HASNA_MACHINES_HEAL_CONFIG_PATH"] || join10(getDataDir(), "heal-config.json");
10640
11962
  }
10641
11963
  function getHealStatePath() {
10642
- return process.env["HASNA_MACHINES_HEAL_STATE_PATH"] || join8(getDataDir(), "heal-state.json");
11964
+ return process.env["HASNA_MACHINES_HEAL_STATE_PATH"] || join10(getDataDir(), "heal-state.json");
10643
11965
  }
10644
11966
  function readHealConfig(path) {
10645
11967
  const p = path || getHealConfigPath();
10646
- if (!existsSync9(p))
11968
+ if (!existsSync11(p))
10647
11969
  return { ...DEFAULT_HEAL_CONFIG, thresholds: { ...DEFAULT_THRESHOLDS } };
10648
11970
  const parsed = JSON.parse(readFileSync9(p, "utf8"));
10649
11971
  return {
@@ -10661,7 +11983,7 @@ function writeHealConfig(config, path) {
10661
11983
  }
10662
11984
  function readHealState(path) {
10663
11985
  const p = path || getHealStatePath();
10664
- if (!existsSync9(p))
11986
+ if (!existsSync11(p))
10665
11987
  return defaultHealState();
10666
11988
  try {
10667
11989
  return { ...defaultHealState(), ...JSON.parse(readFileSync9(p, "utf8")) };
@@ -10701,7 +12023,7 @@ function evaluateHealth(probe, config, state) {
10701
12023
  return { healthy: localOk && quorumOk, remoteScore, reasons };
10702
12024
  }
10703
12025
  function decideAction(input) {
10704
- const { healthy, now, gpuBusy, config, currentBootId } = input;
12026
+ const { healthy, now: now3, gpuBusy, config, currentBootId } = input;
10705
12027
  const s = { ...input.state };
10706
12028
  const t = config.thresholds;
10707
12029
  if (s.bootId !== currentBootId) {
@@ -10712,13 +12034,13 @@ function decideAction(input) {
10712
12034
  if (healthy) {
10713
12035
  s.failCount = 0;
10714
12036
  if (s.bootHealthySince === null)
10715
- s.bootHealthySince = now;
10716
- if (now - s.bootHealthySince >= config.healthyWindowSec) {
12037
+ s.bootHealthySince = now3;
12038
+ if (now3 - s.bootHealthySince >= config.healthyWindowSec) {
10717
12039
  s.failedBootRecoveries = 0;
10718
12040
  s.rebootSuppressUntil = 0;
10719
12041
  s.pendingRebootRecovery = false;
10720
12042
  }
10721
- if (s.degradedUntil > 0 && now >= s.degradedUntil) {
12043
+ if (s.degradedUntil > 0 && now3 >= s.degradedUntil) {
10722
12044
  s.degradedUntil = 0;
10723
12045
  return { action: "restore_preferred", state: s };
10724
12046
  }
@@ -10736,8 +12058,8 @@ function decideAction(input) {
10736
12058
  else if (s.failCount >= t.reconnect)
10737
12059
  tier = "reconnect";
10738
12060
  const tryReconnect = (reason) => {
10739
- if (now - s.lastReconnect >= config.reconnectMinIntervalSec) {
10740
- s.lastReconnect = now;
12061
+ if (now3 - s.lastReconnect >= config.reconnectMinIntervalSec) {
12062
+ s.lastReconnect = now3;
10741
12063
  return { action: "reconnect_wifi", suppressedReason: reason, state: s };
10742
12064
  }
10743
12065
  return { action: "none", suppressedReason: reason, state: s };
@@ -10746,15 +12068,15 @@ function decideAction(input) {
10746
12068
  case "reconnect":
10747
12069
  return tryReconnect();
10748
12070
  case "nmRestart":
10749
- if (now - s.lastNmRestart >= config.nmRestartMinIntervalSec) {
10750
- s.lastNmRestart = now;
12071
+ if (now3 - s.lastNmRestart >= config.nmRestartMinIntervalSec) {
12072
+ s.lastNmRestart = now3;
10751
12073
  return { action: "restart_nm", state: s };
10752
12074
  }
10753
12075
  return tryReconnect();
10754
12076
  case "fallback":
10755
- if (now - s.lastFallback >= config.fallbackWindowSec) {
10756
- s.lastFallback = now;
10757
- s.degradedUntil = now + config.fallbackWindowSec;
12077
+ if (now3 - s.lastFallback >= config.fallbackWindowSec) {
12078
+ s.lastFallback = now3;
12079
+ s.degradedUntil = now3 + config.fallbackWindowSec;
10758
12080
  return { action: "fallback_ssid", state: s };
10759
12081
  }
10760
12082
  return tryReconnect();
@@ -10762,22 +12084,22 @@ function decideAction(input) {
10762
12084
  let reason = null;
10763
12085
  if (!config.allowReboot)
10764
12086
  reason = "disabled";
10765
- else if (now < s.rebootSuppressUntil)
12087
+ else if (now3 < s.rebootSuppressUntil)
10766
12088
  reason = "loop";
10767
12089
  else if (config.gpuJobGuard && gpuBusy)
10768
12090
  reason = "gpu";
10769
- else if (now - s.lastRebootAttempt < config.rebootMinIntervalSec)
12091
+ else if (now3 - s.lastRebootAttempt < config.rebootMinIntervalSec)
10770
12092
  reason = "rate";
10771
12093
  if (reason)
10772
12094
  return tryReconnect(reason);
10773
12095
  if (s.pendingRebootRecovery) {
10774
12096
  s.failedBootRecoveries += 1;
10775
12097
  if (s.failedBootRecoveries >= config.maxFailedBootRecoveries) {
10776
- s.rebootSuppressUntil = now + config.bootBackoffSec;
12098
+ s.rebootSuppressUntil = now3 + config.bootBackoffSec;
10777
12099
  return tryReconnect("loop");
10778
12100
  }
10779
12101
  }
10780
- s.lastRebootAttempt = now;
12102
+ s.lastRebootAttempt = now3;
10781
12103
  s.pendingRebootRecovery = true;
10782
12104
  return { action: "reboot", state: s };
10783
12105
  }
@@ -10877,9 +12199,9 @@ function executeAction(action, config) {
10877
12199
 
10878
12200
  // src/commands/heal-daemon.ts
10879
12201
  init_paths();
10880
- import { existsSync as existsSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
10881
- import { join as join9 } from "path";
10882
- var DAEMON_PID_PATH2 = join9(getDataDir(), "heal-daemon.pid");
12202
+ import { existsSync as existsSync12, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
12203
+ import { join as join11 } from "path";
12204
+ var DAEMON_PID_PATH2 = join11(getDataDir(), "heal-daemon.pid");
10883
12205
  var SERVICE_PATH = "/etc/systemd/system/machines-heal.service";
10884
12206
  var SYSTEM_CONF = "/etc/systemd/system.conf";
10885
12207
  function log(msg) {
@@ -10997,7 +12319,7 @@ function applyDeterminism(config) {
10997
12319
  }
10998
12320
  function enableHardwareWatchdog() {
10999
12321
  const log2 = [];
11000
- if (!existsSync10(SYSTEM_CONF))
12322
+ if (!existsSync12(SYSTEM_CONF))
11001
12323
  return ["/etc/systemd/system.conf not found; skipping hardware watchdog"];
11002
12324
  let conf = readFileSync10(SYSTEM_CONF, "utf8");
11003
12325
  const set = (key, value) => {
@@ -11027,7 +12349,7 @@ function binPath() {
11027
12349
  const home = process.env["HOME"] || "/home/hasna";
11028
12350
  candidates.push(`${home}/.bun/bin/machines`, "/home/hasna/.bun/bin/machines", "/root/.bun/bin/machines", "/usr/local/bin/machines");
11029
12351
  for (const c of candidates) {
11030
- if (c && existsSync10(c))
12352
+ if (c && existsSync12(c))
11031
12353
  return c;
11032
12354
  }
11033
12355
  return "machines";
@@ -11063,7 +12385,7 @@ WantedBy=multi-user.target
11063
12385
  function uninstallHealService() {
11064
12386
  const log2 = [];
11065
12387
  sh2("systemctl disable --now machines-heal.service 2>/dev/null || true");
11066
- if (existsSync10(SERVICE_PATH)) {
12388
+ if (existsSync12(SERVICE_PATH)) {
11067
12389
  sh2(`rm -f ${SERVICE_PATH}`);
11068
12390
  sh2("systemctl daemon-reload");
11069
12391
  log2.push(`removed ${SERVICE_PATH}`);
@@ -11074,7 +12396,7 @@ function uninstallHealService() {
11074
12396
  }
11075
12397
  function healServiceStatus() {
11076
12398
  return {
11077
- installed: existsSync10(SERVICE_PATH),
12399
+ installed: existsSync12(SERVICE_PATH),
11078
12400
  active: sh2("systemctl is-active machines-heal.service").out === "active",
11079
12401
  enabled: sh2("systemctl is-enabled machines-heal.service 2>/dev/null").out === "enabled"
11080
12402
  };
@@ -11221,6 +12543,19 @@ function renderSelfTestResult(result) {
11221
12543
  ].join(`
11222
12544
  `);
11223
12545
  }
12546
+ function checkSecretPresence(secretsCommand, key) {
12547
+ const result = Bun.spawnSync([secretsCommand, "get", key], {
12548
+ stdout: "pipe",
12549
+ stderr: "pipe",
12550
+ env: process.env
12551
+ });
12552
+ const stdout = result.stdout.toString().trim();
12553
+ return {
12554
+ checked: true,
12555
+ present: result.exitCode === 0 && stdout.length > 0,
12556
+ error: result.exitCode === 0 ? undefined : result.stderr.toString().trim() || `secrets get exited ${result.exitCode}`
12557
+ };
12558
+ }
11224
12559
  function parseCommandSpec(value) {
11225
12560
  const [command, expectedVersion] = value.split(":");
11226
12561
  return {
@@ -11332,6 +12667,7 @@ program2.name("machines").description("Machine fleet management CLI + MCP for de
11332
12667
  var manifestCommand = program2.command("manifest").description("Manage the fleet manifest");
11333
12668
  var appsCommand = program2.command("apps").description("Manage installed applications per machine");
11334
12669
  var notificationsCommand = program2.command("notifications").description("Manage fleet alert delivery channels");
12670
+ registerEventsCommands(program2, { source: "machines" });
11335
12671
  var clipboardCommand = program2.command("clipboard").description("Real-time clipboard sync across fleet machines");
11336
12672
  var installClaudeCommand = program2.command("install-claude").description("Install or inspect Claude, Codex, and Gemini CLIs");
11337
12673
  manifestCommand.command("init").description("Create an empty fleet manifest").action(() => {
@@ -11727,23 +13063,58 @@ program2.command("screen").description("Open Screen Sharing (VNC) to a machine u
11727
13063
  execFileSync("open", [resolved.url], { stdio: "ignore" });
11728
13064
  console.log(`Opening Screen Sharing \u2192 ${resolved.url} (route: ${resolved.route})`);
11729
13065
  });
11730
- program2.command("screen-enable").description("Enable Remote Management / Screen Sharing on a macOS machine over SSH").requiredOption("--machine <id>", "Machine identifier").option("--user <user>", "macOS user to grant screen-sharing (overrides manifest)").option("--vnc-password <pw>", "Legacy VNC password (<=8 chars honored)", "").option("--print", "Print the remote command instead of running it", false).action((options) => {
11731
- const screen = resolveScreenTarget(options.machine);
11732
- const user = options.user ?? screen.user;
11733
- if (!user) {
11734
- console.error(`No SSH user known for ${options.machine}; pass --user <name> or set metadata.user in the manifest.`);
13066
+ program2.command("screen-credentials").description("Inspect screen-sharing user and password secret references without printing secrets").option("--machine <id>", "Machine identifier").option("--all", "Inspect every discovered machine", false).option("--check-secret", "Check whether the password secret exists in the local secrets vault", false).option("--secrets-command <command>", "Secrets CLI command to inspect", "secrets").option("--no-tailscale", "Skip tailscale status probing").option("-j, --json", "Print JSON output", false).action((options) => {
13067
+ const topology = discoverMachineTopology({ includeTailscale: options.tailscale !== false });
13068
+ const machineIds = options.all ? topology.machines.map((machine) => machine.machine_id) : [options.machine].filter((machine) => Boolean(machine));
13069
+ if (machineIds.length === 0) {
13070
+ console.error("Provide --machine <id> or --all");
11735
13071
  process.exitCode = 1;
11736
13072
  return;
11737
13073
  }
11738
- const vncPw = (options.vncPassword || "").slice(0, 8);
11739
- const remoteCmd = buildScreenEnableRemoteCommand(user, vncPw);
11740
- const sshCmd = buildSshCommand(options.machine, `sudo -p '' bash -c ${JSON.stringify(remoteCmd)}`);
13074
+ const results = machineIds.map((machineId) => {
13075
+ try {
13076
+ const credentials = resolveScreenCredentials(machineId, { topology });
13077
+ const secret = options.checkSecret ? checkSecretPresence(options.secretsCommand, credentials.passwordSecretKey) : { checked: false, present: null };
13078
+ return { ok: true, ...credentials, passwordSecret: secret };
13079
+ } catch (error) {
13080
+ return { ok: false, machineId, error: error instanceof Error ? error.message : String(error) };
13081
+ }
13082
+ });
13083
+ const hasFailures = results.some((result) => !result.ok || result.ok && result.passwordSecret.checked && !result.passwordSecret.present);
13084
+ if (options.json) {
13085
+ console.log(JSON.stringify(results, null, 2));
13086
+ if (hasFailures)
13087
+ process.exitCode = 1;
13088
+ return;
13089
+ }
13090
+ for (const result of results) {
13091
+ if (!result.ok) {
13092
+ console.log(`\u2717 ${result.machineId.padEnd(14)} ${result.error}`);
13093
+ continue;
13094
+ }
13095
+ const secret = result.passwordSecret.checked ? result.passwordSecret.present ? source_default.green("present") : source_default.red("missing") : source_default.yellow("unchecked");
13096
+ console.log(`${result.machineId.padEnd(14)} user=${result.user ?? "(missing)"} passwordSecret=${result.passwordSecretKey} (${secret})`);
13097
+ }
13098
+ if (hasFailures)
13099
+ process.exitCode = 1;
13100
+ });
13101
+ program2.command("screen-enable").description("Enable Remote Management / Screen Sharing on a macOS machine over SSH").requiredOption("--machine <id>", "Machine identifier").option("--user <user>", "macOS user to grant screen-sharing (overrides manifest)").option("--vnc-password-secret <key>", "Secret key containing the legacy VNC password").option("--secrets-command <command>", "Secrets CLI command to read the password", "secrets").option("--vnc-password <pw>", "Deprecated: use --vnc-password-secret instead", "").option("--print", "Print the remote command instead of running it", false).action((options) => {
13102
+ if (options.vncPassword) {
13103
+ console.error("Direct --vnc-password values are not accepted. Store the password with `secrets set` and pass --vnc-password-secret.");
13104
+ process.exitCode = 1;
13105
+ return;
13106
+ }
13107
+ const plan = buildScreenEnableCommand(options.machine, {
13108
+ user: options.user,
13109
+ passwordSecretKey: options.vncPasswordSecret,
13110
+ secretsCommand: options.secretsCommand
13111
+ });
11741
13112
  if (options.print) {
11742
- console.log(sshCmd);
13113
+ console.log(plan.command);
11743
13114
  return;
11744
13115
  }
11745
- console.log(`Run this to enable Screen Sharing on ${options.machine} (will prompt for sudo on the target):`);
11746
- console.log(` ${sshCmd}`);
13116
+ console.log(`Run this to enable Screen Sharing on ${options.machine} (password comes from ${plan.passwordSecretKey}):`);
13117
+ console.log(` ${plan.command}`);
11747
13118
  });
11748
13119
  program2.command("ports").description("List listening ports on a machine").option("--machine <id>", "Machine identifier").option("-j, --json", "Print JSON output", false).action((options) => {
11749
13120
  const result = listPorts(options.machine);