@hasna/machines 0.0.28 → 0.0.30

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