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