@hasna/machines 0.0.28 → 0.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -9830,1272 +9830,1362 @@ function listPorts(machineId) {
9830
9830
  };
9831
9831
  }
9832
9832
 
9833
- // src/commands/screen.ts
9834
- var DEFAULT_SCREEN_SECRET_NAMESPACE = "hasna/xyz/opensource/machines/prod";
9835
- function shellQuote6(value) {
9836
- return `'${value.replace(/'/g, "'\\''")}'`;
9833
+ // src/commands/runtime.ts
9834
+ import { spawnSync as spawnSync4 } from "child_process";
9835
+ import { setTimeout as sleep } from "timers/promises";
9836
+
9837
+ // node_modules/@hasna/events/dist/index.js
9838
+ import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
9839
+ import { existsSync as existsSync8 } from "fs";
9840
+ import { homedir as homedir6 } from "os";
9841
+ import { join as join7 } from "path";
9842
+ import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
9843
+ import { randomUUID as randomUUID3 } from "crypto";
9844
+ import { spawn as spawn2 } from "child_process";
9845
+ import { randomUUID as randomUUID22 } from "crypto";
9846
+ function getPathValue2(input, path) {
9847
+ return path.split(".").reduce((value, part) => {
9848
+ if (value && typeof value === "object" && part in value) {
9849
+ return value[part];
9850
+ }
9851
+ return;
9852
+ }, input);
9837
9853
  }
9838
- function shellCommand2(command) {
9839
- return command.map(shellQuote6).join(" ");
9854
+ function wildcardToRegExp2(pattern) {
9855
+ const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
9856
+ return new RegExp(`^${escaped}$`);
9840
9857
  }
9841
- function metadataString2(metadata, keys) {
9842
- if (!metadata)
9843
- return null;
9844
- for (const key of keys) {
9845
- const value = metadata[key];
9846
- if (typeof value === "string" && value.trim())
9847
- return value.trim();
9848
- }
9849
- return null;
9858
+ function matchString2(value, matcher) {
9859
+ if (matcher === undefined)
9860
+ return true;
9861
+ if (value === undefined)
9862
+ return false;
9863
+ const matchers = Array.isArray(matcher) ? matcher : [matcher];
9864
+ return matchers.some((item) => wildcardToRegExp2(item).test(value));
9850
9865
  }
9851
- function splitTarget(target) {
9852
- const at = target.indexOf("@");
9853
- if (at === -1)
9854
- return [null, target];
9855
- return [target.slice(0, at), target.slice(at + 1)];
9866
+ function matchRecord2(input, matcher) {
9867
+ if (!matcher)
9868
+ return true;
9869
+ return Object.entries(matcher).every(([path, expected]) => {
9870
+ const actual = getPathValue2(input, path);
9871
+ if (typeof expected === "string" || Array.isArray(expected)) {
9872
+ return matchString2(actual === undefined ? undefined : String(actual), expected);
9873
+ }
9874
+ return actual === expected;
9875
+ });
9856
9876
  }
9857
- function defaultScreenPasswordSecretKey(machineId) {
9858
- return `${DEFAULT_SCREEN_SECRET_NAMESPACE}/screen-${machineId}-vnc-password`;
9877
+ function eventMatchesFilter2(event, filter) {
9878
+ 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);
9859
9879
  }
9860
- function resolveScreenTarget(machineId, options = {}) {
9861
- const resolved = resolveMachineRoute(machineId, options);
9862
- if (!resolved.ok || !resolved.target) {
9863
- throw new Error(`Machine route not found: ${machineId}`);
9880
+ function channelMatchesEvent2(channel, event) {
9881
+ if (!channel.enabled)
9882
+ return false;
9883
+ if (!channel.filters || channel.filters.length === 0)
9884
+ return true;
9885
+ return channel.filters.some((filter) => eventMatchesFilter2(event, filter));
9886
+ }
9887
+ var HASNA_EVENTS_DIR_ENV2 = "HASNA_EVENTS_DIR";
9888
+ var HASNA_EVENTS_HOME_ENV2 = "HASNA_EVENTS_HOME";
9889
+ function getEventsDataDir2(override) {
9890
+ return override || process.env[HASNA_EVENTS_DIR_ENV2] || process.env[HASNA_EVENTS_HOME_ENV2] || join7(homedir6(), ".hasna", "events");
9891
+ }
9892
+
9893
+ class JsonEventsStore2 {
9894
+ dataDir;
9895
+ channelsPath;
9896
+ eventsPath;
9897
+ deliveriesPath;
9898
+ constructor(dataDir = getEventsDataDir2()) {
9899
+ this.dataDir = dataDir;
9900
+ this.channelsPath = join7(dataDir, "channels.json");
9901
+ this.eventsPath = join7(dataDir, "events.json");
9902
+ this.deliveriesPath = join7(dataDir, "deliveries.json");
9864
9903
  }
9865
- if (resolved.route === "unknown") {
9866
- throw new Error(`Machine route is not reachable for screen sharing: ${machineId}`);
9904
+ async init() {
9905
+ await mkdir2(this.dataDir, { recursive: true, mode: 448 });
9906
+ await chmod2(this.dataDir, 448).catch(() => {
9907
+ return;
9908
+ });
9909
+ await this.ensureArrayFile(this.channelsPath);
9910
+ await this.ensureArrayFile(this.eventsPath);
9911
+ await this.ensureArrayFile(this.deliveriesPath);
9867
9912
  }
9868
- let [user, host] = splitTarget(resolved.target);
9869
- if (!user) {
9870
- const topology = options.topology ?? discoverMachineTopology(options);
9871
- const entry = topology.machines.find((m) => m.machine_id === (resolved.machine_id ?? machineId));
9872
- user = entry?.user ?? null;
9913
+ async addChannel(channel) {
9914
+ await this.init();
9915
+ const channels = await this.readJson(this.channelsPath, []);
9916
+ const index = channels.findIndex((item) => item.id === channel.id);
9917
+ if (index >= 0) {
9918
+ channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
9919
+ } else {
9920
+ channels.push(channel);
9921
+ }
9922
+ await this.writeJson(this.channelsPath, channels);
9923
+ return index >= 0 ? channels[index] : channel;
9873
9924
  }
9874
- const url = user ? `vnc://${user}@${host}` : `vnc://${host}`;
9875
- return {
9876
- machineId: resolved.machine_id ?? machineId,
9877
- user,
9878
- host,
9879
- url,
9880
- route: resolved.route,
9881
- confidence: resolved.confidence,
9882
- warnings: resolved.warnings
9883
- };
9884
- }
9885
- function resolveScreenCredentials(machineId, options = {}) {
9886
- const topology = options.topology ?? discoverMachineTopology(options);
9887
- const screen = resolveScreenTarget(machineId, { ...options, topology });
9888
- const entry = topology.machines.find((machine) => machine.machine_id === screen.machineId);
9889
- const metadata = entry?.metadata;
9890
- const metadataUser = metadataString2(metadata, ["screenUser", "screen_user", "user", "username"]);
9891
- const metadataPasswordSecret = metadataString2(metadata, [
9892
- "screenPasswordSecret",
9893
- "screen_password_secret",
9894
- "screenVncPasswordSecret",
9895
- "screen_vnc_password_secret",
9896
- "vncPasswordSecret",
9897
- "vnc_password_secret"
9898
- ]);
9899
- const user = options.user ?? screen.user ?? metadataUser;
9900
- const passwordSecretKey = options.passwordSecretKey ?? metadataPasswordSecret ?? defaultScreenPasswordSecretKey(screen.machineId);
9901
- return {
9902
- machineId: screen.machineId,
9903
- user: user ?? null,
9904
- userSource: options.user ? "option" : screen.user ? "route" : metadataUser ? "metadata" : "missing",
9905
- passwordSecretKey,
9906
- passwordSecretSource: options.passwordSecretKey ? "option" : metadataPasswordSecret ? "metadata" : "default"
9907
- };
9908
- }
9909
- function buildScreenEnableRemoteCommandFromStdin(user) {
9910
- const kickstart = "/System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart";
9911
- const script = [
9912
- "set -euo pipefail",
9913
- 'user="$1"',
9914
- "IFS= read -r vnc_pw",
9915
- 'if [ -z "$vnc_pw" ]; then echo "missing VNC password on stdin" >&2; exit 1; fi',
9916
- `kickstart=${shellQuote6(kickstart)}`,
9917
- 'dseditgroup -o edit -a "$user" -t user com.apple.access_screensharing 2>/dev/null || true',
9918
- "defaults write /Library/Preferences/com.apple.RemoteManagement AllowSRPForNetworkNodes -bool true",
9919
- '"$kickstart" -configure -clientopts -setvnclegacy -vnclegacy yes -setvncpw -vncpw "$vnc_pw"',
9920
- '"$kickstart" -activate -configure -access -on -users "$user" -privs -all -restart -agent -menu'
9921
- ].join(`
9922
- `);
9923
- return `sudo -n -p '' /bin/bash -c ${shellQuote6(script)} -- ${shellQuote6(user)}`;
9924
- }
9925
- function buildScreenEnableCommand(machineId, options = {}) {
9926
- const credentials = resolveScreenCredentials(machineId, options);
9927
- if (!credentials.user) {
9928
- throw new Error(`No screen-sharing user known for ${machineId}; pass --user <name> or set metadata.user in the manifest.`);
9925
+ async listChannels() {
9926
+ await this.init();
9927
+ return this.readJson(this.channelsPath, []);
9929
9928
  }
9930
- const secretsCommand = options.secretsCommand || "secrets";
9931
- const remoteCommand = buildScreenEnableRemoteCommandFromStdin(credentials.user);
9932
- return {
9933
- machineId: credentials.machineId,
9934
- user: credentials.user,
9935
- passwordSecretKey: credentials.passwordSecretKey,
9936
- remoteCommand,
9937
- command: `${shellCommand2([secretsCommand, "get", credentials.passwordSecretKey])} | ${buildSshCommand(machineId, remoteCommand, options)}`
9938
- };
9939
- }
9940
-
9941
- // src/commands/sync.ts
9942
- import { existsSync as existsSync8, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
9943
- import { homedir as homedir6 } from "os";
9944
- init_paths();
9945
- init_db();
9946
- function quote4(value) {
9947
- return `'${value.replace(/'/g, `'\\''`)}'`;
9948
- }
9949
- function packageCheckCommand(machine, packageName, manager = machine.platform === "macos" ? "brew" : "apt") {
9950
- if (manager === "bun") {
9951
- return `bun pm ls -g --all | grep -F ${quote4(packageName)}`;
9929
+ async getChannel(id) {
9930
+ const channels = await this.listChannels();
9931
+ return channels.find((channel) => channel.id === id);
9952
9932
  }
9953
- if (manager === "brew") {
9954
- return `brew list --versions ${quote4(packageName)}`;
9933
+ async removeChannel(id) {
9934
+ await this.init();
9935
+ const channels = await this.readJson(this.channelsPath, []);
9936
+ const next = channels.filter((channel) => channel.id !== id);
9937
+ await this.writeJson(this.channelsPath, next);
9938
+ return next.length !== channels.length;
9955
9939
  }
9956
- if (manager === "apt") {
9957
- return `dpkg -s ${quote4(packageName)} >/dev/null 2>&1`;
9940
+ async appendEvent(event) {
9941
+ await this.init();
9942
+ const events = await this.readJson(this.eventsPath, []);
9943
+ events.push(event);
9944
+ await this.writeJson(this.eventsPath, events);
9945
+ return event;
9958
9946
  }
9959
- return `command -v ${quote4(packageName)} >/dev/null 2>&1`;
9960
- }
9961
- function packageInstallCommand(machine, packageName, manager = machine.platform === "macos" ? "brew" : "apt") {
9962
- if (manager === "bun") {
9963
- return `bun install -g ${quote4(packageName)}`;
9947
+ async listEvents() {
9948
+ await this.init();
9949
+ return this.readJson(this.eventsPath, []);
9964
9950
  }
9965
- if (manager === "brew") {
9966
- return `brew install ${quote4(packageName)}`;
9951
+ async findEventByIdentity(identity) {
9952
+ const events = await this.listEvents();
9953
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
9967
9954
  }
9968
- if (manager === "apt") {
9969
- return `sudo apt-get install -y ${quote4(packageName)}`;
9955
+ async appendDelivery(result) {
9956
+ await this.init();
9957
+ const deliveries = await this.readJson(this.deliveriesPath, []);
9958
+ deliveries.push(result);
9959
+ await this.writeJson(this.deliveriesPath, deliveries);
9960
+ return result;
9970
9961
  }
9971
- return packageName;
9972
- }
9973
- function detectPackageActions(machine) {
9974
- return (machine.packages || []).map((pkg, index) => {
9975
- const manager = pkg.manager || (machine.platform === "macos" ? "brew" : "apt");
9976
- const check = Bun.spawnSync(["bash", "-lc", packageCheckCommand(machine, pkg.name, manager)], {
9977
- stdout: "ignore",
9978
- stderr: "ignore",
9979
- env: process.env
9980
- });
9981
- const installed = check.exitCode === 0;
9962
+ async listDeliveries() {
9963
+ await this.init();
9964
+ return this.readJson(this.deliveriesPath, []);
9965
+ }
9966
+ async exportData() {
9982
9967
  return {
9983
- id: `package-${index + 1}`,
9984
- title: `${installed ? "Package present" : "Install package"} ${pkg.name}`,
9985
- command: packageInstallCommand(machine, pkg.name, manager),
9986
- status: installed ? "ok" : "missing",
9987
- kind: "package"
9968
+ channels: await this.listChannels(),
9969
+ events: await this.listEvents(),
9970
+ deliveries: await this.listDeliveries()
9988
9971
  };
9989
- });
9990
- }
9991
- function detectFileActions(machine) {
9992
- return (machine.files || []).map((file, index) => {
9993
- const sourceExists = existsSync8(file.source);
9994
- const targetExists = existsSync8(file.target);
9995
- let status = "missing";
9996
- if (sourceExists && targetExists) {
9997
- if (file.mode === "symlink") {
9998
- status = lstatSync(file.target).isSymbolicLink() ? "ok" : "drifted";
9999
- } else {
10000
- const source = readFileSync5(file.source, "utf8");
10001
- const target = readFileSync5(file.target, "utf8");
10002
- status = source === target ? "ok" : "drifted";
10003
- }
10004
- }
10005
- const command = file.mode === "symlink" ? `ln -sfn ${quote4(file.source)} ${quote4(file.target)}` : `cp ${quote4(file.source)} ${quote4(file.target)}`;
10006
- return {
10007
- id: `file-${index + 1}`,
10008
- title: `${status === "ok" ? "File in sync" : "Reconcile file"} ${file.target}`,
10009
- command,
10010
- status,
10011
- kind: "file"
10012
- };
10013
- });
10014
- }
10015
- function buildSyncPlan(machineId) {
10016
- const manifest = readManifest();
10017
- const currentMachineId = getLocalMachineId();
10018
- const selected = machineId ? manifest.machines.find((machine) => machine.id === machineId) : manifest.machines.find((machine) => machine.id === currentMachineId);
10019
- const target = selected || {
10020
- id: currentMachineId,
10021
- platform: "linux",
10022
- workspacePath: `${homedir6()}/workspace`
10023
- };
10024
- const actions = [
10025
- ...detectPackageActions(target),
10026
- ...detectFileActions(target)
10027
- ];
10028
- return {
10029
- machineId: target.id,
10030
- mode: "plan",
10031
- actions,
10032
- executed: 0
10033
- };
10034
- }
10035
- function applyFileAction(command) {
10036
- const [verb, source, target] = command.split(" ");
10037
- if (verb === "cp" && source && target) {
10038
- ensureParentDir(target);
10039
- copyFileSync(source.slice(1, -1), target.slice(1, -1));
10040
- return;
10041
9972
  }
10042
- if (verb === "ln" && source && target) {
10043
- const sourcePath = command.match(/ln -sfn '(.+)' '(.+)'/)?.[1];
10044
- const targetPath = command.match(/ln -sfn '(.+)' '(.+)'/)?.[2];
10045
- if (!sourcePath || !targetPath) {
10046
- throw new Error(`Unable to parse symlink command: ${command}`);
9973
+ async ensureArrayFile(path) {
9974
+ if (!existsSync8(path)) {
9975
+ await writeFile2(path, `[]
9976
+ `, { encoding: "utf-8", mode: 384 });
10047
9977
  }
10048
- ensureParentDir(targetPath);
10049
- try {
10050
- Bun.file(targetPath).delete();
10051
- } catch {}
10052
- symlinkSync(sourcePath, targetPath);
10053
- }
10054
- }
10055
- function runSync(machineId, options = {}) {
10056
- const plan = buildSyncPlan(machineId);
10057
- if (!options.apply) {
10058
- return plan;
10059
- }
10060
- if (!options.yes) {
10061
- throw new Error("Sync execution requires --yes.");
9978
+ await chmod2(path, 384).catch(() => {
9979
+ return;
9980
+ });
10062
9981
  }
10063
- let executed = 0;
10064
- for (const action of plan.actions) {
10065
- if (action.status === "ok")
10066
- continue;
10067
- if (action.kind === "file") {
10068
- applyFileAction(action.command);
10069
- } else {
10070
- const result = Bun.spawnSync(["bash", "-lc", action.command], {
10071
- stdout: "pipe",
10072
- stderr: "pipe",
10073
- env: process.env
10074
- });
10075
- if (result.exitCode !== 0) {
10076
- recordSyncRun(plan.machineId, "failed", {
10077
- executed,
10078
- failedAction: action,
10079
- stderr: result.stderr.toString()
10080
- });
10081
- throw new Error(`Sync action failed (${action.id}): ${result.stderr.toString().trim()}`);
10082
- }
9982
+ async readJson(path, fallback) {
9983
+ try {
9984
+ const raw = await readFile2(path, "utf-8");
9985
+ if (!raw.trim())
9986
+ return fallback;
9987
+ return JSON.parse(raw);
9988
+ } catch (error) {
9989
+ if (error.code === "ENOENT")
9990
+ return fallback;
9991
+ throw error;
10083
9992
  }
10084
- executed += 1;
10085
9993
  }
10086
- const summary = {
10087
- machineId: plan.machineId,
10088
- mode: "apply",
10089
- actions: plan.actions,
10090
- executed
10091
- };
10092
- recordSyncRun(plan.machineId, "completed", summary);
10093
- return summary;
9994
+ async writeJson(path, value) {
9995
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
9996
+ await writeFile2(tempPath, `${JSON.stringify(value, null, 2)}
9997
+ `, { encoding: "utf-8", mode: 384 });
9998
+ await rename2(tempPath, path);
9999
+ await chmod2(path, 384).catch(() => {
10000
+ return;
10001
+ });
10002
+ }
10094
10003
  }
10095
-
10096
- // src/commands/status.ts
10097
- init_db();
10098
- init_paths();
10099
- function getStatus() {
10100
- const manifest = readManifest();
10101
- const heartbeats = listHeartbeats();
10102
- const heartbeatByMachine = new Map(heartbeats.map((heartbeat) => [heartbeat.machine_id, heartbeat]));
10103
- const machineIds = new Set([
10104
- ...manifest.machines.map((machine) => machine.id),
10105
- ...heartbeats.map((heartbeat) => heartbeat.machine_id)
10106
- ]);
10107
- return {
10108
- machineId: getLocalMachineId(),
10109
- manifestPath: getManifestPath(),
10110
- dbPath: getDbPath(),
10111
- notificationsPath: getNotificationsPath(),
10112
- manifestMachineCount: manifest.machines.length,
10113
- heartbeatCount: heartbeats.length,
10114
- machines: [...machineIds].sort().map((machineId) => {
10115
- const declared = manifest.machines.find((machine) => machine.id === machineId);
10116
- const heartbeat = heartbeatByMachine.get(machineId);
10117
- return {
10118
- machineId,
10119
- platform: declared?.platform,
10120
- manifestDeclared: Boolean(declared),
10121
- heartbeatStatus: heartbeat?.status || "unknown",
10122
- lastHeartbeatAt: heartbeat?.updated_at
10123
- };
10124
- }),
10125
- recentSetupRuns: countRuns("setup_runs"),
10126
- recentSyncRuns: countRuns("sync_runs")
10127
- };
10004
+ var DEFAULT_SIGNATURE_TOLERANCE_MS2 = 5 * 60 * 1000;
10005
+ function buildSignatureBase2(timestamp, body) {
10006
+ return `${timestamp}.${body}`;
10128
10007
  }
10129
-
10130
- // src/commands/workspace.ts
10131
- init_paths();
10132
- function isRecord2(value) {
10133
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
10008
+ function signPayload2(secret, timestamp, body) {
10009
+ const digest = createHmac2("sha256", secret).update(buildSignatureBase2(timestamp, body)).digest("hex");
10010
+ return `sha256=${digest}`;
10134
10011
  }
10135
- function cloneMetadata(metadata) {
10136
- return isRecord2(metadata) ? { ...metadata } : {};
10012
+ function now2() {
10013
+ return new Date().toISOString();
10137
10014
  }
10138
- function mappedPath(metadata, field, key) {
10139
- const container = metadata[field];
10140
- if (!isRecord2(container))
10141
- return null;
10142
- const value = container[key];
10143
- if (typeof value === "string" && value.trim())
10144
- return value.trim();
10145
- if (isRecord2(value)) {
10146
- const nested = value["path"] ?? value["root"] ?? value["workspacePath"] ?? value["workspace_path"];
10147
- if (typeof nested === "string" && nested.trim())
10148
- return nested.trim();
10149
- }
10150
- return null;
10015
+ function truncate2(value, max = 4096) {
10016
+ return value.length > max ? `${value.slice(0, max)}...` : value;
10151
10017
  }
10152
- function writeMappedPath(metadata, field, key, path) {
10153
- const existing = metadata[field];
10154
- const container = isRecord2(existing) ? { ...existing } : {};
10155
- container[key] = path;
10156
- metadata[field] = container;
10018
+ function buildWebhookRequest2(event, channel) {
10019
+ if (!channel.webhook)
10020
+ throw new Error(`Channel ${channel.id} has no webhook config`);
10021
+ const body = JSON.stringify(event);
10022
+ const timestamp = event.time;
10023
+ const headers = {
10024
+ "Content-Type": "application/json",
10025
+ "User-Agent": "@hasna/events",
10026
+ "X-Hasna-Event-Id": event.id,
10027
+ "X-Hasna-Event-Type": event.type,
10028
+ "X-Hasna-Timestamp": timestamp,
10029
+ ...channel.webhook.headers
10030
+ };
10031
+ if (channel.webhook.secret) {
10032
+ headers["X-Hasna-Signature"] = signPayload2(channel.webhook.secret, timestamp, body);
10033
+ }
10034
+ return { body, headers };
10157
10035
  }
10158
- function buildPatch(input) {
10159
- const previous = mappedPath(input.metadata, input.field, input.key);
10160
- if (!input.path) {
10036
+ async function dispatchWebhook3(event, channel, options = {}) {
10037
+ if (!channel.webhook)
10038
+ throw new Error(`Channel ${channel.id} has no webhook config`);
10039
+ const startedAt = now2();
10040
+ const { body, headers } = buildWebhookRequest2(event, channel);
10041
+ const controller = new AbortController;
10042
+ const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
10043
+ try {
10044
+ const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
10045
+ method: "POST",
10046
+ headers,
10047
+ body,
10048
+ signal: controller.signal
10049
+ });
10050
+ const responseBody = truncate2(await response.text());
10161
10051
  return {
10162
- field: input.field,
10163
- key: input.key,
10164
- path: null,
10165
- previous_path: previous,
10166
- status: "unresolved"
10052
+ attempt: 1,
10053
+ status: response.ok ? "success" : "failed",
10054
+ startedAt,
10055
+ completedAt: now2(),
10056
+ responseStatus: response.status,
10057
+ responseBody,
10058
+ error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
10059
+ };
10060
+ } catch (error) {
10061
+ return {
10062
+ attempt: 1,
10063
+ status: "failed",
10064
+ startedAt,
10065
+ completedAt: now2(),
10066
+ error: error instanceof Error ? error.message : String(error)
10167
10067
  };
10068
+ } finally {
10069
+ clearTimeout(timeout);
10168
10070
  }
10169
- const status = previous === input.path ? "unchanged" : input.apply ? "written" : "would_write";
10170
- return {
10171
- field: input.field,
10172
- key: input.key,
10173
- path: input.path,
10174
- previous_path: previous,
10175
- status
10176
- };
10177
10071
  }
10178
- function upsertMachineMetadata(manifest, machineId, metadata) {
10179
- return {
10180
- ...manifest,
10181
- machines: manifest.machines.map((machine) => machine.id === machineId ? { ...machine, metadata } : machine)
10072
+ async function dispatchCommand3(event, channel) {
10073
+ if (!channel.command)
10074
+ throw new Error(`Channel ${channel.id} has no command config`);
10075
+ const startedAt = now2();
10076
+ const eventJson = JSON.stringify(event);
10077
+ const env2 = {
10078
+ ...process.env,
10079
+ ...channel.command.env,
10080
+ HASNA_CHANNEL_ID: channel.id,
10081
+ HASNA_EVENT_ID: event.id,
10082
+ HASNA_EVENT_TYPE: event.type,
10083
+ HASNA_EVENT_SOURCE: event.source,
10084
+ HASNA_EVENT_SUBJECT: event.subject ?? "",
10085
+ HASNA_EVENT_SEVERITY: event.severity,
10086
+ HASNA_EVENT_TIME: event.time,
10087
+ HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
10088
+ HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
10089
+ HASNA_EVENT_JSON: eventJson
10182
10090
  };
10183
- }
10184
- function repairWorkspaceManifestMappings(options) {
10185
- const projectId = options.projectId;
10186
- const repoName = options.repoName ?? projectId;
10187
- const openFilesRepoName = options.openFilesRepoName ?? "open-files";
10188
- const apply = options.apply === true;
10189
- const resolution = resolveMachineWorkspace({
10190
- machineId: options.machineId,
10191
- projectId,
10192
- repoName,
10193
- openFilesRepoName,
10194
- projectRoot: options.projectRoot,
10195
- openFilesRoot: options.openFilesRoot,
10196
- workspaceRoot: options.workspaceRoot,
10197
- includeTailscale: options.includeTailscale,
10198
- now: options.now
10091
+ return new Promise((resolve2) => {
10092
+ const child = spawn2(channel.command.command, channel.command.args ?? [], {
10093
+ cwd: channel.command.cwd,
10094
+ env: env2,
10095
+ stdio: ["pipe", "pipe", "pipe"]
10096
+ });
10097
+ let stdout = "";
10098
+ let stderr = "";
10099
+ const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
10100
+ child.stdin.end(eventJson);
10101
+ child.stdout.on("data", (chunk) => {
10102
+ stdout += chunk.toString();
10103
+ });
10104
+ child.stderr.on("data", (chunk) => {
10105
+ stderr += chunk.toString();
10106
+ });
10107
+ child.on("error", (error) => {
10108
+ clearTimeout(timeout);
10109
+ resolve2({
10110
+ attempt: 1,
10111
+ status: "failed",
10112
+ startedAt,
10113
+ completedAt: now2(),
10114
+ stdout: truncate2(stdout),
10115
+ stderr: truncate2(stderr),
10116
+ error: error.message
10117
+ });
10118
+ });
10119
+ child.on("close", (code, signal) => {
10120
+ clearTimeout(timeout);
10121
+ const success = code === 0;
10122
+ resolve2({
10123
+ attempt: 1,
10124
+ status: success ? "success" : "failed",
10125
+ startedAt,
10126
+ completedAt: now2(),
10127
+ stdout: truncate2(stdout),
10128
+ stderr: truncate2(stderr),
10129
+ error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
10130
+ });
10131
+ });
10199
10132
  });
10200
- const warnings = [...resolution.warnings];
10201
- const manifest = readManifest();
10202
- const manifestMachineId = resolution.machine_id ?? options.machineId;
10203
- const machine = manifest.machines.find((entry) => entry.id === manifestMachineId) ?? null;
10204
- const trusted = resolution.machine.trust_status === "trusted" || options.allowUntrusted === true;
10205
- if (!machine) {
10206
- warnings.push(`manifest_machine_missing:${manifestMachineId}`);
10207
- return {
10208
- ok: false,
10209
- applied: false,
10210
- manifest_path: getManifestPath(),
10211
- machine_id: resolution.machine_id,
10212
- project_id: projectId,
10213
- repo_name: repoName,
10214
- open_files_repo_name: openFilesRepoName,
10215
- trusted,
10216
- resolution,
10217
- patches: [],
10218
- warnings
10219
- };
10220
- }
10221
- const metadata = cloneMetadata(machine.metadata);
10222
- const patches = [
10223
- buildPatch({
10224
- metadata,
10225
- field: "workspace_paths",
10226
- key: projectId,
10227
- path: options.projectRoot ?? resolution.paths.project_root.path,
10228
- apply
10229
- }),
10230
- buildPatch({
10231
- metadata,
10232
- field: "open_files_roots",
10233
- key: projectId,
10234
- path: options.openFilesRoot ?? resolution.paths.open_files_root.path,
10235
- apply
10236
- })
10237
- ];
10238
- const hasUnresolved = patches.some((patch) => patch.status === "unresolved");
10239
- const hasWrites = patches.some((patch) => patch.status === "would_write" || patch.status === "written");
10240
- if (apply && hasWrites && !trusted) {
10241
- warnings.push(`manifest_repair_requires_trusted_machine:${manifestMachineId}`);
10242
- return {
10243
- ok: false,
10244
- applied: false,
10245
- manifest_path: getManifestPath(),
10246
- machine_id: manifestMachineId,
10247
- project_id: projectId,
10248
- repo_name: repoName,
10249
- open_files_repo_name: openFilesRepoName,
10250
- trusted,
10251
- resolution,
10252
- patches: patches.map((patch) => patch.status === "written" ? { ...patch, status: "would_write" } : patch),
10253
- warnings
10254
- };
10255
- }
10256
- let applied = false;
10257
- if (apply && !hasUnresolved && hasWrites) {
10258
- for (const patch of patches) {
10259
- if (patch.path && patch.status === "written")
10260
- writeMappedPath(metadata, patch.field, patch.key, patch.path);
10261
- }
10262
- writeManifest(upsertMachineMetadata(manifest, manifestMachineId, metadata));
10263
- applied = true;
10264
- }
10133
+ }
10134
+ async function dispatchChannel3(event, channel, options = {}) {
10135
+ if (channel.transport === "webhook")
10136
+ return dispatchWebhook3(event, channel, options);
10137
+ if (channel.transport === "command")
10138
+ return dispatchCommand3(event, channel);
10265
10139
  return {
10266
- ok: resolution.ok && !hasUnresolved && (!apply || applied || !hasWrites),
10267
- applied,
10268
- manifest_path: getManifestPath(),
10269
- machine_id: manifestMachineId,
10270
- project_id: projectId,
10271
- repo_name: repoName,
10272
- open_files_repo_name: openFilesRepoName,
10273
- trusted,
10274
- resolution,
10275
- patches,
10276
- warnings
10140
+ attempt: 1,
10141
+ status: "skipped",
10142
+ startedAt: now2(),
10143
+ completedAt: now2(),
10144
+ error: `Unsupported transport: ${channel.transport}`
10277
10145
  };
10278
10146
  }
10279
-
10280
- // src/compatibility.ts
10281
- init_db();
10282
- var DEFAULT_COMMANDS = [
10283
- { command: "bun", required: true },
10284
- { command: "machines", required: true }
10285
- ];
10286
- function defaultPackages() {
10287
- return [{ name: "@hasna/machines", command: "machines", expectedVersion: getPackageVersion(), required: true }];
10288
- }
10289
- function shellQuote7(value) {
10290
- return `'${value.replace(/'/g, "'\\''")}'`;
10291
- }
10292
- function commandId(value) {
10293
- return value.replace(/[^a-zA-Z0-9_.@/-]+/g, "-").replace(/^-+|-+$/g, "");
10294
- }
10295
- function packageCommand(name) {
10296
- if (name === "@hasna/knowledge")
10297
- return "knowledge";
10298
- if (name === "@hasna/machines")
10299
- return "machines";
10300
- return name.split("/").pop() ?? name;
10301
- }
10302
- function firstLine(value) {
10303
- return value.trim().split(/\r?\n/).find(Boolean) ?? "";
10304
- }
10305
- function extractVersion(value) {
10306
- const match = value.match(/\b\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?\b/);
10307
- return match?.[0] ?? null;
10308
- }
10309
- function statusFor(required, ok) {
10310
- if (ok)
10311
- return "ok";
10312
- return required === false ? "warn" : "fail";
10147
+ function createDeliveryResult2(event, channel, attempts) {
10148
+ const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
10149
+ return {
10150
+ id: randomUUID3(),
10151
+ eventId: event.id,
10152
+ channelId: channel.id,
10153
+ transport: channel.transport,
10154
+ status,
10155
+ attempts,
10156
+ createdAt: attempts[0]?.startedAt ?? now2(),
10157
+ completedAt: attempts.at(-1)?.completedAt ?? now2()
10158
+ };
10313
10159
  }
10314
- function makeCheck(input) {
10160
+ function createEvent2(input) {
10315
10161
  return {
10316
- id: input.id,
10317
- kind: input.kind,
10318
- status: input.status,
10319
- target: input.target,
10320
- expected: input.expected ?? null,
10321
- actual: input.actual ?? null,
10322
- detail: input.detail,
10323
- source: input.source
10162
+ id: input.id ?? randomUUID22(),
10163
+ source: input.source,
10164
+ type: input.type,
10165
+ time: normalizeTime2(input.time),
10166
+ subject: input.subject,
10167
+ severity: input.severity ?? "info",
10168
+ data: input.data ?? {},
10169
+ message: input.message,
10170
+ dedupeKey: input.dedupeKey,
10171
+ schemaVersion: input.schemaVersion ?? "1.0",
10172
+ metadata: input.metadata ?? {}
10324
10173
  };
10325
10174
  }
10326
- function parseKeyValue(stdout) {
10327
- const result = {};
10328
- for (const line of stdout.split(/\r?\n/)) {
10329
- const idx = line.indexOf("=");
10330
- if (idx <= 0)
10331
- continue;
10332
- result[line.slice(0, idx)] = line.slice(idx + 1);
10175
+
10176
+ class EventsClient2 {
10177
+ store;
10178
+ redactors;
10179
+ transportOptions;
10180
+ constructor(options = {}) {
10181
+ this.store = options.store ?? new JsonEventsStore2(options.dataDir);
10182
+ this.redactors = options.redactors ?? [];
10183
+ this.transportOptions = { fetchImpl: options.fetchImpl };
10333
10184
  }
10334
- return result;
10335
- }
10336
- function defaultRunner2(machineId, command) {
10337
- return runMachineCommand(machineId, command);
10338
- }
10339
- function inspectCommand(machineId, spec, runner) {
10340
- const command = shellQuote7(spec.command);
10341
- const versionArgs = spec.versionArgs ?? "--version";
10342
- const script = [
10343
- `cmd=${command}`,
10344
- 'path="$(command -v "$cmd" 2>/dev/null || true)"',
10345
- 'printf "path=%s\\n" "$path"',
10346
- 'if [ -n "$path" ]; then version="$("$cmd" ' + versionArgs + ' 2>/dev/null || true)"; printf "version=%s\\n" "$version"; fi'
10347
- ].join("; ");
10348
- const result = runner(machineId, script);
10349
- const parsed = parseKeyValue(result.stdout);
10350
- return {
10351
- path: parsed.path || null,
10352
- version: parsed.version ? firstLine(parsed.version) : null,
10353
- exitCode: result.exitCode,
10354
- source: result.source,
10355
- stderr: result.stderr
10356
- };
10357
- }
10358
- function fieldCommand(field) {
10359
- const regex = field === "name" ? String.raw`s/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p` : String.raw`s/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p`;
10360
- return [
10361
- `if command -v bun >/dev/null 2>&1; then bun -e "const p=JSON.parse(await Bun.file(process.argv[1]).text()); console.log(p.${field} ?? '')" "$pkg" 2>/dev/null`,
10362
- `elif command -v node >/dev/null 2>&1; then node -e "const fs=require('fs'); const p=JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); console.log(p.${field} || '')" "$pkg" 2>/dev/null`,
10363
- `else sed -n '${regex}' "$pkg" | head -n 1`,
10364
- "fi"
10365
- ].join("; ");
10366
- }
10367
- function inspectWorkspace(machineId, spec, runner) {
10368
- const script = [
10369
- `path=${shellQuote7(spec.path)}`,
10370
- 'printf "exists=%s\\n" "$(test -d "$path" && printf yes || printf no)"',
10371
- 'pkg="$path/package.json"',
10372
- 'printf "package_json=%s\\n" "$(test -f "$pkg" && printf yes || printf no)"',
10373
- `if [ -f "$pkg" ]; then printf "package_name=%s\\n" "$(${fieldCommand("name")})"; printf "version=%s\\n" "$(${fieldCommand("version")})"; fi`
10374
- ].join("; ");
10375
- const result = runner(machineId, script);
10376
- const parsed = parseKeyValue(result.stdout);
10377
- return {
10378
- exists: parsed.exists === "yes",
10379
- packageJson: parsed.package_json === "yes",
10380
- packageName: parsed.package_name || null,
10381
- version: parsed.version || null,
10382
- source: result.source,
10383
- stderr: result.stderr
10384
- };
10385
- }
10386
- function commandCheck(machineId, spec, runner) {
10387
- const inspection = inspectCommand(machineId, spec, runner);
10388
- const found = Boolean(inspection.path);
10389
- const checks = [
10390
- makeCheck({
10391
- id: `command:${commandId(spec.command)}:path`,
10392
- kind: "command",
10393
- status: statusFor(spec.required, found),
10394
- target: spec.command,
10395
- expected: "available",
10396
- actual: inspection.path ?? "missing",
10397
- detail: found ? `found at ${inspection.path}` : inspection.stderr || "command missing",
10398
- source: inspection.source
10399
- })
10400
- ];
10401
- if (spec.expectedVersion) {
10402
- const actualVersion = extractVersion(inspection.version ?? "");
10403
- checks.push(makeCheck({
10404
- id: `command:${commandId(spec.command)}:version`,
10405
- kind: "command",
10406
- status: actualVersion === spec.expectedVersion ? "ok" : statusFor(spec.required, false),
10407
- target: spec.command,
10408
- expected: spec.expectedVersion,
10409
- actual: actualVersion ?? inspection.version ?? "missing",
10410
- detail: actualVersion ? `version output: ${inspection.version}` : "version unavailable",
10411
- source: inspection.source
10412
- }));
10185
+ async addChannel(input) {
10186
+ const timestamp = new Date().toISOString();
10187
+ return this.store.addChannel({
10188
+ ...input,
10189
+ createdAt: input.createdAt ?? timestamp,
10190
+ updatedAt: input.updatedAt ?? timestamp
10191
+ });
10192
+ }
10193
+ async listChannels() {
10194
+ return this.store.listChannels();
10195
+ }
10196
+ async removeChannel(id) {
10197
+ return this.store.removeChannel(id);
10198
+ }
10199
+ async emit(input, options = {}) {
10200
+ const event = options.redactSensitiveData === false ? createEvent2(input) : redactSensitiveKeys2(createEvent2(input));
10201
+ if (options.dedupe !== false) {
10202
+ const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
10203
+ if (existing) {
10204
+ return { event: existing, deliveries: [], deduped: true };
10205
+ }
10206
+ }
10207
+ await this.store.appendEvent(event);
10208
+ const deliveries = options.deliver === false ? [] : await this.deliver(event);
10209
+ return { event, deliveries, deduped: false };
10210
+ }
10211
+ async listEvents() {
10212
+ return this.store.listEvents();
10213
+ }
10214
+ async listDeliveries() {
10215
+ return this.store.listDeliveries();
10216
+ }
10217
+ async deliver(event) {
10218
+ const channels = await this.store.listChannels();
10219
+ const selected = channels.filter((channel) => channelMatchesEvent2(channel, event));
10220
+ const deliveries = [];
10221
+ for (const channel of selected) {
10222
+ const eventForChannel = await this.applyRedaction(event, channel);
10223
+ const result = await this.deliverWithRetry(eventForChannel, channel);
10224
+ await this.store.appendDelivery(result);
10225
+ deliveries.push(result);
10226
+ }
10227
+ return deliveries;
10228
+ }
10229
+ async testChannel(id, input = {}) {
10230
+ const channel = await this.store.getChannel(id);
10231
+ if (!channel)
10232
+ throw new Error(`Channel not found: ${id}`);
10233
+ const event = createEvent2({
10234
+ source: input.source ?? "hasna.events",
10235
+ type: input.type ?? "events.test",
10236
+ subject: input.subject ?? id,
10237
+ severity: input.severity ?? "info",
10238
+ data: input.data ?? { test: true },
10239
+ message: input.message ?? "Hasna events test delivery",
10240
+ dedupeKey: input.dedupeKey,
10241
+ schemaVersion: input.schemaVersion,
10242
+ metadata: input.metadata,
10243
+ time: input.time,
10244
+ id: input.id
10245
+ });
10246
+ const eventForChannel = await this.applyRedaction(event, channel);
10247
+ const result = await this.deliverWithRetry(eventForChannel, channel);
10248
+ await this.store.appendDelivery(result);
10249
+ return result;
10250
+ }
10251
+ async replay(options = {}) {
10252
+ const events = (await this.store.listEvents()).filter((event) => {
10253
+ if (options.eventId && event.id !== options.eventId)
10254
+ return false;
10255
+ if (options.source && event.source !== options.source)
10256
+ return false;
10257
+ if (options.type && event.type !== options.type)
10258
+ return false;
10259
+ return true;
10260
+ });
10261
+ if (options.dryRun)
10262
+ return { events, deliveries: [] };
10263
+ const deliveries = [];
10264
+ for (const event of events) {
10265
+ deliveries.push(...await this.deliver(event));
10266
+ }
10267
+ return { events, deliveries };
10268
+ }
10269
+ async applyRedaction(event, channel) {
10270
+ let next = redactPaths2(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
10271
+ for (const redactor of this.redactors) {
10272
+ next = await redactor(next, channel);
10273
+ }
10274
+ return next;
10275
+ }
10276
+ async deliverWithRetry(event, channel) {
10277
+ const policy = normalizeRetryPolicy2(channel.retry);
10278
+ const attempts = [];
10279
+ for (let index = 0;index < policy.maxAttempts; index += 1) {
10280
+ const attempt = await dispatchChannel3(event, channel, this.transportOptions);
10281
+ attempt.attempt = index + 1;
10282
+ if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
10283
+ attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
10284
+ }
10285
+ attempts.push(attempt);
10286
+ if (attempt.status !== "failed")
10287
+ break;
10288
+ if (attempt.nextBackoffMs)
10289
+ await Bun.sleep(attempt.nextBackoffMs);
10290
+ }
10291
+ return createDeliveryResult2(event, channel, attempts);
10413
10292
  }
10414
- return checks;
10415
10293
  }
10416
- function packageCheck(machineId, spec, runner) {
10417
- const command = spec.command ?? packageCommand(spec.name);
10418
- const inspection = inspectCommand(machineId, { command, expectedVersion: spec.expectedVersion, required: spec.required }, runner);
10419
- const found = Boolean(inspection.path);
10420
- const checks = [
10421
- makeCheck({
10422
- id: `package:${commandId(spec.name)}:command`,
10423
- kind: "package",
10424
- status: statusFor(spec.required, found),
10425
- target: spec.name,
10426
- expected: command,
10427
- actual: inspection.path ?? "missing",
10428
- detail: found ? `${command} found at ${inspection.path}` : `${command} command missing`,
10429
- source: inspection.source
10430
- })
10431
- ];
10432
- if (spec.expectedVersion) {
10433
- const actualVersion = extractVersion(inspection.version ?? "");
10434
- checks.push(makeCheck({
10435
- id: `package:${commandId(spec.name)}:version`,
10436
- kind: "package",
10437
- status: actualVersion === spec.expectedVersion ? "ok" : statusFor(spec.required, false),
10438
- target: spec.name,
10439
- expected: spec.expectedVersion,
10440
- actual: actualVersion ?? inspection.version ?? "missing",
10441
- detail: actualVersion ? `version output: ${inspection.version}` : "version unavailable",
10442
- source: inspection.source
10443
- }));
10294
+ function redactPaths2(event, paths, replacement = "[REDACTED]") {
10295
+ if (paths.length === 0)
10296
+ return event;
10297
+ const copy = structuredClone(event);
10298
+ for (const path of paths) {
10299
+ setPath2(copy, path, replacement);
10444
10300
  }
10445
- return checks;
10301
+ return copy;
10446
10302
  }
10447
- function workspaceCheck(machineId, spec, runner) {
10448
- const inspection = inspectWorkspace(machineId, spec, runner);
10449
- const target = spec.label ?? spec.path;
10450
- const checks = [
10451
- makeCheck({
10452
- id: `workspace:${commandId(target)}:path`,
10453
- kind: "workspace",
10454
- status: statusFor(spec.required, inspection.exists),
10455
- target,
10456
- expected: spec.path,
10457
- actual: inspection.exists ? "exists" : "missing",
10458
- detail: inspection.exists ? `workspace exists at ${spec.path}` : inspection.stderr || `workspace missing at ${spec.path}`,
10459
- source: inspection.source
10460
- })
10461
- ];
10462
- if (spec.expectedPackageName) {
10463
- checks.push(makeCheck({
10464
- id: `workspace:${commandId(target)}:package-name`,
10465
- kind: "workspace",
10466
- status: inspection.packageName === spec.expectedPackageName ? "ok" : statusFor(spec.required, false),
10467
- target,
10468
- expected: spec.expectedPackageName,
10469
- actual: inspection.packageName ?? (inspection.packageJson ? "missing-name" : "missing-package-json"),
10470
- detail: inspection.packageJson ? "package.json inspected" : "package.json missing",
10471
- source: inspection.source
10472
- }));
10303
+ function sanitizeChannelForOutput2(channel) {
10304
+ const copy = structuredClone(channel);
10305
+ if (copy.webhook?.secret)
10306
+ copy.webhook.secret = "[REDACTED]";
10307
+ if (copy.command?.env) {
10308
+ copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey2(key) ? "[REDACTED]" : value]));
10473
10309
  }
10474
- if (spec.expectedVersion) {
10475
- checks.push(makeCheck({
10476
- id: `workspace:${commandId(target)}:version`,
10477
- kind: "workspace",
10478
- status: inspection.version === spec.expectedVersion ? "ok" : statusFor(spec.required, false),
10479
- target,
10480
- expected: spec.expectedVersion,
10481
- actual: inspection.version ?? (inspection.packageJson ? "missing-version" : "missing-package-json"),
10482
- detail: inspection.packageJson ? "package.json inspected" : "package.json missing",
10483
- source: inspection.source
10484
- }));
10310
+ return copy;
10311
+ }
10312
+ function sanitizeChannelsForOutput2(channels) {
10313
+ return channels.map(sanitizeChannelForOutput2);
10314
+ }
10315
+ function redactSensitiveKeys2(event, replacement = "[REDACTED]") {
10316
+ return redactValue2(event, replacement);
10317
+ }
10318
+ function shouldRedactKey2(key) {
10319
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
10320
+ }
10321
+ function redactValue2(value, replacement) {
10322
+ if (Array.isArray(value))
10323
+ return value.map((item) => redactValue2(item, replacement));
10324
+ if (!value || typeof value !== "object")
10325
+ return value;
10326
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
10327
+ key,
10328
+ shouldRedactKey2(key) ? replacement : redactValue2(item, replacement)
10329
+ ]));
10330
+ }
10331
+ function setPath2(input, path, replacement) {
10332
+ const parts = path.split(".");
10333
+ let cursor = input;
10334
+ for (const part of parts.slice(0, -1)) {
10335
+ const next = cursor[part];
10336
+ if (!next || typeof next !== "object")
10337
+ return;
10338
+ cursor = next;
10485
10339
  }
10486
- return checks;
10340
+ const last = parts.at(-1);
10341
+ if (last && last in cursor)
10342
+ cursor[last] = replacement;
10487
10343
  }
10488
- function checkMachineCompatibility(options = {}) {
10489
- const machineId = options.machineId ?? getLocalMachineId();
10490
- const runner = options.runner ?? defaultRunner2;
10491
- const commands = options.commands ?? DEFAULT_COMMANDS;
10492
- const packages = options.packages ?? defaultPackages();
10493
- const workspaces = options.workspaces ?? [];
10494
- const checks = [];
10495
- for (const spec of commands)
10496
- checks.push(...commandCheck(machineId, spec, runner));
10497
- for (const spec of packages)
10498
- checks.push(...packageCheck(machineId, spec, runner));
10499
- for (const spec of workspaces)
10500
- checks.push(...workspaceCheck(machineId, spec, runner));
10501
- const summary = {
10502
- ok: checks.filter((check) => check.status === "ok").length,
10503
- warn: checks.filter((check) => check.status === "warn").length,
10504
- fail: checks.filter((check) => check.status === "fail").length
10505
- };
10344
+ function normalizeTime2(value) {
10345
+ if (!value)
10346
+ return new Date().toISOString();
10347
+ return value instanceof Date ? value.toISOString() : value;
10348
+ }
10349
+ function normalizeRetryPolicy2(policy) {
10506
10350
  return {
10507
- schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
10508
- package: {
10509
- name: MACHINES_PACKAGE_NAME,
10510
- version: getPackageVersion()
10511
- },
10512
- capabilities: getMachinesConsumerCapabilities(),
10513
- ok: summary.fail === 0,
10514
- machine_id: machineId,
10515
- source: checks[0]?.source ?? "local",
10516
- generated_at: (options.now ?? new Date).toISOString(),
10517
- checks,
10518
- summary
10351
+ maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
10352
+ backoffMs: Math.max(0, policy?.backoffMs ?? 250),
10353
+ multiplier: Math.max(1, policy?.multiplier ?? 2)
10519
10354
  };
10520
10355
  }
10521
10356
 
10522
- // src/commands/doctor.ts
10523
- init_db();
10524
- function makeCheck2(id, status, summary, detail) {
10525
- return { id, status, summary, detail };
10526
- }
10527
- function parseKeyValueOutput(stdout) {
10528
- return Object.fromEntries(stdout.trim().split(`
10529
- `).map((line) => line.split("=")).filter((parts) => parts.length === 2).map(([key, value]) => [key, value]));
10530
- }
10531
- function buildDoctorCommand() {
10532
- return [
10533
- 'data_dir="${HASNA_MACHINES_DIR:-$HOME/.hasna/machines}"',
10534
- 'manifest_path="${HASNA_MACHINES_MANIFEST_PATH:-$data_dir/machines.json}"',
10535
- 'db_path="${HASNA_MACHINES_DB_PATH:-$data_dir/machines.db}"',
10536
- 'notifications_path="${HASNA_MACHINES_NOTIFICATIONS_PATH:-$data_dir/notifications.json}"',
10537
- `printf 'manifest_path=%s\\n' "$manifest_path"`,
10538
- `printf 'db_path=%s\\n' "$db_path"`,
10539
- `printf 'notifications_path=%s\\n' "$notifications_path"`,
10540
- `printf 'manifest_exists=%s\\n' "$(test -e "$manifest_path" && printf yes || printf no)"`,
10541
- `printf 'db_exists=%s\\n' "$(test -e "$db_path" && printf yes || printf no)"`,
10542
- `printf 'notifications_exists=%s\\n' "$(test -e "$notifications_path" && printf yes || printf no)"`,
10543
- `printf 'bun=%s\\n' "$(bun --version 2>/dev/null || printf missing)"`,
10544
- `printf 'ssh=%s\\n' "$(command -v ssh >/dev/null 2>&1 && printf ok || printf missing)"`,
10545
- `printf 'machines=%s\\n' "$(command -v machines 2>/dev/null || printf missing)"`,
10546
- `printf 'machines_agent=%s\\n' "$(command -v machines-agent 2>/dev/null || printf missing)"`,
10547
- `printf 'machines_mcp=%s\\n' "$(command -v machines-mcp 2>/dev/null || printf missing)"`
10548
- ].join("; ");
10357
+ // src/commands/runtime.ts
10358
+ function probeTmuxPane(target, tmuxCommand = process.env["HASNA_MACHINES_TMUX_BIN"] || "tmux") {
10359
+ const checkedAt = new Date().toISOString();
10360
+ const result = spawnSync4(tmuxCommand, ["display-message", "-p", "-t", target, "#{pane_id}"], {
10361
+ encoding: "utf8",
10362
+ timeout: 5000
10363
+ });
10364
+ const stdout = result.stdout?.trim();
10365
+ const stderr = result.stderr?.trim();
10366
+ const exists = result.status === 0 && Boolean(stdout);
10367
+ return {
10368
+ target,
10369
+ exists,
10370
+ paneId: exists ? stdout : undefined,
10371
+ checkedAt,
10372
+ exitCode: result.status,
10373
+ error: result.error?.message,
10374
+ stderr: stderr || undefined
10375
+ };
10549
10376
  }
10550
- function runDoctor(machineId = getLocalMachineId()) {
10551
- const manifest = readManifest();
10552
- const commandChecks = runMachineCommand(machineId, buildDoctorCommand());
10553
- const details = parseKeyValueOutput(commandChecks.stdout);
10554
- const machineInManifest = manifest.machines.find((machine) => machine.id === machineId);
10555
- const checks = [
10556
- makeCheck2("manifest-entry", machineInManifest ? "ok" : "warn", machineInManifest ? "Machine exists in manifest" : "Machine missing from manifest", machineInManifest ? JSON.stringify(machineInManifest) : `No manifest entry for ${machineId}`),
10557
- makeCheck2("manifest-path", details["manifest_exists"] === "yes" ? "ok" : "warn", "Manifest path check", `${details["manifest_path"] || "unknown"} ${details["manifest_exists"] === "yes" ? "exists" : "missing"}`),
10558
- makeCheck2("db-path", details["db_exists"] === "yes" ? "ok" : "warn", "DB path check", `${details["db_path"] || "unknown"} ${details["db_exists"] === "yes" ? "exists" : "missing"}`),
10559
- makeCheck2("notifications-path", details["notifications_exists"] === "yes" ? "ok" : "warn", "Notifications path check", `${details["notifications_path"] || "unknown"} ${details["notifications_exists"] === "yes" ? "exists" : "missing"}`),
10560
- makeCheck2("bun", details["bun"] && details["bun"] !== "missing" ? "ok" : "fail", "Bun availability", details["bun"] || "missing"),
10561
- makeCheck2("machines-cli", details["machines"] && details["machines"] !== "missing" ? "ok" : "warn", "machines CLI availability", details["machines"] || "missing"),
10562
- makeCheck2("machines-agent-cli", details["machines_agent"] && details["machines_agent"] !== "missing" ? "ok" : "warn", "machines-agent availability", details["machines_agent"] || "missing"),
10563
- makeCheck2("machines-mcp-cli", details["machines_mcp"] && details["machines_mcp"] !== "missing" ? "ok" : "warn", "machines-mcp availability", details["machines_mcp"] || "missing"),
10564
- makeCheck2("ssh", details["ssh"] === "ok" ? "ok" : "warn", "SSH availability", details["ssh"] || "missing")
10565
- ];
10377
+ async function watchTmuxPane(options) {
10378
+ const target = options.target.trim();
10379
+ if (!target)
10380
+ throw new Error("tmux pane target is required");
10381
+ const intervalMs = Math.max(0, options.intervalMs ?? 5000);
10382
+ const maxChecks = options.maxChecks ?? Number.POSITIVE_INFINITY;
10383
+ const client = options.client ?? new EventsClient2;
10384
+ const probe = options.probe ?? ((paneTarget) => probeTmuxPane(paneTarget, options.tmuxCommand));
10385
+ const wait = options.sleep ?? sleep;
10386
+ let lastPresent;
10387
+ let lastProbe;
10388
+ for (let checks = 1;checks <= maxChecks; checks += 1) {
10389
+ const current = await probe(target);
10390
+ lastProbe = current;
10391
+ options.onProbe?.(current);
10392
+ if (current.exists) {
10393
+ lastPresent = current;
10394
+ } else if (lastPresent) {
10395
+ const emitted = await emitTmuxEvent(client, "machines.tmux.pane_died", current, lastPresent, options.deliver !== false);
10396
+ return { target, checks, status: "died", lastProbe: current, emitted };
10397
+ } else if (options.emitInitialMissing) {
10398
+ const emitted = await emitTmuxEvent(client, "machines.tmux.pane_missing", current, undefined, options.deliver !== false);
10399
+ return { target, checks, status: "missing", lastProbe: current, emitted };
10400
+ }
10401
+ if (checks < maxChecks)
10402
+ await wait(intervalMs);
10403
+ }
10404
+ if (!lastProbe) {
10405
+ lastProbe = {
10406
+ target,
10407
+ exists: false,
10408
+ checkedAt: new Date().toISOString(),
10409
+ error: "No probe executed"
10410
+ };
10411
+ }
10566
10412
  return {
10567
- machineId,
10568
- source: commandChecks.source,
10569
- manifestPath: details["manifest_path"],
10570
- dbPath: details["db_path"],
10571
- notificationsPath: details["notifications_path"],
10572
- checks
10413
+ target,
10414
+ checks: Number.isFinite(maxChecks) ? maxChecks : 0,
10415
+ status: lastProbe.exists ? "present" : "stopped",
10416
+ lastProbe
10573
10417
  };
10574
10418
  }
10575
-
10576
- // src/commands/self-test.ts
10577
- init_db();
10578
-
10579
- // node_modules/@hasna/events/dist/index.js
10580
- import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
10581
- import { existsSync as existsSync9 } from "fs";
10582
- import { homedir as homedir7 } from "os";
10583
- import { join as join7 } from "path";
10584
- import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
10585
- import { randomUUID as randomUUID3 } from "crypto";
10586
- import { spawn as spawn2 } from "child_process";
10587
- import { randomUUID as randomUUID22 } from "crypto";
10588
- function getPathValue2(input, path) {
10589
- return path.split(".").reduce((value, part) => {
10590
- if (value && typeof value === "object" && part in value) {
10591
- return value[part];
10419
+ async function emitTmuxEvent(client, type, probe, lastPresent, deliver) {
10420
+ const input = {
10421
+ source: "machines",
10422
+ type,
10423
+ subject: `tmux:${probe.target}`,
10424
+ severity: type === "machines.tmux.pane_died" ? "warning" : "notice",
10425
+ message: type === "machines.tmux.pane_died" ? `tmux pane disappeared: ${probe.target}` : `tmux pane is missing: ${probe.target}`,
10426
+ data: {
10427
+ target: probe.target,
10428
+ paneId: lastPresent?.paneId,
10429
+ lastSeenAt: lastPresent?.checkedAt,
10430
+ checkedAt: probe.checkedAt,
10431
+ exitCode: probe.exitCode,
10432
+ error: probe.error,
10433
+ stderr: probe.stderr
10434
+ },
10435
+ metadata: {
10436
+ monitor: "tmux-pane",
10437
+ runtime: "machines"
10592
10438
  }
10593
- return;
10594
- }, input);
10439
+ };
10440
+ return client.emit(input, { deliver });
10595
10441
  }
10596
- function wildcardToRegExp2(pattern) {
10597
- const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
10598
- return new RegExp(`^${escaped}$`);
10442
+
10443
+ // src/commands/screen.ts
10444
+ var DEFAULT_SCREEN_SECRET_NAMESPACE = "hasna/xyz/opensource/machines/prod";
10445
+ function shellQuote6(value) {
10446
+ return `'${value.replace(/'/g, "'\\''")}'`;
10599
10447
  }
10600
- function matchString2(value, matcher) {
10601
- if (matcher === undefined)
10602
- return true;
10603
- if (value === undefined)
10604
- return false;
10605
- const matchers = Array.isArray(matcher) ? matcher : [matcher];
10606
- return matchers.some((item) => wildcardToRegExp2(item).test(value));
10448
+ function shellCommand2(command) {
10449
+ return command.map(shellQuote6).join(" ");
10607
10450
  }
10608
- function matchRecord2(input, matcher) {
10609
- if (!matcher)
10610
- return true;
10611
- return Object.entries(matcher).every(([path, expected]) => {
10612
- const actual = getPathValue2(input, path);
10613
- if (typeof expected === "string" || Array.isArray(expected)) {
10614
- return matchString2(actual === undefined ? undefined : String(actual), expected);
10615
- }
10616
- return actual === expected;
10617
- });
10451
+ function metadataString2(metadata, keys) {
10452
+ if (!metadata)
10453
+ return null;
10454
+ for (const key of keys) {
10455
+ const value = metadata[key];
10456
+ if (typeof value === "string" && value.trim())
10457
+ return value.trim();
10458
+ }
10459
+ return null;
10618
10460
  }
10619
- function eventMatchesFilter2(event, filter) {
10620
- return matchString2(event.source, filter.source) && matchString2(event.type, filter.type) && matchString2(event.subject, filter.subject) && matchString2(event.severity, filter.severity) && matchRecord2(event.data, filter.data) && matchRecord2(event.metadata, filter.metadata);
10461
+ function splitTarget(target) {
10462
+ const at = target.indexOf("@");
10463
+ if (at === -1)
10464
+ return [null, target];
10465
+ return [target.slice(0, at), target.slice(at + 1)];
10621
10466
  }
10622
- function channelMatchesEvent2(channel, event) {
10623
- if (!channel.enabled)
10624
- return false;
10625
- if (!channel.filters || channel.filters.length === 0)
10626
- return true;
10627
- return channel.filters.some((filter) => eventMatchesFilter2(event, filter));
10467
+ function defaultScreenPasswordSecretKey(machineId) {
10468
+ return `${DEFAULT_SCREEN_SECRET_NAMESPACE}/screen-${machineId}-vnc-password`;
10469
+ }
10470
+ function resolveScreenTarget(machineId, options = {}) {
10471
+ const resolved = resolveMachineRoute(machineId, options);
10472
+ if (!resolved.ok || !resolved.target) {
10473
+ throw new Error(`Machine route not found: ${machineId}`);
10474
+ }
10475
+ if (resolved.route === "unknown") {
10476
+ throw new Error(`Machine route is not reachable for screen sharing: ${machineId}`);
10477
+ }
10478
+ let [user, host] = splitTarget(resolved.target);
10479
+ if (!user) {
10480
+ const topology = options.topology ?? discoverMachineTopology(options);
10481
+ const entry = topology.machines.find((m) => m.machine_id === (resolved.machine_id ?? machineId));
10482
+ user = entry?.user ?? null;
10483
+ }
10484
+ const url = user ? `vnc://${user}@${host}` : `vnc://${host}`;
10485
+ return {
10486
+ machineId: resolved.machine_id ?? machineId,
10487
+ user,
10488
+ host,
10489
+ url,
10490
+ route: resolved.route,
10491
+ confidence: resolved.confidence,
10492
+ warnings: resolved.warnings
10493
+ };
10494
+ }
10495
+ function resolveScreenCredentials(machineId, options = {}) {
10496
+ const topology = options.topology ?? discoverMachineTopology(options);
10497
+ const screen = resolveScreenTarget(machineId, { ...options, topology });
10498
+ const entry = topology.machines.find((machine) => machine.machine_id === screen.machineId);
10499
+ const metadata = entry?.metadata;
10500
+ const metadataUser = metadataString2(metadata, ["screenUser", "screen_user", "user", "username"]);
10501
+ const metadataPasswordSecret = metadataString2(metadata, [
10502
+ "screenPasswordSecret",
10503
+ "screen_password_secret",
10504
+ "screenVncPasswordSecret",
10505
+ "screen_vnc_password_secret",
10506
+ "vncPasswordSecret",
10507
+ "vnc_password_secret"
10508
+ ]);
10509
+ const user = options.user ?? screen.user ?? metadataUser;
10510
+ const passwordSecretKey = options.passwordSecretKey ?? metadataPasswordSecret ?? defaultScreenPasswordSecretKey(screen.machineId);
10511
+ return {
10512
+ machineId: screen.machineId,
10513
+ user: user ?? null,
10514
+ userSource: options.user ? "option" : screen.user ? "route" : metadataUser ? "metadata" : "missing",
10515
+ passwordSecretKey,
10516
+ passwordSecretSource: options.passwordSecretKey ? "option" : metadataPasswordSecret ? "metadata" : "default"
10517
+ };
10518
+ }
10519
+ function buildScreenEnableRemoteCommandFromStdin(user) {
10520
+ const kickstart = "/System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart";
10521
+ const script = [
10522
+ "set -euo pipefail",
10523
+ 'user="$1"',
10524
+ "IFS= read -r vnc_pw",
10525
+ 'if [ -z "$vnc_pw" ]; then echo "missing VNC password on stdin" >&2; exit 1; fi',
10526
+ `kickstart=${shellQuote6(kickstart)}`,
10527
+ 'dseditgroup -o edit -a "$user" -t user com.apple.access_screensharing 2>/dev/null || true',
10528
+ "defaults write /Library/Preferences/com.apple.RemoteManagement AllowSRPForNetworkNodes -bool true",
10529
+ '"$kickstart" -configure -clientopts -setvnclegacy -vnclegacy yes -setvncpw -vncpw "$vnc_pw"',
10530
+ '"$kickstart" -activate -configure -access -on -users "$user" -privs -all -restart -agent -menu'
10531
+ ].join(`
10532
+ `);
10533
+ return `sudo -n -p '' /bin/bash -c ${shellQuote6(script)} -- ${shellQuote6(user)}`;
10628
10534
  }
10629
- var HASNA_EVENTS_DIR_ENV2 = "HASNA_EVENTS_DIR";
10630
- var HASNA_EVENTS_HOME_ENV2 = "HASNA_EVENTS_HOME";
10631
- function getEventsDataDir2(override) {
10632
- return override || process.env[HASNA_EVENTS_DIR_ENV2] || process.env[HASNA_EVENTS_HOME_ENV2] || join7(homedir7(), ".hasna", "events");
10535
+ function buildScreenEnableCommand(machineId, options = {}) {
10536
+ const credentials = resolveScreenCredentials(machineId, options);
10537
+ if (!credentials.user) {
10538
+ throw new Error(`No screen-sharing user known for ${machineId}; pass --user <name> or set metadata.user in the manifest.`);
10539
+ }
10540
+ const secretsCommand = options.secretsCommand || "secrets";
10541
+ const remoteCommand = buildScreenEnableRemoteCommandFromStdin(credentials.user);
10542
+ return {
10543
+ machineId: credentials.machineId,
10544
+ user: credentials.user,
10545
+ passwordSecretKey: credentials.passwordSecretKey,
10546
+ remoteCommand,
10547
+ command: `${shellCommand2([secretsCommand, "get", credentials.passwordSecretKey])} | ${buildSshCommand(machineId, remoteCommand, options)}`
10548
+ };
10633
10549
  }
10634
10550
 
10635
- class JsonEventsStore2 {
10636
- dataDir;
10637
- channelsPath;
10638
- eventsPath;
10639
- deliveriesPath;
10640
- constructor(dataDir = getEventsDataDir2()) {
10641
- this.dataDir = dataDir;
10642
- this.channelsPath = join7(dataDir, "channels.json");
10643
- this.eventsPath = join7(dataDir, "events.json");
10644
- this.deliveriesPath = join7(dataDir, "deliveries.json");
10645
- }
10646
- async init() {
10647
- await mkdir2(this.dataDir, { recursive: true, mode: 448 });
10648
- await chmod2(this.dataDir, 448).catch(() => {
10649
- return;
10650
- });
10651
- await this.ensureArrayFile(this.channelsPath);
10652
- await this.ensureArrayFile(this.eventsPath);
10653
- await this.ensureArrayFile(this.deliveriesPath);
10654
- }
10655
- async addChannel(channel) {
10656
- await this.init();
10657
- const channels = await this.readJson(this.channelsPath, []);
10658
- const index = channels.findIndex((item) => item.id === channel.id);
10659
- if (index >= 0) {
10660
- channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
10661
- } else {
10662
- channels.push(channel);
10663
- }
10664
- await this.writeJson(this.channelsPath, channels);
10665
- return index >= 0 ? channels[index] : channel;
10666
- }
10667
- async listChannels() {
10668
- await this.init();
10669
- return this.readJson(this.channelsPath, []);
10670
- }
10671
- async getChannel(id) {
10672
- const channels = await this.listChannels();
10673
- return channels.find((channel) => channel.id === id);
10674
- }
10675
- async removeChannel(id) {
10676
- await this.init();
10677
- const channels = await this.readJson(this.channelsPath, []);
10678
- const next = channels.filter((channel) => channel.id !== id);
10679
- await this.writeJson(this.channelsPath, next);
10680
- return next.length !== channels.length;
10551
+ // src/commands/sync.ts
10552
+ import { existsSync as existsSync9, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
10553
+ import { homedir as homedir7 } from "os";
10554
+ init_paths();
10555
+ init_db();
10556
+ function quote4(value) {
10557
+ return `'${value.replace(/'/g, `'\\''`)}'`;
10558
+ }
10559
+ function packageCheckCommand(machine, packageName, manager = machine.platform === "macos" ? "brew" : "apt") {
10560
+ if (manager === "bun") {
10561
+ return `bun pm ls -g --all | grep -F ${quote4(packageName)}`;
10681
10562
  }
10682
- async appendEvent(event) {
10683
- await this.init();
10684
- const events = await this.readJson(this.eventsPath, []);
10685
- events.push(event);
10686
- await this.writeJson(this.eventsPath, events);
10687
- return event;
10563
+ if (manager === "brew") {
10564
+ return `brew list --versions ${quote4(packageName)}`;
10688
10565
  }
10689
- async listEvents() {
10690
- await this.init();
10691
- return this.readJson(this.eventsPath, []);
10566
+ if (manager === "apt") {
10567
+ return `dpkg -s ${quote4(packageName)} >/dev/null 2>&1`;
10692
10568
  }
10693
- async findEventByIdentity(identity) {
10694
- const events = await this.listEvents();
10695
- return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
10569
+ return `command -v ${quote4(packageName)} >/dev/null 2>&1`;
10570
+ }
10571
+ function packageInstallCommand(machine, packageName, manager = machine.platform === "macos" ? "brew" : "apt") {
10572
+ if (manager === "bun") {
10573
+ return `bun install -g ${quote4(packageName)}`;
10696
10574
  }
10697
- async appendDelivery(result) {
10698
- await this.init();
10699
- const deliveries = await this.readJson(this.deliveriesPath, []);
10700
- deliveries.push(result);
10701
- await this.writeJson(this.deliveriesPath, deliveries);
10702
- return result;
10575
+ if (manager === "brew") {
10576
+ return `brew install ${quote4(packageName)}`;
10703
10577
  }
10704
- async listDeliveries() {
10705
- await this.init();
10706
- return this.readJson(this.deliveriesPath, []);
10578
+ if (manager === "apt") {
10579
+ return `sudo apt-get install -y ${quote4(packageName)}`;
10707
10580
  }
10708
- async exportData() {
10581
+ return packageName;
10582
+ }
10583
+ function detectPackageActions(machine) {
10584
+ return (machine.packages || []).map((pkg, index) => {
10585
+ const manager = pkg.manager || (machine.platform === "macos" ? "brew" : "apt");
10586
+ const check = Bun.spawnSync(["bash", "-lc", packageCheckCommand(machine, pkg.name, manager)], {
10587
+ stdout: "ignore",
10588
+ stderr: "ignore",
10589
+ env: process.env
10590
+ });
10591
+ const installed = check.exitCode === 0;
10709
10592
  return {
10710
- channels: await this.listChannels(),
10711
- events: await this.listEvents(),
10712
- deliveries: await this.listDeliveries()
10593
+ id: `package-${index + 1}`,
10594
+ title: `${installed ? "Package present" : "Install package"} ${pkg.name}`,
10595
+ command: packageInstallCommand(machine, pkg.name, manager),
10596
+ status: installed ? "ok" : "missing",
10597
+ kind: "package"
10713
10598
  };
10714
- }
10715
- async ensureArrayFile(path) {
10716
- if (!existsSync9(path)) {
10717
- await writeFile2(path, `[]
10718
- `, { encoding: "utf-8", mode: 384 });
10599
+ });
10600
+ }
10601
+ function detectFileActions(machine) {
10602
+ return (machine.files || []).map((file, index) => {
10603
+ const sourceExists = existsSync9(file.source);
10604
+ const targetExists = existsSync9(file.target);
10605
+ let status = "missing";
10606
+ if (sourceExists && targetExists) {
10607
+ if (file.mode === "symlink") {
10608
+ status = lstatSync(file.target).isSymbolicLink() ? "ok" : "drifted";
10609
+ } else {
10610
+ const source = readFileSync5(file.source, "utf8");
10611
+ const target = readFileSync5(file.target, "utf8");
10612
+ status = source === target ? "ok" : "drifted";
10613
+ }
10719
10614
  }
10720
- await chmod2(path, 384).catch(() => {
10721
- return;
10722
- });
10615
+ const command = file.mode === "symlink" ? `ln -sfn ${quote4(file.source)} ${quote4(file.target)}` : `cp ${quote4(file.source)} ${quote4(file.target)}`;
10616
+ return {
10617
+ id: `file-${index + 1}`,
10618
+ title: `${status === "ok" ? "File in sync" : "Reconcile file"} ${file.target}`,
10619
+ command,
10620
+ status,
10621
+ kind: "file"
10622
+ };
10623
+ });
10624
+ }
10625
+ function buildSyncPlan(machineId) {
10626
+ const manifest = readManifest();
10627
+ const currentMachineId = getLocalMachineId();
10628
+ const selected = machineId ? manifest.machines.find((machine) => machine.id === machineId) : manifest.machines.find((machine) => machine.id === currentMachineId);
10629
+ const target = selected || {
10630
+ id: currentMachineId,
10631
+ platform: "linux",
10632
+ workspacePath: `${homedir7()}/workspace`
10633
+ };
10634
+ const actions = [
10635
+ ...detectPackageActions(target),
10636
+ ...detectFileActions(target)
10637
+ ];
10638
+ return {
10639
+ machineId: target.id,
10640
+ mode: "plan",
10641
+ actions,
10642
+ executed: 0
10643
+ };
10644
+ }
10645
+ function applyFileAction(command) {
10646
+ const [verb, source, target] = command.split(" ");
10647
+ if (verb === "cp" && source && target) {
10648
+ ensureParentDir(target);
10649
+ copyFileSync(source.slice(1, -1), target.slice(1, -1));
10650
+ return;
10723
10651
  }
10724
- async readJson(path, fallback) {
10725
- try {
10726
- const raw = await readFile2(path, "utf-8");
10727
- if (!raw.trim())
10728
- return fallback;
10729
- return JSON.parse(raw);
10730
- } catch (error) {
10731
- if (error.code === "ENOENT")
10732
- return fallback;
10733
- throw error;
10652
+ if (verb === "ln" && source && target) {
10653
+ const sourcePath = command.match(/ln -sfn '(.+)' '(.+)'/)?.[1];
10654
+ const targetPath = command.match(/ln -sfn '(.+)' '(.+)'/)?.[2];
10655
+ if (!sourcePath || !targetPath) {
10656
+ throw new Error(`Unable to parse symlink command: ${command}`);
10734
10657
  }
10658
+ ensureParentDir(targetPath);
10659
+ try {
10660
+ Bun.file(targetPath).delete();
10661
+ } catch {}
10662
+ symlinkSync(sourcePath, targetPath);
10735
10663
  }
10736
- async writeJson(path, value) {
10737
- const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
10738
- await writeFile2(tempPath, `${JSON.stringify(value, null, 2)}
10739
- `, { encoding: "utf-8", mode: 384 });
10740
- await rename2(tempPath, path);
10741
- await chmod2(path, 384).catch(() => {
10742
- return;
10743
- });
10744
- }
10745
- }
10746
- var DEFAULT_SIGNATURE_TOLERANCE_MS2 = 5 * 60 * 1000;
10747
- function buildSignatureBase2(timestamp, body) {
10748
- return `${timestamp}.${body}`;
10749
10664
  }
10750
- function signPayload2(secret, timestamp, body) {
10751
- const digest = createHmac2("sha256", secret).update(buildSignatureBase2(timestamp, body)).digest("hex");
10752
- return `sha256=${digest}`;
10665
+ function runSync(machineId, options = {}) {
10666
+ const plan = buildSyncPlan(machineId);
10667
+ if (!options.apply) {
10668
+ return plan;
10669
+ }
10670
+ if (!options.yes) {
10671
+ throw new Error("Sync execution requires --yes.");
10672
+ }
10673
+ let executed = 0;
10674
+ for (const action of plan.actions) {
10675
+ if (action.status === "ok")
10676
+ continue;
10677
+ if (action.kind === "file") {
10678
+ applyFileAction(action.command);
10679
+ } else {
10680
+ const result = Bun.spawnSync(["bash", "-lc", action.command], {
10681
+ stdout: "pipe",
10682
+ stderr: "pipe",
10683
+ env: process.env
10684
+ });
10685
+ if (result.exitCode !== 0) {
10686
+ recordSyncRun(plan.machineId, "failed", {
10687
+ executed,
10688
+ failedAction: action,
10689
+ stderr: result.stderr.toString()
10690
+ });
10691
+ throw new Error(`Sync action failed (${action.id}): ${result.stderr.toString().trim()}`);
10692
+ }
10693
+ }
10694
+ executed += 1;
10695
+ }
10696
+ const summary = {
10697
+ machineId: plan.machineId,
10698
+ mode: "apply",
10699
+ actions: plan.actions,
10700
+ executed
10701
+ };
10702
+ recordSyncRun(plan.machineId, "completed", summary);
10703
+ return summary;
10753
10704
  }
10754
- function now2() {
10755
- return new Date().toISOString();
10705
+
10706
+ // src/commands/status.ts
10707
+ init_db();
10708
+ init_paths();
10709
+ function getStatus() {
10710
+ const manifest = readManifest();
10711
+ const heartbeats = listHeartbeats();
10712
+ const heartbeatByMachine = new Map(heartbeats.map((heartbeat) => [heartbeat.machine_id, heartbeat]));
10713
+ const machineIds = new Set([
10714
+ ...manifest.machines.map((machine) => machine.id),
10715
+ ...heartbeats.map((heartbeat) => heartbeat.machine_id)
10716
+ ]);
10717
+ return {
10718
+ machineId: getLocalMachineId(),
10719
+ manifestPath: getManifestPath(),
10720
+ dbPath: getDbPath(),
10721
+ notificationsPath: getNotificationsPath(),
10722
+ manifestMachineCount: manifest.machines.length,
10723
+ heartbeatCount: heartbeats.length,
10724
+ machines: [...machineIds].sort().map((machineId) => {
10725
+ const declared = manifest.machines.find((machine) => machine.id === machineId);
10726
+ const heartbeat = heartbeatByMachine.get(machineId);
10727
+ return {
10728
+ machineId,
10729
+ platform: declared?.platform,
10730
+ manifestDeclared: Boolean(declared),
10731
+ heartbeatStatus: heartbeat?.status || "unknown",
10732
+ lastHeartbeatAt: heartbeat?.updated_at
10733
+ };
10734
+ }),
10735
+ recentSetupRuns: countRuns("setup_runs"),
10736
+ recentSyncRuns: countRuns("sync_runs")
10737
+ };
10756
10738
  }
10757
- function truncate2(value, max = 4096) {
10758
- return value.length > max ? `${value.slice(0, max)}...` : value;
10739
+
10740
+ // src/commands/workspace.ts
10741
+ init_paths();
10742
+ function isRecord2(value) {
10743
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
10759
10744
  }
10760
- function buildWebhookRequest2(event, channel) {
10761
- if (!channel.webhook)
10762
- throw new Error(`Channel ${channel.id} has no webhook config`);
10763
- const body = JSON.stringify(event);
10764
- const timestamp = event.time;
10765
- const headers = {
10766
- "Content-Type": "application/json",
10767
- "User-Agent": "@hasna/events",
10768
- "X-Hasna-Event-Id": event.id,
10769
- "X-Hasna-Event-Type": event.type,
10770
- "X-Hasna-Timestamp": timestamp,
10771
- ...channel.webhook.headers
10772
- };
10773
- if (channel.webhook.secret) {
10774
- headers["X-Hasna-Signature"] = signPayload2(channel.webhook.secret, timestamp, body);
10745
+ function cloneMetadata(metadata) {
10746
+ return isRecord2(metadata) ? { ...metadata } : {};
10747
+ }
10748
+ function mappedPath(metadata, field, key) {
10749
+ const container = metadata[field];
10750
+ if (!isRecord2(container))
10751
+ return null;
10752
+ const value = container[key];
10753
+ if (typeof value === "string" && value.trim())
10754
+ return value.trim();
10755
+ if (isRecord2(value)) {
10756
+ const nested = value["path"] ?? value["root"] ?? value["workspacePath"] ?? value["workspace_path"];
10757
+ if (typeof nested === "string" && nested.trim())
10758
+ return nested.trim();
10775
10759
  }
10776
- return { body, headers };
10760
+ return null;
10777
10761
  }
10778
- async function dispatchWebhook3(event, channel, options = {}) {
10779
- if (!channel.webhook)
10780
- throw new Error(`Channel ${channel.id} has no webhook config`);
10781
- const startedAt = now2();
10782
- const { body, headers } = buildWebhookRequest2(event, channel);
10783
- const controller = new AbortController;
10784
- const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
10785
- try {
10786
- const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
10787
- method: "POST",
10788
- headers,
10789
- body,
10790
- signal: controller.signal
10791
- });
10792
- const responseBody = truncate2(await response.text());
10793
- return {
10794
- attempt: 1,
10795
- status: response.ok ? "success" : "failed",
10796
- startedAt,
10797
- completedAt: now2(),
10798
- responseStatus: response.status,
10799
- responseBody,
10800
- error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
10801
- };
10802
- } catch (error) {
10762
+ function writeMappedPath(metadata, field, key, path) {
10763
+ const existing = metadata[field];
10764
+ const container = isRecord2(existing) ? { ...existing } : {};
10765
+ container[key] = path;
10766
+ metadata[field] = container;
10767
+ }
10768
+ function buildPatch(input) {
10769
+ const previous = mappedPath(input.metadata, input.field, input.key);
10770
+ if (!input.path) {
10803
10771
  return {
10804
- attempt: 1,
10805
- status: "failed",
10806
- startedAt,
10807
- completedAt: now2(),
10808
- error: error instanceof Error ? error.message : String(error)
10772
+ field: input.field,
10773
+ key: input.key,
10774
+ path: null,
10775
+ previous_path: previous,
10776
+ status: "unresolved"
10809
10777
  };
10810
- } finally {
10811
- clearTimeout(timeout);
10812
10778
  }
10779
+ const status = previous === input.path ? "unchanged" : input.apply ? "written" : "would_write";
10780
+ return {
10781
+ field: input.field,
10782
+ key: input.key,
10783
+ path: input.path,
10784
+ previous_path: previous,
10785
+ status
10786
+ };
10813
10787
  }
10814
- async function dispatchCommand3(event, channel) {
10815
- if (!channel.command)
10816
- throw new Error(`Channel ${channel.id} has no command config`);
10817
- const startedAt = now2();
10818
- const eventJson = JSON.stringify(event);
10819
- const env2 = {
10820
- ...process.env,
10821
- ...channel.command.env,
10822
- HASNA_CHANNEL_ID: channel.id,
10823
- HASNA_EVENT_ID: event.id,
10824
- HASNA_EVENT_TYPE: event.type,
10825
- HASNA_EVENT_SOURCE: event.source,
10826
- HASNA_EVENT_SUBJECT: event.subject ?? "",
10827
- HASNA_EVENT_SEVERITY: event.severity,
10828
- HASNA_EVENT_TIME: event.time,
10829
- HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
10830
- HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
10831
- HASNA_EVENT_JSON: eventJson
10788
+ function upsertMachineMetadata(manifest, machineId, metadata) {
10789
+ return {
10790
+ ...manifest,
10791
+ machines: manifest.machines.map((machine) => machine.id === machineId ? { ...machine, metadata } : machine)
10832
10792
  };
10833
- return new Promise((resolve2) => {
10834
- const child = spawn2(channel.command.command, channel.command.args ?? [], {
10835
- cwd: channel.command.cwd,
10836
- env: env2,
10837
- stdio: ["pipe", "pipe", "pipe"]
10838
- });
10839
- let stdout = "";
10840
- let stderr = "";
10841
- const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
10842
- child.stdin.end(eventJson);
10843
- child.stdout.on("data", (chunk) => {
10844
- stdout += chunk.toString();
10845
- });
10846
- child.stderr.on("data", (chunk) => {
10847
- stderr += chunk.toString();
10848
- });
10849
- child.on("error", (error) => {
10850
- clearTimeout(timeout);
10851
- resolve2({
10852
- attempt: 1,
10853
- status: "failed",
10854
- startedAt,
10855
- completedAt: now2(),
10856
- stdout: truncate2(stdout),
10857
- stderr: truncate2(stderr),
10858
- error: error.message
10859
- });
10860
- });
10861
- child.on("close", (code, signal) => {
10862
- clearTimeout(timeout);
10863
- const success = code === 0;
10864
- resolve2({
10865
- attempt: 1,
10866
- status: success ? "success" : "failed",
10867
- startedAt,
10868
- completedAt: now2(),
10869
- stdout: truncate2(stdout),
10870
- stderr: truncate2(stderr),
10871
- error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
10872
- });
10873
- });
10874
- });
10875
10793
  }
10876
- async function dispatchChannel3(event, channel, options = {}) {
10877
- if (channel.transport === "webhook")
10878
- return dispatchWebhook3(event, channel, options);
10879
- if (channel.transport === "command")
10880
- return dispatchCommand3(event, channel);
10794
+ function repairWorkspaceManifestMappings(options) {
10795
+ const projectId = options.projectId;
10796
+ const repoName = options.repoName ?? projectId;
10797
+ const openFilesRepoName = options.openFilesRepoName ?? "open-files";
10798
+ const apply = options.apply === true;
10799
+ const resolution = resolveMachineWorkspace({
10800
+ machineId: options.machineId,
10801
+ projectId,
10802
+ repoName,
10803
+ openFilesRepoName,
10804
+ projectRoot: options.projectRoot,
10805
+ openFilesRoot: options.openFilesRoot,
10806
+ workspaceRoot: options.workspaceRoot,
10807
+ includeTailscale: options.includeTailscale,
10808
+ now: options.now
10809
+ });
10810
+ const warnings = [...resolution.warnings];
10811
+ const manifest = readManifest();
10812
+ const manifestMachineId = resolution.machine_id ?? options.machineId;
10813
+ const machine = manifest.machines.find((entry) => entry.id === manifestMachineId) ?? null;
10814
+ const trusted = resolution.machine.trust_status === "trusted" || options.allowUntrusted === true;
10815
+ if (!machine) {
10816
+ warnings.push(`manifest_machine_missing:${manifestMachineId}`);
10817
+ return {
10818
+ ok: false,
10819
+ applied: false,
10820
+ manifest_path: getManifestPath(),
10821
+ machine_id: resolution.machine_id,
10822
+ project_id: projectId,
10823
+ repo_name: repoName,
10824
+ open_files_repo_name: openFilesRepoName,
10825
+ trusted,
10826
+ resolution,
10827
+ patches: [],
10828
+ warnings
10829
+ };
10830
+ }
10831
+ const metadata = cloneMetadata(machine.metadata);
10832
+ const patches = [
10833
+ buildPatch({
10834
+ metadata,
10835
+ field: "workspace_paths",
10836
+ key: projectId,
10837
+ path: options.projectRoot ?? resolution.paths.project_root.path,
10838
+ apply
10839
+ }),
10840
+ buildPatch({
10841
+ metadata,
10842
+ field: "open_files_roots",
10843
+ key: projectId,
10844
+ path: options.openFilesRoot ?? resolution.paths.open_files_root.path,
10845
+ apply
10846
+ })
10847
+ ];
10848
+ const hasUnresolved = patches.some((patch) => patch.status === "unresolved");
10849
+ const hasWrites = patches.some((patch) => patch.status === "would_write" || patch.status === "written");
10850
+ if (apply && hasWrites && !trusted) {
10851
+ warnings.push(`manifest_repair_requires_trusted_machine:${manifestMachineId}`);
10852
+ return {
10853
+ ok: false,
10854
+ applied: false,
10855
+ manifest_path: getManifestPath(),
10856
+ machine_id: manifestMachineId,
10857
+ project_id: projectId,
10858
+ repo_name: repoName,
10859
+ open_files_repo_name: openFilesRepoName,
10860
+ trusted,
10861
+ resolution,
10862
+ patches: patches.map((patch) => patch.status === "written" ? { ...patch, status: "would_write" } : patch),
10863
+ warnings
10864
+ };
10865
+ }
10866
+ let applied = false;
10867
+ if (apply && !hasUnresolved && hasWrites) {
10868
+ for (const patch of patches) {
10869
+ if (patch.path && patch.status === "written")
10870
+ writeMappedPath(metadata, patch.field, patch.key, patch.path);
10871
+ }
10872
+ writeManifest(upsertMachineMetadata(manifest, manifestMachineId, metadata));
10873
+ applied = true;
10874
+ }
10881
10875
  return {
10882
- attempt: 1,
10883
- status: "skipped",
10884
- startedAt: now2(),
10885
- completedAt: now2(),
10886
- error: `Unsupported transport: ${channel.transport}`
10876
+ ok: resolution.ok && !hasUnresolved && (!apply || applied || !hasWrites),
10877
+ applied,
10878
+ manifest_path: getManifestPath(),
10879
+ machine_id: manifestMachineId,
10880
+ project_id: projectId,
10881
+ repo_name: repoName,
10882
+ open_files_repo_name: openFilesRepoName,
10883
+ trusted,
10884
+ resolution,
10885
+ patches,
10886
+ warnings
10887
10887
  };
10888
10888
  }
10889
- function createDeliveryResult2(event, channel, attempts) {
10890
- const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
10889
+
10890
+ // src/compatibility.ts
10891
+ init_db();
10892
+ var DEFAULT_COMMANDS = [
10893
+ { command: "bun", required: true },
10894
+ { command: "machines", required: true }
10895
+ ];
10896
+ function defaultPackages() {
10897
+ return [{ name: "@hasna/machines", command: "machines", expectedVersion: getPackageVersion(), required: true }];
10898
+ }
10899
+ function shellQuote7(value) {
10900
+ return `'${value.replace(/'/g, "'\\''")}'`;
10901
+ }
10902
+ function commandId(value) {
10903
+ return value.replace(/[^a-zA-Z0-9_.@/-]+/g, "-").replace(/^-+|-+$/g, "");
10904
+ }
10905
+ function packageCommand(name) {
10906
+ if (name === "@hasna/knowledge")
10907
+ return "knowledge";
10908
+ if (name === "@hasna/machines")
10909
+ return "machines";
10910
+ return name.split("/").pop() ?? name;
10911
+ }
10912
+ function firstLine(value) {
10913
+ return value.trim().split(/\r?\n/).find(Boolean) ?? "";
10914
+ }
10915
+ function extractVersion(value) {
10916
+ const match = value.match(/\b\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?\b/);
10917
+ return match?.[0] ?? null;
10918
+ }
10919
+ function statusFor(required, ok) {
10920
+ if (ok)
10921
+ return "ok";
10922
+ return required === false ? "warn" : "fail";
10923
+ }
10924
+ function makeCheck(input) {
10891
10925
  return {
10892
- id: randomUUID3(),
10893
- eventId: event.id,
10894
- channelId: channel.id,
10895
- transport: channel.transport,
10896
- status,
10897
- attempts,
10898
- createdAt: attempts[0]?.startedAt ?? now2(),
10899
- completedAt: attempts.at(-1)?.completedAt ?? now2()
10926
+ id: input.id,
10927
+ kind: input.kind,
10928
+ status: input.status,
10929
+ target: input.target,
10930
+ expected: input.expected ?? null,
10931
+ actual: input.actual ?? null,
10932
+ detail: input.detail,
10933
+ source: input.source
10900
10934
  };
10901
10935
  }
10902
- function createEvent2(input) {
10936
+ function parseKeyValue(stdout) {
10937
+ const result = {};
10938
+ for (const line of stdout.split(/\r?\n/)) {
10939
+ const idx = line.indexOf("=");
10940
+ if (idx <= 0)
10941
+ continue;
10942
+ result[line.slice(0, idx)] = line.slice(idx + 1);
10943
+ }
10944
+ return result;
10945
+ }
10946
+ function defaultRunner2(machineId, command) {
10947
+ return runMachineCommand(machineId, command);
10948
+ }
10949
+ function inspectCommand(machineId, spec, runner) {
10950
+ const command = shellQuote7(spec.command);
10951
+ const versionArgs = spec.versionArgs ?? "--version";
10952
+ const script = [
10953
+ `cmd=${command}`,
10954
+ 'path="$(command -v "$cmd" 2>/dev/null || true)"',
10955
+ 'printf "path=%s\\n" "$path"',
10956
+ 'if [ -n "$path" ]; then version="$("$cmd" ' + versionArgs + ' 2>/dev/null || true)"; printf "version=%s\\n" "$version"; fi'
10957
+ ].join("; ");
10958
+ const result = runner(machineId, script);
10959
+ const parsed = parseKeyValue(result.stdout);
10903
10960
  return {
10904
- id: input.id ?? randomUUID22(),
10905
- source: input.source,
10906
- type: input.type,
10907
- time: normalizeTime2(input.time),
10908
- subject: input.subject,
10909
- severity: input.severity ?? "info",
10910
- data: input.data ?? {},
10911
- message: input.message,
10912
- dedupeKey: input.dedupeKey,
10913
- schemaVersion: input.schemaVersion ?? "1.0",
10914
- metadata: input.metadata ?? {}
10961
+ path: parsed.path || null,
10962
+ version: parsed.version ? firstLine(parsed.version) : null,
10963
+ exitCode: result.exitCode,
10964
+ source: result.source,
10965
+ stderr: result.stderr
10915
10966
  };
10916
10967
  }
10917
-
10918
- class EventsClient2 {
10919
- store;
10920
- redactors;
10921
- transportOptions;
10922
- constructor(options = {}) {
10923
- this.store = options.store ?? new JsonEventsStore2(options.dataDir);
10924
- this.redactors = options.redactors ?? [];
10925
- this.transportOptions = { fetchImpl: options.fetchImpl };
10926
- }
10927
- async addChannel(input) {
10928
- const timestamp = new Date().toISOString();
10929
- return this.store.addChannel({
10930
- ...input,
10931
- createdAt: input.createdAt ?? timestamp,
10932
- updatedAt: input.updatedAt ?? timestamp
10933
- });
10934
- }
10935
- async listChannels() {
10936
- return this.store.listChannels();
10937
- }
10938
- async removeChannel(id) {
10939
- return this.store.removeChannel(id);
10940
- }
10941
- async emit(input, options = {}) {
10942
- const event = options.redactSensitiveData === false ? createEvent2(input) : redactSensitiveKeys2(createEvent2(input));
10943
- if (options.dedupe !== false) {
10944
- const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
10945
- if (existing) {
10946
- return { event: existing, deliveries: [], deduped: true };
10947
- }
10948
- }
10949
- await this.store.appendEvent(event);
10950
- const deliveries = options.deliver === false ? [] : await this.deliver(event);
10951
- return { event, deliveries, deduped: false };
10952
- }
10953
- async listEvents() {
10954
- return this.store.listEvents();
10955
- }
10956
- async listDeliveries() {
10957
- return this.store.listDeliveries();
10958
- }
10959
- async deliver(event) {
10960
- const channels = await this.store.listChannels();
10961
- const selected = channels.filter((channel) => channelMatchesEvent2(channel, event));
10962
- const deliveries = [];
10963
- for (const channel of selected) {
10964
- const eventForChannel = await this.applyRedaction(event, channel);
10965
- const result = await this.deliverWithRetry(eventForChannel, channel);
10966
- await this.store.appendDelivery(result);
10967
- deliveries.push(result);
10968
- }
10969
- return deliveries;
10970
- }
10971
- async testChannel(id, input = {}) {
10972
- const channel = await this.store.getChannel(id);
10973
- if (!channel)
10974
- throw new Error(`Channel not found: ${id}`);
10975
- const event = createEvent2({
10976
- source: input.source ?? "hasna.events",
10977
- type: input.type ?? "events.test",
10978
- subject: input.subject ?? id,
10979
- severity: input.severity ?? "info",
10980
- data: input.data ?? { test: true },
10981
- message: input.message ?? "Hasna events test delivery",
10982
- dedupeKey: input.dedupeKey,
10983
- schemaVersion: input.schemaVersion,
10984
- metadata: input.metadata,
10985
- time: input.time,
10986
- id: input.id
10987
- });
10988
- const eventForChannel = await this.applyRedaction(event, channel);
10989
- const result = await this.deliverWithRetry(eventForChannel, channel);
10990
- await this.store.appendDelivery(result);
10991
- return result;
10992
- }
10993
- async replay(options = {}) {
10994
- const events = (await this.store.listEvents()).filter((event) => {
10995
- if (options.eventId && event.id !== options.eventId)
10996
- return false;
10997
- if (options.source && event.source !== options.source)
10998
- return false;
10999
- if (options.type && event.type !== options.type)
11000
- return false;
11001
- return true;
11002
- });
11003
- if (options.dryRun)
11004
- return { events, deliveries: [] };
11005
- const deliveries = [];
11006
- for (const event of events) {
11007
- deliveries.push(...await this.deliver(event));
11008
- }
11009
- return { events, deliveries };
11010
- }
11011
- async applyRedaction(event, channel) {
11012
- let next = redactPaths2(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
11013
- for (const redactor of this.redactors) {
11014
- next = await redactor(next, channel);
11015
- }
11016
- return next;
11017
- }
11018
- async deliverWithRetry(event, channel) {
11019
- const policy = normalizeRetryPolicy2(channel.retry);
11020
- const attempts = [];
11021
- for (let index = 0;index < policy.maxAttempts; index += 1) {
11022
- const attempt = await dispatchChannel3(event, channel, this.transportOptions);
11023
- attempt.attempt = index + 1;
11024
- if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
11025
- attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
11026
- }
11027
- attempts.push(attempt);
11028
- if (attempt.status !== "failed")
11029
- break;
11030
- if (attempt.nextBackoffMs)
11031
- await Bun.sleep(attempt.nextBackoffMs);
11032
- }
11033
- return createDeliveryResult2(event, channel, attempts);
11034
- }
10968
+ function fieldCommand(field) {
10969
+ const regex = field === "name" ? String.raw`s/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p` : String.raw`s/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p`;
10970
+ return [
10971
+ `if command -v bun >/dev/null 2>&1; then bun -e "const p=JSON.parse(await Bun.file(process.argv[1]).text()); console.log(p.${field} ?? '')" "$pkg" 2>/dev/null`,
10972
+ `elif command -v node >/dev/null 2>&1; then node -e "const fs=require('fs'); const p=JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); console.log(p.${field} || '')" "$pkg" 2>/dev/null`,
10973
+ `else sed -n '${regex}' "$pkg" | head -n 1`,
10974
+ "fi"
10975
+ ].join("; ");
11035
10976
  }
11036
- function redactPaths2(event, paths, replacement = "[REDACTED]") {
11037
- if (paths.length === 0)
11038
- return event;
11039
- const copy = structuredClone(event);
11040
- for (const path of paths) {
11041
- setPath2(copy, path, replacement);
11042
- }
11043
- return copy;
10977
+ function inspectWorkspace(machineId, spec, runner) {
10978
+ const script = [
10979
+ `path=${shellQuote7(spec.path)}`,
10980
+ 'printf "exists=%s\\n" "$(test -d "$path" && printf yes || printf no)"',
10981
+ 'pkg="$path/package.json"',
10982
+ 'printf "package_json=%s\\n" "$(test -f "$pkg" && printf yes || printf no)"',
10983
+ `if [ -f "$pkg" ]; then printf "package_name=%s\\n" "$(${fieldCommand("name")})"; printf "version=%s\\n" "$(${fieldCommand("version")})"; fi`
10984
+ ].join("; ");
10985
+ const result = runner(machineId, script);
10986
+ const parsed = parseKeyValue(result.stdout);
10987
+ return {
10988
+ exists: parsed.exists === "yes",
10989
+ packageJson: parsed.package_json === "yes",
10990
+ packageName: parsed.package_name || null,
10991
+ version: parsed.version || null,
10992
+ source: result.source,
10993
+ stderr: result.stderr
10994
+ };
11044
10995
  }
11045
- function sanitizeChannelForOutput2(channel) {
11046
- const copy = structuredClone(channel);
11047
- if (copy.webhook?.secret)
11048
- copy.webhook.secret = "[REDACTED]";
11049
- if (copy.command?.env) {
11050
- copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey2(key) ? "[REDACTED]" : value]));
10996
+ function commandCheck(machineId, spec, runner) {
10997
+ const inspection = inspectCommand(machineId, spec, runner);
10998
+ const found = Boolean(inspection.path);
10999
+ const checks = [
11000
+ makeCheck({
11001
+ id: `command:${commandId(spec.command)}:path`,
11002
+ kind: "command",
11003
+ status: statusFor(spec.required, found),
11004
+ target: spec.command,
11005
+ expected: "available",
11006
+ actual: inspection.path ?? "missing",
11007
+ detail: found ? `found at ${inspection.path}` : inspection.stderr || "command missing",
11008
+ source: inspection.source
11009
+ })
11010
+ ];
11011
+ if (spec.expectedVersion) {
11012
+ const actualVersion = extractVersion(inspection.version ?? "");
11013
+ checks.push(makeCheck({
11014
+ id: `command:${commandId(spec.command)}:version`,
11015
+ kind: "command",
11016
+ status: actualVersion === spec.expectedVersion ? "ok" : statusFor(spec.required, false),
11017
+ target: spec.command,
11018
+ expected: spec.expectedVersion,
11019
+ actual: actualVersion ?? inspection.version ?? "missing",
11020
+ detail: actualVersion ? `version output: ${inspection.version}` : "version unavailable",
11021
+ source: inspection.source
11022
+ }));
11051
11023
  }
11052
- return copy;
11024
+ return checks;
11053
11025
  }
11054
- function sanitizeChannelsForOutput2(channels) {
11055
- return channels.map(sanitizeChannelForOutput2);
11026
+ function packageCheck(machineId, spec, runner) {
11027
+ const command = spec.command ?? packageCommand(spec.name);
11028
+ const inspection = inspectCommand(machineId, { command, expectedVersion: spec.expectedVersion, required: spec.required }, runner);
11029
+ const found = Boolean(inspection.path);
11030
+ const checks = [
11031
+ makeCheck({
11032
+ id: `package:${commandId(spec.name)}:command`,
11033
+ kind: "package",
11034
+ status: statusFor(spec.required, found),
11035
+ target: spec.name,
11036
+ expected: command,
11037
+ actual: inspection.path ?? "missing",
11038
+ detail: found ? `${command} found at ${inspection.path}` : `${command} command missing`,
11039
+ source: inspection.source
11040
+ })
11041
+ ];
11042
+ if (spec.expectedVersion) {
11043
+ const actualVersion = extractVersion(inspection.version ?? "");
11044
+ checks.push(makeCheck({
11045
+ id: `package:${commandId(spec.name)}:version`,
11046
+ kind: "package",
11047
+ status: actualVersion === spec.expectedVersion ? "ok" : statusFor(spec.required, false),
11048
+ target: spec.name,
11049
+ expected: spec.expectedVersion,
11050
+ actual: actualVersion ?? inspection.version ?? "missing",
11051
+ detail: actualVersion ? `version output: ${inspection.version}` : "version unavailable",
11052
+ source: inspection.source
11053
+ }));
11054
+ }
11055
+ return checks;
11056
11056
  }
11057
- function redactSensitiveKeys2(event, replacement = "[REDACTED]") {
11058
- return redactValue2(event, replacement);
11057
+ function workspaceCheck(machineId, spec, runner) {
11058
+ const inspection = inspectWorkspace(machineId, spec, runner);
11059
+ const target = spec.label ?? spec.path;
11060
+ const checks = [
11061
+ makeCheck({
11062
+ id: `workspace:${commandId(target)}:path`,
11063
+ kind: "workspace",
11064
+ status: statusFor(spec.required, inspection.exists),
11065
+ target,
11066
+ expected: spec.path,
11067
+ actual: inspection.exists ? "exists" : "missing",
11068
+ detail: inspection.exists ? `workspace exists at ${spec.path}` : inspection.stderr || `workspace missing at ${spec.path}`,
11069
+ source: inspection.source
11070
+ })
11071
+ ];
11072
+ if (spec.expectedPackageName) {
11073
+ checks.push(makeCheck({
11074
+ id: `workspace:${commandId(target)}:package-name`,
11075
+ kind: "workspace",
11076
+ status: inspection.packageName === spec.expectedPackageName ? "ok" : statusFor(spec.required, false),
11077
+ target,
11078
+ expected: spec.expectedPackageName,
11079
+ actual: inspection.packageName ?? (inspection.packageJson ? "missing-name" : "missing-package-json"),
11080
+ detail: inspection.packageJson ? "package.json inspected" : "package.json missing",
11081
+ source: inspection.source
11082
+ }));
11083
+ }
11084
+ if (spec.expectedVersion) {
11085
+ checks.push(makeCheck({
11086
+ id: `workspace:${commandId(target)}:version`,
11087
+ kind: "workspace",
11088
+ status: inspection.version === spec.expectedVersion ? "ok" : statusFor(spec.required, false),
11089
+ target,
11090
+ expected: spec.expectedVersion,
11091
+ actual: inspection.version ?? (inspection.packageJson ? "missing-version" : "missing-package-json"),
11092
+ detail: inspection.packageJson ? "package.json inspected" : "package.json missing",
11093
+ source: inspection.source
11094
+ }));
11095
+ }
11096
+ return checks;
11059
11097
  }
11060
- function shouldRedactKey2(key) {
11061
- return /secret|token|password|api[_-]?key|authorization/i.test(key);
11098
+ function checkMachineCompatibility(options = {}) {
11099
+ const machineId = options.machineId ?? getLocalMachineId();
11100
+ const runner = options.runner ?? defaultRunner2;
11101
+ const commands = options.commands ?? DEFAULT_COMMANDS;
11102
+ const packages = options.packages ?? defaultPackages();
11103
+ const workspaces = options.workspaces ?? [];
11104
+ const checks = [];
11105
+ for (const spec of commands)
11106
+ checks.push(...commandCheck(machineId, spec, runner));
11107
+ for (const spec of packages)
11108
+ checks.push(...packageCheck(machineId, spec, runner));
11109
+ for (const spec of workspaces)
11110
+ checks.push(...workspaceCheck(machineId, spec, runner));
11111
+ const summary = {
11112
+ ok: checks.filter((check) => check.status === "ok").length,
11113
+ warn: checks.filter((check) => check.status === "warn").length,
11114
+ fail: checks.filter((check) => check.status === "fail").length
11115
+ };
11116
+ return {
11117
+ schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
11118
+ package: {
11119
+ name: MACHINES_PACKAGE_NAME,
11120
+ version: getPackageVersion()
11121
+ },
11122
+ capabilities: getMachinesConsumerCapabilities(),
11123
+ ok: summary.fail === 0,
11124
+ machine_id: machineId,
11125
+ source: checks[0]?.source ?? "local",
11126
+ generated_at: (options.now ?? new Date).toISOString(),
11127
+ checks,
11128
+ summary
11129
+ };
11062
11130
  }
11063
- function redactValue2(value, replacement) {
11064
- if (Array.isArray(value))
11065
- return value.map((item) => redactValue2(item, replacement));
11066
- if (!value || typeof value !== "object")
11067
- return value;
11068
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [
11069
- key,
11070
- shouldRedactKey2(key) ? replacement : redactValue2(item, replacement)
11071
- ]));
11131
+
11132
+ // src/commands/doctor.ts
11133
+ init_db();
11134
+ function makeCheck2(id, status, summary, detail) {
11135
+ return { id, status, summary, detail };
11072
11136
  }
11073
- function setPath2(input, path, replacement) {
11074
- const parts = path.split(".");
11075
- let cursor = input;
11076
- for (const part of parts.slice(0, -1)) {
11077
- const next = cursor[part];
11078
- if (!next || typeof next !== "object")
11079
- return;
11080
- cursor = next;
11081
- }
11082
- const last = parts.at(-1);
11083
- if (last && last in cursor)
11084
- cursor[last] = replacement;
11137
+ function parseKeyValueOutput(stdout) {
11138
+ return Object.fromEntries(stdout.trim().split(`
11139
+ `).map((line) => line.split("=")).filter((parts) => parts.length === 2).map(([key, value]) => [key, value]));
11085
11140
  }
11086
- function normalizeTime2(value) {
11087
- if (!value)
11088
- return new Date().toISOString();
11089
- return value instanceof Date ? value.toISOString() : value;
11141
+ function buildDoctorCommand() {
11142
+ return [
11143
+ 'data_dir="${HASNA_MACHINES_DIR:-$HOME/.hasna/machines}"',
11144
+ 'manifest_path="${HASNA_MACHINES_MANIFEST_PATH:-$data_dir/machines.json}"',
11145
+ 'db_path="${HASNA_MACHINES_DB_PATH:-$data_dir/machines.db}"',
11146
+ 'notifications_path="${HASNA_MACHINES_NOTIFICATIONS_PATH:-$data_dir/notifications.json}"',
11147
+ `printf 'manifest_path=%s\\n' "$manifest_path"`,
11148
+ `printf 'db_path=%s\\n' "$db_path"`,
11149
+ `printf 'notifications_path=%s\\n' "$notifications_path"`,
11150
+ `printf 'manifest_exists=%s\\n' "$(test -e "$manifest_path" && printf yes || printf no)"`,
11151
+ `printf 'db_exists=%s\\n' "$(test -e "$db_path" && printf yes || printf no)"`,
11152
+ `printf 'notifications_exists=%s\\n' "$(test -e "$notifications_path" && printf yes || printf no)"`,
11153
+ `printf 'bun=%s\\n' "$(bun --version 2>/dev/null || printf missing)"`,
11154
+ `printf 'ssh=%s\\n' "$(command -v ssh >/dev/null 2>&1 && printf ok || printf missing)"`,
11155
+ `printf 'machines=%s\\n' "$(command -v machines 2>/dev/null || printf missing)"`,
11156
+ `printf 'machines_agent=%s\\n' "$(command -v machines-agent 2>/dev/null || printf missing)"`,
11157
+ `printf 'machines_mcp=%s\\n' "$(command -v machines-mcp 2>/dev/null || printf missing)"`
11158
+ ].join("; ");
11090
11159
  }
11091
- function normalizeRetryPolicy2(policy) {
11160
+ function runDoctor(machineId = getLocalMachineId()) {
11161
+ const manifest = readManifest();
11162
+ const commandChecks = runMachineCommand(machineId, buildDoctorCommand());
11163
+ const details = parseKeyValueOutput(commandChecks.stdout);
11164
+ const machineInManifest = manifest.machines.find((machine) => machine.id === machineId);
11165
+ const checks = [
11166
+ makeCheck2("manifest-entry", machineInManifest ? "ok" : "warn", machineInManifest ? "Machine exists in manifest" : "Machine missing from manifest", machineInManifest ? JSON.stringify(machineInManifest) : `No manifest entry for ${machineId}`),
11167
+ makeCheck2("manifest-path", details["manifest_exists"] === "yes" ? "ok" : "warn", "Manifest path check", `${details["manifest_path"] || "unknown"} ${details["manifest_exists"] === "yes" ? "exists" : "missing"}`),
11168
+ makeCheck2("db-path", details["db_exists"] === "yes" ? "ok" : "warn", "DB path check", `${details["db_path"] || "unknown"} ${details["db_exists"] === "yes" ? "exists" : "missing"}`),
11169
+ makeCheck2("notifications-path", details["notifications_exists"] === "yes" ? "ok" : "warn", "Notifications path check", `${details["notifications_path"] || "unknown"} ${details["notifications_exists"] === "yes" ? "exists" : "missing"}`),
11170
+ makeCheck2("bun", details["bun"] && details["bun"] !== "missing" ? "ok" : "fail", "Bun availability", details["bun"] || "missing"),
11171
+ makeCheck2("machines-cli", details["machines"] && details["machines"] !== "missing" ? "ok" : "warn", "machines CLI availability", details["machines"] || "missing"),
11172
+ makeCheck2("machines-agent-cli", details["machines_agent"] && details["machines_agent"] !== "missing" ? "ok" : "warn", "machines-agent availability", details["machines_agent"] || "missing"),
11173
+ makeCheck2("machines-mcp-cli", details["machines_mcp"] && details["machines_mcp"] !== "missing" ? "ok" : "warn", "machines-mcp availability", details["machines_mcp"] || "missing"),
11174
+ makeCheck2("ssh", details["ssh"] === "ok" ? "ok" : "warn", "SSH availability", details["ssh"] || "missing")
11175
+ ];
11092
11176
  return {
11093
- maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
11094
- backoffMs: Math.max(0, policy?.backoffMs ?? 250),
11095
- multiplier: Math.max(1, policy?.multiplier ?? 2)
11177
+ machineId,
11178
+ source: commandChecks.source,
11179
+ manifestPath: details["manifest_path"],
11180
+ dbPath: details["db_path"],
11181
+ notificationsPath: details["notifications_path"],
11182
+ checks
11096
11183
  };
11097
11184
  }
11098
11185
 
11186
+ // src/commands/self-test.ts
11187
+ init_db();
11188
+
11099
11189
  // src/commands/serve.ts
11100
11190
  function escapeHtml(value) {
11101
11191
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
@@ -12668,6 +12758,7 @@ var manifestCommand = program2.command("manifest").description("Manage the fleet
12668
12758
  var appsCommand = program2.command("apps").description("Manage installed applications per machine");
12669
12759
  var notificationsCommand = program2.command("notifications").description("Manage fleet alert delivery channels");
12670
12760
  registerEventsCommands(program2, { source: "machines" });
12761
+ var runtimeCommand = program2.command("runtime").description("Watch runtime conditions and emit Hasna events");
12671
12762
  var clipboardCommand = program2.command("clipboard").description("Real-time clipboard sync across fleet machines");
12672
12763
  var installClaudeCommand = program2.command("install-claude").description("Install or inspect Claude, Codex, and Gemini CLIs");
12673
12764
  manifestCommand.command("init").description("Create an empty fleet manifest").action(() => {
@@ -12906,6 +12997,26 @@ notificationsCommand.command("remove").description("Remove a notification channe
12906
12997
  const result = removeNotificationChannel(id);
12907
12998
  printJsonOrText(result, renderNotificationConfigResult(result), options.json);
12908
12999
  });
13000
+ runtimeCommand.command("tmux-watch").description("Watch a tmux pane and emit machines.tmux.pane_died if it disappears").argument("<target>", "tmux pane target, for example %1 or session:window.pane").option("--interval-ms <ms>", "Polling interval in milliseconds", "5000").option("--max-checks <n>", "Stop after N checks instead of watching forever").option("--once", "Probe once and emit machines.tmux.pane_missing when absent", false).option("--no-deliver", "Record the event without webhook delivery").option("-j, --json", "Print JSON output", false).action(async (target, options) => {
13001
+ const maxChecks = options.once ? 1 : options.maxChecks ? parseIntegerOption(options.maxChecks, "max-checks", { min: 1 }) : undefined;
13002
+ const result = await watchTmuxPane({
13003
+ target,
13004
+ intervalMs: parseIntegerOption(options.intervalMs ?? "5000", "interval-ms", { min: 0 }),
13005
+ maxChecks,
13006
+ emitInitialMissing: Boolean(options.once),
13007
+ deliver: options.deliver !== false,
13008
+ onProbe: options.json ? undefined : (probe) => {
13009
+ const status = probe.exists ? source_default.green("present") : source_default.yellow("missing");
13010
+ console.error(`tmux ${probe.target}: ${status}${probe.paneId ? ` ${probe.paneId}` : ""}`);
13011
+ }
13012
+ });
13013
+ printJsonOrText(result, renderKeyValueTable([
13014
+ ["target", result.target],
13015
+ ["status", result.status],
13016
+ ["checks", String(result.checks)],
13017
+ ["event", result.emitted?.event.type ?? "none"]
13018
+ ]), options.json);
13019
+ });
12909
13020
  clipboardCommand.command("init").description("Initialize clipboard sync (generate shared secret)").option("-j, --json", "Print JSON output", false).action((options) => {
12910
13021
  const key = getOrCreateClipboardKey();
12911
13022
  const config = getDefaultClipboardConfig();