@hasna/machines 0.0.32 → 0.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -13
- package/dist/cli/index.js +132 -1318
- package/dist/commands/screen.d.ts +2 -1
- package/dist/commands/screen.d.ts.map +1 -1
- package/dist/index.js +32 -544
- package/dist/mcp/index.js +85 -601
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -2082,34 +2082,34 @@ var require_commander = __commonJS((exports) => {
|
|
|
2082
2082
|
});
|
|
2083
2083
|
|
|
2084
2084
|
// src/paths.ts
|
|
2085
|
-
import { existsSync as
|
|
2086
|
-
import { dirname as dirname2, join as
|
|
2085
|
+
import { existsSync as existsSync2, mkdirSync } from "fs";
|
|
2086
|
+
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
2087
2087
|
function homeDir() {
|
|
2088
2088
|
return process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
2089
2089
|
}
|
|
2090
2090
|
function getDataDir() {
|
|
2091
|
-
return process.env["HASNA_MACHINES_DIR"] ||
|
|
2091
|
+
return process.env["HASNA_MACHINES_DIR"] || join2(homeDir(), ".hasna", "machines");
|
|
2092
2092
|
}
|
|
2093
2093
|
function getDbPath() {
|
|
2094
|
-
return process.env["HASNA_MACHINES_DB_PATH"] ||
|
|
2094
|
+
return process.env["HASNA_MACHINES_DB_PATH"] || join2(getDataDir(), "machines.db");
|
|
2095
2095
|
}
|
|
2096
2096
|
function getManifestPath() {
|
|
2097
|
-
return process.env["HASNA_MACHINES_MANIFEST_PATH"] ||
|
|
2097
|
+
return process.env["HASNA_MACHINES_MANIFEST_PATH"] || join2(getDataDir(), "machines.json");
|
|
2098
2098
|
}
|
|
2099
2099
|
function getNotificationsPath() {
|
|
2100
|
-
return process.env["HASNA_MACHINES_NOTIFICATIONS_PATH"] ||
|
|
2100
|
+
return process.env["HASNA_MACHINES_NOTIFICATIONS_PATH"] || join2(getDataDir(), "notifications.json");
|
|
2101
2101
|
}
|
|
2102
2102
|
function getClipboardKeyPath() {
|
|
2103
|
-
return process.env["HASNA_MACHINES_CLIPBOARD_KEY_PATH"] ||
|
|
2103
|
+
return process.env["HASNA_MACHINES_CLIPBOARD_KEY_PATH"] || join2(getDataDir(), "clipboard.key");
|
|
2104
2104
|
}
|
|
2105
2105
|
function getClipboardHistoryPath() {
|
|
2106
|
-
return process.env["HASNA_MACHINES_CLIPBOARD_HISTORY_PATH"] ||
|
|
2106
|
+
return process.env["HASNA_MACHINES_CLIPBOARD_HISTORY_PATH"] || join2(getDataDir(), "clipboard-history.json");
|
|
2107
2107
|
}
|
|
2108
2108
|
function ensureParentDir(filePath) {
|
|
2109
2109
|
if (filePath === ":memory:")
|
|
2110
2110
|
return;
|
|
2111
2111
|
const dir = dirname2(resolve(filePath));
|
|
2112
|
-
if (!
|
|
2112
|
+
if (!existsSync2(dir)) {
|
|
2113
2113
|
mkdirSync(dir, { recursive: true });
|
|
2114
2114
|
}
|
|
2115
2115
|
}
|
|
@@ -2203,15 +2203,15 @@ function countRuns(table) {
|
|
|
2203
2203
|
}
|
|
2204
2204
|
function recordSetupRun(machineId, status, details) {
|
|
2205
2205
|
const db = getDb();
|
|
2206
|
-
const
|
|
2206
|
+
const now = new Date().toISOString();
|
|
2207
2207
|
db.query(`INSERT INTO setup_runs (id, machine_id, status, details_json, created_at, updated_at)
|
|
2208
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(details),
|
|
2208
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(details), now, now);
|
|
2209
2209
|
}
|
|
2210
2210
|
function recordSyncRun(machineId, status, actions) {
|
|
2211
2211
|
const db = getDb();
|
|
2212
|
-
const
|
|
2212
|
+
const now = new Date().toISOString();
|
|
2213
2213
|
db.query(`INSERT INTO sync_runs (id, machine_id, status, actions_json, created_at, updated_at)
|
|
2214
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(actions),
|
|
2214
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(actions), now, now);
|
|
2215
2215
|
}
|
|
2216
2216
|
var adapter = null;
|
|
2217
2217
|
var init_db = __esm(() => {
|
|
@@ -2479,7 +2479,7 @@ function upsertSqlite(db, table, columns, rows) {
|
|
|
2479
2479
|
}
|
|
2480
2480
|
function recordSyncMeta(db, direction, results) {
|
|
2481
2481
|
ensureSyncMetaTable(db);
|
|
2482
|
-
const
|
|
2482
|
+
const now = new Date().toISOString();
|
|
2483
2483
|
const statement = db.query(`
|
|
2484
2484
|
INSERT INTO _machines_sync_meta (table_name, last_synced_at, direction)
|
|
2485
2485
|
VALUES (?, ?, ?)
|
|
@@ -2488,7 +2488,7 @@ function recordSyncMeta(db, direction, results) {
|
|
|
2488
2488
|
for (const result of results) {
|
|
2489
2489
|
if (result.errors.length > 0)
|
|
2490
2490
|
continue;
|
|
2491
|
-
statement.run(result.table,
|
|
2491
|
+
statement.run(result.table, now, direction);
|
|
2492
2492
|
}
|
|
2493
2493
|
}
|
|
2494
2494
|
function ensureSyncMetaTable(db) {
|
|
@@ -2601,685 +2601,8 @@ var {
|
|
|
2601
2601
|
Help
|
|
2602
2602
|
} = import__.default;
|
|
2603
2603
|
|
|
2604
|
-
// node_modules/@hasna/events/dist/commander.js
|
|
2605
|
-
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
2606
|
-
import { existsSync } from "fs";
|
|
2607
|
-
import { homedir } from "os";
|
|
2608
|
-
import { join } from "path";
|
|
2609
|
-
import { createHmac, timingSafeEqual } from "crypto";
|
|
2610
|
-
import { randomUUID } from "crypto";
|
|
2611
|
-
import { spawn } from "child_process";
|
|
2612
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
2613
|
-
function getPathValue(input, path) {
|
|
2614
|
-
return path.split(".").reduce((value, part) => {
|
|
2615
|
-
if (value && typeof value === "object" && part in value) {
|
|
2616
|
-
return value[part];
|
|
2617
|
-
}
|
|
2618
|
-
return;
|
|
2619
|
-
}, input);
|
|
2620
|
-
}
|
|
2621
|
-
function wildcardToRegExp(pattern) {
|
|
2622
|
-
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
|
|
2623
|
-
return new RegExp(`^${escaped}$`);
|
|
2624
|
-
}
|
|
2625
|
-
function matchString(value, matcher) {
|
|
2626
|
-
if (matcher === undefined)
|
|
2627
|
-
return true;
|
|
2628
|
-
if (value === undefined)
|
|
2629
|
-
return false;
|
|
2630
|
-
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
2631
|
-
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
2632
|
-
}
|
|
2633
|
-
function matchRecord(input, matcher) {
|
|
2634
|
-
if (!matcher)
|
|
2635
|
-
return true;
|
|
2636
|
-
return Object.entries(matcher).every(([path, expected]) => {
|
|
2637
|
-
const actual = getPathValue(input, path);
|
|
2638
|
-
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
2639
|
-
return matchString(actual === undefined ? undefined : String(actual), expected);
|
|
2640
|
-
}
|
|
2641
|
-
return actual === expected;
|
|
2642
|
-
});
|
|
2643
|
-
}
|
|
2644
|
-
function eventMatchesFilter(event, filter) {
|
|
2645
|
-
return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
|
|
2646
|
-
}
|
|
2647
|
-
function channelMatchesEvent(channel, event) {
|
|
2648
|
-
if (!channel.enabled)
|
|
2649
|
-
return false;
|
|
2650
|
-
if (!channel.filters || channel.filters.length === 0)
|
|
2651
|
-
return true;
|
|
2652
|
-
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
2653
|
-
}
|
|
2654
|
-
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
2655
|
-
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
2656
|
-
function getEventsDataDir(override) {
|
|
2657
|
-
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
|
|
2658
|
-
}
|
|
2659
|
-
|
|
2660
|
-
class JsonEventsStore {
|
|
2661
|
-
dataDir;
|
|
2662
|
-
channelsPath;
|
|
2663
|
-
eventsPath;
|
|
2664
|
-
deliveriesPath;
|
|
2665
|
-
constructor(dataDir = getEventsDataDir()) {
|
|
2666
|
-
this.dataDir = dataDir;
|
|
2667
|
-
this.channelsPath = join(dataDir, "channels.json");
|
|
2668
|
-
this.eventsPath = join(dataDir, "events.json");
|
|
2669
|
-
this.deliveriesPath = join(dataDir, "deliveries.json");
|
|
2670
|
-
}
|
|
2671
|
-
async init() {
|
|
2672
|
-
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
2673
|
-
await chmod(this.dataDir, 448).catch(() => {
|
|
2674
|
-
return;
|
|
2675
|
-
});
|
|
2676
|
-
await this.ensureArrayFile(this.channelsPath);
|
|
2677
|
-
await this.ensureArrayFile(this.eventsPath);
|
|
2678
|
-
await this.ensureArrayFile(this.deliveriesPath);
|
|
2679
|
-
}
|
|
2680
|
-
async addChannel(channel) {
|
|
2681
|
-
await this.init();
|
|
2682
|
-
const channels = await this.readJson(this.channelsPath, []);
|
|
2683
|
-
const index = channels.findIndex((item) => item.id === channel.id);
|
|
2684
|
-
if (index >= 0) {
|
|
2685
|
-
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
2686
|
-
} else {
|
|
2687
|
-
channels.push(channel);
|
|
2688
|
-
}
|
|
2689
|
-
await this.writeJson(this.channelsPath, channels);
|
|
2690
|
-
return index >= 0 ? channels[index] : channel;
|
|
2691
|
-
}
|
|
2692
|
-
async listChannels() {
|
|
2693
|
-
await this.init();
|
|
2694
|
-
return this.readJson(this.channelsPath, []);
|
|
2695
|
-
}
|
|
2696
|
-
async getChannel(id) {
|
|
2697
|
-
const channels = await this.listChannels();
|
|
2698
|
-
return channels.find((channel) => channel.id === id);
|
|
2699
|
-
}
|
|
2700
|
-
async removeChannel(id) {
|
|
2701
|
-
await this.init();
|
|
2702
|
-
const channels = await this.readJson(this.channelsPath, []);
|
|
2703
|
-
const next = channels.filter((channel) => channel.id !== id);
|
|
2704
|
-
await this.writeJson(this.channelsPath, next);
|
|
2705
|
-
return next.length !== channels.length;
|
|
2706
|
-
}
|
|
2707
|
-
async appendEvent(event) {
|
|
2708
|
-
await this.init();
|
|
2709
|
-
const events = await this.readJson(this.eventsPath, []);
|
|
2710
|
-
events.push(event);
|
|
2711
|
-
await this.writeJson(this.eventsPath, events);
|
|
2712
|
-
return event;
|
|
2713
|
-
}
|
|
2714
|
-
async listEvents() {
|
|
2715
|
-
await this.init();
|
|
2716
|
-
return this.readJson(this.eventsPath, []);
|
|
2717
|
-
}
|
|
2718
|
-
async findEventByIdentity(identity) {
|
|
2719
|
-
const events = await this.listEvents();
|
|
2720
|
-
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
2721
|
-
}
|
|
2722
|
-
async appendDelivery(result) {
|
|
2723
|
-
await this.init();
|
|
2724
|
-
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
2725
|
-
deliveries.push(result);
|
|
2726
|
-
await this.writeJson(this.deliveriesPath, deliveries);
|
|
2727
|
-
return result;
|
|
2728
|
-
}
|
|
2729
|
-
async listDeliveries() {
|
|
2730
|
-
await this.init();
|
|
2731
|
-
return this.readJson(this.deliveriesPath, []);
|
|
2732
|
-
}
|
|
2733
|
-
async exportData() {
|
|
2734
|
-
return {
|
|
2735
|
-
channels: await this.listChannels(),
|
|
2736
|
-
events: await this.listEvents(),
|
|
2737
|
-
deliveries: await this.listDeliveries()
|
|
2738
|
-
};
|
|
2739
|
-
}
|
|
2740
|
-
async ensureArrayFile(path) {
|
|
2741
|
-
if (!existsSync(path)) {
|
|
2742
|
-
await writeFile(path, `[]
|
|
2743
|
-
`, { encoding: "utf-8", mode: 384 });
|
|
2744
|
-
}
|
|
2745
|
-
await chmod(path, 384).catch(() => {
|
|
2746
|
-
return;
|
|
2747
|
-
});
|
|
2748
|
-
}
|
|
2749
|
-
async readJson(path, fallback) {
|
|
2750
|
-
try {
|
|
2751
|
-
const raw = await readFile(path, "utf-8");
|
|
2752
|
-
if (!raw.trim())
|
|
2753
|
-
return fallback;
|
|
2754
|
-
return JSON.parse(raw);
|
|
2755
|
-
} catch (error) {
|
|
2756
|
-
if (error.code === "ENOENT")
|
|
2757
|
-
return fallback;
|
|
2758
|
-
throw error;
|
|
2759
|
-
}
|
|
2760
|
-
}
|
|
2761
|
-
async writeJson(path, value) {
|
|
2762
|
-
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
2763
|
-
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
|
|
2764
|
-
`, { encoding: "utf-8", mode: 384 });
|
|
2765
|
-
await rename(tempPath, path);
|
|
2766
|
-
await chmod(path, 384).catch(() => {
|
|
2767
|
-
return;
|
|
2768
|
-
});
|
|
2769
|
-
}
|
|
2770
|
-
}
|
|
2771
|
-
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
2772
|
-
function buildSignatureBase(timestamp, body) {
|
|
2773
|
-
return `${timestamp}.${body}`;
|
|
2774
|
-
}
|
|
2775
|
-
function signPayload(secret, timestamp, body) {
|
|
2776
|
-
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
|
|
2777
|
-
return `sha256=${digest}`;
|
|
2778
|
-
}
|
|
2779
|
-
function now() {
|
|
2780
|
-
return new Date().toISOString();
|
|
2781
|
-
}
|
|
2782
|
-
function truncate(value, max = 4096) {
|
|
2783
|
-
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
2784
|
-
}
|
|
2785
|
-
function buildWebhookRequest(event, channel) {
|
|
2786
|
-
if (!channel.webhook)
|
|
2787
|
-
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
2788
|
-
const body = JSON.stringify(event);
|
|
2789
|
-
const timestamp = event.time;
|
|
2790
|
-
const headers = {
|
|
2791
|
-
"Content-Type": "application/json",
|
|
2792
|
-
"User-Agent": "@hasna/events",
|
|
2793
|
-
"X-Hasna-Event-Id": event.id,
|
|
2794
|
-
"X-Hasna-Event-Type": event.type,
|
|
2795
|
-
"X-Hasna-Timestamp": timestamp,
|
|
2796
|
-
...channel.webhook.headers
|
|
2797
|
-
};
|
|
2798
|
-
if (channel.webhook.secret) {
|
|
2799
|
-
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
|
|
2800
|
-
}
|
|
2801
|
-
return { body, headers };
|
|
2802
|
-
}
|
|
2803
|
-
async function dispatchWebhook(event, channel, options = {}) {
|
|
2804
|
-
if (!channel.webhook)
|
|
2805
|
-
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
2806
|
-
const startedAt = now();
|
|
2807
|
-
const { body, headers } = buildWebhookRequest(event, channel);
|
|
2808
|
-
const controller = new AbortController;
|
|
2809
|
-
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
2810
|
-
try {
|
|
2811
|
-
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
2812
|
-
method: "POST",
|
|
2813
|
-
headers,
|
|
2814
|
-
body,
|
|
2815
|
-
signal: controller.signal
|
|
2816
|
-
});
|
|
2817
|
-
const responseBody = truncate(await response.text());
|
|
2818
|
-
return {
|
|
2819
|
-
attempt: 1,
|
|
2820
|
-
status: response.ok ? "success" : "failed",
|
|
2821
|
-
startedAt,
|
|
2822
|
-
completedAt: now(),
|
|
2823
|
-
responseStatus: response.status,
|
|
2824
|
-
responseBody,
|
|
2825
|
-
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
2826
|
-
};
|
|
2827
|
-
} catch (error) {
|
|
2828
|
-
return {
|
|
2829
|
-
attempt: 1,
|
|
2830
|
-
status: "failed",
|
|
2831
|
-
startedAt,
|
|
2832
|
-
completedAt: now(),
|
|
2833
|
-
error: error instanceof Error ? error.message : String(error)
|
|
2834
|
-
};
|
|
2835
|
-
} finally {
|
|
2836
|
-
clearTimeout(timeout);
|
|
2837
|
-
}
|
|
2838
|
-
}
|
|
2839
|
-
async function dispatchCommand(event, channel) {
|
|
2840
|
-
if (!channel.command)
|
|
2841
|
-
throw new Error(`Channel ${channel.id} has no command config`);
|
|
2842
|
-
const startedAt = now();
|
|
2843
|
-
const eventJson = JSON.stringify(event);
|
|
2844
|
-
const env = {
|
|
2845
|
-
...process.env,
|
|
2846
|
-
...channel.command.env,
|
|
2847
|
-
HASNA_CHANNEL_ID: channel.id,
|
|
2848
|
-
HASNA_EVENT_ID: event.id,
|
|
2849
|
-
HASNA_EVENT_TYPE: event.type,
|
|
2850
|
-
HASNA_EVENT_SOURCE: event.source,
|
|
2851
|
-
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
2852
|
-
HASNA_EVENT_SEVERITY: event.severity,
|
|
2853
|
-
HASNA_EVENT_TIME: event.time,
|
|
2854
|
-
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
2855
|
-
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
2856
|
-
HASNA_EVENT_JSON: eventJson
|
|
2857
|
-
};
|
|
2858
|
-
return new Promise((resolve) => {
|
|
2859
|
-
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
2860
|
-
cwd: channel.command.cwd,
|
|
2861
|
-
env,
|
|
2862
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
2863
|
-
});
|
|
2864
|
-
let stdout = "";
|
|
2865
|
-
let stderr = "";
|
|
2866
|
-
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
2867
|
-
child.stdin.end(eventJson);
|
|
2868
|
-
child.stdout.on("data", (chunk) => {
|
|
2869
|
-
stdout += chunk.toString();
|
|
2870
|
-
});
|
|
2871
|
-
child.stderr.on("data", (chunk) => {
|
|
2872
|
-
stderr += chunk.toString();
|
|
2873
|
-
});
|
|
2874
|
-
child.on("error", (error) => {
|
|
2875
|
-
clearTimeout(timeout);
|
|
2876
|
-
resolve({
|
|
2877
|
-
attempt: 1,
|
|
2878
|
-
status: "failed",
|
|
2879
|
-
startedAt,
|
|
2880
|
-
completedAt: now(),
|
|
2881
|
-
stdout: truncate(stdout),
|
|
2882
|
-
stderr: truncate(stderr),
|
|
2883
|
-
error: error.message
|
|
2884
|
-
});
|
|
2885
|
-
});
|
|
2886
|
-
child.on("close", (code, signal) => {
|
|
2887
|
-
clearTimeout(timeout);
|
|
2888
|
-
const success = code === 0;
|
|
2889
|
-
resolve({
|
|
2890
|
-
attempt: 1,
|
|
2891
|
-
status: success ? "success" : "failed",
|
|
2892
|
-
startedAt,
|
|
2893
|
-
completedAt: now(),
|
|
2894
|
-
stdout: truncate(stdout),
|
|
2895
|
-
stderr: truncate(stderr),
|
|
2896
|
-
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
2897
|
-
});
|
|
2898
|
-
});
|
|
2899
|
-
});
|
|
2900
|
-
}
|
|
2901
|
-
async function dispatchChannel(event, channel, options = {}) {
|
|
2902
|
-
if (channel.transport === "webhook")
|
|
2903
|
-
return dispatchWebhook(event, channel, options);
|
|
2904
|
-
if (channel.transport === "command")
|
|
2905
|
-
return dispatchCommand(event, channel);
|
|
2906
|
-
return {
|
|
2907
|
-
attempt: 1,
|
|
2908
|
-
status: "skipped",
|
|
2909
|
-
startedAt: now(),
|
|
2910
|
-
completedAt: now(),
|
|
2911
|
-
error: `Unsupported transport: ${channel.transport}`
|
|
2912
|
-
};
|
|
2913
|
-
}
|
|
2914
|
-
function createDeliveryResult(event, channel, attempts) {
|
|
2915
|
-
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
2916
|
-
return {
|
|
2917
|
-
id: randomUUID(),
|
|
2918
|
-
eventId: event.id,
|
|
2919
|
-
channelId: channel.id,
|
|
2920
|
-
transport: channel.transport,
|
|
2921
|
-
status,
|
|
2922
|
-
attempts,
|
|
2923
|
-
createdAt: attempts[0]?.startedAt ?? now(),
|
|
2924
|
-
completedAt: attempts.at(-1)?.completedAt ?? now()
|
|
2925
|
-
};
|
|
2926
|
-
}
|
|
2927
|
-
function createEvent(input) {
|
|
2928
|
-
return {
|
|
2929
|
-
id: input.id ?? randomUUID2(),
|
|
2930
|
-
source: input.source,
|
|
2931
|
-
type: input.type,
|
|
2932
|
-
time: normalizeTime(input.time),
|
|
2933
|
-
subject: input.subject,
|
|
2934
|
-
severity: input.severity ?? "info",
|
|
2935
|
-
data: input.data ?? {},
|
|
2936
|
-
message: input.message,
|
|
2937
|
-
dedupeKey: input.dedupeKey,
|
|
2938
|
-
schemaVersion: input.schemaVersion ?? "1.0",
|
|
2939
|
-
metadata: input.metadata ?? {}
|
|
2940
|
-
};
|
|
2941
|
-
}
|
|
2942
|
-
|
|
2943
|
-
class EventsClient {
|
|
2944
|
-
store;
|
|
2945
|
-
redactors;
|
|
2946
|
-
transportOptions;
|
|
2947
|
-
constructor(options = {}) {
|
|
2948
|
-
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
2949
|
-
this.redactors = options.redactors ?? [];
|
|
2950
|
-
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
2951
|
-
}
|
|
2952
|
-
async addChannel(input) {
|
|
2953
|
-
const timestamp = new Date().toISOString();
|
|
2954
|
-
return this.store.addChannel({
|
|
2955
|
-
...input,
|
|
2956
|
-
createdAt: input.createdAt ?? timestamp,
|
|
2957
|
-
updatedAt: input.updatedAt ?? timestamp
|
|
2958
|
-
});
|
|
2959
|
-
}
|
|
2960
|
-
async listChannels() {
|
|
2961
|
-
return this.store.listChannels();
|
|
2962
|
-
}
|
|
2963
|
-
async removeChannel(id) {
|
|
2964
|
-
return this.store.removeChannel(id);
|
|
2965
|
-
}
|
|
2966
|
-
async emit(input, options = {}) {
|
|
2967
|
-
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
2968
|
-
if (options.dedupe !== false) {
|
|
2969
|
-
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
2970
|
-
if (existing) {
|
|
2971
|
-
return { event: existing, deliveries: [], deduped: true };
|
|
2972
|
-
}
|
|
2973
|
-
}
|
|
2974
|
-
await this.store.appendEvent(event);
|
|
2975
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
2976
|
-
return { event, deliveries, deduped: false };
|
|
2977
|
-
}
|
|
2978
|
-
async listEvents() {
|
|
2979
|
-
return this.store.listEvents();
|
|
2980
|
-
}
|
|
2981
|
-
async listDeliveries() {
|
|
2982
|
-
return this.store.listDeliveries();
|
|
2983
|
-
}
|
|
2984
|
-
async deliver(event) {
|
|
2985
|
-
const channels = await this.store.listChannels();
|
|
2986
|
-
const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
|
|
2987
|
-
const deliveries = [];
|
|
2988
|
-
for (const channel of selected) {
|
|
2989
|
-
const eventForChannel = await this.applyRedaction(event, channel);
|
|
2990
|
-
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
2991
|
-
await this.store.appendDelivery(result);
|
|
2992
|
-
deliveries.push(result);
|
|
2993
|
-
}
|
|
2994
|
-
return deliveries;
|
|
2995
|
-
}
|
|
2996
|
-
async testChannel(id, input = {}) {
|
|
2997
|
-
const channel = await this.store.getChannel(id);
|
|
2998
|
-
if (!channel)
|
|
2999
|
-
throw new Error(`Channel not found: ${id}`);
|
|
3000
|
-
const event = createEvent({
|
|
3001
|
-
source: input.source ?? "hasna.events",
|
|
3002
|
-
type: input.type ?? "events.test",
|
|
3003
|
-
subject: input.subject ?? id,
|
|
3004
|
-
severity: input.severity ?? "info",
|
|
3005
|
-
data: input.data ?? { test: true },
|
|
3006
|
-
message: input.message ?? "Hasna events test delivery",
|
|
3007
|
-
dedupeKey: input.dedupeKey,
|
|
3008
|
-
schemaVersion: input.schemaVersion,
|
|
3009
|
-
metadata: input.metadata,
|
|
3010
|
-
time: input.time,
|
|
3011
|
-
id: input.id
|
|
3012
|
-
});
|
|
3013
|
-
const eventForChannel = await this.applyRedaction(event, channel);
|
|
3014
|
-
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
3015
|
-
await this.store.appendDelivery(result);
|
|
3016
|
-
return result;
|
|
3017
|
-
}
|
|
3018
|
-
async replay(options = {}) {
|
|
3019
|
-
const events = (await this.store.listEvents()).filter((event) => {
|
|
3020
|
-
if (options.eventId && event.id !== options.eventId)
|
|
3021
|
-
return false;
|
|
3022
|
-
if (options.source && event.source !== options.source)
|
|
3023
|
-
return false;
|
|
3024
|
-
if (options.type && event.type !== options.type)
|
|
3025
|
-
return false;
|
|
3026
|
-
return true;
|
|
3027
|
-
});
|
|
3028
|
-
if (options.dryRun)
|
|
3029
|
-
return { events, deliveries: [] };
|
|
3030
|
-
const deliveries = [];
|
|
3031
|
-
for (const event of events) {
|
|
3032
|
-
deliveries.push(...await this.deliver(event));
|
|
3033
|
-
}
|
|
3034
|
-
return { events, deliveries };
|
|
3035
|
-
}
|
|
3036
|
-
async applyRedaction(event, channel) {
|
|
3037
|
-
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
3038
|
-
for (const redactor of this.redactors) {
|
|
3039
|
-
next = await redactor(next, channel);
|
|
3040
|
-
}
|
|
3041
|
-
return next;
|
|
3042
|
-
}
|
|
3043
|
-
async deliverWithRetry(event, channel) {
|
|
3044
|
-
const policy = normalizeRetryPolicy(channel.retry);
|
|
3045
|
-
const attempts = [];
|
|
3046
|
-
for (let index = 0;index < policy.maxAttempts; index += 1) {
|
|
3047
|
-
const attempt = await dispatchChannel(event, channel, this.transportOptions);
|
|
3048
|
-
attempt.attempt = index + 1;
|
|
3049
|
-
if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
|
|
3050
|
-
attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
|
|
3051
|
-
}
|
|
3052
|
-
attempts.push(attempt);
|
|
3053
|
-
if (attempt.status !== "failed")
|
|
3054
|
-
break;
|
|
3055
|
-
if (attempt.nextBackoffMs)
|
|
3056
|
-
await Bun.sleep(attempt.nextBackoffMs);
|
|
3057
|
-
}
|
|
3058
|
-
return createDeliveryResult(event, channel, attempts);
|
|
3059
|
-
}
|
|
3060
|
-
}
|
|
3061
|
-
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
3062
|
-
if (paths.length === 0)
|
|
3063
|
-
return event;
|
|
3064
|
-
const copy = structuredClone(event);
|
|
3065
|
-
for (const path of paths) {
|
|
3066
|
-
setPath(copy, path, replacement);
|
|
3067
|
-
}
|
|
3068
|
-
return copy;
|
|
3069
|
-
}
|
|
3070
|
-
function sanitizeChannelForOutput(channel) {
|
|
3071
|
-
const copy = structuredClone(channel);
|
|
3072
|
-
if (copy.webhook?.secret)
|
|
3073
|
-
copy.webhook.secret = "[REDACTED]";
|
|
3074
|
-
if (copy.command?.env) {
|
|
3075
|
-
copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey(key) ? "[REDACTED]" : value]));
|
|
3076
|
-
}
|
|
3077
|
-
return copy;
|
|
3078
|
-
}
|
|
3079
|
-
function sanitizeChannelsForOutput(channels) {
|
|
3080
|
-
return channels.map(sanitizeChannelForOutput);
|
|
3081
|
-
}
|
|
3082
|
-
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
3083
|
-
return redactValue(event, replacement);
|
|
3084
|
-
}
|
|
3085
|
-
function shouldRedactKey(key) {
|
|
3086
|
-
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
3087
|
-
}
|
|
3088
|
-
function redactValue(value, replacement) {
|
|
3089
|
-
if (Array.isArray(value))
|
|
3090
|
-
return value.map((item) => redactValue(item, replacement));
|
|
3091
|
-
if (!value || typeof value !== "object")
|
|
3092
|
-
return value;
|
|
3093
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
3094
|
-
key,
|
|
3095
|
-
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
3096
|
-
]));
|
|
3097
|
-
}
|
|
3098
|
-
function setPath(input, path, replacement) {
|
|
3099
|
-
const parts = path.split(".");
|
|
3100
|
-
let cursor = input;
|
|
3101
|
-
for (const part of parts.slice(0, -1)) {
|
|
3102
|
-
const next = cursor[part];
|
|
3103
|
-
if (!next || typeof next !== "object")
|
|
3104
|
-
return;
|
|
3105
|
-
cursor = next;
|
|
3106
|
-
}
|
|
3107
|
-
const last = parts.at(-1);
|
|
3108
|
-
if (last && last in cursor)
|
|
3109
|
-
cursor[last] = replacement;
|
|
3110
|
-
}
|
|
3111
|
-
function normalizeTime(value) {
|
|
3112
|
-
if (!value)
|
|
3113
|
-
return new Date().toISOString();
|
|
3114
|
-
return value instanceof Date ? value.toISOString() : value;
|
|
3115
|
-
}
|
|
3116
|
-
function normalizeRetryPolicy(policy) {
|
|
3117
|
-
return {
|
|
3118
|
-
maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
|
|
3119
|
-
backoffMs: Math.max(0, policy?.backoffMs ?? 250),
|
|
3120
|
-
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
3121
|
-
};
|
|
3122
|
-
}
|
|
3123
|
-
function parseJsonObject(value, fallback) {
|
|
3124
|
-
if (!value)
|
|
3125
|
-
return fallback;
|
|
3126
|
-
const parsed = JSON.parse(value);
|
|
3127
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3128
|
-
throw new Error("Expected a JSON object");
|
|
3129
|
-
}
|
|
3130
|
-
return parsed;
|
|
3131
|
-
}
|
|
3132
|
-
function parseHeaders(values) {
|
|
3133
|
-
if (!values?.length)
|
|
3134
|
-
return;
|
|
3135
|
-
const headers = {};
|
|
3136
|
-
for (const value of values) {
|
|
3137
|
-
const separator = value.indexOf("=");
|
|
3138
|
-
if (separator === -1)
|
|
3139
|
-
throw new Error(`Invalid header, expected name=value: ${value}`);
|
|
3140
|
-
headers[value.slice(0, separator)] = value.slice(separator + 1);
|
|
3141
|
-
}
|
|
3142
|
-
return headers;
|
|
3143
|
-
}
|
|
3144
|
-
function parseFilter(options) {
|
|
3145
|
-
const filter2 = {};
|
|
3146
|
-
if (options.source)
|
|
3147
|
-
filter2.source = options.source;
|
|
3148
|
-
if (options.type)
|
|
3149
|
-
filter2.type = options.type;
|
|
3150
|
-
if (options.subject)
|
|
3151
|
-
filter2.subject = options.subject;
|
|
3152
|
-
if (options.severity)
|
|
3153
|
-
filter2.severity = options.severity;
|
|
3154
|
-
return Object.keys(filter2).length > 0 ? [filter2] : undefined;
|
|
3155
|
-
}
|
|
3156
|
-
function createClient(options) {
|
|
3157
|
-
if (options.createClient)
|
|
3158
|
-
return options.createClient();
|
|
3159
|
-
return new EventsClient({ store: new JsonEventsStore(options.dataDir) });
|
|
3160
|
-
}
|
|
3161
|
-
function print(value, json, text) {
|
|
3162
|
-
if (json)
|
|
3163
|
-
console.log(JSON.stringify(value, null, 2));
|
|
3164
|
-
else
|
|
3165
|
-
console.log(text);
|
|
3166
|
-
}
|
|
3167
|
-
function registerWebhookCommands(program2, options) {
|
|
3168
|
-
const webhooks = program2.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
|
|
3169
|
-
webhooks.command("add").description("Add or replace a webhook or command subscription").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Subscription/channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions) => {
|
|
3170
|
-
const timestamp = new Date().toISOString();
|
|
3171
|
-
const channel = {
|
|
3172
|
-
id: actionOptions.id,
|
|
3173
|
-
name: actionOptions.name,
|
|
3174
|
-
enabled: !actionOptions.disabled,
|
|
3175
|
-
transport: actionOptions.transport,
|
|
3176
|
-
filters: parseFilter(actionOptions),
|
|
3177
|
-
retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
|
|
3178
|
-
redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
|
|
3179
|
-
createdAt: timestamp,
|
|
3180
|
-
updatedAt: timestamp
|
|
3181
|
-
};
|
|
3182
|
-
if (actionOptions.transport === "webhook") {
|
|
3183
|
-
channel.webhook = { url: target, secret: actionOptions.secret, headers: parseHeaders(actionOptions.header), timeoutMs: actionOptions.timeoutMs };
|
|
3184
|
-
} else if (actionOptions.transport === "command") {
|
|
3185
|
-
channel.command = { command: target, args: actionOptions.arg ?? [], timeoutMs: actionOptions.timeoutMs };
|
|
3186
|
-
} else {
|
|
3187
|
-
throw new Error(`Transport ${actionOptions.transport} is reserved for future use and cannot be added yet`);
|
|
3188
|
-
}
|
|
3189
|
-
const saved = await createClient(options).addChannel(channel);
|
|
3190
|
-
print(sanitizeChannelForOutput(saved), Boolean(actionOptions.json), `Added ${saved.transport} channel ${saved.id}`);
|
|
3191
|
-
});
|
|
3192
|
-
webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions) => {
|
|
3193
|
-
const channels = await createClient(options).listChannels();
|
|
3194
|
-
if (actionOptions.json) {
|
|
3195
|
-
console.log(JSON.stringify(sanitizeChannelsForOutput(channels), null, 2));
|
|
3196
|
-
return;
|
|
3197
|
-
}
|
|
3198
|
-
if (!channels.length) {
|
|
3199
|
-
console.log("No channels configured.");
|
|
3200
|
-
return;
|
|
3201
|
-
}
|
|
3202
|
-
for (const channel of channels) {
|
|
3203
|
-
console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
|
|
3204
|
-
}
|
|
3205
|
-
});
|
|
3206
|
-
webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions) => {
|
|
3207
|
-
const removed = await createClient(options).removeChannel(id);
|
|
3208
|
-
print({ removed }, Boolean(actionOptions.json), removed ? `Removed ${id}` : `Channel not found: ${id}`);
|
|
3209
|
-
});
|
|
3210
|
-
webhooks.command("test").description("Send a test event to one subscription").argument("<id>", "Subscription/channel identifier").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions) => {
|
|
3211
|
-
const result = await createClient(options).testChannel(id, {
|
|
3212
|
-
source: options.source,
|
|
3213
|
-
type: actionOptions.type,
|
|
3214
|
-
subject: actionOptions.subject ?? id,
|
|
3215
|
-
message: actionOptions.message,
|
|
3216
|
-
data: parseJsonObject(actionOptions.data, { test: true })
|
|
3217
|
-
});
|
|
3218
|
-
print(result, Boolean(actionOptions.json), `${result.status}: ${result.channelId}`);
|
|
3219
|
-
});
|
|
3220
|
-
return webhooks;
|
|
3221
|
-
}
|
|
3222
|
-
function registerEventCommands(program2, options) {
|
|
3223
|
-
const events = program2.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
|
|
3224
|
-
events.command("emit").description("Emit an event from this app").argument("<type>", "Event type").option("--source <source>", "Event source override").option("--subject <subject>", "Event subject").option("--severity <severity>", "Event severity", "info").option("--message <message>", "Event message").option("--dedupe-key <key>", "Dedupe key").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--no-deliver", "Record without delivering").option("--no-dedupe", "Allow duplicate id/dedupeKey events").option("-j, --json", "Print JSON output", false).action(async (type, actionOptions) => {
|
|
3225
|
-
const result = await createClient(options).emit({
|
|
3226
|
-
source: actionOptions.source ?? options.source,
|
|
3227
|
-
type,
|
|
3228
|
-
subject: actionOptions.subject,
|
|
3229
|
-
severity: actionOptions.severity,
|
|
3230
|
-
message: actionOptions.message,
|
|
3231
|
-
dedupeKey: actionOptions.dedupeKey,
|
|
3232
|
-
data: parseJsonObject(actionOptions.data, {}),
|
|
3233
|
-
metadata: parseJsonObject(actionOptions.metadata, {})
|
|
3234
|
-
}, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
|
|
3235
|
-
print(result, Boolean(actionOptions.json), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
|
|
3236
|
-
});
|
|
3237
|
-
events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", "Limit results", parseNumber).option("-j, --json", "Print JSON output", false).action(async (actionOptions) => {
|
|
3238
|
-
let rows = await createClient(options).listEvents();
|
|
3239
|
-
if (actionOptions.source)
|
|
3240
|
-
rows = rows.filter((event) => event.source === actionOptions.source);
|
|
3241
|
-
if (actionOptions.type)
|
|
3242
|
-
rows = rows.filter((event) => event.type === actionOptions.type);
|
|
3243
|
-
if (actionOptions.limit)
|
|
3244
|
-
rows = rows.slice(-actionOptions.limit);
|
|
3245
|
-
if (actionOptions.json) {
|
|
3246
|
-
console.log(JSON.stringify(rows, null, 2));
|
|
3247
|
-
return;
|
|
3248
|
-
}
|
|
3249
|
-
if (!rows.length) {
|
|
3250
|
-
console.log("No events recorded.");
|
|
3251
|
-
return;
|
|
3252
|
-
}
|
|
3253
|
-
for (const event of rows)
|
|
3254
|
-
console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
|
|
3255
|
-
});
|
|
3256
|
-
events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions) => {
|
|
3257
|
-
const result = await createClient(options).replay({
|
|
3258
|
-
eventId: actionOptions.id,
|
|
3259
|
-
source: actionOptions.source,
|
|
3260
|
-
type: actionOptions.type,
|
|
3261
|
-
dryRun: actionOptions.dryRun
|
|
3262
|
-
});
|
|
3263
|
-
print(result, Boolean(actionOptions.json), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
|
|
3264
|
-
});
|
|
3265
|
-
return events;
|
|
3266
|
-
}
|
|
3267
|
-
function registerEventsCommands(program2, options) {
|
|
3268
|
-
registerWebhookCommands(program2, options);
|
|
3269
|
-
registerEventCommands(program2, options);
|
|
3270
|
-
}
|
|
3271
|
-
function parseNumber(value) {
|
|
3272
|
-
const parsed = Number(value);
|
|
3273
|
-
if (!Number.isFinite(parsed))
|
|
3274
|
-
throw new Error(`Expected a number, got ${value}`);
|
|
3275
|
-
return parsed;
|
|
3276
|
-
}
|
|
3277
|
-
function collectValues(value, previous) {
|
|
3278
|
-
previous.push(value);
|
|
3279
|
-
return previous;
|
|
3280
|
-
}
|
|
3281
|
-
|
|
3282
2604
|
// src/cli/index.ts
|
|
2605
|
+
import { registerEventCommands, registerWebhookCommands } from "@hasna/events/commander";
|
|
3283
2606
|
import { execFileSync } from "child_process";
|
|
3284
2607
|
|
|
3285
2608
|
// node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
@@ -3772,14 +3095,14 @@ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
|
|
|
3772
3095
|
var source_default = chalk;
|
|
3773
3096
|
|
|
3774
3097
|
// src/version.ts
|
|
3775
|
-
import { existsSync
|
|
3776
|
-
import { dirname, join
|
|
3098
|
+
import { existsSync, readFileSync } from "fs";
|
|
3099
|
+
import { dirname, join } from "path";
|
|
3777
3100
|
import { fileURLToPath } from "url";
|
|
3778
3101
|
function getPackageVersion() {
|
|
3779
3102
|
try {
|
|
3780
3103
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
3781
|
-
const candidates = [
|
|
3782
|
-
const pkgPath = candidates.find((candidate) =>
|
|
3104
|
+
const candidates = [join(here, "..", "package.json"), join(here, "..", "..", "package.json")];
|
|
3105
|
+
const pkgPath = candidates.find((candidate) => existsSync(candidate));
|
|
3783
3106
|
if (!pkgPath) {
|
|
3784
3107
|
return "0.0.0";
|
|
3785
3108
|
}
|
|
@@ -3790,8 +3113,8 @@ function getPackageVersion() {
|
|
|
3790
3113
|
}
|
|
3791
3114
|
|
|
3792
3115
|
// src/manifests.ts
|
|
3793
|
-
import { existsSync as
|
|
3794
|
-
import { arch, homedir
|
|
3116
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
3117
|
+
import { arch, homedir, hostname, platform, userInfo } from "os";
|
|
3795
3118
|
import { dirname as dirname3 } from "path";
|
|
3796
3119
|
|
|
3797
3120
|
// node_modules/zod/v3/external.js
|
|
@@ -7805,7 +7128,7 @@ var fleetSchema = exports_external.object({
|
|
|
7805
7128
|
machines: exports_external.array(machineSchema)
|
|
7806
7129
|
});
|
|
7807
7130
|
function detectWorkspacePath() {
|
|
7808
|
-
const home =
|
|
7131
|
+
const home = homedir();
|
|
7809
7132
|
if (platform() === "darwin") {
|
|
7810
7133
|
return `${home}/Workspace`;
|
|
7811
7134
|
}
|
|
@@ -7829,7 +7152,7 @@ function getDefaultManifest() {
|
|
|
7829
7152
|
};
|
|
7830
7153
|
}
|
|
7831
7154
|
function readManifest(path = getManifestPath()) {
|
|
7832
|
-
if (!
|
|
7155
|
+
if (!existsSync3(path)) {
|
|
7833
7156
|
return getDefaultManifest();
|
|
7834
7157
|
}
|
|
7835
7158
|
const raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
@@ -7910,7 +7233,7 @@ function manifestValidate() {
|
|
|
7910
7233
|
}
|
|
7911
7234
|
|
|
7912
7235
|
// src/commands/setup.ts
|
|
7913
|
-
import { homedir as
|
|
7236
|
+
import { homedir as homedir2 } from "os";
|
|
7914
7237
|
init_db();
|
|
7915
7238
|
|
|
7916
7239
|
// src/remote.ts
|
|
@@ -7920,7 +7243,7 @@ import { hostname as hostname4 } from "os";
|
|
|
7920
7243
|
|
|
7921
7244
|
// src/topology.ts
|
|
7922
7245
|
init_db();
|
|
7923
|
-
import { existsSync as
|
|
7246
|
+
import { existsSync as existsSync4 } from "fs";
|
|
7924
7247
|
import { arch as arch2, hostname as hostname3, platform as platform2, userInfo as userInfo2 } from "os";
|
|
7925
7248
|
import { spawnSync } from "child_process";
|
|
7926
7249
|
init_paths();
|
|
@@ -8111,7 +7434,7 @@ function buildEntry(input) {
|
|
|
8111
7434
|
};
|
|
8112
7435
|
}
|
|
8113
7436
|
function discoverMachineTopology(options = {}) {
|
|
8114
|
-
const
|
|
7437
|
+
const now = options.now ?? new Date;
|
|
8115
7438
|
const runner = options.runner ?? defaultRunner;
|
|
8116
7439
|
const warnings = [];
|
|
8117
7440
|
const manifest = readManifest();
|
|
@@ -8143,11 +7466,11 @@ function discoverMachineTopology(options = {}) {
|
|
|
8143
7466
|
version: getPackageVersion()
|
|
8144
7467
|
},
|
|
8145
7468
|
capabilities: getMachinesConsumerCapabilities(),
|
|
8146
|
-
generated_at:
|
|
7469
|
+
generated_at: now.toISOString(),
|
|
8147
7470
|
local_machine_id: localMachineId,
|
|
8148
7471
|
local_hostname: hostname3(),
|
|
8149
7472
|
current_platform: normalizePlatform2(),
|
|
8150
|
-
manifest_path_known:
|
|
7473
|
+
manifest_path_known: existsSync4(getManifestPath()),
|
|
8151
7474
|
machines,
|
|
8152
7475
|
warnings
|
|
8153
7476
|
};
|
|
@@ -8266,11 +7589,11 @@ function cacheability(input) {
|
|
|
8266
7589
|
};
|
|
8267
7590
|
}
|
|
8268
7591
|
function resolveMachineRoute(machineId, options = {}) {
|
|
8269
|
-
const
|
|
7592
|
+
const now = options.now ?? new Date;
|
|
8270
7593
|
const topology = options.topology ?? discoverMachineTopology(options);
|
|
8271
7594
|
const warnings = [...topology.warnings];
|
|
8272
7595
|
const { machine, matchedBy } = findRouteMachine(topology, machineId);
|
|
8273
|
-
const generatedAt =
|
|
7596
|
+
const generatedAt = now.toISOString();
|
|
8274
7597
|
if (!machine) {
|
|
8275
7598
|
warnings.push(`machine_not_found:${machineId}`);
|
|
8276
7599
|
return {
|
|
@@ -8296,8 +7619,8 @@ function resolveMachineRoute(machineId, options = {}) {
|
|
|
8296
7619
|
},
|
|
8297
7620
|
cacheability: cacheability({
|
|
8298
7621
|
ok: false,
|
|
8299
|
-
observedAt:
|
|
8300
|
-
now
|
|
7622
|
+
observedAt: now,
|
|
7623
|
+
now,
|
|
8301
7624
|
ttlMs: options.resolverTtlMs,
|
|
8302
7625
|
authority: "unresolved",
|
|
8303
7626
|
confidence: "none",
|
|
@@ -8334,8 +7657,8 @@ function resolveMachineRoute(machineId, options = {}) {
|
|
|
8334
7657
|
},
|
|
8335
7658
|
cacheability: cacheability({
|
|
8336
7659
|
ok,
|
|
8337
|
-
observedAt:
|
|
8338
|
-
now
|
|
7660
|
+
observedAt: now,
|
|
7661
|
+
now,
|
|
8339
7662
|
ttlMs: options.resolverTtlMs,
|
|
8340
7663
|
authority: routeAuthority({ machine, selectedHint, matchedBy }),
|
|
8341
7664
|
confidence,
|
|
@@ -8422,7 +7745,7 @@ function canCheckPathForMachine(machine, localMachineId) {
|
|
|
8422
7745
|
function checkedPathExists(path, check) {
|
|
8423
7746
|
if (!path || !check)
|
|
8424
7747
|
return null;
|
|
8425
|
-
return
|
|
7748
|
+
return existsSync4(path);
|
|
8426
7749
|
}
|
|
8427
7750
|
function repairHint(input) {
|
|
8428
7751
|
const command = [
|
|
@@ -8637,11 +7960,11 @@ function metadataKeysForDiagnostics(metadata) {
|
|
|
8637
7960
|
return Object.keys(metadata).filter((key) => !/(secret|token|key|password|credential)/i.test(key)).sort();
|
|
8638
7961
|
}
|
|
8639
7962
|
function resolveMachineWorkspace(options) {
|
|
8640
|
-
const
|
|
7963
|
+
const now = options.now ?? new Date;
|
|
8641
7964
|
const topology = options.topology ?? discoverMachineTopology(options);
|
|
8642
7965
|
const warnings = [...topology.warnings];
|
|
8643
7966
|
const { machine, matchedBy } = findRouteMachine(topology, options.machineId);
|
|
8644
|
-
const generatedAt =
|
|
7967
|
+
const generatedAt = now.toISOString();
|
|
8645
7968
|
const repoName = options.repoName ?? options.projectId;
|
|
8646
7969
|
const openFilesRepoName = options.openFilesRepoName ?? "open-files";
|
|
8647
7970
|
if (!machine) {
|
|
@@ -8670,8 +7993,8 @@ function resolveMachineWorkspace(options) {
|
|
|
8670
7993
|
},
|
|
8671
7994
|
cacheability: cacheability({
|
|
8672
7995
|
ok: false,
|
|
8673
|
-
observedAt:
|
|
8674
|
-
now
|
|
7996
|
+
observedAt: now,
|
|
7997
|
+
now,
|
|
8675
7998
|
ttlMs: options.resolverTtlMs,
|
|
8676
7999
|
authority: "unresolved",
|
|
8677
8000
|
confidence: "none",
|
|
@@ -8743,8 +8066,8 @@ function resolveMachineWorkspace(options) {
|
|
|
8743
8066
|
},
|
|
8744
8067
|
cacheability: cacheability({
|
|
8745
8068
|
ok: workspaceOk,
|
|
8746
|
-
observedAt:
|
|
8747
|
-
now
|
|
8069
|
+
observedAt: now,
|
|
8070
|
+
now,
|
|
8748
8071
|
ttlMs: options.resolverTtlMs,
|
|
8749
8072
|
authority: workspaceAuthority(workspacePaths),
|
|
8750
8073
|
confidence: workspaceOk ? "medium" : "none",
|
|
@@ -8913,7 +8236,7 @@ function buildSetupPlan(machineId) {
|
|
|
8913
8236
|
const target = selected || {
|
|
8914
8237
|
id: currentMachineId,
|
|
8915
8238
|
platform: "linux",
|
|
8916
|
-
workspacePath: `${
|
|
8239
|
+
workspacePath: `${homedir2()}/workspace`
|
|
8917
8240
|
};
|
|
8918
8241
|
const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
|
|
8919
8242
|
return {
|
|
@@ -8958,8 +8281,8 @@ function runSetup(machineId, options = {}, runner = runMachineCommand) {
|
|
|
8958
8281
|
}
|
|
8959
8282
|
|
|
8960
8283
|
// src/commands/backup.ts
|
|
8961
|
-
import { homedir as
|
|
8962
|
-
import { join as
|
|
8284
|
+
import { homedir as homedir3, hostname as hostname5 } from "os";
|
|
8285
|
+
import { join as join3 } from "path";
|
|
8963
8286
|
var MACHINES_BACKUP_BUCKET_ENV = "HASNA_MACHINES_S3_BUCKET";
|
|
8964
8287
|
var MACHINES_BACKUP_BUCKET_FALLBACK_ENV = "MACHINES_S3_BUCKET";
|
|
8965
8288
|
var MACHINES_BACKUP_PREFIX_ENV = "HASNA_MACHINES_S3_PREFIX";
|
|
@@ -9007,16 +8330,16 @@ function resolveBackupTarget(options = {}) {
|
|
|
9007
8330
|
};
|
|
9008
8331
|
}
|
|
9009
8332
|
function defaultBackupSources() {
|
|
9010
|
-
const home =
|
|
8333
|
+
const home = homedir3();
|
|
9011
8334
|
return [
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
8335
|
+
join3(home, ".hasna"),
|
|
8336
|
+
join3(home, ".ssh"),
|
|
8337
|
+
join3(home, ".secrets")
|
|
9015
8338
|
];
|
|
9016
8339
|
}
|
|
9017
8340
|
function buildBackupPlan(bucket, prefix) {
|
|
9018
8341
|
const target = resolveBackupTarget({ bucket, prefix });
|
|
9019
|
-
const archivePath =
|
|
8342
|
+
const archivePath = join3(homedir3(), ".hasna", "machines", "backup.tgz");
|
|
9020
8343
|
const sources = defaultBackupSources();
|
|
9021
8344
|
const steps = [
|
|
9022
8345
|
{
|
|
@@ -9067,21 +8390,21 @@ function runBackup(bucket, prefix, options = {}) {
|
|
|
9067
8390
|
}
|
|
9068
8391
|
|
|
9069
8392
|
// src/commands/cert.ts
|
|
9070
|
-
import { homedir as
|
|
9071
|
-
import { join as
|
|
8393
|
+
import { homedir as homedir4, platform as platform3 } from "os";
|
|
8394
|
+
import { join as join4 } from "path";
|
|
9072
8395
|
function quote3(value) {
|
|
9073
8396
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
9074
8397
|
}
|
|
9075
8398
|
function certDir() {
|
|
9076
|
-
return
|
|
8399
|
+
return join4(homedir4(), ".hasna", "machines", "certs");
|
|
9077
8400
|
}
|
|
9078
8401
|
function buildCertPlan(domains) {
|
|
9079
8402
|
if (domains.length === 0) {
|
|
9080
8403
|
throw new Error("At least one domain is required.");
|
|
9081
8404
|
}
|
|
9082
8405
|
const primary = domains[0];
|
|
9083
|
-
const certPath =
|
|
9084
|
-
const keyPath =
|
|
8406
|
+
const certPath = join4(certDir(), `${primary}.pem`);
|
|
8407
|
+
const keyPath = join4(certDir(), `${primary}-key.pem`);
|
|
9085
8408
|
const steps = [];
|
|
9086
8409
|
if (platform3() === "darwin") {
|
|
9087
8410
|
steps.push({
|
|
@@ -9146,14 +8469,14 @@ function runCertPlan(domains, options = {}) {
|
|
|
9146
8469
|
|
|
9147
8470
|
// src/commands/dns.ts
|
|
9148
8471
|
init_paths();
|
|
9149
|
-
import { existsSync as
|
|
9150
|
-
import { join as
|
|
8472
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
8473
|
+
import { join as join5 } from "path";
|
|
9151
8474
|
function getDnsPath() {
|
|
9152
|
-
return
|
|
8475
|
+
return join5(getDataDir(), "dns.json");
|
|
9153
8476
|
}
|
|
9154
8477
|
function readMappings() {
|
|
9155
8478
|
const path = getDnsPath();
|
|
9156
|
-
if (!
|
|
8479
|
+
if (!existsSync5(path))
|
|
9157
8480
|
return [];
|
|
9158
8481
|
return JSON.parse(readFileSync3(path, "utf8"));
|
|
9159
8482
|
}
|
|
@@ -9182,10 +8505,10 @@ function renderDomainMapping(domain) {
|
|
|
9182
8505
|
hostsEntry: `${entry.targetHost} ${entry.domain}`,
|
|
9183
8506
|
caddySnippet: `${entry.domain} {
|
|
9184
8507
|
reverse_proxy 127.0.0.1:${entry.port}
|
|
9185
|
-
tls ${
|
|
8508
|
+
tls ${join5(getDataDir(), "certs", `${entry.domain}.pem`)} ${join5(getDataDir(), "certs", `${entry.domain}-key.pem`)}
|
|
9186
8509
|
}`,
|
|
9187
|
-
certPath:
|
|
9188
|
-
keyPath:
|
|
8510
|
+
certPath: join5(getDataDir(), "certs", `${entry.domain}.pem`),
|
|
8511
|
+
keyPath: join5(getDataDir(), "certs", `${entry.domain}-key.pem`)
|
|
9189
8512
|
};
|
|
9190
8513
|
}
|
|
9191
8514
|
|
|
@@ -9542,7 +8865,7 @@ function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand
|
|
|
9542
8865
|
}
|
|
9543
8866
|
|
|
9544
8867
|
// src/commands/notifications.ts
|
|
9545
|
-
import { existsSync as
|
|
8868
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
9546
8869
|
init_paths();
|
|
9547
8870
|
var notificationChannelSchema = exports_external.object({
|
|
9548
8871
|
id: exports_external.string(),
|
|
@@ -9625,7 +8948,7 @@ ${message}
|
|
|
9625
8948
|
}
|
|
9626
8949
|
throw new Error("No local email transport available. Install sendmail or mail.");
|
|
9627
8950
|
}
|
|
9628
|
-
async function
|
|
8951
|
+
async function dispatchWebhook(channel, event, message) {
|
|
9629
8952
|
const response = await fetch(channel.target, {
|
|
9630
8953
|
method: "POST",
|
|
9631
8954
|
headers: {
|
|
@@ -9650,7 +8973,7 @@ async function dispatchWebhook2(channel, event, message) {
|
|
|
9650
8973
|
detail: `Webhook accepted with HTTP ${response.status}`
|
|
9651
8974
|
};
|
|
9652
8975
|
}
|
|
9653
|
-
async function
|
|
8976
|
+
async function dispatchCommand(channel, event, message) {
|
|
9654
8977
|
const result = Bun.spawnSync(["bash", "-lc", channel.target], {
|
|
9655
8978
|
stdout: "pipe",
|
|
9656
8979
|
stderr: "pipe",
|
|
@@ -9673,7 +8996,7 @@ async function dispatchCommand2(channel, event, message) {
|
|
|
9673
8996
|
detail: stdout || "Command completed successfully"
|
|
9674
8997
|
};
|
|
9675
8998
|
}
|
|
9676
|
-
async function
|
|
8999
|
+
async function dispatchChannel(channel, event, message) {
|
|
9677
9000
|
if (!channel.enabled) {
|
|
9678
9001
|
return {
|
|
9679
9002
|
channelId: channel.id,
|
|
@@ -9687,9 +9010,9 @@ async function dispatchChannel2(channel, event, message) {
|
|
|
9687
9010
|
return dispatchEmail(channel, event, message);
|
|
9688
9011
|
}
|
|
9689
9012
|
if (channel.type === "webhook") {
|
|
9690
|
-
return
|
|
9013
|
+
return dispatchWebhook(channel, event, message);
|
|
9691
9014
|
}
|
|
9692
|
-
return
|
|
9015
|
+
return dispatchCommand(channel, event, message);
|
|
9693
9016
|
}
|
|
9694
9017
|
function getDefaultNotificationConfig() {
|
|
9695
9018
|
return {
|
|
@@ -9699,7 +9022,7 @@ function getDefaultNotificationConfig() {
|
|
|
9699
9022
|
};
|
|
9700
9023
|
}
|
|
9701
9024
|
function readNotificationConfig(path = getNotificationsPath()) {
|
|
9702
|
-
if (!
|
|
9025
|
+
if (!existsSync6(path)) {
|
|
9703
9026
|
return getDefaultNotificationConfig();
|
|
9704
9027
|
}
|
|
9705
9028
|
return notificationConfigSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
|
|
@@ -9744,7 +9067,7 @@ async function dispatchNotificationEvent(event, message, options = {}) {
|
|
|
9744
9067
|
const deliveries = [];
|
|
9745
9068
|
for (const channel of channels) {
|
|
9746
9069
|
try {
|
|
9747
|
-
deliveries.push(await
|
|
9070
|
+
deliveries.push(await dispatchChannel(channel, event, message));
|
|
9748
9071
|
} catch (error) {
|
|
9749
9072
|
deliveries.push({
|
|
9750
9073
|
channelId: channel.id,
|
|
@@ -9779,7 +9102,7 @@ async function testNotificationChannel(channelId, event = "manual.test", message
|
|
|
9779
9102
|
if (!options.yes) {
|
|
9780
9103
|
throw new Error("Notification test execution requires --yes.");
|
|
9781
9104
|
}
|
|
9782
|
-
const delivery = await
|
|
9105
|
+
const delivery = await dispatchChannel(channel, event, message);
|
|
9783
9106
|
return {
|
|
9784
9107
|
channelId,
|
|
9785
9108
|
mode: "apply",
|
|
@@ -9847,528 +9170,7 @@ function listPorts(machineId) {
|
|
|
9847
9170
|
// src/commands/runtime.ts
|
|
9848
9171
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
9849
9172
|
import { setTimeout as sleep } from "timers/promises";
|
|
9850
|
-
|
|
9851
|
-
// node_modules/@hasna/events/dist/index.js
|
|
9852
|
-
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
|
|
9853
|
-
import { existsSync as existsSync8 } from "fs";
|
|
9854
|
-
import { homedir as homedir6 } from "os";
|
|
9855
|
-
import { join as join7 } from "path";
|
|
9856
|
-
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
9857
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
9858
|
-
import { spawn as spawn2 } from "child_process";
|
|
9859
|
-
import { randomUUID as randomUUID22 } from "crypto";
|
|
9860
|
-
function getPathValue2(input, path) {
|
|
9861
|
-
return path.split(".").reduce((value, part) => {
|
|
9862
|
-
if (value && typeof value === "object" && part in value) {
|
|
9863
|
-
return value[part];
|
|
9864
|
-
}
|
|
9865
|
-
return;
|
|
9866
|
-
}, input);
|
|
9867
|
-
}
|
|
9868
|
-
function wildcardToRegExp2(pattern) {
|
|
9869
|
-
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
|
|
9870
|
-
return new RegExp(`^${escaped}$`);
|
|
9871
|
-
}
|
|
9872
|
-
function matchString2(value, matcher) {
|
|
9873
|
-
if (matcher === undefined)
|
|
9874
|
-
return true;
|
|
9875
|
-
if (value === undefined)
|
|
9876
|
-
return false;
|
|
9877
|
-
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
9878
|
-
return matchers.some((item) => wildcardToRegExp2(item).test(value));
|
|
9879
|
-
}
|
|
9880
|
-
function matchRecord2(input, matcher) {
|
|
9881
|
-
if (!matcher)
|
|
9882
|
-
return true;
|
|
9883
|
-
return Object.entries(matcher).every(([path, expected]) => {
|
|
9884
|
-
const actual = getPathValue2(input, path);
|
|
9885
|
-
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
9886
|
-
return matchString2(actual === undefined ? undefined : String(actual), expected);
|
|
9887
|
-
}
|
|
9888
|
-
return actual === expected;
|
|
9889
|
-
});
|
|
9890
|
-
}
|
|
9891
|
-
function eventMatchesFilter2(event, filter) {
|
|
9892
|
-
return matchString2(event.source, filter.source) && matchString2(event.type, filter.type) && matchString2(event.subject, filter.subject) && matchString2(event.severity, filter.severity) && matchRecord2(event.data, filter.data) && matchRecord2(event.metadata, filter.metadata);
|
|
9893
|
-
}
|
|
9894
|
-
function channelMatchesEvent2(channel, event) {
|
|
9895
|
-
if (!channel.enabled)
|
|
9896
|
-
return false;
|
|
9897
|
-
if (!channel.filters || channel.filters.length === 0)
|
|
9898
|
-
return true;
|
|
9899
|
-
return channel.filters.some((filter) => eventMatchesFilter2(event, filter));
|
|
9900
|
-
}
|
|
9901
|
-
var HASNA_EVENTS_DIR_ENV2 = "HASNA_EVENTS_DIR";
|
|
9902
|
-
var HASNA_EVENTS_HOME_ENV2 = "HASNA_EVENTS_HOME";
|
|
9903
|
-
function getEventsDataDir2(override) {
|
|
9904
|
-
return override || process.env[HASNA_EVENTS_DIR_ENV2] || process.env[HASNA_EVENTS_HOME_ENV2] || join7(homedir6(), ".hasna", "events");
|
|
9905
|
-
}
|
|
9906
|
-
|
|
9907
|
-
class JsonEventsStore2 {
|
|
9908
|
-
dataDir;
|
|
9909
|
-
channelsPath;
|
|
9910
|
-
eventsPath;
|
|
9911
|
-
deliveriesPath;
|
|
9912
|
-
constructor(dataDir = getEventsDataDir2()) {
|
|
9913
|
-
this.dataDir = dataDir;
|
|
9914
|
-
this.channelsPath = join7(dataDir, "channels.json");
|
|
9915
|
-
this.eventsPath = join7(dataDir, "events.json");
|
|
9916
|
-
this.deliveriesPath = join7(dataDir, "deliveries.json");
|
|
9917
|
-
}
|
|
9918
|
-
async init() {
|
|
9919
|
-
await mkdir2(this.dataDir, { recursive: true, mode: 448 });
|
|
9920
|
-
await chmod2(this.dataDir, 448).catch(() => {
|
|
9921
|
-
return;
|
|
9922
|
-
});
|
|
9923
|
-
await this.ensureArrayFile(this.channelsPath);
|
|
9924
|
-
await this.ensureArrayFile(this.eventsPath);
|
|
9925
|
-
await this.ensureArrayFile(this.deliveriesPath);
|
|
9926
|
-
}
|
|
9927
|
-
async addChannel(channel) {
|
|
9928
|
-
await this.init();
|
|
9929
|
-
const channels = await this.readJson(this.channelsPath, []);
|
|
9930
|
-
const index = channels.findIndex((item) => item.id === channel.id);
|
|
9931
|
-
if (index >= 0) {
|
|
9932
|
-
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
9933
|
-
} else {
|
|
9934
|
-
channels.push(channel);
|
|
9935
|
-
}
|
|
9936
|
-
await this.writeJson(this.channelsPath, channels);
|
|
9937
|
-
return index >= 0 ? channels[index] : channel;
|
|
9938
|
-
}
|
|
9939
|
-
async listChannels() {
|
|
9940
|
-
await this.init();
|
|
9941
|
-
return this.readJson(this.channelsPath, []);
|
|
9942
|
-
}
|
|
9943
|
-
async getChannel(id) {
|
|
9944
|
-
const channels = await this.listChannels();
|
|
9945
|
-
return channels.find((channel) => channel.id === id);
|
|
9946
|
-
}
|
|
9947
|
-
async removeChannel(id) {
|
|
9948
|
-
await this.init();
|
|
9949
|
-
const channels = await this.readJson(this.channelsPath, []);
|
|
9950
|
-
const next = channels.filter((channel) => channel.id !== id);
|
|
9951
|
-
await this.writeJson(this.channelsPath, next);
|
|
9952
|
-
return next.length !== channels.length;
|
|
9953
|
-
}
|
|
9954
|
-
async appendEvent(event) {
|
|
9955
|
-
await this.init();
|
|
9956
|
-
const events = await this.readJson(this.eventsPath, []);
|
|
9957
|
-
events.push(event);
|
|
9958
|
-
await this.writeJson(this.eventsPath, events);
|
|
9959
|
-
return event;
|
|
9960
|
-
}
|
|
9961
|
-
async listEvents() {
|
|
9962
|
-
await this.init();
|
|
9963
|
-
return this.readJson(this.eventsPath, []);
|
|
9964
|
-
}
|
|
9965
|
-
async findEventByIdentity(identity) {
|
|
9966
|
-
const events = await this.listEvents();
|
|
9967
|
-
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
9968
|
-
}
|
|
9969
|
-
async appendDelivery(result) {
|
|
9970
|
-
await this.init();
|
|
9971
|
-
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
9972
|
-
deliveries.push(result);
|
|
9973
|
-
await this.writeJson(this.deliveriesPath, deliveries);
|
|
9974
|
-
return result;
|
|
9975
|
-
}
|
|
9976
|
-
async listDeliveries() {
|
|
9977
|
-
await this.init();
|
|
9978
|
-
return this.readJson(this.deliveriesPath, []);
|
|
9979
|
-
}
|
|
9980
|
-
async exportData() {
|
|
9981
|
-
return {
|
|
9982
|
-
channels: await this.listChannels(),
|
|
9983
|
-
events: await this.listEvents(),
|
|
9984
|
-
deliveries: await this.listDeliveries()
|
|
9985
|
-
};
|
|
9986
|
-
}
|
|
9987
|
-
async ensureArrayFile(path) {
|
|
9988
|
-
if (!existsSync8(path)) {
|
|
9989
|
-
await writeFile2(path, `[]
|
|
9990
|
-
`, { encoding: "utf-8", mode: 384 });
|
|
9991
|
-
}
|
|
9992
|
-
await chmod2(path, 384).catch(() => {
|
|
9993
|
-
return;
|
|
9994
|
-
});
|
|
9995
|
-
}
|
|
9996
|
-
async readJson(path, fallback) {
|
|
9997
|
-
try {
|
|
9998
|
-
const raw = await readFile2(path, "utf-8");
|
|
9999
|
-
if (!raw.trim())
|
|
10000
|
-
return fallback;
|
|
10001
|
-
return JSON.parse(raw);
|
|
10002
|
-
} catch (error) {
|
|
10003
|
-
if (error.code === "ENOENT")
|
|
10004
|
-
return fallback;
|
|
10005
|
-
throw error;
|
|
10006
|
-
}
|
|
10007
|
-
}
|
|
10008
|
-
async writeJson(path, value) {
|
|
10009
|
-
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
10010
|
-
await writeFile2(tempPath, `${JSON.stringify(value, null, 2)}
|
|
10011
|
-
`, { encoding: "utf-8", mode: 384 });
|
|
10012
|
-
await rename2(tempPath, path);
|
|
10013
|
-
await chmod2(path, 384).catch(() => {
|
|
10014
|
-
return;
|
|
10015
|
-
});
|
|
10016
|
-
}
|
|
10017
|
-
}
|
|
10018
|
-
var DEFAULT_SIGNATURE_TOLERANCE_MS2 = 5 * 60 * 1000;
|
|
10019
|
-
function buildSignatureBase2(timestamp, body) {
|
|
10020
|
-
return `${timestamp}.${body}`;
|
|
10021
|
-
}
|
|
10022
|
-
function signPayload2(secret, timestamp, body) {
|
|
10023
|
-
const digest = createHmac2("sha256", secret).update(buildSignatureBase2(timestamp, body)).digest("hex");
|
|
10024
|
-
return `sha256=${digest}`;
|
|
10025
|
-
}
|
|
10026
|
-
function now2() {
|
|
10027
|
-
return new Date().toISOString();
|
|
10028
|
-
}
|
|
10029
|
-
function truncate2(value, max = 4096) {
|
|
10030
|
-
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
10031
|
-
}
|
|
10032
|
-
function buildWebhookRequest2(event, channel) {
|
|
10033
|
-
if (!channel.webhook)
|
|
10034
|
-
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
10035
|
-
const body = JSON.stringify(event);
|
|
10036
|
-
const timestamp = event.time;
|
|
10037
|
-
const headers = {
|
|
10038
|
-
"Content-Type": "application/json",
|
|
10039
|
-
"User-Agent": "@hasna/events",
|
|
10040
|
-
"X-Hasna-Event-Id": event.id,
|
|
10041
|
-
"X-Hasna-Event-Type": event.type,
|
|
10042
|
-
"X-Hasna-Timestamp": timestamp,
|
|
10043
|
-
...channel.webhook.headers
|
|
10044
|
-
};
|
|
10045
|
-
if (channel.webhook.secret) {
|
|
10046
|
-
headers["X-Hasna-Signature"] = signPayload2(channel.webhook.secret, timestamp, body);
|
|
10047
|
-
}
|
|
10048
|
-
return { body, headers };
|
|
10049
|
-
}
|
|
10050
|
-
async function dispatchWebhook3(event, channel, options = {}) {
|
|
10051
|
-
if (!channel.webhook)
|
|
10052
|
-
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
10053
|
-
const startedAt = now2();
|
|
10054
|
-
const { body, headers } = buildWebhookRequest2(event, channel);
|
|
10055
|
-
const controller = new AbortController;
|
|
10056
|
-
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
10057
|
-
try {
|
|
10058
|
-
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
10059
|
-
method: "POST",
|
|
10060
|
-
headers,
|
|
10061
|
-
body,
|
|
10062
|
-
signal: controller.signal
|
|
10063
|
-
});
|
|
10064
|
-
const responseBody = truncate2(await response.text());
|
|
10065
|
-
return {
|
|
10066
|
-
attempt: 1,
|
|
10067
|
-
status: response.ok ? "success" : "failed",
|
|
10068
|
-
startedAt,
|
|
10069
|
-
completedAt: now2(),
|
|
10070
|
-
responseStatus: response.status,
|
|
10071
|
-
responseBody,
|
|
10072
|
-
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
10073
|
-
};
|
|
10074
|
-
} catch (error) {
|
|
10075
|
-
return {
|
|
10076
|
-
attempt: 1,
|
|
10077
|
-
status: "failed",
|
|
10078
|
-
startedAt,
|
|
10079
|
-
completedAt: now2(),
|
|
10080
|
-
error: error instanceof Error ? error.message : String(error)
|
|
10081
|
-
};
|
|
10082
|
-
} finally {
|
|
10083
|
-
clearTimeout(timeout);
|
|
10084
|
-
}
|
|
10085
|
-
}
|
|
10086
|
-
async function dispatchCommand3(event, channel) {
|
|
10087
|
-
if (!channel.command)
|
|
10088
|
-
throw new Error(`Channel ${channel.id} has no command config`);
|
|
10089
|
-
const startedAt = now2();
|
|
10090
|
-
const eventJson = JSON.stringify(event);
|
|
10091
|
-
const env2 = {
|
|
10092
|
-
...process.env,
|
|
10093
|
-
...channel.command.env,
|
|
10094
|
-
HASNA_CHANNEL_ID: channel.id,
|
|
10095
|
-
HASNA_EVENT_ID: event.id,
|
|
10096
|
-
HASNA_EVENT_TYPE: event.type,
|
|
10097
|
-
HASNA_EVENT_SOURCE: event.source,
|
|
10098
|
-
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
10099
|
-
HASNA_EVENT_SEVERITY: event.severity,
|
|
10100
|
-
HASNA_EVENT_TIME: event.time,
|
|
10101
|
-
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
10102
|
-
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
10103
|
-
HASNA_EVENT_JSON: eventJson
|
|
10104
|
-
};
|
|
10105
|
-
return new Promise((resolve2) => {
|
|
10106
|
-
const child = spawn2(channel.command.command, channel.command.args ?? [], {
|
|
10107
|
-
cwd: channel.command.cwd,
|
|
10108
|
-
env: env2,
|
|
10109
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
10110
|
-
});
|
|
10111
|
-
let stdout = "";
|
|
10112
|
-
let stderr = "";
|
|
10113
|
-
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
10114
|
-
child.stdin.end(eventJson);
|
|
10115
|
-
child.stdout.on("data", (chunk) => {
|
|
10116
|
-
stdout += chunk.toString();
|
|
10117
|
-
});
|
|
10118
|
-
child.stderr.on("data", (chunk) => {
|
|
10119
|
-
stderr += chunk.toString();
|
|
10120
|
-
});
|
|
10121
|
-
child.on("error", (error) => {
|
|
10122
|
-
clearTimeout(timeout);
|
|
10123
|
-
resolve2({
|
|
10124
|
-
attempt: 1,
|
|
10125
|
-
status: "failed",
|
|
10126
|
-
startedAt,
|
|
10127
|
-
completedAt: now2(),
|
|
10128
|
-
stdout: truncate2(stdout),
|
|
10129
|
-
stderr: truncate2(stderr),
|
|
10130
|
-
error: error.message
|
|
10131
|
-
});
|
|
10132
|
-
});
|
|
10133
|
-
child.on("close", (code, signal) => {
|
|
10134
|
-
clearTimeout(timeout);
|
|
10135
|
-
const success = code === 0;
|
|
10136
|
-
resolve2({
|
|
10137
|
-
attempt: 1,
|
|
10138
|
-
status: success ? "success" : "failed",
|
|
10139
|
-
startedAt,
|
|
10140
|
-
completedAt: now2(),
|
|
10141
|
-
stdout: truncate2(stdout),
|
|
10142
|
-
stderr: truncate2(stderr),
|
|
10143
|
-
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
10144
|
-
});
|
|
10145
|
-
});
|
|
10146
|
-
});
|
|
10147
|
-
}
|
|
10148
|
-
async function dispatchChannel3(event, channel, options = {}) {
|
|
10149
|
-
if (channel.transport === "webhook")
|
|
10150
|
-
return dispatchWebhook3(event, channel, options);
|
|
10151
|
-
if (channel.transport === "command")
|
|
10152
|
-
return dispatchCommand3(event, channel);
|
|
10153
|
-
return {
|
|
10154
|
-
attempt: 1,
|
|
10155
|
-
status: "skipped",
|
|
10156
|
-
startedAt: now2(),
|
|
10157
|
-
completedAt: now2(),
|
|
10158
|
-
error: `Unsupported transport: ${channel.transport}`
|
|
10159
|
-
};
|
|
10160
|
-
}
|
|
10161
|
-
function createDeliveryResult2(event, channel, attempts) {
|
|
10162
|
-
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
10163
|
-
return {
|
|
10164
|
-
id: randomUUID3(),
|
|
10165
|
-
eventId: event.id,
|
|
10166
|
-
channelId: channel.id,
|
|
10167
|
-
transport: channel.transport,
|
|
10168
|
-
status,
|
|
10169
|
-
attempts,
|
|
10170
|
-
createdAt: attempts[0]?.startedAt ?? now2(),
|
|
10171
|
-
completedAt: attempts.at(-1)?.completedAt ?? now2()
|
|
10172
|
-
};
|
|
10173
|
-
}
|
|
10174
|
-
function createEvent2(input) {
|
|
10175
|
-
return {
|
|
10176
|
-
id: input.id ?? randomUUID22(),
|
|
10177
|
-
source: input.source,
|
|
10178
|
-
type: input.type,
|
|
10179
|
-
time: normalizeTime2(input.time),
|
|
10180
|
-
subject: input.subject,
|
|
10181
|
-
severity: input.severity ?? "info",
|
|
10182
|
-
data: input.data ?? {},
|
|
10183
|
-
message: input.message,
|
|
10184
|
-
dedupeKey: input.dedupeKey,
|
|
10185
|
-
schemaVersion: input.schemaVersion ?? "1.0",
|
|
10186
|
-
metadata: input.metadata ?? {}
|
|
10187
|
-
};
|
|
10188
|
-
}
|
|
10189
|
-
|
|
10190
|
-
class EventsClient2 {
|
|
10191
|
-
store;
|
|
10192
|
-
redactors;
|
|
10193
|
-
transportOptions;
|
|
10194
|
-
constructor(options = {}) {
|
|
10195
|
-
this.store = options.store ?? new JsonEventsStore2(options.dataDir);
|
|
10196
|
-
this.redactors = options.redactors ?? [];
|
|
10197
|
-
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
10198
|
-
}
|
|
10199
|
-
async addChannel(input) {
|
|
10200
|
-
const timestamp = new Date().toISOString();
|
|
10201
|
-
return this.store.addChannel({
|
|
10202
|
-
...input,
|
|
10203
|
-
createdAt: input.createdAt ?? timestamp,
|
|
10204
|
-
updatedAt: input.updatedAt ?? timestamp
|
|
10205
|
-
});
|
|
10206
|
-
}
|
|
10207
|
-
async listChannels() {
|
|
10208
|
-
return this.store.listChannels();
|
|
10209
|
-
}
|
|
10210
|
-
async removeChannel(id) {
|
|
10211
|
-
return this.store.removeChannel(id);
|
|
10212
|
-
}
|
|
10213
|
-
async emit(input, options = {}) {
|
|
10214
|
-
const event = options.redactSensitiveData === false ? createEvent2(input) : redactSensitiveKeys2(createEvent2(input));
|
|
10215
|
-
if (options.dedupe !== false) {
|
|
10216
|
-
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
10217
|
-
if (existing) {
|
|
10218
|
-
return { event: existing, deliveries: [], deduped: true };
|
|
10219
|
-
}
|
|
10220
|
-
}
|
|
10221
|
-
await this.store.appendEvent(event);
|
|
10222
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
10223
|
-
return { event, deliveries, deduped: false };
|
|
10224
|
-
}
|
|
10225
|
-
async listEvents() {
|
|
10226
|
-
return this.store.listEvents();
|
|
10227
|
-
}
|
|
10228
|
-
async listDeliveries() {
|
|
10229
|
-
return this.store.listDeliveries();
|
|
10230
|
-
}
|
|
10231
|
-
async deliver(event) {
|
|
10232
|
-
const channels = await this.store.listChannels();
|
|
10233
|
-
const selected = channels.filter((channel) => channelMatchesEvent2(channel, event));
|
|
10234
|
-
const deliveries = [];
|
|
10235
|
-
for (const channel of selected) {
|
|
10236
|
-
const eventForChannel = await this.applyRedaction(event, channel);
|
|
10237
|
-
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
10238
|
-
await this.store.appendDelivery(result);
|
|
10239
|
-
deliveries.push(result);
|
|
10240
|
-
}
|
|
10241
|
-
return deliveries;
|
|
10242
|
-
}
|
|
10243
|
-
async testChannel(id, input = {}) {
|
|
10244
|
-
const channel = await this.store.getChannel(id);
|
|
10245
|
-
if (!channel)
|
|
10246
|
-
throw new Error(`Channel not found: ${id}`);
|
|
10247
|
-
const event = createEvent2({
|
|
10248
|
-
source: input.source ?? "hasna.events",
|
|
10249
|
-
type: input.type ?? "events.test",
|
|
10250
|
-
subject: input.subject ?? id,
|
|
10251
|
-
severity: input.severity ?? "info",
|
|
10252
|
-
data: input.data ?? { test: true },
|
|
10253
|
-
message: input.message ?? "Hasna events test delivery",
|
|
10254
|
-
dedupeKey: input.dedupeKey,
|
|
10255
|
-
schemaVersion: input.schemaVersion,
|
|
10256
|
-
metadata: input.metadata,
|
|
10257
|
-
time: input.time,
|
|
10258
|
-
id: input.id
|
|
10259
|
-
});
|
|
10260
|
-
const eventForChannel = await this.applyRedaction(event, channel);
|
|
10261
|
-
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
10262
|
-
await this.store.appendDelivery(result);
|
|
10263
|
-
return result;
|
|
10264
|
-
}
|
|
10265
|
-
async replay(options = {}) {
|
|
10266
|
-
const events = (await this.store.listEvents()).filter((event) => {
|
|
10267
|
-
if (options.eventId && event.id !== options.eventId)
|
|
10268
|
-
return false;
|
|
10269
|
-
if (options.source && event.source !== options.source)
|
|
10270
|
-
return false;
|
|
10271
|
-
if (options.type && event.type !== options.type)
|
|
10272
|
-
return false;
|
|
10273
|
-
return true;
|
|
10274
|
-
});
|
|
10275
|
-
if (options.dryRun)
|
|
10276
|
-
return { events, deliveries: [] };
|
|
10277
|
-
const deliveries = [];
|
|
10278
|
-
for (const event of events) {
|
|
10279
|
-
deliveries.push(...await this.deliver(event));
|
|
10280
|
-
}
|
|
10281
|
-
return { events, deliveries };
|
|
10282
|
-
}
|
|
10283
|
-
async applyRedaction(event, channel) {
|
|
10284
|
-
let next = redactPaths2(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
10285
|
-
for (const redactor of this.redactors) {
|
|
10286
|
-
next = await redactor(next, channel);
|
|
10287
|
-
}
|
|
10288
|
-
return next;
|
|
10289
|
-
}
|
|
10290
|
-
async deliverWithRetry(event, channel) {
|
|
10291
|
-
const policy = normalizeRetryPolicy2(channel.retry);
|
|
10292
|
-
const attempts = [];
|
|
10293
|
-
for (let index = 0;index < policy.maxAttempts; index += 1) {
|
|
10294
|
-
const attempt = await dispatchChannel3(event, channel, this.transportOptions);
|
|
10295
|
-
attempt.attempt = index + 1;
|
|
10296
|
-
if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
|
|
10297
|
-
attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
|
|
10298
|
-
}
|
|
10299
|
-
attempts.push(attempt);
|
|
10300
|
-
if (attempt.status !== "failed")
|
|
10301
|
-
break;
|
|
10302
|
-
if (attempt.nextBackoffMs)
|
|
10303
|
-
await Bun.sleep(attempt.nextBackoffMs);
|
|
10304
|
-
}
|
|
10305
|
-
return createDeliveryResult2(event, channel, attempts);
|
|
10306
|
-
}
|
|
10307
|
-
}
|
|
10308
|
-
function redactPaths2(event, paths, replacement = "[REDACTED]") {
|
|
10309
|
-
if (paths.length === 0)
|
|
10310
|
-
return event;
|
|
10311
|
-
const copy = structuredClone(event);
|
|
10312
|
-
for (const path of paths) {
|
|
10313
|
-
setPath2(copy, path, replacement);
|
|
10314
|
-
}
|
|
10315
|
-
return copy;
|
|
10316
|
-
}
|
|
10317
|
-
function sanitizeChannelForOutput2(channel) {
|
|
10318
|
-
const copy = structuredClone(channel);
|
|
10319
|
-
if (copy.webhook?.secret)
|
|
10320
|
-
copy.webhook.secret = "[REDACTED]";
|
|
10321
|
-
if (copy.command?.env) {
|
|
10322
|
-
copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey2(key) ? "[REDACTED]" : value]));
|
|
10323
|
-
}
|
|
10324
|
-
return copy;
|
|
10325
|
-
}
|
|
10326
|
-
function sanitizeChannelsForOutput2(channels) {
|
|
10327
|
-
return channels.map(sanitizeChannelForOutput2);
|
|
10328
|
-
}
|
|
10329
|
-
function redactSensitiveKeys2(event, replacement = "[REDACTED]") {
|
|
10330
|
-
return redactValue2(event, replacement);
|
|
10331
|
-
}
|
|
10332
|
-
function shouldRedactKey2(key) {
|
|
10333
|
-
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
10334
|
-
}
|
|
10335
|
-
function redactValue2(value, replacement) {
|
|
10336
|
-
if (Array.isArray(value))
|
|
10337
|
-
return value.map((item) => redactValue2(item, replacement));
|
|
10338
|
-
if (!value || typeof value !== "object")
|
|
10339
|
-
return value;
|
|
10340
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
10341
|
-
key,
|
|
10342
|
-
shouldRedactKey2(key) ? replacement : redactValue2(item, replacement)
|
|
10343
|
-
]));
|
|
10344
|
-
}
|
|
10345
|
-
function setPath2(input, path, replacement) {
|
|
10346
|
-
const parts = path.split(".");
|
|
10347
|
-
let cursor = input;
|
|
10348
|
-
for (const part of parts.slice(0, -1)) {
|
|
10349
|
-
const next = cursor[part];
|
|
10350
|
-
if (!next || typeof next !== "object")
|
|
10351
|
-
return;
|
|
10352
|
-
cursor = next;
|
|
10353
|
-
}
|
|
10354
|
-
const last = parts.at(-1);
|
|
10355
|
-
if (last && last in cursor)
|
|
10356
|
-
cursor[last] = replacement;
|
|
10357
|
-
}
|
|
10358
|
-
function normalizeTime2(value) {
|
|
10359
|
-
if (!value)
|
|
10360
|
-
return new Date().toISOString();
|
|
10361
|
-
return value instanceof Date ? value.toISOString() : value;
|
|
10362
|
-
}
|
|
10363
|
-
function normalizeRetryPolicy2(policy) {
|
|
10364
|
-
return {
|
|
10365
|
-
maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
|
|
10366
|
-
backoffMs: Math.max(0, policy?.backoffMs ?? 250),
|
|
10367
|
-
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
10368
|
-
};
|
|
10369
|
-
}
|
|
10370
|
-
|
|
10371
|
-
// src/commands/runtime.ts
|
|
9173
|
+
import { EventsClient } from "@hasna/events";
|
|
10372
9174
|
function probeTmuxPane(target, tmuxCommand = process.env["HASNA_MACHINES_TMUX_BIN"] || "tmux") {
|
|
10373
9175
|
const checkedAt = new Date().toISOString();
|
|
10374
9176
|
const result = spawnSync4(tmuxCommand, ["display-message", "-p", "-t", target, "#{pane_id}"], {
|
|
@@ -10394,7 +9196,7 @@ async function watchTmuxPane(options) {
|
|
|
10394
9196
|
throw new Error("tmux pane target is required");
|
|
10395
9197
|
const intervalMs = Math.max(0, options.intervalMs ?? 5000);
|
|
10396
9198
|
const maxChecks = options.maxChecks ?? Number.POSITIVE_INFINITY;
|
|
10397
|
-
const client = options.client ?? new
|
|
9199
|
+
const client = options.client ?? new EventsClient;
|
|
10398
9200
|
const probe = options.probe ?? ((paneTarget) => probeTmuxPane(paneTarget, options.tmuxCommand));
|
|
10399
9201
|
const wait = options.sleep ?? sleep;
|
|
10400
9202
|
let lastPresent;
|
|
@@ -10455,7 +9257,8 @@ async function emitTmuxEvent(client, type, probe, lastPresent, deliver) {
|
|
|
10455
9257
|
}
|
|
10456
9258
|
|
|
10457
9259
|
// src/commands/screen.ts
|
|
10458
|
-
var
|
|
9260
|
+
var SCREEN_SECRET_NAMESPACE_ENV = "HASNA_MACHINES_SCREEN_SECRET_NAMESPACE";
|
|
9261
|
+
var DEFAULT_SCREEN_SECRET_NAMESPACE = "machines/screen-sharing";
|
|
10459
9262
|
function shellQuote6(value) {
|
|
10460
9263
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
10461
9264
|
}
|
|
@@ -10479,7 +9282,8 @@ function splitTarget(target) {
|
|
|
10479
9282
|
return [target.slice(0, at), target.slice(at + 1)];
|
|
10480
9283
|
}
|
|
10481
9284
|
function defaultScreenPasswordSecretKey(machineId) {
|
|
10482
|
-
|
|
9285
|
+
const namespace = process.env[SCREEN_SECRET_NAMESPACE_ENV]?.trim() || DEFAULT_SCREEN_SECRET_NAMESPACE;
|
|
9286
|
+
return `${namespace}/screen-${machineId}-vnc-password`;
|
|
10483
9287
|
}
|
|
10484
9288
|
function resolveScreenTarget(machineId, options = {}) {
|
|
10485
9289
|
const resolved = resolveMachineRoute(machineId, options);
|
|
@@ -10563,8 +9367,8 @@ function buildScreenEnableCommand(machineId, options = {}) {
|
|
|
10563
9367
|
}
|
|
10564
9368
|
|
|
10565
9369
|
// src/commands/sync.ts
|
|
10566
|
-
import { existsSync as
|
|
10567
|
-
import { homedir as
|
|
9370
|
+
import { existsSync as existsSync7, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
|
|
9371
|
+
import { homedir as homedir5 } from "os";
|
|
10568
9372
|
init_paths();
|
|
10569
9373
|
init_db();
|
|
10570
9374
|
function quote4(value) {
|
|
@@ -10618,8 +9422,8 @@ function detectFileActions(machine) {
|
|
|
10618
9422
|
throw new Error(`Remote file sync planning is not supported for ${machine.id}; refusing to inspect or apply local paths as remote state.`);
|
|
10619
9423
|
}
|
|
10620
9424
|
return (machine.files || []).map((file, index) => {
|
|
10621
|
-
const sourceExists =
|
|
10622
|
-
const targetExists =
|
|
9425
|
+
const sourceExists = existsSync7(file.source);
|
|
9426
|
+
const targetExists = existsSync7(file.target);
|
|
10623
9427
|
let status = "missing";
|
|
10624
9428
|
if (sourceExists && targetExists) {
|
|
10625
9429
|
if (file.mode === "symlink") {
|
|
@@ -10650,7 +9454,7 @@ function buildSyncPlan(machineId, runner = runMachineCommand) {
|
|
|
10650
9454
|
const target = selected || {
|
|
10651
9455
|
id: currentMachineId,
|
|
10652
9456
|
platform: "linux",
|
|
10653
|
-
workspacePath: `${
|
|
9457
|
+
workspacePath: `${homedir5()}/workspace`
|
|
10654
9458
|
};
|
|
10655
9459
|
const actions = [
|
|
10656
9460
|
...detectPackageActions(target, runner),
|
|
@@ -11207,6 +10011,7 @@ function runDoctor(machineId = getLocalMachineId()) {
|
|
|
11207
10011
|
init_db();
|
|
11208
10012
|
|
|
11209
10013
|
// src/commands/serve.ts
|
|
10014
|
+
import { EventsClient as EventsClient2, sanitizeChannelsForOutput } from "@hasna/events";
|
|
11210
10015
|
function escapeHtml(value) {
|
|
11211
10016
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
11212
10017
|
}
|
|
@@ -11423,7 +10228,7 @@ function startDashboardServer(options = {}) {
|
|
|
11423
10228
|
if (request.method !== "GET") {
|
|
11424
10229
|
return jsonError("Use GET for webhook channel listing.", 405);
|
|
11425
10230
|
}
|
|
11426
|
-
return Response.json(
|
|
10231
|
+
return Response.json(sanitizeChannelsForOutput(await events.listChannels()));
|
|
11427
10232
|
}
|
|
11428
10233
|
if (url.pathname === "/api/events") {
|
|
11429
10234
|
if (request.method === "GET") {
|
|
@@ -11556,8 +10361,8 @@ function runSelfTest() {
|
|
|
11556
10361
|
// src/commands/clipboard.ts
|
|
11557
10362
|
init_paths();
|
|
11558
10363
|
import { createHash } from "crypto";
|
|
11559
|
-
import { existsSync as
|
|
11560
|
-
import { join as
|
|
10364
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "fs";
|
|
10365
|
+
import { join as join6 } from "path";
|
|
11561
10366
|
var DEFAULT_CONFIG = {
|
|
11562
10367
|
version: 1,
|
|
11563
10368
|
enabled: true,
|
|
@@ -11575,7 +10380,7 @@ var DEFAULT_CONFIG = {
|
|
|
11575
10380
|
function resolveConfigPath(configPath) {
|
|
11576
10381
|
if (configPath)
|
|
11577
10382
|
return configPath;
|
|
11578
|
-
return
|
|
10383
|
+
return join6(getDataDir(), "clipboard-config.json");
|
|
11579
10384
|
}
|
|
11580
10385
|
function resolveHistoryPath(historyPath) {
|
|
11581
10386
|
if (historyPath)
|
|
@@ -11587,7 +10392,7 @@ function getDefaultConfig() {
|
|
|
11587
10392
|
}
|
|
11588
10393
|
function readConfig(configPath) {
|
|
11589
10394
|
const path = resolveConfigPath(configPath);
|
|
11590
|
-
if (!
|
|
10395
|
+
if (!existsSync8(path)) {
|
|
11591
10396
|
return getDefaultConfig();
|
|
11592
10397
|
}
|
|
11593
10398
|
const parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
@@ -11601,7 +10406,7 @@ function writeConfig(config, configPath) {
|
|
|
11601
10406
|
}
|
|
11602
10407
|
function readHistory(historyPath) {
|
|
11603
10408
|
const path = resolveHistoryPath(historyPath);
|
|
11604
|
-
if (!
|
|
10409
|
+
if (!existsSync8(path)) {
|
|
11605
10410
|
return [];
|
|
11606
10411
|
}
|
|
11607
10412
|
try {
|
|
@@ -11634,7 +10439,7 @@ function sanitizeClipboardForRead(content, maxSizeBytes, skipPatterns) {
|
|
|
11634
10439
|
}
|
|
11635
10440
|
function getOrCreateClipboardKey() {
|
|
11636
10441
|
const keyPath = getClipboardKeyPath();
|
|
11637
|
-
if (
|
|
10442
|
+
if (existsSync8(keyPath)) {
|
|
11638
10443
|
return readFileSync6(keyPath, "utf8").trim();
|
|
11639
10444
|
}
|
|
11640
10445
|
const key = createHash("sha256").update(crypto.randomUUID()).digest("hex").slice(0, 32);
|
|
@@ -11674,7 +10479,7 @@ function addClipboardEntry(entry, historyPath) {
|
|
|
11674
10479
|
}
|
|
11675
10480
|
function clearClipboardHistory(historyPath) {
|
|
11676
10481
|
const path = resolveHistoryPath(historyPath);
|
|
11677
|
-
if (
|
|
10482
|
+
if (existsSync8(path)) {
|
|
11678
10483
|
rmSync(path);
|
|
11679
10484
|
}
|
|
11680
10485
|
}
|
|
@@ -11691,7 +10496,7 @@ function getClipboardStatus(historyPath) {
|
|
|
11691
10496
|
// src/commands/clipboard-daemon.ts
|
|
11692
10497
|
init_paths();
|
|
11693
10498
|
import { readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
11694
|
-
import { join as
|
|
10499
|
+
import { join as join7 } from "path";
|
|
11695
10500
|
import { createHash as createHash3 } from "crypto";
|
|
11696
10501
|
|
|
11697
10502
|
// src/commands/clipboard-server.ts
|
|
@@ -11849,7 +10654,7 @@ function handleGetClipboard(response, config) {
|
|
|
11849
10654
|
}
|
|
11850
10655
|
|
|
11851
10656
|
// src/commands/clipboard-daemon.ts
|
|
11852
|
-
var DAEMON_PID_PATH =
|
|
10657
|
+
var DAEMON_PID_PATH = join7(getDataDir(), "clipboard-daemon.pid");
|
|
11853
10658
|
function readLocalClipboardSync2() {
|
|
11854
10659
|
const platform4 = process.platform;
|
|
11855
10660
|
if (platform4 === "darwin") {
|
|
@@ -12023,8 +10828,8 @@ async function discoverPeers() {
|
|
|
12023
10828
|
|
|
12024
10829
|
// src/commands/heal.ts
|
|
12025
10830
|
init_paths();
|
|
12026
|
-
import { existsSync as
|
|
12027
|
-
import { join as
|
|
10831
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
|
|
10832
|
+
import { join as join8 } from "path";
|
|
12028
10833
|
var DEFAULT_THRESHOLDS = {
|
|
12029
10834
|
reconnect: 3,
|
|
12030
10835
|
nmRestart: 7,
|
|
@@ -12068,14 +10873,14 @@ function defaultHealState() {
|
|
|
12068
10873
|
};
|
|
12069
10874
|
}
|
|
12070
10875
|
function getHealConfigPath() {
|
|
12071
|
-
return process.env["HASNA_MACHINES_HEAL_CONFIG_PATH"] ||
|
|
10876
|
+
return process.env["HASNA_MACHINES_HEAL_CONFIG_PATH"] || join8(getDataDir(), "heal-config.json");
|
|
12072
10877
|
}
|
|
12073
10878
|
function getHealStatePath() {
|
|
12074
|
-
return process.env["HASNA_MACHINES_HEAL_STATE_PATH"] ||
|
|
10879
|
+
return process.env["HASNA_MACHINES_HEAL_STATE_PATH"] || join8(getDataDir(), "heal-state.json");
|
|
12075
10880
|
}
|
|
12076
10881
|
function readHealConfig(path) {
|
|
12077
10882
|
const p = path || getHealConfigPath();
|
|
12078
|
-
if (!
|
|
10883
|
+
if (!existsSync9(p))
|
|
12079
10884
|
return { ...DEFAULT_HEAL_CONFIG, thresholds: { ...DEFAULT_THRESHOLDS } };
|
|
12080
10885
|
const parsed = JSON.parse(readFileSync9(p, "utf8"));
|
|
12081
10886
|
return {
|
|
@@ -12093,7 +10898,7 @@ function writeHealConfig(config, path) {
|
|
|
12093
10898
|
}
|
|
12094
10899
|
function readHealState(path) {
|
|
12095
10900
|
const p = path || getHealStatePath();
|
|
12096
|
-
if (!
|
|
10901
|
+
if (!existsSync9(p))
|
|
12097
10902
|
return defaultHealState();
|
|
12098
10903
|
try {
|
|
12099
10904
|
return { ...defaultHealState(), ...JSON.parse(readFileSync9(p, "utf8")) };
|
|
@@ -12133,7 +10938,7 @@ function evaluateHealth(probe, config, state) {
|
|
|
12133
10938
|
return { healthy: localOk && quorumOk, remoteScore, reasons };
|
|
12134
10939
|
}
|
|
12135
10940
|
function decideAction(input) {
|
|
12136
|
-
const { healthy, now
|
|
10941
|
+
const { healthy, now, gpuBusy, config, currentBootId } = input;
|
|
12137
10942
|
const s = { ...input.state };
|
|
12138
10943
|
const t = config.thresholds;
|
|
12139
10944
|
if (s.bootId !== currentBootId) {
|
|
@@ -12144,13 +10949,13 @@ function decideAction(input) {
|
|
|
12144
10949
|
if (healthy) {
|
|
12145
10950
|
s.failCount = 0;
|
|
12146
10951
|
if (s.bootHealthySince === null)
|
|
12147
|
-
s.bootHealthySince =
|
|
12148
|
-
if (
|
|
10952
|
+
s.bootHealthySince = now;
|
|
10953
|
+
if (now - s.bootHealthySince >= config.healthyWindowSec) {
|
|
12149
10954
|
s.failedBootRecoveries = 0;
|
|
12150
10955
|
s.rebootSuppressUntil = 0;
|
|
12151
10956
|
s.pendingRebootRecovery = false;
|
|
12152
10957
|
}
|
|
12153
|
-
if (s.degradedUntil > 0 &&
|
|
10958
|
+
if (s.degradedUntil > 0 && now >= s.degradedUntil) {
|
|
12154
10959
|
s.degradedUntil = 0;
|
|
12155
10960
|
return { action: "restore_preferred", state: s };
|
|
12156
10961
|
}
|
|
@@ -12168,8 +10973,8 @@ function decideAction(input) {
|
|
|
12168
10973
|
else if (s.failCount >= t.reconnect)
|
|
12169
10974
|
tier = "reconnect";
|
|
12170
10975
|
const tryReconnect = (reason) => {
|
|
12171
|
-
if (
|
|
12172
|
-
s.lastReconnect =
|
|
10976
|
+
if (now - s.lastReconnect >= config.reconnectMinIntervalSec) {
|
|
10977
|
+
s.lastReconnect = now;
|
|
12173
10978
|
return { action: "reconnect_wifi", suppressedReason: reason, state: s };
|
|
12174
10979
|
}
|
|
12175
10980
|
return { action: "none", suppressedReason: reason, state: s };
|
|
@@ -12178,15 +10983,15 @@ function decideAction(input) {
|
|
|
12178
10983
|
case "reconnect":
|
|
12179
10984
|
return tryReconnect();
|
|
12180
10985
|
case "nmRestart":
|
|
12181
|
-
if (
|
|
12182
|
-
s.lastNmRestart =
|
|
10986
|
+
if (now - s.lastNmRestart >= config.nmRestartMinIntervalSec) {
|
|
10987
|
+
s.lastNmRestart = now;
|
|
12183
10988
|
return { action: "restart_nm", state: s };
|
|
12184
10989
|
}
|
|
12185
10990
|
return tryReconnect();
|
|
12186
10991
|
case "fallback":
|
|
12187
|
-
if (
|
|
12188
|
-
s.lastFallback =
|
|
12189
|
-
s.degradedUntil =
|
|
10992
|
+
if (now - s.lastFallback >= config.fallbackWindowSec) {
|
|
10993
|
+
s.lastFallback = now;
|
|
10994
|
+
s.degradedUntil = now + config.fallbackWindowSec;
|
|
12190
10995
|
return { action: "fallback_ssid", state: s };
|
|
12191
10996
|
}
|
|
12192
10997
|
return tryReconnect();
|
|
@@ -12194,22 +10999,22 @@ function decideAction(input) {
|
|
|
12194
10999
|
let reason = null;
|
|
12195
11000
|
if (!config.allowReboot)
|
|
12196
11001
|
reason = "disabled";
|
|
12197
|
-
else if (
|
|
11002
|
+
else if (now < s.rebootSuppressUntil)
|
|
12198
11003
|
reason = "loop";
|
|
12199
11004
|
else if (config.gpuJobGuard && gpuBusy)
|
|
12200
11005
|
reason = "gpu";
|
|
12201
|
-
else if (
|
|
11006
|
+
else if (now - s.lastRebootAttempt < config.rebootMinIntervalSec)
|
|
12202
11007
|
reason = "rate";
|
|
12203
11008
|
if (reason)
|
|
12204
11009
|
return tryReconnect(reason);
|
|
12205
11010
|
if (s.pendingRebootRecovery) {
|
|
12206
11011
|
s.failedBootRecoveries += 1;
|
|
12207
11012
|
if (s.failedBootRecoveries >= config.maxFailedBootRecoveries) {
|
|
12208
|
-
s.rebootSuppressUntil =
|
|
11013
|
+
s.rebootSuppressUntil = now + config.bootBackoffSec;
|
|
12209
11014
|
return tryReconnect("loop");
|
|
12210
11015
|
}
|
|
12211
11016
|
}
|
|
12212
|
-
s.lastRebootAttempt =
|
|
11017
|
+
s.lastRebootAttempt = now;
|
|
12213
11018
|
s.pendingRebootRecovery = true;
|
|
12214
11019
|
return { action: "reboot", state: s };
|
|
12215
11020
|
}
|
|
@@ -12309,9 +11114,9 @@ function executeAction(action, config) {
|
|
|
12309
11114
|
|
|
12310
11115
|
// src/commands/heal-daemon.ts
|
|
12311
11116
|
init_paths();
|
|
12312
|
-
import { existsSync as
|
|
12313
|
-
import { join as
|
|
12314
|
-
var DAEMON_PID_PATH2 =
|
|
11117
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
|
|
11118
|
+
import { join as join9 } from "path";
|
|
11119
|
+
var DAEMON_PID_PATH2 = join9(getDataDir(), "heal-daemon.pid");
|
|
12315
11120
|
var SERVICE_PATH = "/etc/systemd/system/machines-heal.service";
|
|
12316
11121
|
var SYSTEM_CONF = "/etc/systemd/system.conf";
|
|
12317
11122
|
function log(msg) {
|
|
@@ -12429,7 +11234,7 @@ function applyDeterminism(config) {
|
|
|
12429
11234
|
}
|
|
12430
11235
|
function enableHardwareWatchdog() {
|
|
12431
11236
|
const log2 = [];
|
|
12432
|
-
if (!
|
|
11237
|
+
if (!existsSync10(SYSTEM_CONF))
|
|
12433
11238
|
return ["/etc/systemd/system.conf not found; skipping hardware watchdog"];
|
|
12434
11239
|
let conf = readFileSync10(SYSTEM_CONF, "utf8");
|
|
12435
11240
|
const set = (key, value) => {
|
|
@@ -12459,7 +11264,7 @@ function binPath() {
|
|
|
12459
11264
|
const home = process.env["HOME"] || "/home/hasna";
|
|
12460
11265
|
candidates.push(`${home}/.bun/bin/machines`, "/home/hasna/.bun/bin/machines", "/root/.bun/bin/machines", "/usr/local/bin/machines");
|
|
12461
11266
|
for (const c of candidates) {
|
|
12462
|
-
if (c &&
|
|
11267
|
+
if (c && existsSync10(c))
|
|
12463
11268
|
return c;
|
|
12464
11269
|
}
|
|
12465
11270
|
return "machines";
|
|
@@ -12495,7 +11300,7 @@ WantedBy=multi-user.target
|
|
|
12495
11300
|
function uninstallHealService() {
|
|
12496
11301
|
const log2 = [];
|
|
12497
11302
|
sh2("systemctl disable --now machines-heal.service 2>/dev/null || true");
|
|
12498
|
-
if (
|
|
11303
|
+
if (existsSync10(SERVICE_PATH)) {
|
|
12499
11304
|
sh2(`rm -f ${SERVICE_PATH}`);
|
|
12500
11305
|
sh2("systemctl daemon-reload");
|
|
12501
11306
|
log2.push(`removed ${SERVICE_PATH}`);
|
|
@@ -12506,7 +11311,7 @@ function uninstallHealService() {
|
|
|
12506
11311
|
}
|
|
12507
11312
|
function healServiceStatus() {
|
|
12508
11313
|
return {
|
|
12509
|
-
installed:
|
|
11314
|
+
installed: existsSync10(SERVICE_PATH),
|
|
12510
11315
|
active: sh2("systemctl is-active machines-heal.service").out === "active",
|
|
12511
11316
|
enabled: sh2("systemctl is-enabled machines-heal.service 2>/dev/null").out === "enabled"
|
|
12512
11317
|
};
|
|
@@ -12777,8 +11582,17 @@ program2.name("machines").description("Machine fleet management CLI + MCP for de
|
|
|
12777
11582
|
var manifestCommand = program2.command("manifest").description("Manage the fleet manifest");
|
|
12778
11583
|
var appsCommand = program2.command("apps").description("Manage installed applications per machine");
|
|
12779
11584
|
var notificationsCommand = program2.command("notifications").description("Manage fleet alert delivery channels");
|
|
12780
|
-
|
|
12781
|
-
|
|
11585
|
+
var eventWebhooksCommand = registerWebhookCommands(program2, { source: "machines" });
|
|
11586
|
+
eventWebhooksCommand.description("Manage shared event webhook subscriptions");
|
|
11587
|
+
var webhookTestCommand = eventWebhooksCommand.commands.find((command) => command.name() === "test");
|
|
11588
|
+
var webhookOptions = webhookTestCommand?.options ?? [];
|
|
11589
|
+
var webhookMessageOption = webhookOptions.find((option) => option.long === "--message");
|
|
11590
|
+
if (webhookMessageOption) {
|
|
11591
|
+
webhookMessageOption.defaultValue = "Shared events test delivery";
|
|
11592
|
+
}
|
|
11593
|
+
var eventsCommand = registerEventCommands(program2, { source: "machines" });
|
|
11594
|
+
eventsCommand.description("Emit, list, and replay shared events");
|
|
11595
|
+
var runtimeCommand = program2.command("runtime").description("Watch runtime conditions and emit shared events");
|
|
12782
11596
|
var clipboardCommand = program2.command("clipboard").description("Real-time clipboard sync across fleet machines");
|
|
12783
11597
|
var installClaudeCommand = program2.command("install-claude").description("Install or inspect Claude, Codex, and Gemini CLIs");
|
|
12784
11598
|
manifestCommand.command("init").description("Create an empty fleet manifest").action(() => {
|