@hasna/machines 0.0.31 → 0.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -13
- package/dist/cli/index.js +157 -1321
- package/dist/commands/apps.d.ts.map +1 -1
- package/dist/commands/install-claude.d.ts.map +1 -1
- package/dist/commands/install-tailscale.d.ts.map +1 -1
- package/dist/commands/screen.d.ts +2 -1
- package/dist/commands/screen.d.ts.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/index.js +57 -547
- package/dist/mcp/index.js +110 -604
- package/package.json +2 -2
package/dist/mcp/index.js
CHANGED
|
@@ -40,527 +40,8 @@ function getPackageVersion() {
|
|
|
40
40
|
import { createServer } from "http";
|
|
41
41
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
42
42
|
|
|
43
|
-
// node_modules/@hasna/events/dist/index.js
|
|
44
|
-
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
45
|
-
import { existsSync as existsSync2 } from "fs";
|
|
46
|
-
import { homedir } from "os";
|
|
47
|
-
import { join as join2 } from "path";
|
|
48
|
-
import { createHmac, timingSafeEqual } from "crypto";
|
|
49
|
-
import { randomUUID } from "crypto";
|
|
50
|
-
import { spawn } from "child_process";
|
|
51
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
52
|
-
function getPathValue(input, path) {
|
|
53
|
-
return path.split(".").reduce((value, part) => {
|
|
54
|
-
if (value && typeof value === "object" && part in value) {
|
|
55
|
-
return value[part];
|
|
56
|
-
}
|
|
57
|
-
return;
|
|
58
|
-
}, input);
|
|
59
|
-
}
|
|
60
|
-
function wildcardToRegExp(pattern) {
|
|
61
|
-
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
|
|
62
|
-
return new RegExp(`^${escaped}$`);
|
|
63
|
-
}
|
|
64
|
-
function matchString(value, matcher) {
|
|
65
|
-
if (matcher === undefined)
|
|
66
|
-
return true;
|
|
67
|
-
if (value === undefined)
|
|
68
|
-
return false;
|
|
69
|
-
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
70
|
-
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
71
|
-
}
|
|
72
|
-
function matchRecord(input, matcher) {
|
|
73
|
-
if (!matcher)
|
|
74
|
-
return true;
|
|
75
|
-
return Object.entries(matcher).every(([path, expected]) => {
|
|
76
|
-
const actual = getPathValue(input, path);
|
|
77
|
-
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
78
|
-
return matchString(actual === undefined ? undefined : String(actual), expected);
|
|
79
|
-
}
|
|
80
|
-
return actual === expected;
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
function eventMatchesFilter(event, filter) {
|
|
84
|
-
return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
|
|
85
|
-
}
|
|
86
|
-
function channelMatchesEvent(channel, event) {
|
|
87
|
-
if (!channel.enabled)
|
|
88
|
-
return false;
|
|
89
|
-
if (!channel.filters || channel.filters.length === 0)
|
|
90
|
-
return true;
|
|
91
|
-
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
92
|
-
}
|
|
93
|
-
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
94
|
-
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
95
|
-
function getEventsDataDir(override) {
|
|
96
|
-
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join2(homedir(), ".hasna", "events");
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
class JsonEventsStore {
|
|
100
|
-
dataDir;
|
|
101
|
-
channelsPath;
|
|
102
|
-
eventsPath;
|
|
103
|
-
deliveriesPath;
|
|
104
|
-
constructor(dataDir = getEventsDataDir()) {
|
|
105
|
-
this.dataDir = dataDir;
|
|
106
|
-
this.channelsPath = join2(dataDir, "channels.json");
|
|
107
|
-
this.eventsPath = join2(dataDir, "events.json");
|
|
108
|
-
this.deliveriesPath = join2(dataDir, "deliveries.json");
|
|
109
|
-
}
|
|
110
|
-
async init() {
|
|
111
|
-
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
112
|
-
await chmod(this.dataDir, 448).catch(() => {
|
|
113
|
-
return;
|
|
114
|
-
});
|
|
115
|
-
await this.ensureArrayFile(this.channelsPath);
|
|
116
|
-
await this.ensureArrayFile(this.eventsPath);
|
|
117
|
-
await this.ensureArrayFile(this.deliveriesPath);
|
|
118
|
-
}
|
|
119
|
-
async addChannel(channel) {
|
|
120
|
-
await this.init();
|
|
121
|
-
const channels = await this.readJson(this.channelsPath, []);
|
|
122
|
-
const index = channels.findIndex((item) => item.id === channel.id);
|
|
123
|
-
if (index >= 0) {
|
|
124
|
-
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
125
|
-
} else {
|
|
126
|
-
channels.push(channel);
|
|
127
|
-
}
|
|
128
|
-
await this.writeJson(this.channelsPath, channels);
|
|
129
|
-
return index >= 0 ? channels[index] : channel;
|
|
130
|
-
}
|
|
131
|
-
async listChannels() {
|
|
132
|
-
await this.init();
|
|
133
|
-
return this.readJson(this.channelsPath, []);
|
|
134
|
-
}
|
|
135
|
-
async getChannel(id) {
|
|
136
|
-
const channels = await this.listChannels();
|
|
137
|
-
return channels.find((channel) => channel.id === id);
|
|
138
|
-
}
|
|
139
|
-
async removeChannel(id) {
|
|
140
|
-
await this.init();
|
|
141
|
-
const channels = await this.readJson(this.channelsPath, []);
|
|
142
|
-
const next = channels.filter((channel) => channel.id !== id);
|
|
143
|
-
await this.writeJson(this.channelsPath, next);
|
|
144
|
-
return next.length !== channels.length;
|
|
145
|
-
}
|
|
146
|
-
async appendEvent(event) {
|
|
147
|
-
await this.init();
|
|
148
|
-
const events = await this.readJson(this.eventsPath, []);
|
|
149
|
-
events.push(event);
|
|
150
|
-
await this.writeJson(this.eventsPath, events);
|
|
151
|
-
return event;
|
|
152
|
-
}
|
|
153
|
-
async listEvents() {
|
|
154
|
-
await this.init();
|
|
155
|
-
return this.readJson(this.eventsPath, []);
|
|
156
|
-
}
|
|
157
|
-
async findEventByIdentity(identity) {
|
|
158
|
-
const events = await this.listEvents();
|
|
159
|
-
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
160
|
-
}
|
|
161
|
-
async appendDelivery(result) {
|
|
162
|
-
await this.init();
|
|
163
|
-
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
164
|
-
deliveries.push(result);
|
|
165
|
-
await this.writeJson(this.deliveriesPath, deliveries);
|
|
166
|
-
return result;
|
|
167
|
-
}
|
|
168
|
-
async listDeliveries() {
|
|
169
|
-
await this.init();
|
|
170
|
-
return this.readJson(this.deliveriesPath, []);
|
|
171
|
-
}
|
|
172
|
-
async exportData() {
|
|
173
|
-
return {
|
|
174
|
-
channels: await this.listChannels(),
|
|
175
|
-
events: await this.listEvents(),
|
|
176
|
-
deliveries: await this.listDeliveries()
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
async ensureArrayFile(path) {
|
|
180
|
-
if (!existsSync2(path)) {
|
|
181
|
-
await writeFile(path, `[]
|
|
182
|
-
`, { encoding: "utf-8", mode: 384 });
|
|
183
|
-
}
|
|
184
|
-
await chmod(path, 384).catch(() => {
|
|
185
|
-
return;
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
async readJson(path, fallback) {
|
|
189
|
-
try {
|
|
190
|
-
const raw = await readFile(path, "utf-8");
|
|
191
|
-
if (!raw.trim())
|
|
192
|
-
return fallback;
|
|
193
|
-
return JSON.parse(raw);
|
|
194
|
-
} catch (error) {
|
|
195
|
-
if (error.code === "ENOENT")
|
|
196
|
-
return fallback;
|
|
197
|
-
throw error;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
async writeJson(path, value) {
|
|
201
|
-
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
202
|
-
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
|
|
203
|
-
`, { encoding: "utf-8", mode: 384 });
|
|
204
|
-
await rename(tempPath, path);
|
|
205
|
-
await chmod(path, 384).catch(() => {
|
|
206
|
-
return;
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
211
|
-
function buildSignatureBase(timestamp, body) {
|
|
212
|
-
return `${timestamp}.${body}`;
|
|
213
|
-
}
|
|
214
|
-
function signPayload(secret, timestamp, body) {
|
|
215
|
-
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
|
|
216
|
-
return `sha256=${digest}`;
|
|
217
|
-
}
|
|
218
|
-
function now() {
|
|
219
|
-
return new Date().toISOString();
|
|
220
|
-
}
|
|
221
|
-
function truncate(value, max = 4096) {
|
|
222
|
-
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
223
|
-
}
|
|
224
|
-
function buildWebhookRequest(event, channel) {
|
|
225
|
-
if (!channel.webhook)
|
|
226
|
-
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
227
|
-
const body = JSON.stringify(event);
|
|
228
|
-
const timestamp = event.time;
|
|
229
|
-
const headers = {
|
|
230
|
-
"Content-Type": "application/json",
|
|
231
|
-
"User-Agent": "@hasna/events",
|
|
232
|
-
"X-Hasna-Event-Id": event.id,
|
|
233
|
-
"X-Hasna-Event-Type": event.type,
|
|
234
|
-
"X-Hasna-Timestamp": timestamp,
|
|
235
|
-
...channel.webhook.headers
|
|
236
|
-
};
|
|
237
|
-
if (channel.webhook.secret) {
|
|
238
|
-
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
|
|
239
|
-
}
|
|
240
|
-
return { body, headers };
|
|
241
|
-
}
|
|
242
|
-
async function dispatchWebhook(event, channel, options = {}) {
|
|
243
|
-
if (!channel.webhook)
|
|
244
|
-
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
245
|
-
const startedAt = now();
|
|
246
|
-
const { body, headers } = buildWebhookRequest(event, channel);
|
|
247
|
-
const controller = new AbortController;
|
|
248
|
-
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
249
|
-
try {
|
|
250
|
-
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
251
|
-
method: "POST",
|
|
252
|
-
headers,
|
|
253
|
-
body,
|
|
254
|
-
signal: controller.signal
|
|
255
|
-
});
|
|
256
|
-
const responseBody = truncate(await response.text());
|
|
257
|
-
return {
|
|
258
|
-
attempt: 1,
|
|
259
|
-
status: response.ok ? "success" : "failed",
|
|
260
|
-
startedAt,
|
|
261
|
-
completedAt: now(),
|
|
262
|
-
responseStatus: response.status,
|
|
263
|
-
responseBody,
|
|
264
|
-
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
265
|
-
};
|
|
266
|
-
} catch (error) {
|
|
267
|
-
return {
|
|
268
|
-
attempt: 1,
|
|
269
|
-
status: "failed",
|
|
270
|
-
startedAt,
|
|
271
|
-
completedAt: now(),
|
|
272
|
-
error: error instanceof Error ? error.message : String(error)
|
|
273
|
-
};
|
|
274
|
-
} finally {
|
|
275
|
-
clearTimeout(timeout);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
async function dispatchCommand(event, channel) {
|
|
279
|
-
if (!channel.command)
|
|
280
|
-
throw new Error(`Channel ${channel.id} has no command config`);
|
|
281
|
-
const startedAt = now();
|
|
282
|
-
const eventJson = JSON.stringify(event);
|
|
283
|
-
const env = {
|
|
284
|
-
...process.env,
|
|
285
|
-
...channel.command.env,
|
|
286
|
-
HASNA_CHANNEL_ID: channel.id,
|
|
287
|
-
HASNA_EVENT_ID: event.id,
|
|
288
|
-
HASNA_EVENT_TYPE: event.type,
|
|
289
|
-
HASNA_EVENT_SOURCE: event.source,
|
|
290
|
-
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
291
|
-
HASNA_EVENT_SEVERITY: event.severity,
|
|
292
|
-
HASNA_EVENT_TIME: event.time,
|
|
293
|
-
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
294
|
-
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
295
|
-
HASNA_EVENT_JSON: eventJson
|
|
296
|
-
};
|
|
297
|
-
return new Promise((resolve) => {
|
|
298
|
-
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
299
|
-
cwd: channel.command.cwd,
|
|
300
|
-
env,
|
|
301
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
302
|
-
});
|
|
303
|
-
let stdout = "";
|
|
304
|
-
let stderr = "";
|
|
305
|
-
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
306
|
-
child.stdin.end(eventJson);
|
|
307
|
-
child.stdout.on("data", (chunk) => {
|
|
308
|
-
stdout += chunk.toString();
|
|
309
|
-
});
|
|
310
|
-
child.stderr.on("data", (chunk) => {
|
|
311
|
-
stderr += chunk.toString();
|
|
312
|
-
});
|
|
313
|
-
child.on("error", (error) => {
|
|
314
|
-
clearTimeout(timeout);
|
|
315
|
-
resolve({
|
|
316
|
-
attempt: 1,
|
|
317
|
-
status: "failed",
|
|
318
|
-
startedAt,
|
|
319
|
-
completedAt: now(),
|
|
320
|
-
stdout: truncate(stdout),
|
|
321
|
-
stderr: truncate(stderr),
|
|
322
|
-
error: error.message
|
|
323
|
-
});
|
|
324
|
-
});
|
|
325
|
-
child.on("close", (code, signal) => {
|
|
326
|
-
clearTimeout(timeout);
|
|
327
|
-
const success = code === 0;
|
|
328
|
-
resolve({
|
|
329
|
-
attempt: 1,
|
|
330
|
-
status: success ? "success" : "failed",
|
|
331
|
-
startedAt,
|
|
332
|
-
completedAt: now(),
|
|
333
|
-
stdout: truncate(stdout),
|
|
334
|
-
stderr: truncate(stderr),
|
|
335
|
-
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
336
|
-
});
|
|
337
|
-
});
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
async function dispatchChannel(event, channel, options = {}) {
|
|
341
|
-
if (channel.transport === "webhook")
|
|
342
|
-
return dispatchWebhook(event, channel, options);
|
|
343
|
-
if (channel.transport === "command")
|
|
344
|
-
return dispatchCommand(event, channel);
|
|
345
|
-
return {
|
|
346
|
-
attempt: 1,
|
|
347
|
-
status: "skipped",
|
|
348
|
-
startedAt: now(),
|
|
349
|
-
completedAt: now(),
|
|
350
|
-
error: `Unsupported transport: ${channel.transport}`
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
function createDeliveryResult(event, channel, attempts) {
|
|
354
|
-
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
355
|
-
return {
|
|
356
|
-
id: randomUUID(),
|
|
357
|
-
eventId: event.id,
|
|
358
|
-
channelId: channel.id,
|
|
359
|
-
transport: channel.transport,
|
|
360
|
-
status,
|
|
361
|
-
attempts,
|
|
362
|
-
createdAt: attempts[0]?.startedAt ?? now(),
|
|
363
|
-
completedAt: attempts.at(-1)?.completedAt ?? now()
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
function createEvent(input) {
|
|
367
|
-
return {
|
|
368
|
-
id: input.id ?? randomUUID2(),
|
|
369
|
-
source: input.source,
|
|
370
|
-
type: input.type,
|
|
371
|
-
time: normalizeTime(input.time),
|
|
372
|
-
subject: input.subject,
|
|
373
|
-
severity: input.severity ?? "info",
|
|
374
|
-
data: input.data ?? {},
|
|
375
|
-
message: input.message,
|
|
376
|
-
dedupeKey: input.dedupeKey,
|
|
377
|
-
schemaVersion: input.schemaVersion ?? "1.0",
|
|
378
|
-
metadata: input.metadata ?? {}
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
class EventsClient {
|
|
383
|
-
store;
|
|
384
|
-
redactors;
|
|
385
|
-
transportOptions;
|
|
386
|
-
constructor(options = {}) {
|
|
387
|
-
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
388
|
-
this.redactors = options.redactors ?? [];
|
|
389
|
-
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
390
|
-
}
|
|
391
|
-
async addChannel(input) {
|
|
392
|
-
const timestamp = new Date().toISOString();
|
|
393
|
-
return this.store.addChannel({
|
|
394
|
-
...input,
|
|
395
|
-
createdAt: input.createdAt ?? timestamp,
|
|
396
|
-
updatedAt: input.updatedAt ?? timestamp
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
async listChannels() {
|
|
400
|
-
return this.store.listChannels();
|
|
401
|
-
}
|
|
402
|
-
async removeChannel(id) {
|
|
403
|
-
return this.store.removeChannel(id);
|
|
404
|
-
}
|
|
405
|
-
async emit(input, options = {}) {
|
|
406
|
-
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
407
|
-
if (options.dedupe !== false) {
|
|
408
|
-
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
409
|
-
if (existing) {
|
|
410
|
-
return { event: existing, deliveries: [], deduped: true };
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
await this.store.appendEvent(event);
|
|
414
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
415
|
-
return { event, deliveries, deduped: false };
|
|
416
|
-
}
|
|
417
|
-
async listEvents() {
|
|
418
|
-
return this.store.listEvents();
|
|
419
|
-
}
|
|
420
|
-
async listDeliveries() {
|
|
421
|
-
return this.store.listDeliveries();
|
|
422
|
-
}
|
|
423
|
-
async deliver(event) {
|
|
424
|
-
const channels = await this.store.listChannels();
|
|
425
|
-
const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
|
|
426
|
-
const deliveries = [];
|
|
427
|
-
for (const channel of selected) {
|
|
428
|
-
const eventForChannel = await this.applyRedaction(event, channel);
|
|
429
|
-
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
430
|
-
await this.store.appendDelivery(result);
|
|
431
|
-
deliveries.push(result);
|
|
432
|
-
}
|
|
433
|
-
return deliveries;
|
|
434
|
-
}
|
|
435
|
-
async testChannel(id, input = {}) {
|
|
436
|
-
const channel = await this.store.getChannel(id);
|
|
437
|
-
if (!channel)
|
|
438
|
-
throw new Error(`Channel not found: ${id}`);
|
|
439
|
-
const event = createEvent({
|
|
440
|
-
source: input.source ?? "hasna.events",
|
|
441
|
-
type: input.type ?? "events.test",
|
|
442
|
-
subject: input.subject ?? id,
|
|
443
|
-
severity: input.severity ?? "info",
|
|
444
|
-
data: input.data ?? { test: true },
|
|
445
|
-
message: input.message ?? "Hasna events test delivery",
|
|
446
|
-
dedupeKey: input.dedupeKey,
|
|
447
|
-
schemaVersion: input.schemaVersion,
|
|
448
|
-
metadata: input.metadata,
|
|
449
|
-
time: input.time,
|
|
450
|
-
id: input.id
|
|
451
|
-
});
|
|
452
|
-
const eventForChannel = await this.applyRedaction(event, channel);
|
|
453
|
-
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
454
|
-
await this.store.appendDelivery(result);
|
|
455
|
-
return result;
|
|
456
|
-
}
|
|
457
|
-
async replay(options = {}) {
|
|
458
|
-
const events = (await this.store.listEvents()).filter((event) => {
|
|
459
|
-
if (options.eventId && event.id !== options.eventId)
|
|
460
|
-
return false;
|
|
461
|
-
if (options.source && event.source !== options.source)
|
|
462
|
-
return false;
|
|
463
|
-
if (options.type && event.type !== options.type)
|
|
464
|
-
return false;
|
|
465
|
-
return true;
|
|
466
|
-
});
|
|
467
|
-
if (options.dryRun)
|
|
468
|
-
return { events, deliveries: [] };
|
|
469
|
-
const deliveries = [];
|
|
470
|
-
for (const event of events) {
|
|
471
|
-
deliveries.push(...await this.deliver(event));
|
|
472
|
-
}
|
|
473
|
-
return { events, deliveries };
|
|
474
|
-
}
|
|
475
|
-
async applyRedaction(event, channel) {
|
|
476
|
-
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
477
|
-
for (const redactor of this.redactors) {
|
|
478
|
-
next = await redactor(next, channel);
|
|
479
|
-
}
|
|
480
|
-
return next;
|
|
481
|
-
}
|
|
482
|
-
async deliverWithRetry(event, channel) {
|
|
483
|
-
const policy = normalizeRetryPolicy(channel.retry);
|
|
484
|
-
const attempts = [];
|
|
485
|
-
for (let index = 0;index < policy.maxAttempts; index += 1) {
|
|
486
|
-
const attempt = await dispatchChannel(event, channel, this.transportOptions);
|
|
487
|
-
attempt.attempt = index + 1;
|
|
488
|
-
if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
|
|
489
|
-
attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
|
|
490
|
-
}
|
|
491
|
-
attempts.push(attempt);
|
|
492
|
-
if (attempt.status !== "failed")
|
|
493
|
-
break;
|
|
494
|
-
if (attempt.nextBackoffMs)
|
|
495
|
-
await Bun.sleep(attempt.nextBackoffMs);
|
|
496
|
-
}
|
|
497
|
-
return createDeliveryResult(event, channel, attempts);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
501
|
-
if (paths.length === 0)
|
|
502
|
-
return event;
|
|
503
|
-
const copy = structuredClone(event);
|
|
504
|
-
for (const path of paths) {
|
|
505
|
-
setPath(copy, path, replacement);
|
|
506
|
-
}
|
|
507
|
-
return copy;
|
|
508
|
-
}
|
|
509
|
-
function sanitizeChannelForOutput(channel) {
|
|
510
|
-
const copy = structuredClone(channel);
|
|
511
|
-
if (copy.webhook?.secret)
|
|
512
|
-
copy.webhook.secret = "[REDACTED]";
|
|
513
|
-
if (copy.command?.env) {
|
|
514
|
-
copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey(key) ? "[REDACTED]" : value]));
|
|
515
|
-
}
|
|
516
|
-
return copy;
|
|
517
|
-
}
|
|
518
|
-
function sanitizeChannelsForOutput(channels) {
|
|
519
|
-
return channels.map(sanitizeChannelForOutput);
|
|
520
|
-
}
|
|
521
|
-
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
522
|
-
return redactValue(event, replacement);
|
|
523
|
-
}
|
|
524
|
-
function shouldRedactKey(key) {
|
|
525
|
-
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
526
|
-
}
|
|
527
|
-
function redactValue(value, replacement) {
|
|
528
|
-
if (Array.isArray(value))
|
|
529
|
-
return value.map((item) => redactValue(item, replacement));
|
|
530
|
-
if (!value || typeof value !== "object")
|
|
531
|
-
return value;
|
|
532
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
533
|
-
key,
|
|
534
|
-
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
535
|
-
]));
|
|
536
|
-
}
|
|
537
|
-
function setPath(input, path, replacement) {
|
|
538
|
-
const parts = path.split(".");
|
|
539
|
-
let cursor = input;
|
|
540
|
-
for (const part of parts.slice(0, -1)) {
|
|
541
|
-
const next = cursor[part];
|
|
542
|
-
if (!next || typeof next !== "object")
|
|
543
|
-
return;
|
|
544
|
-
cursor = next;
|
|
545
|
-
}
|
|
546
|
-
const last = parts.at(-1);
|
|
547
|
-
if (last && last in cursor)
|
|
548
|
-
cursor[last] = replacement;
|
|
549
|
-
}
|
|
550
|
-
function normalizeTime(value) {
|
|
551
|
-
if (!value)
|
|
552
|
-
return new Date().toISOString();
|
|
553
|
-
return value instanceof Date ? value.toISOString() : value;
|
|
554
|
-
}
|
|
555
|
-
function normalizeRetryPolicy(policy) {
|
|
556
|
-
return {
|
|
557
|
-
maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
|
|
558
|
-
backoffMs: Math.max(0, policy?.backoffMs ?? 250),
|
|
559
|
-
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
|
|
563
43
|
// src/mcp/server.ts
|
|
44
|
+
import { EventsClient as EventsClient2, sanitizeChannelForOutput, sanitizeChannelsForOutput as sanitizeChannelsForOutput2 } from "@hasna/events";
|
|
564
45
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
565
46
|
|
|
566
47
|
// node_modules/zod/v3/external.js
|
|
@@ -4537,8 +4018,8 @@ var coerce = {
|
|
|
4537
4018
|
};
|
|
4538
4019
|
var NEVER = INVALID;
|
|
4539
4020
|
// src/commands/backup.ts
|
|
4540
|
-
import { homedir
|
|
4541
|
-
import { join as
|
|
4021
|
+
import { homedir, hostname } from "os";
|
|
4022
|
+
import { join as join2 } from "path";
|
|
4542
4023
|
var MACHINES_BACKUP_BUCKET_ENV = "HASNA_MACHINES_S3_BUCKET";
|
|
4543
4024
|
var MACHINES_BACKUP_BUCKET_FALLBACK_ENV = "MACHINES_S3_BUCKET";
|
|
4544
4025
|
var MACHINES_BACKUP_PREFIX_ENV = "HASNA_MACHINES_S3_PREFIX";
|
|
@@ -4586,16 +4067,16 @@ function resolveBackupTarget(options = {}) {
|
|
|
4586
4067
|
};
|
|
4587
4068
|
}
|
|
4588
4069
|
function defaultBackupSources() {
|
|
4589
|
-
const home =
|
|
4070
|
+
const home = homedir();
|
|
4590
4071
|
return [
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4072
|
+
join2(home, ".hasna"),
|
|
4073
|
+
join2(home, ".ssh"),
|
|
4074
|
+
join2(home, ".secrets")
|
|
4594
4075
|
];
|
|
4595
4076
|
}
|
|
4596
4077
|
function buildBackupPlan(bucket, prefix) {
|
|
4597
4078
|
const target = resolveBackupTarget({ bucket, prefix });
|
|
4598
|
-
const archivePath =
|
|
4079
|
+
const archivePath = join2(homedir(), ".hasna", "machines", "backup.tgz");
|
|
4599
4080
|
const sources = defaultBackupSources();
|
|
4600
4081
|
const steps = [
|
|
4601
4082
|
{
|
|
@@ -4646,33 +4127,33 @@ function runBackup(bucket, prefix, options = {}) {
|
|
|
4646
4127
|
}
|
|
4647
4128
|
|
|
4648
4129
|
// src/manifests.ts
|
|
4649
|
-
import { existsSync as
|
|
4650
|
-
import { arch, homedir as
|
|
4130
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
4131
|
+
import { arch, homedir as homedir2, hostname as hostname2, platform, userInfo } from "os";
|
|
4651
4132
|
import { dirname as dirname3 } from "path";
|
|
4652
4133
|
|
|
4653
4134
|
// src/paths.ts
|
|
4654
|
-
import { existsSync as
|
|
4655
|
-
import { dirname as dirname2, join as
|
|
4135
|
+
import { existsSync as existsSync2, mkdirSync } from "fs";
|
|
4136
|
+
import { dirname as dirname2, join as join3, resolve } from "path";
|
|
4656
4137
|
function homeDir() {
|
|
4657
4138
|
return process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
4658
4139
|
}
|
|
4659
4140
|
function getDataDir() {
|
|
4660
|
-
return process.env["HASNA_MACHINES_DIR"] ||
|
|
4141
|
+
return process.env["HASNA_MACHINES_DIR"] || join3(homeDir(), ".hasna", "machines");
|
|
4661
4142
|
}
|
|
4662
4143
|
function getDbPath() {
|
|
4663
|
-
return process.env["HASNA_MACHINES_DB_PATH"] ||
|
|
4144
|
+
return process.env["HASNA_MACHINES_DB_PATH"] || join3(getDataDir(), "machines.db");
|
|
4664
4145
|
}
|
|
4665
4146
|
function getManifestPath() {
|
|
4666
|
-
return process.env["HASNA_MACHINES_MANIFEST_PATH"] ||
|
|
4147
|
+
return process.env["HASNA_MACHINES_MANIFEST_PATH"] || join3(getDataDir(), "machines.json");
|
|
4667
4148
|
}
|
|
4668
4149
|
function getNotificationsPath() {
|
|
4669
|
-
return process.env["HASNA_MACHINES_NOTIFICATIONS_PATH"] ||
|
|
4150
|
+
return process.env["HASNA_MACHINES_NOTIFICATIONS_PATH"] || join3(getDataDir(), "notifications.json");
|
|
4670
4151
|
}
|
|
4671
4152
|
function ensureParentDir(filePath) {
|
|
4672
4153
|
if (filePath === ":memory:")
|
|
4673
4154
|
return;
|
|
4674
4155
|
const dir = dirname2(resolve(filePath));
|
|
4675
|
-
if (!
|
|
4156
|
+
if (!existsSync2(dir)) {
|
|
4676
4157
|
mkdirSync(dir, { recursive: true });
|
|
4677
4158
|
}
|
|
4678
4159
|
}
|
|
@@ -4714,7 +4195,7 @@ var fleetSchema = exports_external.object({
|
|
|
4714
4195
|
machines: exports_external.array(machineSchema)
|
|
4715
4196
|
});
|
|
4716
4197
|
function detectWorkspacePath() {
|
|
4717
|
-
const home =
|
|
4198
|
+
const home = homedir2();
|
|
4718
4199
|
if (platform() === "darwin") {
|
|
4719
4200
|
return `${home}/Workspace`;
|
|
4720
4201
|
}
|
|
@@ -4738,7 +4219,7 @@ function getDefaultManifest() {
|
|
|
4738
4219
|
};
|
|
4739
4220
|
}
|
|
4740
4221
|
function readManifest(path = getManifestPath()) {
|
|
4741
|
-
if (!
|
|
4222
|
+
if (!existsSync3(path)) {
|
|
4742
4223
|
return getDefaultManifest();
|
|
4743
4224
|
}
|
|
4744
4225
|
const raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
@@ -4874,19 +4355,19 @@ function countRuns(table) {
|
|
|
4874
4355
|
}
|
|
4875
4356
|
function recordSetupRun(machineId, status, details) {
|
|
4876
4357
|
const db = getDb();
|
|
4877
|
-
const
|
|
4358
|
+
const now = new Date().toISOString();
|
|
4878
4359
|
db.query(`INSERT INTO setup_runs (id, machine_id, status, details_json, created_at, updated_at)
|
|
4879
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(details),
|
|
4360
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(details), now, now);
|
|
4880
4361
|
}
|
|
4881
4362
|
function recordSyncRun(machineId, status, actions) {
|
|
4882
4363
|
const db = getDb();
|
|
4883
|
-
const
|
|
4364
|
+
const now = new Date().toISOString();
|
|
4884
4365
|
db.query(`INSERT INTO sync_runs (id, machine_id, status, actions_json, created_at, updated_at)
|
|
4885
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(actions),
|
|
4366
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(actions), now, now);
|
|
4886
4367
|
}
|
|
4887
4368
|
|
|
4888
4369
|
// src/topology.ts
|
|
4889
|
-
import { existsSync as
|
|
4370
|
+
import { existsSync as existsSync4 } from "fs";
|
|
4890
4371
|
import { arch as arch2, hostname as hostname4, platform as platform2, userInfo as userInfo2 } from "os";
|
|
4891
4372
|
import { spawnSync } from "child_process";
|
|
4892
4373
|
var MACHINES_CONSUMER_CONTRACT_VERSION = 1;
|
|
@@ -5076,7 +4557,7 @@ function buildEntry(input) {
|
|
|
5076
4557
|
};
|
|
5077
4558
|
}
|
|
5078
4559
|
function discoverMachineTopology(options = {}) {
|
|
5079
|
-
const
|
|
4560
|
+
const now = options.now ?? new Date;
|
|
5080
4561
|
const runner = options.runner ?? defaultRunner;
|
|
5081
4562
|
const warnings = [];
|
|
5082
4563
|
const manifest = readManifest();
|
|
@@ -5108,11 +4589,11 @@ function discoverMachineTopology(options = {}) {
|
|
|
5108
4589
|
version: getPackageVersion()
|
|
5109
4590
|
},
|
|
5110
4591
|
capabilities: getMachinesConsumerCapabilities(),
|
|
5111
|
-
generated_at:
|
|
4592
|
+
generated_at: now.toISOString(),
|
|
5112
4593
|
local_machine_id: localMachineId,
|
|
5113
4594
|
local_hostname: hostname4(),
|
|
5114
4595
|
current_platform: normalizePlatform2(),
|
|
5115
|
-
manifest_path_known:
|
|
4596
|
+
manifest_path_known: existsSync4(getManifestPath()),
|
|
5116
4597
|
machines,
|
|
5117
4598
|
warnings
|
|
5118
4599
|
};
|
|
@@ -5231,11 +4712,11 @@ function cacheability(input) {
|
|
|
5231
4712
|
};
|
|
5232
4713
|
}
|
|
5233
4714
|
function resolveMachineRoute(machineId, options = {}) {
|
|
5234
|
-
const
|
|
4715
|
+
const now = options.now ?? new Date;
|
|
5235
4716
|
const topology = options.topology ?? discoverMachineTopology(options);
|
|
5236
4717
|
const warnings = [...topology.warnings];
|
|
5237
4718
|
const { machine, matchedBy } = findRouteMachine(topology, machineId);
|
|
5238
|
-
const generatedAt =
|
|
4719
|
+
const generatedAt = now.toISOString();
|
|
5239
4720
|
if (!machine) {
|
|
5240
4721
|
warnings.push(`machine_not_found:${machineId}`);
|
|
5241
4722
|
return {
|
|
@@ -5261,8 +4742,8 @@ function resolveMachineRoute(machineId, options = {}) {
|
|
|
5261
4742
|
},
|
|
5262
4743
|
cacheability: cacheability({
|
|
5263
4744
|
ok: false,
|
|
5264
|
-
observedAt:
|
|
5265
|
-
now
|
|
4745
|
+
observedAt: now,
|
|
4746
|
+
now,
|
|
5266
4747
|
ttlMs: options.resolverTtlMs,
|
|
5267
4748
|
authority: "unresolved",
|
|
5268
4749
|
confidence: "none",
|
|
@@ -5299,8 +4780,8 @@ function resolveMachineRoute(machineId, options = {}) {
|
|
|
5299
4780
|
},
|
|
5300
4781
|
cacheability: cacheability({
|
|
5301
4782
|
ok,
|
|
5302
|
-
observedAt:
|
|
5303
|
-
now
|
|
4783
|
+
observedAt: now,
|
|
4784
|
+
now,
|
|
5304
4785
|
ttlMs: options.resolverTtlMs,
|
|
5305
4786
|
authority: routeAuthority({ machine, selectedHint, matchedBy }),
|
|
5306
4787
|
confidence,
|
|
@@ -5387,7 +4868,7 @@ function canCheckPathForMachine(machine, localMachineId) {
|
|
|
5387
4868
|
function checkedPathExists(path, check) {
|
|
5388
4869
|
if (!path || !check)
|
|
5389
4870
|
return null;
|
|
5390
|
-
return
|
|
4871
|
+
return existsSync4(path);
|
|
5391
4872
|
}
|
|
5392
4873
|
function repairHint(input) {
|
|
5393
4874
|
const command = [
|
|
@@ -5602,11 +5083,11 @@ function metadataKeysForDiagnostics(metadata) {
|
|
|
5602
5083
|
return Object.keys(metadata).filter((key) => !/(secret|token|key|password|credential)/i.test(key)).sort();
|
|
5603
5084
|
}
|
|
5604
5085
|
function resolveMachineWorkspace(options) {
|
|
5605
|
-
const
|
|
5086
|
+
const now = options.now ?? new Date;
|
|
5606
5087
|
const topology = options.topology ?? discoverMachineTopology(options);
|
|
5607
5088
|
const warnings = [...topology.warnings];
|
|
5608
5089
|
const { machine, matchedBy } = findRouteMachine(topology, options.machineId);
|
|
5609
|
-
const generatedAt =
|
|
5090
|
+
const generatedAt = now.toISOString();
|
|
5610
5091
|
const repoName = options.repoName ?? options.projectId;
|
|
5611
5092
|
const openFilesRepoName = options.openFilesRepoName ?? "open-files";
|
|
5612
5093
|
if (!machine) {
|
|
@@ -5635,8 +5116,8 @@ function resolveMachineWorkspace(options) {
|
|
|
5635
5116
|
},
|
|
5636
5117
|
cacheability: cacheability({
|
|
5637
5118
|
ok: false,
|
|
5638
|
-
observedAt:
|
|
5639
|
-
now
|
|
5119
|
+
observedAt: now,
|
|
5120
|
+
now,
|
|
5640
5121
|
ttlMs: options.resolverTtlMs,
|
|
5641
5122
|
authority: "unresolved",
|
|
5642
5123
|
confidence: "none",
|
|
@@ -5708,8 +5189,8 @@ function resolveMachineWorkspace(options) {
|
|
|
5708
5189
|
},
|
|
5709
5190
|
cacheability: cacheability({
|
|
5710
5191
|
ok: workspaceOk,
|
|
5711
|
-
observedAt:
|
|
5712
|
-
now
|
|
5192
|
+
observedAt: now,
|
|
5193
|
+
now,
|
|
5713
5194
|
ttlMs: options.resolverTtlMs,
|
|
5714
5195
|
authority: workspaceAuthority(workspacePaths),
|
|
5715
5196
|
confidence: workspaceOk ? "medium" : "none",
|
|
@@ -5866,7 +5347,14 @@ function buildAppSteps(machine) {
|
|
|
5866
5347
|
}));
|
|
5867
5348
|
}
|
|
5868
5349
|
function resolveMachine(machineId) {
|
|
5869
|
-
|
|
5350
|
+
if (!machineId)
|
|
5351
|
+
return detectCurrentMachineManifest();
|
|
5352
|
+
return getManifestMachine(machineId) || {
|
|
5353
|
+
id: machineId,
|
|
5354
|
+
platform: "linux",
|
|
5355
|
+
workspacePath: "",
|
|
5356
|
+
apps: []
|
|
5357
|
+
};
|
|
5870
5358
|
}
|
|
5871
5359
|
function parseProbeOutput(app, machine, stdout) {
|
|
5872
5360
|
const lines = stdout.trim().split(`
|
|
@@ -5939,21 +5427,21 @@ function runAppsInstall(machineId, options = {}, runner = runMachineCommand) {
|
|
|
5939
5427
|
}
|
|
5940
5428
|
|
|
5941
5429
|
// src/commands/cert.ts
|
|
5942
|
-
import { homedir as
|
|
5943
|
-
import { join as
|
|
5430
|
+
import { homedir as homedir3, platform as platform3 } from "os";
|
|
5431
|
+
import { join as join4 } from "path";
|
|
5944
5432
|
function quote2(value) {
|
|
5945
5433
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
5946
5434
|
}
|
|
5947
5435
|
function certDir() {
|
|
5948
|
-
return
|
|
5436
|
+
return join4(homedir3(), ".hasna", "machines", "certs");
|
|
5949
5437
|
}
|
|
5950
5438
|
function buildCertPlan(domains) {
|
|
5951
5439
|
if (domains.length === 0) {
|
|
5952
5440
|
throw new Error("At least one domain is required.");
|
|
5953
5441
|
}
|
|
5954
5442
|
const primary = domains[0];
|
|
5955
|
-
const certPath =
|
|
5956
|
-
const keyPath =
|
|
5443
|
+
const certPath = join4(certDir(), `${primary}.pem`);
|
|
5444
|
+
const keyPath = join4(certDir(), `${primary}-key.pem`);
|
|
5957
5445
|
const steps = [];
|
|
5958
5446
|
if (platform3() === "darwin") {
|
|
5959
5447
|
steps.push({
|
|
@@ -6017,14 +5505,14 @@ function runCertPlan(domains, options = {}) {
|
|
|
6017
5505
|
}
|
|
6018
5506
|
|
|
6019
5507
|
// src/commands/dns.ts
|
|
6020
|
-
import { existsSync as
|
|
6021
|
-
import { join as
|
|
5508
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
5509
|
+
import { join as join5 } from "path";
|
|
6022
5510
|
function getDnsPath() {
|
|
6023
|
-
return
|
|
5511
|
+
return join5(getDataDir(), "dns.json");
|
|
6024
5512
|
}
|
|
6025
5513
|
function readMappings() {
|
|
6026
5514
|
const path = getDnsPath();
|
|
6027
|
-
if (!
|
|
5515
|
+
if (!existsSync5(path))
|
|
6028
5516
|
return [];
|
|
6029
5517
|
return JSON.parse(readFileSync3(path, "utf8"));
|
|
6030
5518
|
}
|
|
@@ -6053,10 +5541,10 @@ function renderDomainMapping(domain) {
|
|
|
6053
5541
|
hostsEntry: `${entry.targetHost} ${entry.domain}`,
|
|
6054
5542
|
caddySnippet: `${entry.domain} {
|
|
6055
5543
|
reverse_proxy 127.0.0.1:${entry.port}
|
|
6056
|
-
tls ${
|
|
5544
|
+
tls ${join5(getDataDir(), "certs", `${entry.domain}.pem`)} ${join5(getDataDir(), "certs", `${entry.domain}-key.pem`)}
|
|
6057
5545
|
}`,
|
|
6058
|
-
certPath:
|
|
6059
|
-
keyPath:
|
|
5546
|
+
certPath: join5(getDataDir(), "certs", `${entry.domain}.pem`),
|
|
5547
|
+
keyPath: join5(getDataDir(), "certs", `${entry.domain}-key.pem`)
|
|
6060
5548
|
};
|
|
6061
5549
|
}
|
|
6062
5550
|
|
|
@@ -6187,7 +5675,13 @@ function buildInstallSteps(machine, tools) {
|
|
|
6187
5675
|
}));
|
|
6188
5676
|
}
|
|
6189
5677
|
function resolveMachine2(machineId) {
|
|
6190
|
-
|
|
5678
|
+
if (!machineId)
|
|
5679
|
+
return detectCurrentMachineManifest();
|
|
5680
|
+
return getManifestMachine(machineId) || {
|
|
5681
|
+
id: machineId,
|
|
5682
|
+
platform: "linux",
|
|
5683
|
+
workspacePath: ""
|
|
5684
|
+
};
|
|
6191
5685
|
}
|
|
6192
5686
|
function buildProbeCommand(tool) {
|
|
6193
5687
|
const binary = getToolBinary(tool);
|
|
@@ -6288,7 +5782,10 @@ function buildInstallSteps2(machine) {
|
|
|
6288
5782
|
];
|
|
6289
5783
|
}
|
|
6290
5784
|
function buildTailscaleInstallPlan(machineId) {
|
|
6291
|
-
const machine =
|
|
5785
|
+
const machine = machineId ? getManifestMachine(machineId) : detectCurrentMachineManifest();
|
|
5786
|
+
if (!machine) {
|
|
5787
|
+
throw new Error(`Machine not found in manifest: ${machineId}`);
|
|
5788
|
+
}
|
|
6292
5789
|
return {
|
|
6293
5790
|
machineId: machine.id,
|
|
6294
5791
|
mode: "plan",
|
|
@@ -6317,7 +5814,7 @@ function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand
|
|
|
6317
5814
|
}
|
|
6318
5815
|
|
|
6319
5816
|
// src/commands/notifications.ts
|
|
6320
|
-
import { existsSync as
|
|
5817
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
6321
5818
|
var notificationChannelSchema = exports_external.object({
|
|
6322
5819
|
id: exports_external.string(),
|
|
6323
5820
|
type: exports_external.enum(["email", "webhook", "command"]),
|
|
@@ -6399,7 +5896,7 @@ ${message}
|
|
|
6399
5896
|
}
|
|
6400
5897
|
throw new Error("No local email transport available. Install sendmail or mail.");
|
|
6401
5898
|
}
|
|
6402
|
-
async function
|
|
5899
|
+
async function dispatchWebhook(channel, event, message) {
|
|
6403
5900
|
const response = await fetch(channel.target, {
|
|
6404
5901
|
method: "POST",
|
|
6405
5902
|
headers: {
|
|
@@ -6424,7 +5921,7 @@ async function dispatchWebhook2(channel, event, message) {
|
|
|
6424
5921
|
detail: `Webhook accepted with HTTP ${response.status}`
|
|
6425
5922
|
};
|
|
6426
5923
|
}
|
|
6427
|
-
async function
|
|
5924
|
+
async function dispatchCommand(channel, event, message) {
|
|
6428
5925
|
const result = Bun.spawnSync(["bash", "-lc", channel.target], {
|
|
6429
5926
|
stdout: "pipe",
|
|
6430
5927
|
stderr: "pipe",
|
|
@@ -6447,7 +5944,7 @@ async function dispatchCommand2(channel, event, message) {
|
|
|
6447
5944
|
detail: stdout || "Command completed successfully"
|
|
6448
5945
|
};
|
|
6449
5946
|
}
|
|
6450
|
-
async function
|
|
5947
|
+
async function dispatchChannel(channel, event, message) {
|
|
6451
5948
|
if (!channel.enabled) {
|
|
6452
5949
|
return {
|
|
6453
5950
|
channelId: channel.id,
|
|
@@ -6461,9 +5958,9 @@ async function dispatchChannel2(channel, event, message) {
|
|
|
6461
5958
|
return dispatchEmail(channel, event, message);
|
|
6462
5959
|
}
|
|
6463
5960
|
if (channel.type === "webhook") {
|
|
6464
|
-
return
|
|
5961
|
+
return dispatchWebhook(channel, event, message);
|
|
6465
5962
|
}
|
|
6466
|
-
return
|
|
5963
|
+
return dispatchCommand(channel, event, message);
|
|
6467
5964
|
}
|
|
6468
5965
|
function getDefaultNotificationConfig() {
|
|
6469
5966
|
return {
|
|
@@ -6473,7 +5970,7 @@ function getDefaultNotificationConfig() {
|
|
|
6473
5970
|
};
|
|
6474
5971
|
}
|
|
6475
5972
|
function readNotificationConfig(path = getNotificationsPath()) {
|
|
6476
|
-
if (!
|
|
5973
|
+
if (!existsSync6(path)) {
|
|
6477
5974
|
return getDefaultNotificationConfig();
|
|
6478
5975
|
}
|
|
6479
5976
|
return notificationConfigSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
|
|
@@ -6518,7 +6015,7 @@ async function dispatchNotificationEvent(event, message, options = {}) {
|
|
|
6518
6015
|
const deliveries = [];
|
|
6519
6016
|
for (const channel of channels) {
|
|
6520
6017
|
try {
|
|
6521
|
-
deliveries.push(await
|
|
6018
|
+
deliveries.push(await dispatchChannel(channel, event, message));
|
|
6522
6019
|
} catch (error) {
|
|
6523
6020
|
deliveries.push({
|
|
6524
6021
|
channelId: channel.id,
|
|
@@ -6553,7 +6050,7 @@ async function testNotificationChannel(channelId, event = "manual.test", message
|
|
|
6553
6050
|
if (!options.yes) {
|
|
6554
6051
|
throw new Error("Notification test execution requires --yes.");
|
|
6555
6052
|
}
|
|
6556
|
-
const delivery = await
|
|
6053
|
+
const delivery = await dispatchChannel(channel, event, message);
|
|
6557
6054
|
return {
|
|
6558
6055
|
channelId,
|
|
6559
6056
|
mode: "apply",
|
|
@@ -6617,6 +6114,9 @@ function listPorts(machineId) {
|
|
|
6617
6114
|
};
|
|
6618
6115
|
}
|
|
6619
6116
|
|
|
6117
|
+
// src/commands/serve.ts
|
|
6118
|
+
import { EventsClient, sanitizeChannelsForOutput } from "@hasna/events";
|
|
6119
|
+
|
|
6620
6120
|
// src/commands/manifest.ts
|
|
6621
6121
|
function manifestList() {
|
|
6622
6122
|
return readManifest();
|
|
@@ -6894,7 +6394,7 @@ function renderDashboardHtml() {
|
|
|
6894
6394
|
}
|
|
6895
6395
|
|
|
6896
6396
|
// src/commands/setup.ts
|
|
6897
|
-
import { homedir as
|
|
6397
|
+
import { homedir as homedir4 } from "os";
|
|
6898
6398
|
function quote3(value) {
|
|
6899
6399
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
6900
6400
|
}
|
|
@@ -6961,10 +6461,13 @@ function buildSetupPlan(machineId) {
|
|
|
6961
6461
|
const manifest = readManifest();
|
|
6962
6462
|
const currentMachineId = getLocalMachineId();
|
|
6963
6463
|
const selected = machineId ? manifest.machines.find((machine) => machine.id === machineId) : manifest.machines.find((machine) => machine.id === currentMachineId);
|
|
6464
|
+
if (machineId && !selected) {
|
|
6465
|
+
throw new Error(`Machine not found in manifest: ${machineId}`);
|
|
6466
|
+
}
|
|
6964
6467
|
const target = selected || {
|
|
6965
6468
|
id: currentMachineId,
|
|
6966
6469
|
platform: "linux",
|
|
6967
|
-
workspacePath: `${
|
|
6470
|
+
workspacePath: `${homedir4()}/workspace`
|
|
6968
6471
|
};
|
|
6969
6472
|
const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
|
|
6970
6473
|
return {
|
|
@@ -7009,8 +6512,8 @@ function runSetup(machineId, options = {}, runner = runMachineCommand) {
|
|
|
7009
6512
|
}
|
|
7010
6513
|
|
|
7011
6514
|
// src/commands/sync.ts
|
|
7012
|
-
import { existsSync as
|
|
7013
|
-
import { homedir as
|
|
6515
|
+
import { existsSync as existsSync7, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
|
|
6516
|
+
import { homedir as homedir5 } from "os";
|
|
7014
6517
|
function quote4(value) {
|
|
7015
6518
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
7016
6519
|
}
|
|
@@ -7062,8 +6565,8 @@ function detectFileActions(machine) {
|
|
|
7062
6565
|
throw new Error(`Remote file sync planning is not supported for ${machine.id}; refusing to inspect or apply local paths as remote state.`);
|
|
7063
6566
|
}
|
|
7064
6567
|
return (machine.files || []).map((file, index) => {
|
|
7065
|
-
const sourceExists =
|
|
7066
|
-
const targetExists =
|
|
6568
|
+
const sourceExists = existsSync7(file.source);
|
|
6569
|
+
const targetExists = existsSync7(file.target);
|
|
7067
6570
|
let status = "missing";
|
|
7068
6571
|
if (sourceExists && targetExists) {
|
|
7069
6572
|
if (file.mode === "symlink") {
|
|
@@ -7088,10 +6591,13 @@ function buildSyncPlan(machineId, runner = runMachineCommand) {
|
|
|
7088
6591
|
const manifest = readManifest();
|
|
7089
6592
|
const currentMachineId = getLocalMachineId();
|
|
7090
6593
|
const selected = machineId ? manifest.machines.find((machine) => machine.id === machineId) : manifest.machines.find((machine) => machine.id === currentMachineId);
|
|
6594
|
+
if (machineId && !selected) {
|
|
6595
|
+
throw new Error(`Machine not found in manifest: ${machineId}`);
|
|
6596
|
+
}
|
|
7091
6597
|
const target = selected || {
|
|
7092
6598
|
id: currentMachineId,
|
|
7093
6599
|
platform: "linux",
|
|
7094
|
-
workspacePath: `${
|
|
6600
|
+
workspacePath: `${homedir5()}/workspace`
|
|
7095
6601
|
};
|
|
7096
6602
|
const actions = [
|
|
7097
6603
|
...detectPackageActions(target, runner),
|
|
@@ -7682,7 +7188,7 @@ function upsertSqlite(db, table, columns, rows) {
|
|
|
7682
7188
|
}
|
|
7683
7189
|
function recordSyncMeta(db, direction, results) {
|
|
7684
7190
|
ensureSyncMetaTable(db);
|
|
7685
|
-
const
|
|
7191
|
+
const now = new Date().toISOString();
|
|
7686
7192
|
const statement = db.query(`
|
|
7687
7193
|
INSERT INTO _machines_sync_meta (table_name, last_synced_at, direction)
|
|
7688
7194
|
VALUES (?, ?, ?)
|
|
@@ -7691,7 +7197,7 @@ function recordSyncMeta(db, direction, results) {
|
|
|
7691
7197
|
for (const result of results) {
|
|
7692
7198
|
if (result.errors.length > 0)
|
|
7693
7199
|
continue;
|
|
7694
|
-
statement.run(result.table,
|
|
7200
|
+
statement.run(result.table, now, direction);
|
|
7695
7201
|
}
|
|
7696
7202
|
}
|
|
7697
7203
|
function ensureSyncMetaTable(db) {
|
|
@@ -7741,7 +7247,7 @@ function buildServer(version = getPackageVersion()) {
|
|
|
7741
7247
|
}
|
|
7742
7248
|
function createMcpServer(version) {
|
|
7743
7249
|
const server = new McpServer({ name: "machines", version });
|
|
7744
|
-
const events = new
|
|
7250
|
+
const events = new EventsClient2;
|
|
7745
7251
|
server.tool("machines_status", "Return local machine fleet status paths and machine identity.", {}, async () => ({
|
|
7746
7252
|
content: [{ type: "text", text: JSON.stringify(getStatus(), null, 2) }]
|
|
7747
7253
|
}));
|
|
@@ -7895,7 +7401,7 @@ function createMcpServer(version) {
|
|
|
7895
7401
|
}));
|
|
7896
7402
|
server.tool("machines_notifications_dispatch", "Dispatch an event to matching notification channels.", { event: exports_external.string().describe("Event name"), message: exports_external.string().describe("Message body"), channel_id: exports_external.string().optional().describe("Limit delivery to one channel") }, async ({ event, message, channel_id }) => ({ content: [{ type: "text", text: JSON.stringify(await dispatchNotificationEvent(event, message, { channelId: channel_id }), null, 2) }] }));
|
|
7897
7403
|
server.tool("machines_notifications_remove", "Remove a notification channel.", { channel_id: exports_external.string().describe("Channel identifier") }, async ({ channel_id }) => ({ content: [{ type: "text", text: JSON.stringify(removeNotificationChannel(channel_id), null, 2) }] }));
|
|
7898
|
-
server.tool("machines_webhooks_add", "Add or replace a shared
|
|
7404
|
+
server.tool("machines_webhooks_add", "Add or replace a shared event webhook channel.", {
|
|
7899
7405
|
channel_id: exports_external.string().describe("Channel identifier"),
|
|
7900
7406
|
url: exports_external.string().url().describe("Webhook URL"),
|
|
7901
7407
|
event_type: exports_external.string().optional().describe("Optional event type filter, e.g. machines.*"),
|
|
@@ -7903,26 +7409,26 @@ function createMcpServer(version) {
|
|
|
7903
7409
|
secret: exports_external.string().optional().describe("Optional HMAC secret"),
|
|
7904
7410
|
enabled: exports_external.boolean().optional().describe("Whether the channel is enabled")
|
|
7905
7411
|
}, async ({ channel_id, url, event_type, source, secret, enabled }) => {
|
|
7906
|
-
const
|
|
7412
|
+
const now = new Date().toISOString();
|
|
7907
7413
|
const channel = await events.addChannel({
|
|
7908
7414
|
id: channel_id,
|
|
7909
7415
|
enabled: enabled ?? true,
|
|
7910
7416
|
transport: "webhook",
|
|
7911
7417
|
filters: event_type || source ? [{ type: event_type, source }] : undefined,
|
|
7912
7418
|
webhook: { url, secret },
|
|
7913
|
-
createdAt:
|
|
7914
|
-
updatedAt:
|
|
7419
|
+
createdAt: now,
|
|
7420
|
+
updatedAt: now
|
|
7915
7421
|
});
|
|
7916
7422
|
return { content: [{ type: "text", text: JSON.stringify(sanitizeChannelForOutput(channel), null, 2) }] };
|
|
7917
7423
|
});
|
|
7918
|
-
server.tool("machines_webhooks_list", "List shared
|
|
7919
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
7424
|
+
server.tool("machines_webhooks_list", "List shared event webhook channels.", {}, async () => ({
|
|
7425
|
+
content: [{ type: "text", text: JSON.stringify(sanitizeChannelsForOutput2(await events.listChannels()), null, 2) }]
|
|
7920
7426
|
}));
|
|
7921
|
-
server.tool("machines_webhooks_test", "Send a test event to one shared
|
|
7427
|
+
server.tool("machines_webhooks_test", "Send a test event to one shared event channel.", { channel_id: exports_external.string().describe("Channel identifier"), event_type: exports_external.string().optional().describe("Event type"), message: exports_external.string().optional().describe("Message body") }, async ({ channel_id, event_type, message }) => ({
|
|
7922
7428
|
content: [{ type: "text", text: JSON.stringify(await events.testChannel(channel_id, { source: "machines", type: event_type ?? "events.test", message }), null, 2) }]
|
|
7923
7429
|
}));
|
|
7924
|
-
server.tool("machines_webhooks_remove", "Remove a shared
|
|
7925
|
-
server.tool("machines_events_emit", "Emit a shared
|
|
7430
|
+
server.tool("machines_webhooks_remove", "Remove a shared event channel.", { channel_id: exports_external.string().describe("Channel identifier") }, async ({ channel_id }) => ({ content: [{ type: "text", text: JSON.stringify({ removed: await events.removeChannel(channel_id) }, null, 2) }] }));
|
|
7431
|
+
server.tool("machines_events_emit", "Emit a shared event from machines.", {
|
|
7926
7432
|
event_type: exports_external.string().describe("Event type"),
|
|
7927
7433
|
subject: exports_external.string().optional().describe("Event subject"),
|
|
7928
7434
|
severity: exports_external.enum(["debug", "info", "notice", "warning", "error", "critical"]).optional().describe("Event severity"),
|
|
@@ -7943,10 +7449,10 @@ function createMcpServer(version) {
|
|
|
7943
7449
|
dedupeKey: dedupe_key
|
|
7944
7450
|
}, { deliver: deliver !== false }), null, 2) }]
|
|
7945
7451
|
}));
|
|
7946
|
-
server.tool("machines_events_list", "List shared
|
|
7452
|
+
server.tool("machines_events_list", "List shared events.", {}, async () => ({
|
|
7947
7453
|
content: [{ type: "text", text: JSON.stringify(await events.listEvents(), null, 2) }]
|
|
7948
7454
|
}));
|
|
7949
|
-
server.tool("machines_events_replay", "Replay shared
|
|
7455
|
+
server.tool("machines_events_replay", "Replay shared events.", { event_id: exports_external.string().optional().describe("Event id"), source: exports_external.string().optional().describe("Source filter"), event_type: exports_external.string().optional().describe("Event type filter"), dry_run: exports_external.boolean().optional().describe("Preview without delivery") }, async ({ event_id, source, event_type, dry_run }) => ({
|
|
7950
7456
|
content: [{ type: "text", text: JSON.stringify(await events.replay({ eventId: event_id, source, type: event_type, dryRun: dry_run }), null, 2) }]
|
|
7951
7457
|
}));
|
|
7952
7458
|
server.tool("machines_serve_info", "Preview the dashboard server bind address and routes.", { host: exports_external.string().optional().describe("Host interface"), port: exports_external.number().optional().describe("Port number") }, async ({ host, port }) => ({ content: [{ type: "text", text: JSON.stringify(getServeInfo({ host, port }), null, 2) }] }));
|