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