@hasna/machines 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp/index.js CHANGED
@@ -16,9 +16,9 @@ var __export = (target, all) => {
16
16
  };
17
17
 
18
18
  // src/mcp/index.ts
19
- import { readFileSync as readFileSync6 } from "fs";
20
- import { dirname as dirname4, join as join7 } from "path";
21
- import { fileURLToPath } from "url";
19
+ import { readFileSync as readFileSync7 } from "fs";
20
+ import { dirname as dirname5, join as join9 } from "path";
21
+ import { fileURLToPath as fileURLToPath2 } from "url";
22
22
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
23
23
 
24
24
  // src/mcp/server.ts
@@ -4197,1240 +4197,769 @@ function detectCurrentMachineManifest() {
4197
4197
  };
4198
4198
  }
4199
4199
 
4200
- // src/commands/apps.ts
4201
- function getPackageName(app) {
4202
- return app.packageName || app.name;
4200
+ // src/remote.ts
4201
+ import { spawnSync as spawnSync2 } from "child_process";
4202
+
4203
+ // src/db.ts
4204
+ import { hostname as hostname2 } from "os";
4205
+
4206
+ // node_modules/@hasna/cloud/dist/index.js
4207
+ import { createRequire } from "module";
4208
+ import { Database } from "bun:sqlite";
4209
+ import {
4210
+ existsSync as existsSync3,
4211
+ mkdirSync as mkdirSync2,
4212
+ readdirSync,
4213
+ copyFileSync
4214
+ } from "fs";
4215
+ import { homedir as homedir3 } from "os";
4216
+ import { join as join3, relative } from "path";
4217
+ import { existsSync as existsSync22, mkdirSync as mkdirSync22, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
4218
+ import { homedir as homedir22 } from "os";
4219
+ import { join as join22 } from "path";
4220
+ import { readdirSync as readdirSync2, existsSync as existsSync32 } from "fs";
4221
+ import { join as join32 } from "path";
4222
+ import { homedir as homedir32 } from "os";
4223
+ import { homedir as homedir4 } from "os";
4224
+ import { join as join4 } from "path";
4225
+ import { join as join6, dirname as dirname3 } from "path";
4226
+ import { homedir as homedir5, platform as platform2 } from "os";
4227
+ var __create = Object.create;
4228
+ var __getProtoOf = Object.getPrototypeOf;
4229
+ var __defProp2 = Object.defineProperty;
4230
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4231
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
4232
+ function __accessProp(key) {
4233
+ return this[key];
4203
4234
  }
4204
- function buildAppCommand(machine, app) {
4205
- const packageName = getPackageName(app);
4206
- if (app.manager === "custom") {
4207
- return packageName;
4208
- }
4209
- if (machine.platform === "macos") {
4210
- if (app.manager === "cask") {
4211
- return `brew install --cask ${packageName}`;
4212
- }
4213
- return `brew install ${packageName}`;
4214
- }
4215
- if (machine.platform === "windows") {
4216
- return `winget install ${packageName}`;
4235
+ var __toESMCache_node;
4236
+ var __toESMCache_esm;
4237
+ var __toESM = (mod, isNodeMode, target) => {
4238
+ var canCache = mod != null && typeof mod === "object";
4239
+ if (canCache) {
4240
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
4241
+ var cached = cache.get(mod);
4242
+ if (cached)
4243
+ return cached;
4217
4244
  }
4218
- return `sudo apt-get install -y ${packageName}`;
4219
- }
4220
- function buildAppSteps(machine) {
4221
- return (machine.apps || []).map((app) => ({
4222
- id: `app-${app.name}`,
4223
- title: `Install ${app.name} on ${machine.id}`,
4224
- command: buildAppCommand(machine, app),
4225
- manager: app.manager === "custom" ? "custom" : machine.platform === "macos" ? "brew" : machine.platform === "windows" ? "custom" : "apt",
4226
- privileged: machine.platform === "linux"
4227
- }));
4228
- }
4229
- function resolveMachine(machineId) {
4230
- return (machineId ? getManifestMachine(machineId) : null) || detectCurrentMachineManifest();
4231
- }
4232
- function listApps(machineId) {
4233
- const machine = resolveMachine(machineId);
4234
- return {
4235
- machineId: machine.id,
4236
- apps: machine.apps || []
4237
- };
4238
- }
4239
- function buildAppsPlan(machineId) {
4240
- const machine = resolveMachine(machineId);
4241
- return {
4242
- machineId: machine.id,
4243
- mode: "plan",
4244
- steps: buildAppSteps(machine),
4245
- executed: 0
4246
- };
4245
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
4246
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp2(target, "default", { value: mod, enumerable: true }) : target;
4247
+ for (let key of __getOwnPropNames(mod))
4248
+ if (!__hasOwnProp.call(to, key))
4249
+ __defProp2(to, key, {
4250
+ get: __accessProp.bind(mod, key),
4251
+ enumerable: true
4252
+ });
4253
+ if (canCache)
4254
+ cache.set(mod, to);
4255
+ return to;
4256
+ };
4257
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
4258
+ var __returnValue2 = (v) => v;
4259
+ function __exportSetter2(name, newValue) {
4260
+ this[name] = __returnValue2.bind(null, newValue);
4247
4261
  }
4248
- function runAppsInstall(machineId, options = {}) {
4249
- const plan = buildAppsPlan(machineId);
4250
- if (!options.apply)
4251
- return plan;
4252
- if (!options.yes) {
4253
- throw new Error("App installation requires --yes.");
4254
- }
4255
- let executed = 0;
4256
- for (const step of plan.steps) {
4257
- const result = Bun.spawnSync(["bash", "-lc", step.command], {
4258
- stdout: "pipe",
4259
- stderr: "pipe",
4260
- env: process.env
4262
+ var __export2 = (target, all) => {
4263
+ for (var name in all)
4264
+ __defProp2(target, name, {
4265
+ get: all[name],
4266
+ enumerable: true,
4267
+ configurable: true,
4268
+ set: __exportSetter2.bind(all, name)
4261
4269
  });
4262
- if (result.exitCode !== 0) {
4263
- throw new Error(`App install failed (${step.id}): ${result.stderr.toString().trim()}`);
4264
- }
4265
- executed += 1;
4266
- }
4267
- return {
4268
- machineId: plan.machineId,
4269
- mode: "apply",
4270
- steps: plan.steps,
4271
- executed
4270
+ };
4271
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
4272
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
4273
+ var require_postgres_array = __commonJS((exports) => {
4274
+ exports.parse = function(source, transform) {
4275
+ return new ArrayParser(source, transform).parse();
4272
4276
  };
4273
- }
4274
4277
 
4275
- // src/commands/cert.ts
4276
- import { homedir as homedir3, platform as platform2 } from "os";
4277
- import { join as join3 } from "path";
4278
- function quote2(value) {
4279
- return `'${value.replace(/'/g, `'\\''`)}'`;
4280
- }
4281
- function certDir() {
4282
- return join3(homedir3(), ".hasna", "machines", "certs");
4283
- }
4284
- function buildCertPlan(domains) {
4285
- if (domains.length === 0) {
4286
- throw new Error("At least one domain is required.");
4278
+ class ArrayParser {
4279
+ constructor(source, transform) {
4280
+ this.source = source;
4281
+ this.transform = transform || identity;
4282
+ this.position = 0;
4283
+ this.entries = [];
4284
+ this.recorded = [];
4285
+ this.dimension = 0;
4286
+ }
4287
+ isEof() {
4288
+ return this.position >= this.source.length;
4289
+ }
4290
+ nextCharacter() {
4291
+ var character = this.source[this.position++];
4292
+ if (character === "\\") {
4293
+ return {
4294
+ value: this.source[this.position++],
4295
+ escaped: true
4296
+ };
4297
+ }
4298
+ return {
4299
+ value: character,
4300
+ escaped: false
4301
+ };
4302
+ }
4303
+ record(character) {
4304
+ this.recorded.push(character);
4305
+ }
4306
+ newEntry(includeEmpty) {
4307
+ var entry;
4308
+ if (this.recorded.length > 0 || includeEmpty) {
4309
+ entry = this.recorded.join("");
4310
+ if (entry === "NULL" && !includeEmpty) {
4311
+ entry = null;
4312
+ }
4313
+ if (entry !== null)
4314
+ entry = this.transform(entry);
4315
+ this.entries.push(entry);
4316
+ this.recorded = [];
4317
+ }
4318
+ }
4319
+ consumeDimensions() {
4320
+ if (this.source[0] === "[") {
4321
+ while (!this.isEof()) {
4322
+ var char = this.nextCharacter();
4323
+ if (char.value === "=")
4324
+ break;
4325
+ }
4326
+ }
4327
+ }
4328
+ parse(nested) {
4329
+ var character, parser, quote2;
4330
+ this.consumeDimensions();
4331
+ while (!this.isEof()) {
4332
+ character = this.nextCharacter();
4333
+ if (character.value === "{" && !quote2) {
4334
+ this.dimension++;
4335
+ if (this.dimension > 1) {
4336
+ parser = new ArrayParser(this.source.substr(this.position - 1), this.transform);
4337
+ this.entries.push(parser.parse(true));
4338
+ this.position += parser.position - 2;
4339
+ }
4340
+ } else if (character.value === "}" && !quote2) {
4341
+ this.dimension--;
4342
+ if (!this.dimension) {
4343
+ this.newEntry();
4344
+ if (nested)
4345
+ return this.entries;
4346
+ }
4347
+ } else if (character.value === '"' && !character.escaped) {
4348
+ if (quote2)
4349
+ this.newEntry(true);
4350
+ quote2 = !quote2;
4351
+ } else if (character.value === "," && !quote2) {
4352
+ this.newEntry();
4353
+ } else {
4354
+ this.record(character.value);
4355
+ }
4356
+ }
4357
+ if (this.dimension !== 0) {
4358
+ throw new Error("array dimension not balanced");
4359
+ }
4360
+ return this.entries;
4361
+ }
4287
4362
  }
4288
- const primary = domains[0];
4289
- const certPath = join3(certDir(), `${primary}.pem`);
4290
- const keyPath = join3(certDir(), `${primary}-key.pem`);
4291
- const steps = [];
4292
- if (platform2() === "darwin") {
4293
- steps.push({
4294
- id: "mkcert-install-macos",
4295
- title: "Install mkcert on macOS",
4296
- command: "brew install mkcert nss",
4297
- manager: "brew"
4298
- });
4299
- } else {
4300
- steps.push({
4301
- id: "mkcert-install-linux",
4302
- title: "Install mkcert on Linux",
4303
- command: "sudo apt-get update && sudo apt-get install -y mkcert libnss3-tools",
4304
- manager: "apt",
4305
- privileged: true
4306
- });
4363
+ function identity(value) {
4364
+ return value;
4307
4365
  }
4308
- steps.push({
4309
- id: "mkcert-local-ca",
4310
- title: "Install local mkcert CA",
4311
- command: "mkcert -install",
4312
- manager: "custom"
4313
- }, {
4314
- id: "mkcert-issue",
4315
- title: `Issue certificate for ${domains.join(", ")}`,
4316
- command: `mkdir -p ${quote2(certDir())} && mkcert -cert-file ${quote2(certPath)} -key-file ${quote2(keyPath)} ${domains.map((domain) => quote2(domain)).join(" ")}`,
4317
- manager: "custom"
4318
- });
4319
- return {
4320
- machineId: process.env["HASNA_MACHINES_MACHINE_ID"] || "local",
4321
- mode: "plan",
4322
- steps,
4323
- executed: 0
4324
- };
4325
- }
4326
- function runCertPlan(domains, options = {}) {
4327
- const plan = buildCertPlan(domains);
4328
- if (!options.apply)
4329
- return plan;
4330
- if (!options.yes) {
4331
- throw new Error("Certificate generation requires --yes.");
4366
+ });
4367
+ var require_arrayParser = __commonJS((exports, module) => {
4368
+ var array = require_postgres_array();
4369
+ module.exports = {
4370
+ create: function(source, transform) {
4371
+ return {
4372
+ parse: function() {
4373
+ return array.parse(source, transform);
4374
+ }
4375
+ };
4376
+ }
4377
+ };
4378
+ });
4379
+ var require_postgres_date = __commonJS((exports, module) => {
4380
+ var DATE_TIME = /(\d{1,})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})(\.\d{1,})?.*?( BC)?$/;
4381
+ var DATE = /^(\d{1,})-(\d{2})-(\d{2})( BC)?$/;
4382
+ var TIME_ZONE = /([Z+-])(\d{2})?:?(\d{2})?:?(\d{2})?/;
4383
+ var INFINITY = /^-?infinity$/;
4384
+ module.exports = function parseDate(isoDate) {
4385
+ if (INFINITY.test(isoDate)) {
4386
+ return Number(isoDate.replace("i", "I"));
4387
+ }
4388
+ var matches = DATE_TIME.exec(isoDate);
4389
+ if (!matches) {
4390
+ return getDate(isoDate) || null;
4391
+ }
4392
+ var isBC = !!matches[8];
4393
+ var year = parseInt(matches[1], 10);
4394
+ if (isBC) {
4395
+ year = bcYearToNegativeYear(year);
4396
+ }
4397
+ var month = parseInt(matches[2], 10) - 1;
4398
+ var day = matches[3];
4399
+ var hour = parseInt(matches[4], 10);
4400
+ var minute = parseInt(matches[5], 10);
4401
+ var second = parseInt(matches[6], 10);
4402
+ var ms = matches[7];
4403
+ ms = ms ? 1000 * parseFloat(ms) : 0;
4404
+ var date;
4405
+ var offset = timeZoneOffset(isoDate);
4406
+ if (offset != null) {
4407
+ date = new Date(Date.UTC(year, month, day, hour, minute, second, ms));
4408
+ if (is0To99(year)) {
4409
+ date.setUTCFullYear(year);
4410
+ }
4411
+ if (offset !== 0) {
4412
+ date.setTime(date.getTime() - offset);
4413
+ }
4414
+ } else {
4415
+ date = new Date(year, month, day, hour, minute, second, ms);
4416
+ if (is0To99(year)) {
4417
+ date.setFullYear(year);
4418
+ }
4419
+ }
4420
+ return date;
4421
+ };
4422
+ function getDate(isoDate) {
4423
+ var matches = DATE.exec(isoDate);
4424
+ if (!matches) {
4425
+ return;
4426
+ }
4427
+ var year = parseInt(matches[1], 10);
4428
+ var isBC = !!matches[4];
4429
+ if (isBC) {
4430
+ year = bcYearToNegativeYear(year);
4431
+ }
4432
+ var month = parseInt(matches[2], 10) - 1;
4433
+ var day = matches[3];
4434
+ var date = new Date(year, month, day);
4435
+ if (is0To99(year)) {
4436
+ date.setFullYear(year);
4437
+ }
4438
+ return date;
4332
4439
  }
4333
- let executed = 0;
4334
- for (const step of plan.steps) {
4335
- const result = Bun.spawnSync(["bash", "-lc", step.command], {
4336
- stdout: "pipe",
4337
- stderr: "pipe",
4338
- env: process.env
4339
- });
4340
- if (result.exitCode !== 0) {
4341
- throw new Error(`Certificate step failed (${step.id}): ${result.stderr.toString().trim()}`);
4440
+ function timeZoneOffset(isoDate) {
4441
+ if (isoDate.endsWith("+00")) {
4442
+ return 0;
4342
4443
  }
4343
- executed += 1;
4444
+ var zone = TIME_ZONE.exec(isoDate.split(" ")[1]);
4445
+ if (!zone)
4446
+ return;
4447
+ var type = zone[1];
4448
+ if (type === "Z") {
4449
+ return 0;
4450
+ }
4451
+ var sign = type === "-" ? -1 : 1;
4452
+ var offset = parseInt(zone[2], 10) * 3600 + parseInt(zone[3] || 0, 10) * 60 + parseInt(zone[4] || 0, 10);
4453
+ return offset * sign * 1000;
4344
4454
  }
4345
- return {
4346
- machineId: plan.machineId,
4347
- mode: "apply",
4348
- steps: plan.steps,
4349
- executed
4350
- };
4351
- }
4352
-
4353
- // src/commands/dns.ts
4354
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
4355
- import { join as join4 } from "path";
4356
- function getDnsPath() {
4357
- return join4(getDataDir(), "dns.json");
4358
- }
4359
- function readMappings() {
4360
- const path = getDnsPath();
4361
- if (!existsSync3(path))
4362
- return [];
4363
- return JSON.parse(readFileSync2(path, "utf8"));
4364
- }
4365
- function writeMappings(mappings) {
4366
- const path = getDnsPath();
4367
- ensureParentDir(path);
4368
- writeFileSync2(path, `${JSON.stringify(mappings, null, 2)}
4369
- `, "utf8");
4370
- return path;
4371
- }
4372
- function addDomainMapping(domain, port, targetHost = "127.0.0.1") {
4373
- const mappings = readMappings().filter((entry) => entry.domain !== domain);
4374
- mappings.push({ domain, port, targetHost });
4375
- writeMappings(mappings);
4376
- return mappings.sort((left, right) => left.domain.localeCompare(right.domain));
4377
- }
4378
- function listDomainMappings() {
4379
- return readMappings().sort((left, right) => left.domain.localeCompare(right.domain));
4380
- }
4381
- function renderDomainMapping(domain) {
4382
- const entry = readMappings().find((mapping) => mapping.domain === domain);
4383
- if (!entry) {
4384
- throw new Error(`Domain mapping not found: ${domain}`);
4455
+ function bcYearToNegativeYear(year) {
4456
+ return -(year - 1);
4385
4457
  }
4386
- return {
4387
- hostsEntry: `${entry.targetHost} ${entry.domain}`,
4388
- caddySnippet: `${entry.domain} {
4389
- reverse_proxy 127.0.0.1:${entry.port}
4390
- tls ${join4(getDataDir(), "certs", `${entry.domain}.pem`)} ${join4(getDataDir(), "certs", `${entry.domain}-key.pem`)}
4391
- }`,
4392
- certPath: join4(getDataDir(), "certs", `${entry.domain}.pem`),
4393
- keyPath: join4(getDataDir(), "certs", `${entry.domain}-key.pem`)
4394
- };
4395
- }
4396
-
4397
- // src/commands/diff.ts
4398
- function packageNames(machine) {
4399
- return (machine.packages || []).map((pkg) => pkg.name).sort();
4400
- }
4401
- function fileTargets(machine) {
4402
- return (machine.files || []).map((file) => `${file.source}->${file.target}`).sort();
4403
- }
4404
- function diffMachines(leftMachineId, rightMachineId) {
4405
- const left = getManifestMachine(leftMachineId);
4406
- if (!left) {
4407
- throw new Error(`Machine not found in manifest: ${leftMachineId}`);
4458
+ function is0To99(num) {
4459
+ return num >= 0 && num < 100;
4408
4460
  }
4409
- const right = rightMachineId ? getManifestMachine(rightMachineId) : detectCurrentMachineManifest();
4410
- if (!right) {
4411
- throw new Error(`Machine not found in manifest: ${rightMachineId}`);
4461
+ });
4462
+ var require_mutable = __commonJS((exports, module) => {
4463
+ module.exports = extend;
4464
+ var hasOwnProperty = Object.prototype.hasOwnProperty;
4465
+ function extend(target) {
4466
+ for (var i = 1;i < arguments.length; i++) {
4467
+ var source = arguments[i];
4468
+ for (var key in source) {
4469
+ if (hasOwnProperty.call(source, key)) {
4470
+ target[key] = source[key];
4471
+ }
4472
+ }
4473
+ }
4474
+ return target;
4412
4475
  }
4413
- const changedFields = [
4414
- left.platform !== right.platform ? "platform" : null,
4415
- left.connection !== right.connection ? "connection" : null,
4416
- left.workspacePath !== right.workspacePath ? "workspacePath" : null,
4417
- left.bunPath !== right.bunPath ? "bunPath" : null
4418
- ].filter(Boolean);
4419
- const leftPackages = new Set(packageNames(left));
4420
- const rightPackages = new Set(packageNames(right));
4421
- const leftFiles = new Set(fileTargets(left));
4422
- const rightFiles = new Set(fileTargets(right));
4423
- return {
4424
- leftMachineId: left.id,
4425
- rightMachineId: right.id,
4426
- changedFields,
4427
- missingPackages: {
4428
- leftOnly: [...leftPackages].filter((pkg) => !rightPackages.has(pkg)),
4429
- rightOnly: [...rightPackages].filter((pkg) => !leftPackages.has(pkg))
4430
- },
4431
- missingFiles: {
4432
- leftOnly: [...leftFiles].filter((file) => !rightFiles.has(file)),
4433
- rightOnly: [...rightFiles].filter((file) => !leftFiles.has(file))
4476
+ });
4477
+ var require_postgres_interval = __commonJS((exports, module) => {
4478
+ var extend = require_mutable();
4479
+ module.exports = PostgresInterval;
4480
+ function PostgresInterval(raw) {
4481
+ if (!(this instanceof PostgresInterval)) {
4482
+ return new PostgresInterval(raw);
4434
4483
  }
4435
- };
4436
- }
4437
-
4438
- // src/commands/install-claude.ts
4439
- var AI_CLI_PACKAGES = {
4440
- claude: "@anthropic-ai/claude-code",
4441
- codex: "@openai/codex",
4442
- gemini: "@google/gemini-cli"
4443
- };
4444
- function normalizeTools(tools) {
4445
- if (!tools || tools.length === 0) {
4446
- return ["claude", "codex", "gemini"];
4484
+ extend(this, parse(raw));
4447
4485
  }
4448
- return [...new Set(tools)].map((tool) => {
4449
- if (!(tool in AI_CLI_PACKAGES)) {
4450
- throw new Error(`Unsupported AI CLI tool: ${tool}`);
4486
+ var properties = ["seconds", "minutes", "hours", "days", "months", "years"];
4487
+ PostgresInterval.prototype.toPostgres = function() {
4488
+ var filtered = properties.filter(this.hasOwnProperty, this);
4489
+ if (this.milliseconds && filtered.indexOf("seconds") < 0) {
4490
+ filtered.push("seconds");
4451
4491
  }
4452
- return tool;
4453
- });
4454
- }
4455
- function buildInstallSteps(machine, tools) {
4456
- return normalizeTools(tools).map((tool) => ({
4457
- id: `install-${tool}`,
4458
- title: `Install or update ${tool} CLI on ${machine.id}`,
4459
- command: `bun install -g ${AI_CLI_PACKAGES[tool]}`,
4460
- manager: "bun"
4461
- }));
4462
- }
4463
- function buildClaudeInstallPlan(machineId, tools) {
4464
- const machine = (machineId ? getManifestMachine(machineId) : null) || detectCurrentMachineManifest();
4465
- return {
4466
- machineId: machine.id,
4467
- mode: "plan",
4468
- steps: buildInstallSteps(machine, tools),
4469
- executed: 0
4492
+ if (filtered.length === 0)
4493
+ return "0";
4494
+ return filtered.map(function(property) {
4495
+ var value = this[property] || 0;
4496
+ if (property === "seconds" && this.milliseconds) {
4497
+ value = (value + this.milliseconds / 1000).toFixed(6).replace(/\.?0+$/, "");
4498
+ }
4499
+ return value + " " + property;
4500
+ }, this).join(" ");
4470
4501
  };
4471
- }
4472
- function runClaudeInstall(machineId, tools, options = {}) {
4473
- const plan = buildClaudeInstallPlan(machineId, tools);
4474
- if (!options.apply)
4475
- return plan;
4476
- if (!options.yes) {
4477
- throw new Error("Claude CLI installation requires --yes.");
4478
- }
4479
- let executed = 0;
4480
- for (const step of plan.steps) {
4481
- const result = Bun.spawnSync(["bash", "-lc", step.command], {
4482
- stdout: "pipe",
4483
- stderr: "pipe",
4484
- env: process.env
4485
- });
4486
- if (result.exitCode !== 0) {
4487
- throw new Error(`AI CLI install failed (${step.id}): ${result.stderr.toString().trim()}`);
4488
- }
4489
- executed += 1;
4490
- }
4491
- return {
4492
- machineId: plan.machineId,
4493
- mode: "apply",
4494
- steps: plan.steps,
4495
- executed
4502
+ var propertiesISOEquivalent = {
4503
+ years: "Y",
4504
+ months: "M",
4505
+ days: "D",
4506
+ hours: "H",
4507
+ minutes: "M",
4508
+ seconds: "S"
4496
4509
  };
4497
- }
4498
-
4499
- // src/commands/install-tailscale.ts
4500
- function buildInstallSteps2(machine) {
4501
- if (machine.platform === "macos") {
4502
- return [
4503
- {
4504
- id: "tailscale-brew",
4505
- title: "Install Tailscale via Homebrew",
4506
- command: "brew install --cask tailscale",
4507
- manager: "brew"
4510
+ var dateProperties = ["years", "months", "days"];
4511
+ var timeProperties = ["hours", "minutes", "seconds"];
4512
+ PostgresInterval.prototype.toISOString = PostgresInterval.prototype.toISO = function() {
4513
+ var datePart = dateProperties.map(buildProperty, this).join("");
4514
+ var timePart = timeProperties.map(buildProperty, this).join("");
4515
+ return "P" + datePart + "T" + timePart;
4516
+ function buildProperty(property) {
4517
+ var value = this[property] || 0;
4518
+ if (property === "seconds" && this.milliseconds) {
4519
+ value = (value + this.milliseconds / 1000).toFixed(6).replace(/0+$/, "");
4508
4520
  }
4509
- ];
4521
+ return value + propertiesISOEquivalent[property];
4522
+ }
4523
+ };
4524
+ var NUMBER = "([+-]?\\d+)";
4525
+ var YEAR = NUMBER + "\\s+years?";
4526
+ var MONTH = NUMBER + "\\s+mons?";
4527
+ var DAY = NUMBER + "\\s+days?";
4528
+ var TIME = "([+-])?([\\d]*):(\\d\\d):(\\d\\d)\\.?(\\d{1,6})?";
4529
+ var INTERVAL = new RegExp([YEAR, MONTH, DAY, TIME].map(function(regexString) {
4530
+ return "(" + regexString + ")?";
4531
+ }).join("\\s*"));
4532
+ var positions = {
4533
+ years: 2,
4534
+ months: 4,
4535
+ days: 6,
4536
+ hours: 9,
4537
+ minutes: 10,
4538
+ seconds: 11,
4539
+ milliseconds: 12
4540
+ };
4541
+ var negatives = ["hours", "minutes", "seconds", "milliseconds"];
4542
+ function parseMilliseconds(fraction) {
4543
+ var microseconds = fraction + "000000".slice(fraction.length);
4544
+ return parseInt(microseconds, 10) / 1000;
4510
4545
  }
4511
- if (machine.platform === "windows") {
4512
- return [
4513
- {
4514
- id: "tailscale-winget",
4515
- title: "Install Tailscale via winget",
4516
- command: "winget install Tailscale.Tailscale",
4517
- manager: "custom"
4546
+ function parse(interval) {
4547
+ if (!interval)
4548
+ return {};
4549
+ var matches = INTERVAL.exec(interval);
4550
+ var isNegative = matches[8] === "-";
4551
+ return Object.keys(positions).reduce(function(parsed, property) {
4552
+ var position = positions[property];
4553
+ var value = matches[position];
4554
+ if (!value)
4555
+ return parsed;
4556
+ value = property === "milliseconds" ? parseMilliseconds(value) : parseInt(value, 10);
4557
+ if (!value)
4558
+ return parsed;
4559
+ if (isNegative && ~negatives.indexOf(property)) {
4560
+ value *= -1;
4518
4561
  }
4519
- ];
4562
+ parsed[property] = value;
4563
+ return parsed;
4564
+ }, {});
4520
4565
  }
4521
- return [
4522
- {
4523
- id: "tailscale-linux",
4524
- title: "Install Tailscale on Linux",
4525
- command: "curl -fsSL https://tailscale.com/install.sh | sh",
4526
- manager: "custom",
4527
- privileged: true
4566
+ });
4567
+ var require_postgres_bytea = __commonJS((exports, module) => {
4568
+ var bufferFrom = Buffer.from || Buffer;
4569
+ module.exports = function parseBytea(input) {
4570
+ if (/^\\x/.test(input)) {
4571
+ return bufferFrom(input.substr(2), "hex");
4528
4572
  }
4529
- ];
4530
- }
4531
- function buildTailscaleInstallPlan(machineId) {
4532
- const machine = (machineId ? getManifestMachine(machineId) : null) || detectCurrentMachineManifest();
4533
- return {
4534
- machineId: machine.id,
4535
- mode: "plan",
4536
- steps: buildInstallSteps2(machine),
4537
- executed: 0
4538
- };
4539
- }
4540
- function runTailscaleInstall(machineId, options = {}) {
4541
- const plan = buildTailscaleInstallPlan(machineId);
4542
- if (!options.apply)
4543
- return plan;
4544
- if (!options.yes) {
4545
- throw new Error("Tailscale install requires --yes.");
4546
- }
4547
- let executed = 0;
4548
- for (const step of plan.steps) {
4549
- const result = Bun.spawnSync(["bash", "-lc", step.command], {
4550
- stdout: "pipe",
4551
- stderr: "pipe",
4552
- env: process.env
4553
- });
4554
- if (result.exitCode !== 0) {
4555
- throw new Error(`Tailscale install failed (${step.id}): ${result.stderr.toString().trim()}`);
4573
+ var output = "";
4574
+ var i = 0;
4575
+ while (i < input.length) {
4576
+ if (input[i] !== "\\") {
4577
+ output += input[i];
4578
+ ++i;
4579
+ } else {
4580
+ if (/[0-7]{3}/.test(input.substr(i + 1, 3))) {
4581
+ output += String.fromCharCode(parseInt(input.substr(i + 1, 3), 8));
4582
+ i += 4;
4583
+ } else {
4584
+ var backslashes = 1;
4585
+ while (i + backslashes < input.length && input[i + backslashes] === "\\") {
4586
+ backslashes++;
4587
+ }
4588
+ for (var k = 0;k < Math.floor(backslashes / 2); ++k) {
4589
+ output += "\\";
4590
+ }
4591
+ i += Math.floor(backslashes / 2) * 2;
4592
+ }
4593
+ }
4556
4594
  }
4557
- executed += 1;
4558
- }
4559
- return {
4560
- machineId: plan.machineId,
4561
- mode: "apply",
4562
- steps: plan.steps,
4563
- executed
4595
+ return bufferFrom(output, "binary");
4564
4596
  };
4565
- }
4566
-
4567
- // src/commands/notifications.ts
4568
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
4569
- var notificationChannelSchema = exports_external.object({
4570
- id: exports_external.string(),
4571
- type: exports_external.enum(["email", "webhook", "command"]),
4572
- target: exports_external.string(),
4573
- events: exports_external.array(exports_external.string()),
4574
- enabled: exports_external.boolean()
4575
- });
4576
- var notificationConfigSchema = exports_external.object({
4577
- version: exports_external.literal(1),
4578
- updatedAt: exports_external.string().optional(),
4579
- channels: exports_external.array(notificationChannelSchema)
4580
4597
  });
4581
- function sortChannels(channels) {
4582
- return [...channels].sort((left, right) => left.id.localeCompare(right.id));
4583
- }
4584
- function getDefaultNotificationConfig() {
4585
- return {
4586
- version: 1,
4587
- updatedAt: new Date().toISOString(),
4588
- channels: []
4589
- };
4590
- }
4591
- function readNotificationConfig(path = getNotificationsPath()) {
4592
- if (!existsSync4(path)) {
4593
- return getDefaultNotificationConfig();
4598
+ var require_textParsers = __commonJS((exports, module) => {
4599
+ var array = require_postgres_array();
4600
+ var arrayParser = require_arrayParser();
4601
+ var parseDate = require_postgres_date();
4602
+ var parseInterval = require_postgres_interval();
4603
+ var parseByteA = require_postgres_bytea();
4604
+ function allowNull(fn) {
4605
+ return function nullAllowed(value) {
4606
+ if (value === null)
4607
+ return value;
4608
+ return fn(value);
4609
+ };
4594
4610
  }
4595
- return notificationConfigSchema.parse(JSON.parse(readFileSync3(path, "utf8")));
4596
- }
4597
- function writeNotificationConfig(config, path = getNotificationsPath()) {
4598
- ensureParentDir(path);
4599
- const nextConfig = {
4600
- version: 1,
4601
- updatedAt: new Date().toISOString(),
4602
- channels: sortChannels(config.channels)
4603
- };
4604
- writeFileSync3(path, `${JSON.stringify(nextConfig, null, 2)}
4605
- `, "utf8");
4606
- return nextConfig;
4607
- }
4608
- function listNotificationChannels() {
4609
- return readNotificationConfig();
4610
- }
4611
- function addNotificationChannel(channel) {
4612
- const config = readNotificationConfig();
4613
- const channels = config.channels.filter((entry) => entry.id !== channel.id);
4614
- channels.push({
4615
- ...channel,
4616
- events: [...new Set(channel.events)]
4617
- });
4618
- return writeNotificationConfig({ ...config, channels });
4619
- }
4620
- function removeNotificationChannel(channelId) {
4621
- const config = readNotificationConfig();
4622
- return writeNotificationConfig({
4623
- ...config,
4624
- channels: config.channels.filter((channel) => channel.id !== channelId)
4625
- });
4626
- }
4627
- function buildNotificationPreview(channel, event, message) {
4628
- if (channel.type === "email") {
4629
- return `send email to ${channel.target}: [${event}] ${message}`;
4611
+ function parseBool(value) {
4612
+ if (value === null)
4613
+ return value;
4614
+ return value === "TRUE" || value === "t" || value === "true" || value === "y" || value === "yes" || value === "on" || value === "1";
4630
4615
  }
4631
- if (channel.type === "webhook") {
4632
- return `POST ${channel.target} with payload {"event":"${event}","message":"${message}"}`;
4616
+ function parseBoolArray(value) {
4617
+ if (!value)
4618
+ return null;
4619
+ return array.parse(value, parseBool);
4633
4620
  }
4634
- return `${channel.target} --event ${event} --message ${JSON.stringify(message)}`;
4635
- }
4636
- function testNotificationChannel(channelId, event = "manual.test", message = "machines notification test", options = {}) {
4637
- const channel = readNotificationConfig().channels.find((entry) => entry.id === channelId);
4638
- if (!channel) {
4639
- throw new Error(`Notification channel not found: ${channelId}`);
4621
+ function parseBaseTenInt(string) {
4622
+ return parseInt(string, 10);
4640
4623
  }
4641
- const preview = buildNotificationPreview(channel, event, message);
4642
- if (!options.apply) {
4643
- return {
4644
- channelId,
4645
- mode: "plan",
4646
- delivered: false,
4647
- preview
4648
- };
4624
+ function parseIntegerArray(value) {
4625
+ if (!value)
4626
+ return null;
4627
+ return array.parse(value, allowNull(parseBaseTenInt));
4649
4628
  }
4650
- if (!options.yes) {
4651
- throw new Error("Notification test execution requires --yes.");
4629
+ function parseBigIntegerArray(value) {
4630
+ if (!value)
4631
+ return null;
4632
+ return array.parse(value, allowNull(function(entry) {
4633
+ return parseBigInteger(entry).trim();
4634
+ }));
4652
4635
  }
4653
- if (channel.type === "command") {
4654
- const result = Bun.spawnSync(["bash", "-lc", preview], {
4655
- stdout: "pipe",
4656
- stderr: "pipe",
4657
- env: process.env
4658
- });
4659
- if (result.exitCode !== 0) {
4660
- throw new Error(`Notification command failed (${channel.id}): ${result.stderr.toString().trim()}`);
4636
+ var parsePointArray = function(value) {
4637
+ if (!value) {
4638
+ return null;
4661
4639
  }
4662
- }
4663
- return {
4664
- channelId,
4665
- mode: "apply",
4666
- delivered: channel.enabled,
4667
- preview
4668
- };
4669
- }
4670
-
4671
- // src/commands/ports.ts
4672
- import { spawnSync as spawnSync2 } from "child_process";
4673
-
4674
- // src/db.ts
4675
- import { hostname as hostname2 } from "os";
4676
-
4677
- // node_modules/@hasna/cloud/dist/index.js
4678
- import { createRequire } from "module";
4679
- import { Database } from "bun:sqlite";
4680
- import {
4681
- existsSync as existsSync5,
4682
- mkdirSync as mkdirSync2,
4683
- readdirSync,
4684
- copyFileSync
4685
- } from "fs";
4686
- import { homedir as homedir4 } from "os";
4687
- import { join as join5, relative } from "path";
4688
- import { existsSync as existsSync22, mkdirSync as mkdirSync22, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
4689
- import { homedir as homedir22 } from "os";
4690
- import { join as join22 } from "path";
4691
- import { readdirSync as readdirSync2, existsSync as existsSync32 } from "fs";
4692
- import { join as join32 } from "path";
4693
- import { homedir as homedir32 } from "os";
4694
- import { homedir as homedir42 } from "os";
4695
- import { join as join42 } from "path";
4696
- import { join as join6, dirname as dirname3 } from "path";
4697
- import { homedir as homedir5, platform as platform3 } from "os";
4698
- var __create = Object.create;
4699
- var __getProtoOf = Object.getPrototypeOf;
4700
- var __defProp2 = Object.defineProperty;
4701
- var __getOwnPropNames = Object.getOwnPropertyNames;
4702
- var __hasOwnProp = Object.prototype.hasOwnProperty;
4703
- function __accessProp(key) {
4704
- return this[key];
4705
- }
4706
- var __toESMCache_node;
4707
- var __toESMCache_esm;
4708
- var __toESM = (mod, isNodeMode, target) => {
4709
- var canCache = mod != null && typeof mod === "object";
4710
- if (canCache) {
4711
- var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
4712
- var cached = cache.get(mod);
4713
- if (cached)
4714
- return cached;
4715
- }
4716
- target = mod != null ? __create(__getProtoOf(mod)) : {};
4717
- const to = isNodeMode || !mod || !mod.__esModule ? __defProp2(target, "default", { value: mod, enumerable: true }) : target;
4718
- for (let key of __getOwnPropNames(mod))
4719
- if (!__hasOwnProp.call(to, key))
4720
- __defProp2(to, key, {
4721
- get: __accessProp.bind(mod, key),
4722
- enumerable: true
4723
- });
4724
- if (canCache)
4725
- cache.set(mod, to);
4726
- return to;
4727
- };
4728
- var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
4729
- var __returnValue2 = (v) => v;
4730
- function __exportSetter2(name, newValue) {
4731
- this[name] = __returnValue2.bind(null, newValue);
4732
- }
4733
- var __export2 = (target, all) => {
4734
- for (var name in all)
4735
- __defProp2(target, name, {
4736
- get: all[name],
4737
- enumerable: true,
4738
- configurable: true,
4739
- set: __exportSetter2.bind(all, name)
4640
+ var p = arrayParser.create(value, function(entry) {
4641
+ if (entry !== null) {
4642
+ entry = parsePoint(entry);
4643
+ }
4644
+ return entry;
4740
4645
  });
4741
- };
4742
- var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
4743
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
4744
- var require_postgres_array = __commonJS((exports) => {
4745
- exports.parse = function(source, transform) {
4746
- return new ArrayParser(source, transform).parse();
4646
+ return p.parse();
4747
4647
  };
4748
-
4749
- class ArrayParser {
4750
- constructor(source, transform) {
4751
- this.source = source;
4752
- this.transform = transform || identity;
4753
- this.position = 0;
4754
- this.entries = [];
4755
- this.recorded = [];
4756
- this.dimension = 0;
4757
- }
4758
- isEof() {
4759
- return this.position >= this.source.length;
4648
+ var parseFloatArray = function(value) {
4649
+ if (!value) {
4650
+ return null;
4760
4651
  }
4761
- nextCharacter() {
4762
- var character = this.source[this.position++];
4763
- if (character === "\\") {
4764
- return {
4765
- value: this.source[this.position++],
4766
- escaped: true
4767
- };
4652
+ var p = arrayParser.create(value, function(entry) {
4653
+ if (entry !== null) {
4654
+ entry = parseFloat(entry);
4768
4655
  }
4769
- return {
4770
- value: character,
4771
- escaped: false
4772
- };
4656
+ return entry;
4657
+ });
4658
+ return p.parse();
4659
+ };
4660
+ var parseStringArray = function(value) {
4661
+ if (!value) {
4662
+ return null;
4773
4663
  }
4774
- record(character) {
4775
- this.recorded.push(character);
4664
+ var p = arrayParser.create(value);
4665
+ return p.parse();
4666
+ };
4667
+ var parseDateArray = function(value) {
4668
+ if (!value) {
4669
+ return null;
4776
4670
  }
4777
- newEntry(includeEmpty) {
4778
- var entry;
4779
- if (this.recorded.length > 0 || includeEmpty) {
4780
- entry = this.recorded.join("");
4781
- if (entry === "NULL" && !includeEmpty) {
4782
- entry = null;
4783
- }
4784
- if (entry !== null)
4785
- entry = this.transform(entry);
4786
- this.entries.push(entry);
4787
- this.recorded = [];
4671
+ var p = arrayParser.create(value, function(entry) {
4672
+ if (entry !== null) {
4673
+ entry = parseDate(entry);
4788
4674
  }
4675
+ return entry;
4676
+ });
4677
+ return p.parse();
4678
+ };
4679
+ var parseIntervalArray = function(value) {
4680
+ if (!value) {
4681
+ return null;
4789
4682
  }
4790
- consumeDimensions() {
4791
- if (this.source[0] === "[") {
4792
- while (!this.isEof()) {
4793
- var char = this.nextCharacter();
4794
- if (char.value === "=")
4795
- break;
4796
- }
4683
+ var p = arrayParser.create(value, function(entry) {
4684
+ if (entry !== null) {
4685
+ entry = parseInterval(entry);
4797
4686
  }
4687
+ return entry;
4688
+ });
4689
+ return p.parse();
4690
+ };
4691
+ var parseByteAArray = function(value) {
4692
+ if (!value) {
4693
+ return null;
4798
4694
  }
4799
- parse(nested) {
4800
- var character, parser, quote3;
4801
- this.consumeDimensions();
4802
- while (!this.isEof()) {
4803
- character = this.nextCharacter();
4804
- if (character.value === "{" && !quote3) {
4805
- this.dimension++;
4806
- if (this.dimension > 1) {
4807
- parser = new ArrayParser(this.source.substr(this.position - 1), this.transform);
4808
- this.entries.push(parser.parse(true));
4809
- this.position += parser.position - 2;
4810
- }
4811
- } else if (character.value === "}" && !quote3) {
4812
- this.dimension--;
4813
- if (!this.dimension) {
4814
- this.newEntry();
4815
- if (nested)
4816
- return this.entries;
4817
- }
4818
- } else if (character.value === '"' && !character.escaped) {
4819
- if (quote3)
4820
- this.newEntry(true);
4821
- quote3 = !quote3;
4822
- } else if (character.value === "," && !quote3) {
4823
- this.newEntry();
4824
- } else {
4825
- this.record(character.value);
4826
- }
4827
- }
4828
- if (this.dimension !== 0) {
4829
- throw new Error("array dimension not balanced");
4830
- }
4831
- return this.entries;
4695
+ return array.parse(value, allowNull(parseByteA));
4696
+ };
4697
+ var parseInteger = function(value) {
4698
+ return parseInt(value, 10);
4699
+ };
4700
+ var parseBigInteger = function(value) {
4701
+ var valStr = String(value);
4702
+ if (/^\d+$/.test(valStr)) {
4703
+ return valStr;
4832
4704
  }
4833
- }
4834
- function identity(value) {
4835
4705
  return value;
4836
- }
4837
- });
4838
- var require_arrayParser = __commonJS((exports, module) => {
4839
- var array = require_postgres_array();
4840
- module.exports = {
4841
- create: function(source, transform) {
4842
- return {
4843
- parse: function() {
4844
- return array.parse(source, transform);
4845
- }
4846
- };
4847
- }
4848
4706
  };
4849
- });
4850
- var require_postgres_date = __commonJS((exports, module) => {
4851
- var DATE_TIME = /(\d{1,})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})(\.\d{1,})?.*?( BC)?$/;
4852
- var DATE = /^(\d{1,})-(\d{2})-(\d{2})( BC)?$/;
4853
- var TIME_ZONE = /([Z+-])(\d{2})?:?(\d{2})?:?(\d{2})?/;
4854
- var INFINITY = /^-?infinity$/;
4855
- module.exports = function parseDate(isoDate) {
4856
- if (INFINITY.test(isoDate)) {
4857
- return Number(isoDate.replace("i", "I"));
4707
+ var parseJsonArray = function(value) {
4708
+ if (!value) {
4709
+ return null;
4858
4710
  }
4859
- var matches = DATE_TIME.exec(isoDate);
4860
- if (!matches) {
4861
- return getDate(isoDate) || null;
4711
+ return array.parse(value, allowNull(JSON.parse));
4712
+ };
4713
+ var parsePoint = function(value) {
4714
+ if (value[0] !== "(") {
4715
+ return null;
4862
4716
  }
4863
- var isBC = !!matches[8];
4864
- var year = parseInt(matches[1], 10);
4865
- if (isBC) {
4866
- year = bcYearToNegativeYear(year);
4717
+ value = value.substring(1, value.length - 1).split(",");
4718
+ return {
4719
+ x: parseFloat(value[0]),
4720
+ y: parseFloat(value[1])
4721
+ };
4722
+ };
4723
+ var parseCircle = function(value) {
4724
+ if (value[0] !== "<" && value[1] !== "(") {
4725
+ return null;
4867
4726
  }
4868
- var month = parseInt(matches[2], 10) - 1;
4869
- var day = matches[3];
4870
- var hour = parseInt(matches[4], 10);
4871
- var minute = parseInt(matches[5], 10);
4872
- var second = parseInt(matches[6], 10);
4873
- var ms = matches[7];
4874
- ms = ms ? 1000 * parseFloat(ms) : 0;
4875
- var date;
4876
- var offset = timeZoneOffset(isoDate);
4877
- if (offset != null) {
4878
- date = new Date(Date.UTC(year, month, day, hour, minute, second, ms));
4879
- if (is0To99(year)) {
4880
- date.setUTCFullYear(year);
4727
+ var point = "(";
4728
+ var radius = "";
4729
+ var pointParsed = false;
4730
+ for (var i = 2;i < value.length - 1; i++) {
4731
+ if (!pointParsed) {
4732
+ point += value[i];
4881
4733
  }
4882
- if (offset !== 0) {
4883
- date.setTime(date.getTime() - offset);
4734
+ if (value[i] === ")") {
4735
+ pointParsed = true;
4736
+ continue;
4737
+ } else if (!pointParsed) {
4738
+ continue;
4884
4739
  }
4885
- } else {
4886
- date = new Date(year, month, day, hour, minute, second, ms);
4887
- if (is0To99(year)) {
4888
- date.setFullYear(year);
4740
+ if (value[i] === ",") {
4741
+ continue;
4889
4742
  }
4743
+ radius += value[i];
4890
4744
  }
4891
- return date;
4745
+ var result = parsePoint(point);
4746
+ result.radius = parseFloat(radius);
4747
+ return result;
4892
4748
  };
4893
- function getDate(isoDate) {
4894
- var matches = DATE.exec(isoDate);
4895
- if (!matches) {
4896
- return;
4749
+ var init = function(register) {
4750
+ register(20, parseBigInteger);
4751
+ register(21, parseInteger);
4752
+ register(23, parseInteger);
4753
+ register(26, parseInteger);
4754
+ register(700, parseFloat);
4755
+ register(701, parseFloat);
4756
+ register(16, parseBool);
4757
+ register(1082, parseDate);
4758
+ register(1114, parseDate);
4759
+ register(1184, parseDate);
4760
+ register(600, parsePoint);
4761
+ register(651, parseStringArray);
4762
+ register(718, parseCircle);
4763
+ register(1000, parseBoolArray);
4764
+ register(1001, parseByteAArray);
4765
+ register(1005, parseIntegerArray);
4766
+ register(1007, parseIntegerArray);
4767
+ register(1028, parseIntegerArray);
4768
+ register(1016, parseBigIntegerArray);
4769
+ register(1017, parsePointArray);
4770
+ register(1021, parseFloatArray);
4771
+ register(1022, parseFloatArray);
4772
+ register(1231, parseFloatArray);
4773
+ register(1014, parseStringArray);
4774
+ register(1015, parseStringArray);
4775
+ register(1008, parseStringArray);
4776
+ register(1009, parseStringArray);
4777
+ register(1040, parseStringArray);
4778
+ register(1041, parseStringArray);
4779
+ register(1115, parseDateArray);
4780
+ register(1182, parseDateArray);
4781
+ register(1185, parseDateArray);
4782
+ register(1186, parseInterval);
4783
+ register(1187, parseIntervalArray);
4784
+ register(17, parseByteA);
4785
+ register(114, JSON.parse.bind(JSON));
4786
+ register(3802, JSON.parse.bind(JSON));
4787
+ register(199, parseJsonArray);
4788
+ register(3807, parseJsonArray);
4789
+ register(3907, parseStringArray);
4790
+ register(2951, parseStringArray);
4791
+ register(791, parseStringArray);
4792
+ register(1183, parseStringArray);
4793
+ register(1270, parseStringArray);
4794
+ };
4795
+ module.exports = {
4796
+ init
4797
+ };
4798
+ });
4799
+ var require_pg_int8 = __commonJS((exports, module) => {
4800
+ var BASE = 1e6;
4801
+ function readInt8(buffer) {
4802
+ var high = buffer.readInt32BE(0);
4803
+ var low = buffer.readUInt32BE(4);
4804
+ var sign = "";
4805
+ if (high < 0) {
4806
+ high = ~high + (low === 0);
4807
+ low = ~low + 1 >>> 0;
4808
+ sign = "-";
4897
4809
  }
4898
- var year = parseInt(matches[1], 10);
4899
- var isBC = !!matches[4];
4900
- if (isBC) {
4901
- year = bcYearToNegativeYear(year);
4810
+ var result = "";
4811
+ var carry;
4812
+ var t;
4813
+ var digits;
4814
+ var pad;
4815
+ var l;
4816
+ var i;
4817
+ {
4818
+ carry = high % BASE;
4819
+ high = high / BASE >>> 0;
4820
+ t = 4294967296 * carry + low;
4821
+ low = t / BASE >>> 0;
4822
+ digits = "" + (t - BASE * low);
4823
+ if (low === 0 && high === 0) {
4824
+ return sign + digits + result;
4825
+ }
4826
+ pad = "";
4827
+ l = 6 - digits.length;
4828
+ for (i = 0;i < l; i++) {
4829
+ pad += "0";
4830
+ }
4831
+ result = pad + digits + result;
4902
4832
  }
4903
- var month = parseInt(matches[2], 10) - 1;
4904
- var day = matches[3];
4905
- var date = new Date(year, month, day);
4906
- if (is0To99(year)) {
4907
- date.setFullYear(year);
4833
+ {
4834
+ carry = high % BASE;
4835
+ high = high / BASE >>> 0;
4836
+ t = 4294967296 * carry + low;
4837
+ low = t / BASE >>> 0;
4838
+ digits = "" + (t - BASE * low);
4839
+ if (low === 0 && high === 0) {
4840
+ return sign + digits + result;
4841
+ }
4842
+ pad = "";
4843
+ l = 6 - digits.length;
4844
+ for (i = 0;i < l; i++) {
4845
+ pad += "0";
4846
+ }
4847
+ result = pad + digits + result;
4908
4848
  }
4909
- return date;
4910
- }
4911
- function timeZoneOffset(isoDate) {
4912
- if (isoDate.endsWith("+00")) {
4913
- return 0;
4849
+ {
4850
+ carry = high % BASE;
4851
+ high = high / BASE >>> 0;
4852
+ t = 4294967296 * carry + low;
4853
+ low = t / BASE >>> 0;
4854
+ digits = "" + (t - BASE * low);
4855
+ if (low === 0 && high === 0) {
4856
+ return sign + digits + result;
4857
+ }
4858
+ pad = "";
4859
+ l = 6 - digits.length;
4860
+ for (i = 0;i < l; i++) {
4861
+ pad += "0";
4862
+ }
4863
+ result = pad + digits + result;
4914
4864
  }
4915
- var zone = TIME_ZONE.exec(isoDate.split(" ")[1]);
4916
- if (!zone)
4917
- return;
4918
- var type = zone[1];
4919
- if (type === "Z") {
4920
- return 0;
4865
+ {
4866
+ carry = high % BASE;
4867
+ t = 4294967296 * carry + low;
4868
+ digits = "" + t % BASE;
4869
+ return sign + digits + result;
4921
4870
  }
4922
- var sign = type === "-" ? -1 : 1;
4923
- var offset = parseInt(zone[2], 10) * 3600 + parseInt(zone[3] || 0, 10) * 60 + parseInt(zone[4] || 0, 10);
4924
- return offset * sign * 1000;
4925
- }
4926
- function bcYearToNegativeYear(year) {
4927
- return -(year - 1);
4928
- }
4929
- function is0To99(num) {
4930
- return num >= 0 && num < 100;
4931
4871
  }
4872
+ module.exports = readInt8;
4932
4873
  });
4933
- var require_mutable = __commonJS((exports, module) => {
4934
- module.exports = extend;
4935
- var hasOwnProperty = Object.prototype.hasOwnProperty;
4936
- function extend(target) {
4937
- for (var i = 1;i < arguments.length; i++) {
4938
- var source = arguments[i];
4939
- for (var key in source) {
4940
- if (hasOwnProperty.call(source, key)) {
4941
- target[key] = source[key];
4942
- }
4874
+ var require_binaryParsers = __commonJS((exports, module) => {
4875
+ var parseInt64 = require_pg_int8();
4876
+ var parseBits = function(data, bits, offset, invert, callback) {
4877
+ offset = offset || 0;
4878
+ invert = invert || false;
4879
+ callback = callback || function(lastValue, newValue, bits2) {
4880
+ return lastValue * Math.pow(2, bits2) + newValue;
4881
+ };
4882
+ var offsetBytes = offset >> 3;
4883
+ var inv = function(value) {
4884
+ if (invert) {
4885
+ return ~value & 255;
4943
4886
  }
4887
+ return value;
4888
+ };
4889
+ var mask = 255;
4890
+ var firstBits = 8 - offset % 8;
4891
+ if (bits < firstBits) {
4892
+ mask = 255 << 8 - bits & 255;
4893
+ firstBits = bits;
4944
4894
  }
4945
- return target;
4946
- }
4947
- });
4948
- var require_postgres_interval = __commonJS((exports, module) => {
4949
- var extend = require_mutable();
4950
- module.exports = PostgresInterval;
4951
- function PostgresInterval(raw) {
4952
- if (!(this instanceof PostgresInterval)) {
4953
- return new PostgresInterval(raw);
4895
+ if (offset) {
4896
+ mask = mask >> offset % 8;
4954
4897
  }
4955
- extend(this, parse(raw));
4956
- }
4957
- var properties = ["seconds", "minutes", "hours", "days", "months", "years"];
4958
- PostgresInterval.prototype.toPostgres = function() {
4959
- var filtered = properties.filter(this.hasOwnProperty, this);
4960
- if (this.milliseconds && filtered.indexOf("seconds") < 0) {
4961
- filtered.push("seconds");
4898
+ var result = 0;
4899
+ if (offset % 8 + bits >= 8) {
4900
+ result = callback(0, inv(data[offsetBytes]) & mask, firstBits);
4962
4901
  }
4963
- if (filtered.length === 0)
4964
- return "0";
4965
- return filtered.map(function(property) {
4966
- var value = this[property] || 0;
4967
- if (property === "seconds" && this.milliseconds) {
4968
- value = (value + this.milliseconds / 1000).toFixed(6).replace(/\.?0+$/, "");
4969
- }
4970
- return value + " " + property;
4971
- }, this).join(" ");
4972
- };
4973
- var propertiesISOEquivalent = {
4974
- years: "Y",
4975
- months: "M",
4976
- days: "D",
4977
- hours: "H",
4978
- minutes: "M",
4979
- seconds: "S"
4980
- };
4981
- var dateProperties = ["years", "months", "days"];
4982
- var timeProperties = ["hours", "minutes", "seconds"];
4983
- PostgresInterval.prototype.toISOString = PostgresInterval.prototype.toISO = function() {
4984
- var datePart = dateProperties.map(buildProperty, this).join("");
4985
- var timePart = timeProperties.map(buildProperty, this).join("");
4986
- return "P" + datePart + "T" + timePart;
4987
- function buildProperty(property) {
4988
- var value = this[property] || 0;
4989
- if (property === "seconds" && this.milliseconds) {
4990
- value = (value + this.milliseconds / 1000).toFixed(6).replace(/0+$/, "");
4991
- }
4992
- return value + propertiesISOEquivalent[property];
4902
+ var bytes = bits + offset >> 3;
4903
+ for (var i = offsetBytes + 1;i < bytes; i++) {
4904
+ result = callback(result, inv(data[i]), 8);
4993
4905
  }
4906
+ var lastBits = (bits + offset) % 8;
4907
+ if (lastBits > 0) {
4908
+ result = callback(result, inv(data[bytes]) >> 8 - lastBits, lastBits);
4909
+ }
4910
+ return result;
4994
4911
  };
4995
- var NUMBER = "([+-]?\\d+)";
4996
- var YEAR = NUMBER + "\\s+years?";
4997
- var MONTH = NUMBER + "\\s+mons?";
4998
- var DAY = NUMBER + "\\s+days?";
4999
- var TIME = "([+-])?([\\d]*):(\\d\\d):(\\d\\d)\\.?(\\d{1,6})?";
5000
- var INTERVAL = new RegExp([YEAR, MONTH, DAY, TIME].map(function(regexString) {
5001
- return "(" + regexString + ")?";
5002
- }).join("\\s*"));
5003
- var positions = {
5004
- years: 2,
5005
- months: 4,
5006
- days: 6,
5007
- hours: 9,
5008
- minutes: 10,
5009
- seconds: 11,
5010
- milliseconds: 12
5011
- };
5012
- var negatives = ["hours", "minutes", "seconds", "milliseconds"];
5013
- function parseMilliseconds(fraction) {
5014
- var microseconds = fraction + "000000".slice(fraction.length);
5015
- return parseInt(microseconds, 10) / 1000;
5016
- }
5017
- function parse(interval) {
5018
- if (!interval)
5019
- return {};
5020
- var matches = INTERVAL.exec(interval);
5021
- var isNegative = matches[8] === "-";
5022
- return Object.keys(positions).reduce(function(parsed, property) {
5023
- var position = positions[property];
5024
- var value = matches[position];
5025
- if (!value)
5026
- return parsed;
5027
- value = property === "milliseconds" ? parseMilliseconds(value) : parseInt(value, 10);
5028
- if (!value)
5029
- return parsed;
5030
- if (isNegative && ~negatives.indexOf(property)) {
5031
- value *= -1;
5032
- }
5033
- parsed[property] = value;
5034
- return parsed;
5035
- }, {});
5036
- }
5037
- });
5038
- var require_postgres_bytea = __commonJS((exports, module) => {
5039
- var bufferFrom = Buffer.from || Buffer;
5040
- module.exports = function parseBytea(input) {
5041
- if (/^\\x/.test(input)) {
5042
- return bufferFrom(input.substr(2), "hex");
4912
+ var parseFloatFromBits = function(data, precisionBits, exponentBits) {
4913
+ var bias = Math.pow(2, exponentBits - 1) - 1;
4914
+ var sign = parseBits(data, 1);
4915
+ var exponent = parseBits(data, exponentBits, 1);
4916
+ if (exponent === 0) {
4917
+ return 0;
5043
4918
  }
5044
- var output = "";
5045
- var i = 0;
5046
- while (i < input.length) {
5047
- if (input[i] !== "\\") {
5048
- output += input[i];
5049
- ++i;
5050
- } else {
5051
- if (/[0-7]{3}/.test(input.substr(i + 1, 3))) {
5052
- output += String.fromCharCode(parseInt(input.substr(i + 1, 3), 8));
5053
- i += 4;
5054
- } else {
5055
- var backslashes = 1;
5056
- while (i + backslashes < input.length && input[i + backslashes] === "\\") {
5057
- backslashes++;
5058
- }
5059
- for (var k = 0;k < Math.floor(backslashes / 2); ++k) {
5060
- output += "\\";
5061
- }
5062
- i += Math.floor(backslashes / 2) * 2;
4919
+ var precisionBitsCounter = 1;
4920
+ var parsePrecisionBits = function(lastValue, newValue, bits) {
4921
+ if (lastValue === 0) {
4922
+ lastValue = 1;
4923
+ }
4924
+ for (var i = 1;i <= bits; i++) {
4925
+ precisionBitsCounter /= 2;
4926
+ if ((newValue & 1 << bits - i) > 0) {
4927
+ lastValue += precisionBitsCounter;
5063
4928
  }
5064
4929
  }
5065
- }
5066
- return bufferFrom(output, "binary");
5067
- };
5068
- });
5069
- var require_textParsers = __commonJS((exports, module) => {
5070
- var array = require_postgres_array();
5071
- var arrayParser = require_arrayParser();
5072
- var parseDate = require_postgres_date();
5073
- var parseInterval = require_postgres_interval();
5074
- var parseByteA = require_postgres_bytea();
5075
- function allowNull(fn) {
5076
- return function nullAllowed(value) {
5077
- if (value === null)
5078
- return value;
5079
- return fn(value);
4930
+ return lastValue;
5080
4931
  };
5081
- }
5082
- function parseBool(value) {
5083
- if (value === null)
5084
- return value;
5085
- return value === "TRUE" || value === "t" || value === "true" || value === "y" || value === "yes" || value === "on" || value === "1";
5086
- }
5087
- function parseBoolArray(value) {
5088
- if (!value)
5089
- return null;
5090
- return array.parse(value, parseBool);
5091
- }
5092
- function parseBaseTenInt(string) {
5093
- return parseInt(string, 10);
5094
- }
5095
- function parseIntegerArray(value) {
5096
- if (!value)
5097
- return null;
5098
- return array.parse(value, allowNull(parseBaseTenInt));
5099
- }
5100
- function parseBigIntegerArray(value) {
5101
- if (!value)
5102
- return null;
5103
- return array.parse(value, allowNull(function(entry) {
5104
- return parseBigInteger(entry).trim();
5105
- }));
5106
- }
5107
- var parsePointArray = function(value) {
5108
- if (!value) {
5109
- return null;
5110
- }
5111
- var p = arrayParser.create(value, function(entry) {
5112
- if (entry !== null) {
5113
- entry = parsePoint(entry);
5114
- }
5115
- return entry;
5116
- });
5117
- return p.parse();
5118
- };
5119
- var parseFloatArray = function(value) {
5120
- if (!value) {
5121
- return null;
5122
- }
5123
- var p = arrayParser.create(value, function(entry) {
5124
- if (entry !== null) {
5125
- entry = parseFloat(entry);
4932
+ var mantissa = parseBits(data, precisionBits, exponentBits + 1, false, parsePrecisionBits);
4933
+ if (exponent == Math.pow(2, exponentBits + 1) - 1) {
4934
+ if (mantissa === 0) {
4935
+ return sign === 0 ? Infinity : -Infinity;
5126
4936
  }
5127
- return entry;
5128
- });
5129
- return p.parse();
5130
- };
5131
- var parseStringArray = function(value) {
5132
- if (!value) {
5133
- return null;
5134
- }
5135
- var p = arrayParser.create(value);
5136
- return p.parse();
5137
- };
5138
- var parseDateArray = function(value) {
5139
- if (!value) {
5140
- return null;
4937
+ return NaN;
5141
4938
  }
5142
- var p = arrayParser.create(value, function(entry) {
5143
- if (entry !== null) {
5144
- entry = parseDate(entry);
5145
- }
5146
- return entry;
5147
- });
5148
- return p.parse();
4939
+ return (sign === 0 ? 1 : -1) * Math.pow(2, exponent - bias) * mantissa;
5149
4940
  };
5150
- var parseIntervalArray = function(value) {
5151
- if (!value) {
5152
- return null;
4941
+ var parseInt16 = function(value) {
4942
+ if (parseBits(value, 1) == 1) {
4943
+ return -1 * (parseBits(value, 15, 1, true) + 1);
5153
4944
  }
5154
- var p = arrayParser.create(value, function(entry) {
5155
- if (entry !== null) {
5156
- entry = parseInterval(entry);
5157
- }
5158
- return entry;
5159
- });
5160
- return p.parse();
4945
+ return parseBits(value, 15, 1);
5161
4946
  };
5162
- var parseByteAArray = function(value) {
5163
- if (!value) {
5164
- return null;
4947
+ var parseInt32 = function(value) {
4948
+ if (parseBits(value, 1) == 1) {
4949
+ return -1 * (parseBits(value, 31, 1, true) + 1);
5165
4950
  }
5166
- return array.parse(value, allowNull(parseByteA));
4951
+ return parseBits(value, 31, 1);
5167
4952
  };
5168
- var parseInteger = function(value) {
5169
- return parseInt(value, 10);
4953
+ var parseFloat32 = function(value) {
4954
+ return parseFloatFromBits(value, 23, 8);
5170
4955
  };
5171
- var parseBigInteger = function(value) {
5172
- var valStr = String(value);
5173
- if (/^\d+$/.test(valStr)) {
5174
- return valStr;
5175
- }
5176
- return value;
4956
+ var parseFloat64 = function(value) {
4957
+ return parseFloatFromBits(value, 52, 11);
5177
4958
  };
5178
- var parseJsonArray = function(value) {
5179
- if (!value) {
5180
- return null;
5181
- }
5182
- return array.parse(value, allowNull(JSON.parse));
5183
- };
5184
- var parsePoint = function(value) {
5185
- if (value[0] !== "(") {
5186
- return null;
5187
- }
5188
- value = value.substring(1, value.length - 1).split(",");
5189
- return {
5190
- x: parseFloat(value[0]),
5191
- y: parseFloat(value[1])
5192
- };
5193
- };
5194
- var parseCircle = function(value) {
5195
- if (value[0] !== "<" && value[1] !== "(") {
5196
- return null;
5197
- }
5198
- var point = "(";
5199
- var radius = "";
5200
- var pointParsed = false;
5201
- for (var i = 2;i < value.length - 1; i++) {
5202
- if (!pointParsed) {
5203
- point += value[i];
5204
- }
5205
- if (value[i] === ")") {
5206
- pointParsed = true;
5207
- continue;
5208
- } else if (!pointParsed) {
5209
- continue;
5210
- }
5211
- if (value[i] === ",") {
5212
- continue;
5213
- }
5214
- radius += value[i];
5215
- }
5216
- var result = parsePoint(point);
5217
- result.radius = parseFloat(radius);
5218
- return result;
5219
- };
5220
- var init = function(register) {
5221
- register(20, parseBigInteger);
5222
- register(21, parseInteger);
5223
- register(23, parseInteger);
5224
- register(26, parseInteger);
5225
- register(700, parseFloat);
5226
- register(701, parseFloat);
5227
- register(16, parseBool);
5228
- register(1082, parseDate);
5229
- register(1114, parseDate);
5230
- register(1184, parseDate);
5231
- register(600, parsePoint);
5232
- register(651, parseStringArray);
5233
- register(718, parseCircle);
5234
- register(1000, parseBoolArray);
5235
- register(1001, parseByteAArray);
5236
- register(1005, parseIntegerArray);
5237
- register(1007, parseIntegerArray);
5238
- register(1028, parseIntegerArray);
5239
- register(1016, parseBigIntegerArray);
5240
- register(1017, parsePointArray);
5241
- register(1021, parseFloatArray);
5242
- register(1022, parseFloatArray);
5243
- register(1231, parseFloatArray);
5244
- register(1014, parseStringArray);
5245
- register(1015, parseStringArray);
5246
- register(1008, parseStringArray);
5247
- register(1009, parseStringArray);
5248
- register(1040, parseStringArray);
5249
- register(1041, parseStringArray);
5250
- register(1115, parseDateArray);
5251
- register(1182, parseDateArray);
5252
- register(1185, parseDateArray);
5253
- register(1186, parseInterval);
5254
- register(1187, parseIntervalArray);
5255
- register(17, parseByteA);
5256
- register(114, JSON.parse.bind(JSON));
5257
- register(3802, JSON.parse.bind(JSON));
5258
- register(199, parseJsonArray);
5259
- register(3807, parseJsonArray);
5260
- register(3907, parseStringArray);
5261
- register(2951, parseStringArray);
5262
- register(791, parseStringArray);
5263
- register(1183, parseStringArray);
5264
- register(1270, parseStringArray);
5265
- };
5266
- module.exports = {
5267
- init
5268
- };
5269
- });
5270
- var require_pg_int8 = __commonJS((exports, module) => {
5271
- var BASE = 1e6;
5272
- function readInt8(buffer) {
5273
- var high = buffer.readInt32BE(0);
5274
- var low = buffer.readUInt32BE(4);
5275
- var sign = "";
5276
- if (high < 0) {
5277
- high = ~high + (low === 0);
5278
- low = ~low + 1 >>> 0;
5279
- sign = "-";
5280
- }
5281
- var result = "";
5282
- var carry;
5283
- var t;
5284
- var digits;
5285
- var pad;
5286
- var l;
5287
- var i;
5288
- {
5289
- carry = high % BASE;
5290
- high = high / BASE >>> 0;
5291
- t = 4294967296 * carry + low;
5292
- low = t / BASE >>> 0;
5293
- digits = "" + (t - BASE * low);
5294
- if (low === 0 && high === 0) {
5295
- return sign + digits + result;
5296
- }
5297
- pad = "";
5298
- l = 6 - digits.length;
5299
- for (i = 0;i < l; i++) {
5300
- pad += "0";
5301
- }
5302
- result = pad + digits + result;
5303
- }
5304
- {
5305
- carry = high % BASE;
5306
- high = high / BASE >>> 0;
5307
- t = 4294967296 * carry + low;
5308
- low = t / BASE >>> 0;
5309
- digits = "" + (t - BASE * low);
5310
- if (low === 0 && high === 0) {
5311
- return sign + digits + result;
5312
- }
5313
- pad = "";
5314
- l = 6 - digits.length;
5315
- for (i = 0;i < l; i++) {
5316
- pad += "0";
5317
- }
5318
- result = pad + digits + result;
5319
- }
5320
- {
5321
- carry = high % BASE;
5322
- high = high / BASE >>> 0;
5323
- t = 4294967296 * carry + low;
5324
- low = t / BASE >>> 0;
5325
- digits = "" + (t - BASE * low);
5326
- if (low === 0 && high === 0) {
5327
- return sign + digits + result;
5328
- }
5329
- pad = "";
5330
- l = 6 - digits.length;
5331
- for (i = 0;i < l; i++) {
5332
- pad += "0";
5333
- }
5334
- result = pad + digits + result;
5335
- }
5336
- {
5337
- carry = high % BASE;
5338
- t = 4294967296 * carry + low;
5339
- digits = "" + t % BASE;
5340
- return sign + digits + result;
5341
- }
5342
- }
5343
- module.exports = readInt8;
5344
- });
5345
- var require_binaryParsers = __commonJS((exports, module) => {
5346
- var parseInt64 = require_pg_int8();
5347
- var parseBits = function(data, bits, offset, invert, callback) {
5348
- offset = offset || 0;
5349
- invert = invert || false;
5350
- callback = callback || function(lastValue, newValue, bits2) {
5351
- return lastValue * Math.pow(2, bits2) + newValue;
5352
- };
5353
- var offsetBytes = offset >> 3;
5354
- var inv = function(value) {
5355
- if (invert) {
5356
- return ~value & 255;
5357
- }
5358
- return value;
5359
- };
5360
- var mask = 255;
5361
- var firstBits = 8 - offset % 8;
5362
- if (bits < firstBits) {
5363
- mask = 255 << 8 - bits & 255;
5364
- firstBits = bits;
5365
- }
5366
- if (offset) {
5367
- mask = mask >> offset % 8;
5368
- }
5369
- var result = 0;
5370
- if (offset % 8 + bits >= 8) {
5371
- result = callback(0, inv(data[offsetBytes]) & mask, firstBits);
5372
- }
5373
- var bytes = bits + offset >> 3;
5374
- for (var i = offsetBytes + 1;i < bytes; i++) {
5375
- result = callback(result, inv(data[i]), 8);
5376
- }
5377
- var lastBits = (bits + offset) % 8;
5378
- if (lastBits > 0) {
5379
- result = callback(result, inv(data[bytes]) >> 8 - lastBits, lastBits);
5380
- }
5381
- return result;
5382
- };
5383
- var parseFloatFromBits = function(data, precisionBits, exponentBits) {
5384
- var bias = Math.pow(2, exponentBits - 1) - 1;
5385
- var sign = parseBits(data, 1);
5386
- var exponent = parseBits(data, exponentBits, 1);
5387
- if (exponent === 0) {
5388
- return 0;
5389
- }
5390
- var precisionBitsCounter = 1;
5391
- var parsePrecisionBits = function(lastValue, newValue, bits) {
5392
- if (lastValue === 0) {
5393
- lastValue = 1;
5394
- }
5395
- for (var i = 1;i <= bits; i++) {
5396
- precisionBitsCounter /= 2;
5397
- if ((newValue & 1 << bits - i) > 0) {
5398
- lastValue += precisionBitsCounter;
5399
- }
5400
- }
5401
- return lastValue;
5402
- };
5403
- var mantissa = parseBits(data, precisionBits, exponentBits + 1, false, parsePrecisionBits);
5404
- if (exponent == Math.pow(2, exponentBits + 1) - 1) {
5405
- if (mantissa === 0) {
5406
- return sign === 0 ? Infinity : -Infinity;
5407
- }
5408
- return NaN;
5409
- }
5410
- return (sign === 0 ? 1 : -1) * Math.pow(2, exponent - bias) * mantissa;
5411
- };
5412
- var parseInt16 = function(value) {
5413
- if (parseBits(value, 1) == 1) {
5414
- return -1 * (parseBits(value, 15, 1, true) + 1);
5415
- }
5416
- return parseBits(value, 15, 1);
5417
- };
5418
- var parseInt32 = function(value) {
5419
- if (parseBits(value, 1) == 1) {
5420
- return -1 * (parseBits(value, 31, 1, true) + 1);
5421
- }
5422
- return parseBits(value, 31, 1);
5423
- };
5424
- var parseFloat32 = function(value) {
5425
- return parseFloatFromBits(value, 23, 8);
5426
- };
5427
- var parseFloat64 = function(value) {
5428
- return parseFloatFromBits(value, 52, 11);
5429
- };
5430
- var parseNumeric = function(value) {
5431
- var sign = parseBits(value, 16, 32);
5432
- if (sign == 49152) {
5433
- return NaN;
4959
+ var parseNumeric = function(value) {
4960
+ var sign = parseBits(value, 16, 32);
4961
+ if (sign == 49152) {
4962
+ return NaN;
5434
4963
  }
5435
4964
  var weight = Math.pow(1e4, parseBits(value, 16, 16));
5436
4965
  var result = 0;
@@ -13858,473 +13387,1267 @@ var init_zod = __esm(() => {
13858
13387
  init_external();
13859
13388
  });
13860
13389
  function getDataDir2(serviceName) {
13861
- const dir = join5(HASNA_DIR, serviceName);
13390
+ const dir = join3(HASNA_DIR, serviceName);
13862
13391
  mkdirSync2(dir, { recursive: true });
13863
13392
  return dir;
13864
13393
  }
13865
- function getDbPath2(serviceName) {
13866
- const dir = getDataDir2(serviceName);
13867
- return join5(dir, `${serviceName}.db`);
13394
+ function getDbPath2(serviceName) {
13395
+ const dir = getDataDir2(serviceName);
13396
+ return join3(dir, `${serviceName}.db`);
13397
+ }
13398
+ var HASNA_DIR;
13399
+ var init_dotfile = __esm(() => {
13400
+ HASNA_DIR = join3(homedir3(), ".hasna");
13401
+ });
13402
+ var exports_config = {};
13403
+ __export2(exports_config, {
13404
+ saveCloudConfig: () => saveCloudConfig,
13405
+ getConnectionString: () => getConnectionString,
13406
+ getConfigPath: () => getConfigPath,
13407
+ getConfigDir: () => getConfigDir,
13408
+ getCloudConfig: () => getCloudConfig,
13409
+ createDatabase: () => createDatabase,
13410
+ CloudConfigSchema: () => CloudConfigSchema
13411
+ });
13412
+ function getConfigDir() {
13413
+ return CONFIG_DIR;
13414
+ }
13415
+ function getConfigPath() {
13416
+ return CONFIG_PATH;
13417
+ }
13418
+ function getCloudConfig() {
13419
+ if (!existsSync22(CONFIG_PATH)) {
13420
+ return CloudConfigSchema.parse({});
13421
+ }
13422
+ try {
13423
+ const raw = readFileSync2(CONFIG_PATH, "utf-8");
13424
+ return CloudConfigSchema.parse(JSON.parse(raw));
13425
+ } catch {
13426
+ return CloudConfigSchema.parse({});
13427
+ }
13428
+ }
13429
+ function saveCloudConfig(config) {
13430
+ mkdirSync22(CONFIG_DIR, { recursive: true });
13431
+ writeFileSync2(CONFIG_PATH, JSON.stringify(config, null, 2) + `
13432
+ `, "utf-8");
13433
+ }
13434
+ function getConnectionString(dbName) {
13435
+ const config = getCloudConfig();
13436
+ const { host, port, username, password_env, ssl } = config.rds;
13437
+ if (!host || !username) {
13438
+ throw new Error("Cloud RDS not configured. Run `cloud setup` to configure.");
13439
+ }
13440
+ const password = process.env[password_env];
13441
+ if (password === undefined || password === "") {
13442
+ throw new Error(`RDS password not set. Export ${password_env} in your shell or add it to ~/.secrets/hasna/rds/live.env`);
13443
+ }
13444
+ const sslParam = ssl ? "?sslmode=require" : "";
13445
+ return `postgres://${username}:${encodeURIComponent(password)}@${host}:${port}/${dbName}${sslParam}`;
13446
+ }
13447
+ function createDatabase(options) {
13448
+ const config = getCloudConfig();
13449
+ const mode = options.mode ?? config.mode;
13450
+ if (mode === "cloud") {
13451
+ const connStr = options.pgConnectionString ?? getConnectionString(options.service);
13452
+ return new PgAdapter(connStr);
13453
+ }
13454
+ const dbPath = options.sqlitePath ?? getDbPath2(options.service);
13455
+ return new SqliteAdapter(dbPath);
13456
+ }
13457
+ var CloudConfigSchema;
13458
+ var CONFIG_DIR;
13459
+ var CONFIG_PATH;
13460
+ var init_config = __esm(() => {
13461
+ init_zod();
13462
+ init_adapter();
13463
+ init_dotfile();
13464
+ CloudConfigSchema = exports_external2.object({
13465
+ rds: exports_external2.object({
13466
+ host: exports_external2.string().default(""),
13467
+ port: exports_external2.number().default(5432),
13468
+ username: exports_external2.string().default(""),
13469
+ password_env: exports_external2.string().default("HASNA_RDS_PASSWORD"),
13470
+ ssl: exports_external2.boolean().default(true)
13471
+ }).default({}),
13472
+ mode: exports_external2.enum(["local", "cloud", "hybrid"]).default("hybrid"),
13473
+ auto_sync_interval_minutes: exports_external2.number().default(0),
13474
+ feedback_endpoint: exports_external2.string().default("https://feedback.hasna.com/api/v1/feedback"),
13475
+ sync: exports_external2.object({
13476
+ schedule_minutes: exports_external2.number().default(0)
13477
+ }).default({})
13478
+ });
13479
+ CONFIG_DIR = join22(homedir22(), ".hasna", "cloud");
13480
+ CONFIG_PATH = join22(CONFIG_DIR, "config.json");
13481
+ });
13482
+ var exports_discover = {};
13483
+ __export2(exports_discover, {
13484
+ isSyncExcludedTable: () => isSyncExcludedTable,
13485
+ getServiceDbPath: () => getServiceDbPath,
13486
+ discoverSyncableServices: () => discoverSyncableServices,
13487
+ discoverServices: () => discoverServices,
13488
+ SYNC_EXCLUDED_TABLE_PATTERNS: () => SYNC_EXCLUDED_TABLE_PATTERNS,
13489
+ KNOWN_PG_SERVICES: () => KNOWN_PG_SERVICES
13490
+ });
13491
+ function isSyncExcludedTable(table) {
13492
+ return SYNC_EXCLUDED_TABLE_PATTERNS.some((p) => p.test(table));
13493
+ }
13494
+ function discoverServices() {
13495
+ const dataDir = join32(homedir32(), ".hasna");
13496
+ if (!existsSync32(dataDir))
13497
+ return [];
13498
+ try {
13499
+ const entries = readdirSync2(dataDir, { withFileTypes: true });
13500
+ return entries.filter((e) => {
13501
+ if (!e.isDirectory())
13502
+ return false;
13503
+ if (e.name === "cloud" || e.name.startsWith("."))
13504
+ return false;
13505
+ return true;
13506
+ }).map((e) => e.name).sort();
13507
+ } catch {
13508
+ return [];
13509
+ }
13510
+ }
13511
+ function discoverSyncableServices() {
13512
+ const local = discoverServices();
13513
+ const pgSet = new Set(KNOWN_PG_SERVICES);
13514
+ return local.filter((s) => pgSet.has(s));
13515
+ }
13516
+ function getServiceDbPath(service) {
13517
+ const dataDir = join32(homedir32(), ".hasna", service);
13518
+ if (!existsSync32(dataDir))
13519
+ return null;
13520
+ const candidates = [
13521
+ join32(dataDir, `${service}.db`),
13522
+ join32(dataDir, "data.db"),
13523
+ join32(dataDir, "database.db")
13524
+ ];
13525
+ try {
13526
+ const files = readdirSync2(dataDir);
13527
+ for (const f of files) {
13528
+ if (f.endsWith(".db") && !f.endsWith("-wal") && !f.endsWith("-shm")) {
13529
+ candidates.push(join32(dataDir, f));
13530
+ }
13531
+ }
13532
+ } catch {}
13533
+ for (const p of candidates) {
13534
+ if (existsSync32(p))
13535
+ return p;
13536
+ }
13537
+ return null;
13538
+ }
13539
+ var KNOWN_PG_SERVICES;
13540
+ var SYNC_EXCLUDED_TABLE_PATTERNS;
13541
+ var init_discover = __esm(() => {
13542
+ KNOWN_PG_SERVICES = [
13543
+ "assistants",
13544
+ "attachments",
13545
+ "brains",
13546
+ "configs",
13547
+ "connectors",
13548
+ "contacts",
13549
+ "context",
13550
+ "conversations",
13551
+ "crawl",
13552
+ "deployment",
13553
+ "economy",
13554
+ "emails",
13555
+ "files",
13556
+ "hooks",
13557
+ "implementations",
13558
+ "logs",
13559
+ "mcps",
13560
+ "mementos",
13561
+ "microservices",
13562
+ "predictor",
13563
+ "prompts",
13564
+ "recordings",
13565
+ "researcher",
13566
+ "sandboxes",
13567
+ "search",
13568
+ "secrets",
13569
+ "sessions",
13570
+ "signatures",
13571
+ "skills",
13572
+ "telephony",
13573
+ "terminal",
13574
+ "testers",
13575
+ "tickets",
13576
+ "todos",
13577
+ "wallets"
13578
+ ];
13579
+ SYNC_EXCLUDED_TABLE_PATTERNS = [
13580
+ /^sqlite_/,
13581
+ /_fts$/,
13582
+ /_fts_/,
13583
+ /^_sync_/,
13584
+ /^_pg_migrations$/
13585
+ ];
13586
+ });
13587
+ init_adapter();
13588
+ init_config();
13589
+ init_config();
13590
+ init_dotfile();
13591
+
13592
+ class SyncProgressTracker {
13593
+ db;
13594
+ progress = new Map;
13595
+ startTimes = new Map;
13596
+ callback;
13597
+ constructor(db, callback) {
13598
+ this.db = db;
13599
+ this.callback = callback;
13600
+ this.ensureResumeTable();
13601
+ }
13602
+ ensureResumeTable() {
13603
+ this.db.exec(`
13604
+ CREATE TABLE IF NOT EXISTS _sync_resume (
13605
+ table_name TEXT PRIMARY KEY,
13606
+ last_row_id TEXT,
13607
+ direction TEXT,
13608
+ started_at TEXT,
13609
+ status TEXT DEFAULT 'in_progress'
13610
+ )
13611
+ `);
13612
+ }
13613
+ start(table, total, direction) {
13614
+ const resumed = this.canResume(table);
13615
+ const now = Date.now();
13616
+ this.startTimes.set(table, now);
13617
+ const status = resumed ? "resumed" : "in_progress";
13618
+ const info = {
13619
+ table,
13620
+ total,
13621
+ done: 0,
13622
+ percent: 0,
13623
+ elapsed_ms: 0,
13624
+ eta_ms: 0,
13625
+ status
13626
+ };
13627
+ this.progress.set(table, info);
13628
+ this.db.run(`INSERT INTO _sync_resume (table_name, last_row_id, direction, started_at, status)
13629
+ VALUES (?, ?, ?, datetime('now'), ?)
13630
+ ON CONFLICT (table_name) DO UPDATE SET
13631
+ direction = excluded.direction,
13632
+ started_at = datetime('now'),
13633
+ status = excluded.status`, table, "", direction, status);
13634
+ this.notify(table);
13635
+ }
13636
+ update(table, done, lastRowId) {
13637
+ const info = this.progress.get(table);
13638
+ if (!info)
13639
+ return;
13640
+ const startTime = this.startTimes.get(table) ?? Date.now();
13641
+ const elapsed = Date.now() - startTime;
13642
+ const rate = done > 0 ? elapsed / done : 0;
13643
+ const remaining = info.total - done;
13644
+ const eta = remaining > 0 ? Math.round(rate * remaining) : 0;
13645
+ info.done = done;
13646
+ info.percent = info.total > 0 ? Math.round(done / info.total * 100) : 0;
13647
+ info.elapsed_ms = elapsed;
13648
+ info.eta_ms = eta;
13649
+ info.status = "in_progress";
13650
+ this.db.run(`UPDATE _sync_resume SET last_row_id = ?, status = 'in_progress' WHERE table_name = ?`, lastRowId, table);
13651
+ this.notify(table);
13652
+ }
13653
+ markComplete(table) {
13654
+ const info = this.progress.get(table);
13655
+ if (info) {
13656
+ const startTime = this.startTimes.get(table) ?? Date.now();
13657
+ info.elapsed_ms = Date.now() - startTime;
13658
+ info.done = info.total;
13659
+ info.percent = 100;
13660
+ info.eta_ms = 0;
13661
+ info.status = "completed";
13662
+ this.notify(table);
13663
+ }
13664
+ this.db.run(`UPDATE _sync_resume SET status = 'completed' WHERE table_name = ?`, table);
13665
+ }
13666
+ markFailed(table, _error) {
13667
+ const info = this.progress.get(table);
13668
+ if (info) {
13669
+ const startTime = this.startTimes.get(table) ?? Date.now();
13670
+ info.elapsed_ms = Date.now() - startTime;
13671
+ info.status = "failed";
13672
+ this.notify(table);
13673
+ }
13674
+ this.db.run(`UPDATE _sync_resume SET status = 'failed' WHERE table_name = ?`, table);
13675
+ }
13676
+ canResume(table) {
13677
+ const row = this.db.get(`SELECT status FROM _sync_resume WHERE table_name = ?`, table);
13678
+ if (!row)
13679
+ return false;
13680
+ return row.status === "in_progress" || row.status === "resumed";
13681
+ }
13682
+ getResumePoint(table) {
13683
+ const row = this.db.get(`SELECT table_name, last_row_id, direction, started_at, status FROM _sync_resume WHERE table_name = ?`, table);
13684
+ if (!row)
13685
+ return null;
13686
+ if (row.status !== "in_progress" && row.status !== "resumed")
13687
+ return null;
13688
+ return row;
13689
+ }
13690
+ clearResume(table) {
13691
+ this.db.run(`DELETE FROM _sync_resume WHERE table_name = ?`, table);
13692
+ this.progress.delete(table);
13693
+ this.startTimes.delete(table);
13694
+ }
13695
+ getProgress(table) {
13696
+ return this.progress.get(table) ?? null;
13697
+ }
13698
+ getAllProgress() {
13699
+ return Array.from(this.progress.values());
13700
+ }
13701
+ listResumeRecords() {
13702
+ return this.db.all(`SELECT table_name, last_row_id, direction, started_at, status FROM _sync_resume ORDER BY started_at DESC`);
13703
+ }
13704
+ notify(table) {
13705
+ const info = this.progress.get(table);
13706
+ if (info && this.callback) {
13707
+ this.callback({ ...info });
13708
+ }
13709
+ }
13710
+ }
13711
+ init_adapter();
13712
+ init_config();
13713
+ init_discover();
13714
+ var AUTO_SYNC_CONFIG_PATH = join4(homedir4(), ".hasna", "cloud", "config.json");
13715
+ init_config();
13716
+ init_adapter();
13717
+ init_dotfile();
13718
+ init_config();
13719
+ var CONFIG_DIR2 = join6(homedir5(), ".hasna", "cloud");
13720
+ init_adapter();
13721
+ init_config();
13722
+ init_discover();
13723
+ init_zod();
13724
+ init_config();
13725
+ init_dotfile();
13726
+ init_adapter();
13727
+ init_config();
13728
+ init_dotfile();
13729
+ init_adapter();
13730
+
13731
+ // src/db.ts
13732
+ var adapter = null;
13733
+ function createTables(db) {
13734
+ db.exec(`
13735
+ CREATE TABLE IF NOT EXISTS agent_heartbeats (
13736
+ machine_id TEXT NOT NULL,
13737
+ pid INTEGER NOT NULL,
13738
+ status TEXT NOT NULL,
13739
+ updated_at TEXT NOT NULL,
13740
+ PRIMARY KEY (machine_id, pid)
13741
+ )
13742
+ `);
13743
+ db.exec(`
13744
+ CREATE TABLE IF NOT EXISTS setup_runs (
13745
+ id TEXT PRIMARY KEY,
13746
+ machine_id TEXT NOT NULL,
13747
+ status TEXT NOT NULL,
13748
+ details_json TEXT NOT NULL DEFAULT '[]',
13749
+ created_at TEXT NOT NULL,
13750
+ updated_at TEXT NOT NULL
13751
+ )
13752
+ `);
13753
+ db.exec(`
13754
+ CREATE TABLE IF NOT EXISTS sync_runs (
13755
+ id TEXT PRIMARY KEY,
13756
+ machine_id TEXT NOT NULL,
13757
+ status TEXT NOT NULL,
13758
+ actions_json TEXT NOT NULL DEFAULT '[]',
13759
+ created_at TEXT NOT NULL,
13760
+ updated_at TEXT NOT NULL
13761
+ )
13762
+ `);
13763
+ }
13764
+ function getAdapter(path = getDbPath()) {
13765
+ if (path === ":memory:") {
13766
+ const memoryAdapter = new SqliteAdapter(path);
13767
+ createTables(memoryAdapter.raw);
13768
+ return memoryAdapter;
13769
+ }
13770
+ if (!adapter) {
13771
+ ensureParentDir(path);
13772
+ adapter = new SqliteAdapter(path);
13773
+ createTables(adapter.raw);
13774
+ }
13775
+ return adapter;
13776
+ }
13777
+ function getDb(path = getDbPath()) {
13778
+ return getAdapter(path).raw;
13779
+ }
13780
+ function getLocalMachineId() {
13781
+ return process.env["HASNA_MACHINES_MACHINE_ID"] || hostname2();
13782
+ }
13783
+ function listHeartbeats(machineId) {
13784
+ const db = getDb();
13785
+ if (machineId) {
13786
+ return db.query(`SELECT machine_id, pid, status, updated_at
13787
+ FROM agent_heartbeats
13788
+ WHERE machine_id = ?
13789
+ ORDER BY updated_at DESC`).all(machineId);
13790
+ }
13791
+ return db.query(`SELECT machine_id, pid, status, updated_at
13792
+ FROM agent_heartbeats
13793
+ ORDER BY updated_at DESC`).all();
13794
+ }
13795
+ function countRuns(table) {
13796
+ const db = getDb();
13797
+ const row = db.query(`SELECT COUNT(*) as count FROM ${table}`).get();
13798
+ return row.count;
13799
+ }
13800
+ function recordSetupRun(machineId, status, details) {
13801
+ const db = getDb();
13802
+ const now = new Date().toISOString();
13803
+ db.query(`INSERT INTO setup_runs (id, machine_id, status, details_json, created_at, updated_at)
13804
+ VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(details), now, now);
13805
+ }
13806
+ function recordSyncRun(machineId, status, actions) {
13807
+ const db = getDb();
13808
+ const now = new Date().toISOString();
13809
+ db.query(`INSERT INTO sync_runs (id, machine_id, status, actions_json, created_at, updated_at)
13810
+ VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(actions), now, now);
13811
+ }
13812
+
13813
+ // src/commands/ssh.ts
13814
+ import { spawnSync } from "child_process";
13815
+ function envReachableHosts() {
13816
+ const raw = process.env["HASNA_MACHINES_REACHABLE_HOSTS"];
13817
+ return new Set((raw || "").split(",").map((value) => value.trim()).filter(Boolean));
13818
+ }
13819
+ function isReachable(host) {
13820
+ const overrides = envReachableHosts();
13821
+ if (overrides.size > 0) {
13822
+ return overrides.has(host);
13823
+ }
13824
+ const probe = spawnSync("bash", ["-lc", `getent hosts ${host} >/dev/null 2>&1 || ping -c 1 -W 1 ${host} >/dev/null 2>&1`], {
13825
+ stdio: "ignore"
13826
+ });
13827
+ return probe.status === 0;
13828
+ }
13829
+ function resolveSshTarget(machineId) {
13830
+ const machine = getManifestMachine(machineId);
13831
+ if (!machine) {
13832
+ throw new Error(`Machine not found in manifest: ${machineId}`);
13833
+ }
13834
+ const current = detectCurrentMachineManifest();
13835
+ if (machine.id === current.id) {
13836
+ return {
13837
+ machineId,
13838
+ target: "localhost",
13839
+ route: "local"
13840
+ };
13841
+ }
13842
+ const lanTarget = machine.sshAddress || machine.hostname || machine.id;
13843
+ const tailscaleTarget = machine.tailscaleName || machine.hostname || machine.id;
13844
+ const route = isReachable(lanTarget) ? "lan" : "tailscale";
13845
+ return {
13846
+ machineId,
13847
+ target: route === "lan" ? lanTarget : tailscaleTarget,
13848
+ route
13849
+ };
13850
+ }
13851
+ function buildSshCommand(machineId, remoteCommand) {
13852
+ const resolved = resolveSshTarget(machineId);
13853
+ return remoteCommand ? `ssh ${resolved.target} ${JSON.stringify(remoteCommand)}` : `ssh ${resolved.target}`;
13868
13854
  }
13869
- var HASNA_DIR;
13870
- var init_dotfile = __esm(() => {
13871
- HASNA_DIR = join5(homedir4(), ".hasna");
13872
- });
13873
- var exports_config = {};
13874
- __export2(exports_config, {
13875
- saveCloudConfig: () => saveCloudConfig,
13876
- getConnectionString: () => getConnectionString,
13877
- getConfigPath: () => getConfigPath,
13878
- getConfigDir: () => getConfigDir,
13879
- getCloudConfig: () => getCloudConfig,
13880
- createDatabase: () => createDatabase,
13881
- CloudConfigSchema: () => CloudConfigSchema
13882
- });
13883
- function getConfigDir() {
13884
- return CONFIG_DIR;
13855
+
13856
+ // src/remote.ts
13857
+ function runMachineCommand(machineId, command) {
13858
+ const localMachineId = getLocalMachineId();
13859
+ const isLocal = machineId === localMachineId;
13860
+ const route = isLocal ? "local" : resolveSshTarget(machineId).route;
13861
+ const shellCommand = isLocal ? command : buildSshCommand(machineId, command);
13862
+ const result = spawnSync2("bash", ["-lc", shellCommand], {
13863
+ encoding: "utf8",
13864
+ env: process.env
13865
+ });
13866
+ return {
13867
+ machineId,
13868
+ source: route,
13869
+ stdout: result.stdout || "",
13870
+ stderr: result.stderr || "",
13871
+ exitCode: result.status ?? 1
13872
+ };
13885
13873
  }
13886
- function getConfigPath() {
13887
- return CONFIG_PATH;
13874
+
13875
+ // src/commands/apps.ts
13876
+ function getPackageName(app) {
13877
+ return app.packageName || app.name;
13888
13878
  }
13889
- function getCloudConfig() {
13890
- if (!existsSync22(CONFIG_PATH)) {
13891
- return CloudConfigSchema.parse({});
13879
+ function getAppManager(machine, app) {
13880
+ if (app.manager)
13881
+ return app.manager;
13882
+ if (machine.platform === "macos")
13883
+ return "brew";
13884
+ if (machine.platform === "windows")
13885
+ return "winget";
13886
+ return "apt";
13887
+ }
13888
+ function shellQuote(value) {
13889
+ return `'${value.replace(/'/g, `'\\''`)}'`;
13890
+ }
13891
+ function buildAppCommand(machine, app) {
13892
+ const packageName = getPackageName(app);
13893
+ const manager = getAppManager(machine, app);
13894
+ if (manager === "custom") {
13895
+ return packageName;
13892
13896
  }
13893
- try {
13894
- const raw = readFileSync4(CONFIG_PATH, "utf-8");
13895
- return CloudConfigSchema.parse(JSON.parse(raw));
13896
- } catch {
13897
- return CloudConfigSchema.parse({});
13897
+ if (machine.platform === "macos") {
13898
+ if (manager === "cask") {
13899
+ return `brew install --cask ${packageName}`;
13900
+ }
13901
+ return `brew install ${packageName}`;
13898
13902
  }
13903
+ if (machine.platform === "windows") {
13904
+ return `winget install ${packageName}`;
13905
+ }
13906
+ return `sudo apt-get install -y ${packageName}`;
13899
13907
  }
13900
- function saveCloudConfig(config) {
13901
- mkdirSync22(CONFIG_DIR, { recursive: true });
13902
- writeFileSync4(CONFIG_PATH, JSON.stringify(config, null, 2) + `
13903
- `, "utf-8");
13904
- }
13905
- function getConnectionString(dbName) {
13906
- const config = getCloudConfig();
13907
- const { host, port, username, password_env, ssl } = config.rds;
13908
- if (!host || !username) {
13909
- throw new Error("Cloud RDS not configured. Run `cloud setup` to configure.");
13908
+ function buildAppProbeCommand(machine, app) {
13909
+ const packageName = shellQuote(getPackageName(app));
13910
+ const manager = getAppManager(machine, app);
13911
+ if (manager === "custom") {
13912
+ return `if command -v ${packageName} >/dev/null 2>&1; then printf 'installed=1\\nversion=custom\\n'; else printf 'installed=0\\n'; fi`;
13910
13913
  }
13911
- const password = process.env[password_env];
13912
- if (password === undefined || password === "") {
13913
- throw new Error(`RDS password not set. Export ${password_env} in your shell or add it to ~/.secrets/hasna/rds/live.env`);
13914
+ if (machine.platform === "macos") {
13915
+ if (manager === "cask") {
13916
+ return `if brew list --cask ${packageName} >/dev/null 2>&1; then printf 'installed=1\\nversion=installed\\n'; else printf 'installed=0\\n'; fi`;
13917
+ }
13918
+ return `if brew list --versions ${packageName} >/dev/null 2>&1; then printf 'installed=1\\nversion='; brew list --versions ${packageName} | awk '{print $2}'; printf '\\n'; else printf 'installed=0\\n'; fi`;
13914
13919
  }
13915
- const sslParam = ssl ? "?sslmode=require" : "";
13916
- return `postgres://${username}:${encodeURIComponent(password)}@${host}:${port}/${dbName}${sslParam}`;
13920
+ if (machine.platform === "windows") {
13921
+ return `if winget list --id ${packageName} --exact >/dev/null 2>&1; then printf 'installed=1\\nversion=installed\\n'; else printf 'installed=0\\n'; fi`;
13922
+ }
13923
+ return `if dpkg-query -W -f='${"${Version}"}' ${packageName} >/tmp/machines-app-version 2>/dev/null; then printf 'installed=1\\nversion='; cat /tmp/machines-app-version; printf '\\n'; rm -f /tmp/machines-app-version; else printf 'installed=0\\n'; fi`;
13917
13924
  }
13918
- function createDatabase(options) {
13919
- const config = getCloudConfig();
13920
- const mode = options.mode ?? config.mode;
13921
- if (mode === "cloud") {
13922
- const connStr = options.pgConnectionString ?? getConnectionString(options.service);
13923
- return new PgAdapter(connStr);
13925
+ function buildAppSteps(machine) {
13926
+ return (machine.apps || []).map((app) => ({
13927
+ id: `app-${app.name}`,
13928
+ title: `Install ${app.name} on ${machine.id}`,
13929
+ command: buildAppCommand(machine, app),
13930
+ manager: getAppManager(machine, app) === "custom" ? "custom" : machine.platform === "macos" ? "brew" : machine.platform === "windows" ? "custom" : "apt",
13931
+ privileged: machine.platform === "linux"
13932
+ }));
13933
+ }
13934
+ function resolveMachine(machineId) {
13935
+ return (machineId ? getManifestMachine(machineId) : null) || detectCurrentMachineManifest();
13936
+ }
13937
+ function parseProbeOutput(app, machine, stdout) {
13938
+ const lines = stdout.trim().split(`
13939
+ `).filter(Boolean);
13940
+ const installedLine = lines.find((line) => line.startsWith("installed="));
13941
+ const versionLine = lines.find((line) => line.startsWith("version="));
13942
+ return {
13943
+ name: app.name,
13944
+ packageName: getPackageName(app),
13945
+ manager: getAppManager(machine, app),
13946
+ installed: installedLine === "installed=1",
13947
+ version: versionLine?.slice("version=".length) || undefined
13948
+ };
13949
+ }
13950
+ function listApps(machineId) {
13951
+ const machine = resolveMachine(machineId);
13952
+ return {
13953
+ machineId: machine.id,
13954
+ apps: machine.apps || []
13955
+ };
13956
+ }
13957
+ function buildAppsPlan(machineId) {
13958
+ const machine = resolveMachine(machineId);
13959
+ return {
13960
+ machineId: machine.id,
13961
+ mode: "plan",
13962
+ steps: buildAppSteps(machine),
13963
+ executed: 0
13964
+ };
13965
+ }
13966
+ function getAppsStatus(machineId) {
13967
+ const machine = resolveMachine(machineId);
13968
+ const apps = (machine.apps || []).map((app) => {
13969
+ const probe = runMachineCommand(machine.id, buildAppProbeCommand(machine, app));
13970
+ return parseProbeOutput(app, machine, probe.stdout);
13971
+ });
13972
+ return {
13973
+ machineId: machine.id,
13974
+ source: apps.length > 0 ? runMachineCommand(machine.id, "true").source : machine.id === detectCurrentMachineManifest().id ? "local" : runMachineCommand(machine.id, "true").source,
13975
+ apps
13976
+ };
13977
+ }
13978
+ function diffApps(machineId) {
13979
+ const status = getAppsStatus(machineId);
13980
+ return {
13981
+ ...status,
13982
+ missing: status.apps.filter((app) => !app.installed).map((app) => app.name),
13983
+ installed: status.apps.filter((app) => app.installed).map((app) => app.name)
13984
+ };
13985
+ }
13986
+ function runAppsInstall(machineId, options = {}) {
13987
+ const plan = buildAppsPlan(machineId);
13988
+ if (!options.apply)
13989
+ return plan;
13990
+ if (!options.yes) {
13991
+ throw new Error("App installation requires --yes.");
13924
13992
  }
13925
- const dbPath = options.sqlitePath ?? getDbPath2(options.service);
13926
- return new SqliteAdapter(dbPath);
13993
+ let executed = 0;
13994
+ for (const step of plan.steps) {
13995
+ const result = Bun.spawnSync(["bash", "-lc", step.command], {
13996
+ stdout: "pipe",
13997
+ stderr: "pipe",
13998
+ env: process.env
13999
+ });
14000
+ if (result.exitCode !== 0) {
14001
+ throw new Error(`App install failed (${step.id}): ${result.stderr.toString().trim()}`);
14002
+ }
14003
+ executed += 1;
14004
+ }
14005
+ return {
14006
+ machineId: plan.machineId,
14007
+ mode: "apply",
14008
+ steps: plan.steps,
14009
+ executed
14010
+ };
13927
14011
  }
13928
- var CloudConfigSchema;
13929
- var CONFIG_DIR;
13930
- var CONFIG_PATH;
13931
- var init_config = __esm(() => {
13932
- init_zod();
13933
- init_adapter();
13934
- init_dotfile();
13935
- CloudConfigSchema = exports_external2.object({
13936
- rds: exports_external2.object({
13937
- host: exports_external2.string().default(""),
13938
- port: exports_external2.number().default(5432),
13939
- username: exports_external2.string().default(""),
13940
- password_env: exports_external2.string().default("HASNA_RDS_PASSWORD"),
13941
- ssl: exports_external2.boolean().default(true)
13942
- }).default({}),
13943
- mode: exports_external2.enum(["local", "cloud", "hybrid"]).default("hybrid"),
13944
- auto_sync_interval_minutes: exports_external2.number().default(0),
13945
- feedback_endpoint: exports_external2.string().default("https://feedback.hasna.com/api/v1/feedback"),
13946
- sync: exports_external2.object({
13947
- schedule_minutes: exports_external2.number().default(0)
13948
- }).default({})
14012
+
14013
+ // src/commands/cert.ts
14014
+ import { homedir as homedir6, platform as platform3 } from "os";
14015
+ import { join as join5 } from "path";
14016
+ function quote2(value) {
14017
+ return `'${value.replace(/'/g, `'\\''`)}'`;
14018
+ }
14019
+ function certDir() {
14020
+ return join5(homedir6(), ".hasna", "machines", "certs");
14021
+ }
14022
+ function buildCertPlan(domains) {
14023
+ if (domains.length === 0) {
14024
+ throw new Error("At least one domain is required.");
14025
+ }
14026
+ const primary = domains[0];
14027
+ const certPath = join5(certDir(), `${primary}.pem`);
14028
+ const keyPath = join5(certDir(), `${primary}-key.pem`);
14029
+ const steps = [];
14030
+ if (platform3() === "darwin") {
14031
+ steps.push({
14032
+ id: "mkcert-install-macos",
14033
+ title: "Install mkcert on macOS",
14034
+ command: "brew install mkcert nss",
14035
+ manager: "brew"
14036
+ });
14037
+ } else {
14038
+ steps.push({
14039
+ id: "mkcert-install-linux",
14040
+ title: "Install mkcert on Linux",
14041
+ command: "sudo apt-get update && sudo apt-get install -y mkcert libnss3-tools",
14042
+ manager: "apt",
14043
+ privileged: true
14044
+ });
14045
+ }
14046
+ steps.push({
14047
+ id: "mkcert-local-ca",
14048
+ title: "Install local mkcert CA",
14049
+ command: "mkcert -install",
14050
+ manager: "custom"
14051
+ }, {
14052
+ id: "mkcert-issue",
14053
+ title: `Issue certificate for ${domains.join(", ")}`,
14054
+ command: `mkdir -p ${quote2(certDir())} && mkcert -cert-file ${quote2(certPath)} -key-file ${quote2(keyPath)} ${domains.map((domain) => quote2(domain)).join(" ")}`,
14055
+ manager: "custom"
13949
14056
  });
13950
- CONFIG_DIR = join22(homedir22(), ".hasna", "cloud");
13951
- CONFIG_PATH = join22(CONFIG_DIR, "config.json");
13952
- });
13953
- var exports_discover = {};
13954
- __export2(exports_discover, {
13955
- isSyncExcludedTable: () => isSyncExcludedTable,
13956
- getServiceDbPath: () => getServiceDbPath,
13957
- discoverSyncableServices: () => discoverSyncableServices,
13958
- discoverServices: () => discoverServices,
13959
- SYNC_EXCLUDED_TABLE_PATTERNS: () => SYNC_EXCLUDED_TABLE_PATTERNS,
13960
- KNOWN_PG_SERVICES: () => KNOWN_PG_SERVICES
13961
- });
13962
- function isSyncExcludedTable(table) {
13963
- return SYNC_EXCLUDED_TABLE_PATTERNS.some((p) => p.test(table));
14057
+ return {
14058
+ machineId: process.env["HASNA_MACHINES_MACHINE_ID"] || "local",
14059
+ mode: "plan",
14060
+ steps,
14061
+ executed: 0
14062
+ };
14063
+ }
14064
+ function runCertPlan(domains, options = {}) {
14065
+ const plan = buildCertPlan(domains);
14066
+ if (!options.apply)
14067
+ return plan;
14068
+ if (!options.yes) {
14069
+ throw new Error("Certificate generation requires --yes.");
14070
+ }
14071
+ let executed = 0;
14072
+ for (const step of plan.steps) {
14073
+ const result = Bun.spawnSync(["bash", "-lc", step.command], {
14074
+ stdout: "pipe",
14075
+ stderr: "pipe",
14076
+ env: process.env
14077
+ });
14078
+ if (result.exitCode !== 0) {
14079
+ throw new Error(`Certificate step failed (${step.id}): ${result.stderr.toString().trim()}`);
14080
+ }
14081
+ executed += 1;
14082
+ }
14083
+ return {
14084
+ machineId: plan.machineId,
14085
+ mode: "apply",
14086
+ steps: plan.steps,
14087
+ executed
14088
+ };
14089
+ }
14090
+
14091
+ // src/commands/dns.ts
14092
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
14093
+ import { join as join7 } from "path";
14094
+ function getDnsPath() {
14095
+ return join7(getDataDir(), "dns.json");
13964
14096
  }
13965
- function discoverServices() {
13966
- const dataDir = join32(homedir32(), ".hasna");
13967
- if (!existsSync32(dataDir))
13968
- return [];
13969
- try {
13970
- const entries = readdirSync2(dataDir, { withFileTypes: true });
13971
- return entries.filter((e) => {
13972
- if (!e.isDirectory())
13973
- return false;
13974
- if (e.name === "cloud" || e.name.startsWith("."))
13975
- return false;
13976
- return true;
13977
- }).map((e) => e.name).sort();
13978
- } catch {
14097
+ function readMappings() {
14098
+ const path = getDnsPath();
14099
+ if (!existsSync4(path))
13979
14100
  return [];
13980
- }
14101
+ return JSON.parse(readFileSync3(path, "utf8"));
13981
14102
  }
13982
- function discoverSyncableServices() {
13983
- const local = discoverServices();
13984
- const pgSet = new Set(KNOWN_PG_SERVICES);
13985
- return local.filter((s) => pgSet.has(s));
14103
+ function writeMappings(mappings) {
14104
+ const path = getDnsPath();
14105
+ ensureParentDir(path);
14106
+ writeFileSync3(path, `${JSON.stringify(mappings, null, 2)}
14107
+ `, "utf8");
14108
+ return path;
13986
14109
  }
13987
- function getServiceDbPath(service) {
13988
- const dataDir = join32(homedir32(), ".hasna", service);
13989
- if (!existsSync32(dataDir))
13990
- return null;
13991
- const candidates = [
13992
- join32(dataDir, `${service}.db`),
13993
- join32(dataDir, "data.db"),
13994
- join32(dataDir, "database.db")
13995
- ];
13996
- try {
13997
- const files = readdirSync2(dataDir);
13998
- for (const f of files) {
13999
- if (f.endsWith(".db") && !f.endsWith("-wal") && !f.endsWith("-shm")) {
14000
- candidates.push(join32(dataDir, f));
14001
- }
14002
- }
14003
- } catch {}
14004
- for (const p of candidates) {
14005
- if (existsSync32(p))
14006
- return p;
14110
+ function addDomainMapping(domain, port, targetHost = "127.0.0.1") {
14111
+ const mappings = readMappings().filter((entry) => entry.domain !== domain);
14112
+ mappings.push({ domain, port, targetHost });
14113
+ writeMappings(mappings);
14114
+ return mappings.sort((left, right) => left.domain.localeCompare(right.domain));
14115
+ }
14116
+ function listDomainMappings() {
14117
+ return readMappings().sort((left, right) => left.domain.localeCompare(right.domain));
14118
+ }
14119
+ function renderDomainMapping(domain) {
14120
+ const entry = readMappings().find((mapping) => mapping.domain === domain);
14121
+ if (!entry) {
14122
+ throw new Error(`Domain mapping not found: ${domain}`);
14007
14123
  }
14008
- return null;
14124
+ return {
14125
+ hostsEntry: `${entry.targetHost} ${entry.domain}`,
14126
+ caddySnippet: `${entry.domain} {
14127
+ reverse_proxy 127.0.0.1:${entry.port}
14128
+ tls ${join7(getDataDir(), "certs", `${entry.domain}.pem`)} ${join7(getDataDir(), "certs", `${entry.domain}-key.pem`)}
14129
+ }`,
14130
+ certPath: join7(getDataDir(), "certs", `${entry.domain}.pem`),
14131
+ keyPath: join7(getDataDir(), "certs", `${entry.domain}-key.pem`)
14132
+ };
14009
14133
  }
14010
- var KNOWN_PG_SERVICES;
14011
- var SYNC_EXCLUDED_TABLE_PATTERNS;
14012
- var init_discover = __esm(() => {
14013
- KNOWN_PG_SERVICES = [
14014
- "assistants",
14015
- "attachments",
14016
- "brains",
14017
- "configs",
14018
- "connectors",
14019
- "contacts",
14020
- "context",
14021
- "conversations",
14022
- "crawl",
14023
- "deployment",
14024
- "economy",
14025
- "emails",
14026
- "files",
14027
- "hooks",
14028
- "implementations",
14029
- "logs",
14030
- "mcps",
14031
- "mementos",
14032
- "microservices",
14033
- "predictor",
14034
- "prompts",
14035
- "recordings",
14036
- "researcher",
14037
- "sandboxes",
14038
- "search",
14039
- "secrets",
14040
- "sessions",
14041
- "signatures",
14042
- "skills",
14043
- "telephony",
14044
- "terminal",
14045
- "testers",
14046
- "tickets",
14047
- "todos",
14048
- "wallets"
14049
- ];
14050
- SYNC_EXCLUDED_TABLE_PATTERNS = [
14051
- /^sqlite_/,
14052
- /_fts$/,
14053
- /_fts_/,
14054
- /^_sync_/,
14055
- /^_pg_migrations$/
14056
- ];
14057
- });
14058
- init_adapter();
14059
- init_config();
14060
- init_config();
14061
- init_dotfile();
14062
14134
 
14063
- class SyncProgressTracker {
14064
- db;
14065
- progress = new Map;
14066
- startTimes = new Map;
14067
- callback;
14068
- constructor(db, callback) {
14069
- this.db = db;
14070
- this.callback = callback;
14071
- this.ensureResumeTable();
14135
+ // src/commands/diff.ts
14136
+ function packageNames(machine) {
14137
+ return (machine.packages || []).map((pkg) => pkg.name).sort();
14138
+ }
14139
+ function fileTargets(machine) {
14140
+ return (machine.files || []).map((file) => `${file.source}->${file.target}`).sort();
14141
+ }
14142
+ function diffMachines(leftMachineId, rightMachineId) {
14143
+ const left = getManifestMachine(leftMachineId);
14144
+ if (!left) {
14145
+ throw new Error(`Machine not found in manifest: ${leftMachineId}`);
14072
14146
  }
14073
- ensureResumeTable() {
14074
- this.db.exec(`
14075
- CREATE TABLE IF NOT EXISTS _sync_resume (
14076
- table_name TEXT PRIMARY KEY,
14077
- last_row_id TEXT,
14078
- direction TEXT,
14079
- started_at TEXT,
14080
- status TEXT DEFAULT 'in_progress'
14081
- )
14082
- `);
14147
+ const right = rightMachineId ? getManifestMachine(rightMachineId) : detectCurrentMachineManifest();
14148
+ if (!right) {
14149
+ throw new Error(`Machine not found in manifest: ${rightMachineId}`);
14083
14150
  }
14084
- start(table, total, direction) {
14085
- const resumed = this.canResume(table);
14086
- const now = Date.now();
14087
- this.startTimes.set(table, now);
14088
- const status = resumed ? "resumed" : "in_progress";
14089
- const info = {
14090
- table,
14091
- total,
14092
- done: 0,
14093
- percent: 0,
14094
- elapsed_ms: 0,
14095
- eta_ms: 0,
14096
- status
14097
- };
14098
- this.progress.set(table, info);
14099
- this.db.run(`INSERT INTO _sync_resume (table_name, last_row_id, direction, started_at, status)
14100
- VALUES (?, ?, ?, datetime('now'), ?)
14101
- ON CONFLICT (table_name) DO UPDATE SET
14102
- direction = excluded.direction,
14103
- started_at = datetime('now'),
14104
- status = excluded.status`, table, "", direction, status);
14105
- this.notify(table);
14151
+ const changedFields = [
14152
+ left.platform !== right.platform ? "platform" : null,
14153
+ left.connection !== right.connection ? "connection" : null,
14154
+ left.workspacePath !== right.workspacePath ? "workspacePath" : null,
14155
+ left.bunPath !== right.bunPath ? "bunPath" : null
14156
+ ].filter(Boolean);
14157
+ const leftPackages = new Set(packageNames(left));
14158
+ const rightPackages = new Set(packageNames(right));
14159
+ const leftFiles = new Set(fileTargets(left));
14160
+ const rightFiles = new Set(fileTargets(right));
14161
+ return {
14162
+ leftMachineId: left.id,
14163
+ rightMachineId: right.id,
14164
+ changedFields,
14165
+ missingPackages: {
14166
+ leftOnly: [...leftPackages].filter((pkg) => !rightPackages.has(pkg)),
14167
+ rightOnly: [...rightPackages].filter((pkg) => !leftPackages.has(pkg))
14168
+ },
14169
+ missingFiles: {
14170
+ leftOnly: [...leftFiles].filter((file) => !rightFiles.has(file)),
14171
+ rightOnly: [...rightFiles].filter((file) => !leftFiles.has(file))
14172
+ }
14173
+ };
14174
+ }
14175
+
14176
+ // src/commands/doctor.ts
14177
+ function makeCheck(id, status, summary, detail) {
14178
+ return { id, status, summary, detail };
14179
+ }
14180
+ function parseKeyValueOutput(stdout) {
14181
+ return Object.fromEntries(stdout.trim().split(`
14182
+ `).map((line) => line.split("=")).filter((parts) => parts.length === 2).map(([key, value]) => [key, value]));
14183
+ }
14184
+ function buildDoctorCommand() {
14185
+ return [
14186
+ 'data_dir="${HASNA_MACHINES_DIR:-$HOME/.hasna/machines}"',
14187
+ 'manifest_path="${HASNA_MACHINES_MANIFEST_PATH:-$data_dir/machines.json}"',
14188
+ 'db_path="${HASNA_MACHINES_DB_PATH:-$data_dir/machines.db}"',
14189
+ 'notifications_path="${HASNA_MACHINES_NOTIFICATIONS_PATH:-$data_dir/notifications.json}"',
14190
+ `printf 'manifest_path=%s\\n' "$manifest_path"`,
14191
+ `printf 'db_path=%s\\n' "$db_path"`,
14192
+ `printf 'notifications_path=%s\\n' "$notifications_path"`,
14193
+ `printf 'manifest_exists=%s\\n' "$(test -e "$manifest_path" && printf yes || printf no)"`,
14194
+ `printf 'db_exists=%s\\n' "$(test -e "$db_path" && printf yes || printf no)"`,
14195
+ `printf 'notifications_exists=%s\\n' "$(test -e "$notifications_path" && printf yes || printf no)"`,
14196
+ `printf 'bun=%s\\n' "$(bun --version 2>/dev/null || printf missing)"`,
14197
+ `printf 'ssh=%s\\n' "$(command -v ssh >/dev/null 2>&1 && printf ok || printf missing)"`,
14198
+ `printf 'machines=%s\\n' "$(command -v machines 2>/dev/null || printf missing)"`,
14199
+ `printf 'machines_agent=%s\\n' "$(command -v machines-agent 2>/dev/null || printf missing)"`,
14200
+ `printf 'machines_mcp=%s\\n' "$(command -v machines-mcp 2>/dev/null || printf missing)"`
14201
+ ].join("; ");
14202
+ }
14203
+ function runDoctor(machineId = getLocalMachineId()) {
14204
+ const manifest = readManifest();
14205
+ const commandChecks = runMachineCommand(machineId, buildDoctorCommand());
14206
+ const details = parseKeyValueOutput(commandChecks.stdout);
14207
+ const machineInManifest = manifest.machines.find((machine) => machine.id === machineId);
14208
+ const checks = [
14209
+ makeCheck("manifest-entry", machineInManifest ? "ok" : "warn", machineInManifest ? "Machine exists in manifest" : "Machine missing from manifest", machineInManifest ? JSON.stringify(machineInManifest) : `No manifest entry for ${machineId}`),
14210
+ makeCheck("manifest-path", details["manifest_exists"] === "yes" ? "ok" : "warn", "Manifest path check", `${details["manifest_path"] || "unknown"} ${details["manifest_exists"] === "yes" ? "exists" : "missing"}`),
14211
+ makeCheck("db-path", details["db_exists"] === "yes" ? "ok" : "warn", "DB path check", `${details["db_path"] || "unknown"} ${details["db_exists"] === "yes" ? "exists" : "missing"}`),
14212
+ makeCheck("notifications-path", details["notifications_exists"] === "yes" ? "ok" : "warn", "Notifications path check", `${details["notifications_path"] || "unknown"} ${details["notifications_exists"] === "yes" ? "exists" : "missing"}`),
14213
+ makeCheck("bun", details["bun"] && details["bun"] !== "missing" ? "ok" : "fail", "Bun availability", details["bun"] || "missing"),
14214
+ makeCheck("machines-cli", details["machines"] && details["machines"] !== "missing" ? "ok" : "warn", "machines CLI availability", details["machines"] || "missing"),
14215
+ makeCheck("machines-agent-cli", details["machines_agent"] && details["machines_agent"] !== "missing" ? "ok" : "warn", "machines-agent availability", details["machines_agent"] || "missing"),
14216
+ makeCheck("machines-mcp-cli", details["machines_mcp"] && details["machines_mcp"] !== "missing" ? "ok" : "warn", "machines-mcp availability", details["machines_mcp"] || "missing"),
14217
+ makeCheck("ssh", details["ssh"] === "ok" ? "ok" : "warn", "SSH availability", details["ssh"] || "missing")
14218
+ ];
14219
+ return {
14220
+ machineId,
14221
+ source: commandChecks.source,
14222
+ manifestPath: details["manifest_path"],
14223
+ dbPath: details["db_path"],
14224
+ notificationsPath: details["notifications_path"],
14225
+ checks
14226
+ };
14227
+ }
14228
+
14229
+ // src/commands/install-claude.ts
14230
+ var AI_CLI_PACKAGES = {
14231
+ claude: "@anthropic-ai/claude-code",
14232
+ codex: "@openai/codex",
14233
+ gemini: "@google/gemini-cli"
14234
+ };
14235
+ function getToolBinary(tool) {
14236
+ if (tool === "claude")
14237
+ return process.env["HASNA_MACHINES_CLAUDE_BINARY"] || "claude";
14238
+ if (tool === "codex")
14239
+ return process.env["HASNA_MACHINES_CODEX_BINARY"] || "codex";
14240
+ return process.env["HASNA_MACHINES_GEMINI_BINARY"] || "gemini";
14241
+ }
14242
+ function normalizeTools(tools) {
14243
+ if (!tools || tools.length === 0) {
14244
+ return ["claude", "codex", "gemini"];
14106
14245
  }
14107
- update(table, done, lastRowId) {
14108
- const info = this.progress.get(table);
14109
- if (!info)
14110
- return;
14111
- const startTime = this.startTimes.get(table) ?? Date.now();
14112
- const elapsed = Date.now() - startTime;
14113
- const rate = done > 0 ? elapsed / done : 0;
14114
- const remaining = info.total - done;
14115
- const eta = remaining > 0 ? Math.round(rate * remaining) : 0;
14116
- info.done = done;
14117
- info.percent = info.total > 0 ? Math.round(done / info.total * 100) : 0;
14118
- info.elapsed_ms = elapsed;
14119
- info.eta_ms = eta;
14120
- info.status = "in_progress";
14121
- this.db.run(`UPDATE _sync_resume SET last_row_id = ?, status = 'in_progress' WHERE table_name = ?`, lastRowId, table);
14122
- this.notify(table);
14246
+ return [...new Set(tools)].map((tool) => {
14247
+ if (!(tool in AI_CLI_PACKAGES)) {
14248
+ throw new Error(`Unsupported AI CLI tool: ${tool}`);
14249
+ }
14250
+ return tool;
14251
+ });
14252
+ }
14253
+ function buildInstallSteps(machine, tools) {
14254
+ return normalizeTools(tools).map((tool) => ({
14255
+ id: `install-${tool}`,
14256
+ title: `Install or update ${tool} CLI on ${machine.id}`,
14257
+ command: `bun install -g ${AI_CLI_PACKAGES[tool]}`,
14258
+ manager: "bun"
14259
+ }));
14260
+ }
14261
+ function resolveMachine2(machineId) {
14262
+ return (machineId ? getManifestMachine(machineId) : null) || detectCurrentMachineManifest();
14263
+ }
14264
+ function buildProbeCommand(tool) {
14265
+ const binary = getToolBinary(tool);
14266
+ return `if command -v ${binary} >/dev/null 2>&1; then printf 'installed=1\\nversion='; ${binary} --version 2>/dev/null | head -n 1; printf '\\n'; else printf 'installed=0\\n'; fi`;
14267
+ }
14268
+ function parseProbe(tool, stdout) {
14269
+ const lines = stdout.trim().split(`
14270
+ `).filter(Boolean);
14271
+ const installedLine = lines.find((line) => line.startsWith("installed="));
14272
+ const versionLine = lines.find((line) => line.startsWith("version="));
14273
+ return {
14274
+ tool,
14275
+ packageName: AI_CLI_PACKAGES[tool],
14276
+ installed: installedLine === "installed=1",
14277
+ version: versionLine?.slice("version=".length) || undefined
14278
+ };
14279
+ }
14280
+ function buildClaudeInstallPlan(machineId, tools) {
14281
+ const machine = resolveMachine2(machineId);
14282
+ return {
14283
+ machineId: machine.id,
14284
+ mode: "plan",
14285
+ steps: buildInstallSteps(machine, tools),
14286
+ executed: 0
14287
+ };
14288
+ }
14289
+ function getClaudeCliStatus(machineId, tools) {
14290
+ const machine = resolveMachine2(machineId);
14291
+ const normalizedTools = normalizeTools(tools);
14292
+ const route = runMachineCommand(machine.id, "true").source;
14293
+ return {
14294
+ machineId: machine.id,
14295
+ source: route,
14296
+ tools: normalizedTools.map((tool) => parseProbe(tool, runMachineCommand(machine.id, buildProbeCommand(tool)).stdout))
14297
+ };
14298
+ }
14299
+ function diffClaudeCli(machineId, tools) {
14300
+ const status = getClaudeCliStatus(machineId, tools);
14301
+ return {
14302
+ ...status,
14303
+ missing: status.tools.filter((tool) => !tool.installed).map((tool) => tool.tool),
14304
+ installed: status.tools.filter((tool) => tool.installed).map((tool) => tool.tool)
14305
+ };
14306
+ }
14307
+ function runClaudeInstall(machineId, tools, options = {}) {
14308
+ const plan = buildClaudeInstallPlan(machineId, tools);
14309
+ if (!options.apply)
14310
+ return plan;
14311
+ if (!options.yes) {
14312
+ throw new Error("Claude CLI installation requires --yes.");
14123
14313
  }
14124
- markComplete(table) {
14125
- const info = this.progress.get(table);
14126
- if (info) {
14127
- const startTime = this.startTimes.get(table) ?? Date.now();
14128
- info.elapsed_ms = Date.now() - startTime;
14129
- info.done = info.total;
14130
- info.percent = 100;
14131
- info.eta_ms = 0;
14132
- info.status = "completed";
14133
- this.notify(table);
14314
+ let executed = 0;
14315
+ for (const step of plan.steps) {
14316
+ const result = Bun.spawnSync(["bash", "-lc", step.command], {
14317
+ stdout: "pipe",
14318
+ stderr: "pipe",
14319
+ env: process.env
14320
+ });
14321
+ if (result.exitCode !== 0) {
14322
+ throw new Error(`AI CLI install failed (${step.id}): ${result.stderr.toString().trim()}`);
14134
14323
  }
14135
- this.db.run(`UPDATE _sync_resume SET status = 'completed' WHERE table_name = ?`, table);
14324
+ executed += 1;
14136
14325
  }
14137
- markFailed(table, _error) {
14138
- const info = this.progress.get(table);
14139
- if (info) {
14140
- const startTime = this.startTimes.get(table) ?? Date.now();
14141
- info.elapsed_ms = Date.now() - startTime;
14142
- info.status = "failed";
14143
- this.notify(table);
14144
- }
14145
- this.db.run(`UPDATE _sync_resume SET status = 'failed' WHERE table_name = ?`, table);
14326
+ return {
14327
+ machineId: plan.machineId,
14328
+ mode: "apply",
14329
+ steps: plan.steps,
14330
+ executed
14331
+ };
14332
+ }
14333
+
14334
+ // src/commands/install-tailscale.ts
14335
+ function buildInstallSteps2(machine) {
14336
+ if (machine.platform === "macos") {
14337
+ return [
14338
+ {
14339
+ id: "tailscale-brew",
14340
+ title: "Install Tailscale via Homebrew",
14341
+ command: "brew install --cask tailscale",
14342
+ manager: "brew"
14343
+ }
14344
+ ];
14146
14345
  }
14147
- canResume(table) {
14148
- const row = this.db.get(`SELECT status FROM _sync_resume WHERE table_name = ?`, table);
14149
- if (!row)
14150
- return false;
14151
- return row.status === "in_progress" || row.status === "resumed";
14346
+ if (machine.platform === "windows") {
14347
+ return [
14348
+ {
14349
+ id: "tailscale-winget",
14350
+ title: "Install Tailscale via winget",
14351
+ command: "winget install Tailscale.Tailscale",
14352
+ manager: "custom"
14353
+ }
14354
+ ];
14152
14355
  }
14153
- getResumePoint(table) {
14154
- const row = this.db.get(`SELECT table_name, last_row_id, direction, started_at, status FROM _sync_resume WHERE table_name = ?`, table);
14155
- if (!row)
14156
- return null;
14157
- if (row.status !== "in_progress" && row.status !== "resumed")
14158
- return null;
14159
- return row;
14356
+ return [
14357
+ {
14358
+ id: "tailscale-linux",
14359
+ title: "Install Tailscale on Linux",
14360
+ command: "curl -fsSL https://tailscale.com/install.sh | sh",
14361
+ manager: "custom",
14362
+ privileged: true
14363
+ }
14364
+ ];
14365
+ }
14366
+ function buildTailscaleInstallPlan(machineId) {
14367
+ const machine = (machineId ? getManifestMachine(machineId) : null) || detectCurrentMachineManifest();
14368
+ return {
14369
+ machineId: machine.id,
14370
+ mode: "plan",
14371
+ steps: buildInstallSteps2(machine),
14372
+ executed: 0
14373
+ };
14374
+ }
14375
+ function runTailscaleInstall(machineId, options = {}) {
14376
+ const plan = buildTailscaleInstallPlan(machineId);
14377
+ if (!options.apply)
14378
+ return plan;
14379
+ if (!options.yes) {
14380
+ throw new Error("Tailscale install requires --yes.");
14160
14381
  }
14161
- clearResume(table) {
14162
- this.db.run(`DELETE FROM _sync_resume WHERE table_name = ?`, table);
14163
- this.progress.delete(table);
14164
- this.startTimes.delete(table);
14382
+ let executed = 0;
14383
+ for (const step of plan.steps) {
14384
+ const result = Bun.spawnSync(["bash", "-lc", step.command], {
14385
+ stdout: "pipe",
14386
+ stderr: "pipe",
14387
+ env: process.env
14388
+ });
14389
+ if (result.exitCode !== 0) {
14390
+ throw new Error(`Tailscale install failed (${step.id}): ${result.stderr.toString().trim()}`);
14391
+ }
14392
+ executed += 1;
14165
14393
  }
14166
- getProgress(table) {
14167
- return this.progress.get(table) ?? null;
14394
+ return {
14395
+ machineId: plan.machineId,
14396
+ mode: "apply",
14397
+ steps: plan.steps,
14398
+ executed
14399
+ };
14400
+ }
14401
+
14402
+ // src/commands/notifications.ts
14403
+ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
14404
+ var notificationChannelSchema = exports_external.object({
14405
+ id: exports_external.string(),
14406
+ type: exports_external.enum(["email", "webhook", "command"]),
14407
+ target: exports_external.string(),
14408
+ events: exports_external.array(exports_external.string()),
14409
+ enabled: exports_external.boolean()
14410
+ });
14411
+ var notificationConfigSchema = exports_external.object({
14412
+ version: exports_external.literal(1),
14413
+ updatedAt: exports_external.string().optional(),
14414
+ channels: exports_external.array(notificationChannelSchema)
14415
+ });
14416
+ function sortChannels(channels) {
14417
+ return [...channels].sort((left, right) => left.id.localeCompare(right.id));
14418
+ }
14419
+ function shellQuote2(value) {
14420
+ return `'${value.replace(/'/g, `'\\''`)}'`;
14421
+ }
14422
+ function hasCommand(binary) {
14423
+ const result = Bun.spawnSync(["bash", "-lc", `command -v ${binary} >/dev/null 2>&1`], {
14424
+ stdout: "ignore",
14425
+ stderr: "ignore",
14426
+ env: process.env
14427
+ });
14428
+ return result.exitCode === 0;
14429
+ }
14430
+ function buildNotificationPreview(channel, event, message) {
14431
+ if (channel.type === "email") {
14432
+ return `send email to ${channel.target}: [${event}] ${message}`;
14168
14433
  }
14169
- getAllProgress() {
14170
- return Array.from(this.progress.values());
14434
+ if (channel.type === "webhook") {
14435
+ return `POST ${channel.target} with payload {"event":"${event}","message":"${message}"}`;
14171
14436
  }
14172
- listResumeRecords() {
14173
- return this.db.all(`SELECT table_name, last_row_id, direction, started_at, status FROM _sync_resume ORDER BY started_at DESC`);
14437
+ return `${channel.target} --event ${event} --message ${JSON.stringify(message)}`;
14438
+ }
14439
+ async function dispatchEmail(channel, event, message) {
14440
+ const subject = `[${event}] machines notification`;
14441
+ const body = `To: ${channel.target}
14442
+ Subject: ${subject}
14443
+ Content-Type: text/plain; charset=utf-8
14444
+
14445
+ ${message}
14446
+ `;
14447
+ if (hasCommand("sendmail")) {
14448
+ const result = Bun.spawnSync(["bash", "-lc", "sendmail -t"], {
14449
+ stdin: new TextEncoder().encode(body),
14450
+ stdout: "pipe",
14451
+ stderr: "pipe",
14452
+ env: process.env
14453
+ });
14454
+ if (result.exitCode !== 0) {
14455
+ throw new Error(result.stderr.toString().trim() || `sendmail exited with ${result.exitCode}`);
14456
+ }
14457
+ return {
14458
+ channelId: channel.id,
14459
+ event,
14460
+ delivered: true,
14461
+ transport: channel.type,
14462
+ detail: `Delivered via sendmail to ${channel.target}`
14463
+ };
14174
14464
  }
14175
- notify(table) {
14176
- const info = this.progress.get(table);
14177
- if (info && this.callback) {
14178
- this.callback({ ...info });
14465
+ if (hasCommand("mail")) {
14466
+ const command = `printf %s ${shellQuote2(message)} | mail -s ${shellQuote2(subject)} ${shellQuote2(channel.target)}`;
14467
+ const result = Bun.spawnSync(["bash", "-lc", command], {
14468
+ stdout: "pipe",
14469
+ stderr: "pipe",
14470
+ env: process.env
14471
+ });
14472
+ if (result.exitCode !== 0) {
14473
+ throw new Error(result.stderr.toString().trim() || `mail exited with ${result.exitCode}`);
14179
14474
  }
14475
+ return {
14476
+ channelId: channel.id,
14477
+ event,
14478
+ delivered: true,
14479
+ transport: channel.type,
14480
+ detail: `Delivered via mail to ${channel.target}`
14481
+ };
14180
14482
  }
14483
+ throw new Error("No local email transport available. Install sendmail or mail.");
14181
14484
  }
14182
- init_adapter();
14183
- init_config();
14184
- init_discover();
14185
- var AUTO_SYNC_CONFIG_PATH = join42(homedir42(), ".hasna", "cloud", "config.json");
14186
- init_config();
14187
- init_adapter();
14188
- init_dotfile();
14189
- init_config();
14190
- var CONFIG_DIR2 = join6(homedir5(), ".hasna", "cloud");
14191
- init_adapter();
14192
- init_config();
14193
- init_discover();
14194
- init_zod();
14195
- init_config();
14196
- init_dotfile();
14197
- init_adapter();
14198
- init_config();
14199
- init_dotfile();
14200
- init_adapter();
14201
-
14202
- // src/db.ts
14203
- var adapter = null;
14204
- function createTables(db) {
14205
- db.exec(`
14206
- CREATE TABLE IF NOT EXISTS agent_heartbeats (
14207
- machine_id TEXT NOT NULL,
14208
- pid INTEGER NOT NULL,
14209
- status TEXT NOT NULL,
14210
- updated_at TEXT NOT NULL,
14211
- PRIMARY KEY (machine_id, pid)
14212
- )
14213
- `);
14214
- db.exec(`
14215
- CREATE TABLE IF NOT EXISTS setup_runs (
14216
- id TEXT PRIMARY KEY,
14217
- machine_id TEXT NOT NULL,
14218
- status TEXT NOT NULL,
14219
- details_json TEXT NOT NULL DEFAULT '[]',
14220
- created_at TEXT NOT NULL,
14221
- updated_at TEXT NOT NULL
14222
- )
14223
- `);
14224
- db.exec(`
14225
- CREATE TABLE IF NOT EXISTS sync_runs (
14226
- id TEXT PRIMARY KEY,
14227
- machine_id TEXT NOT NULL,
14228
- status TEXT NOT NULL,
14229
- actions_json TEXT NOT NULL DEFAULT '[]',
14230
- created_at TEXT NOT NULL,
14231
- updated_at TEXT NOT NULL
14232
- )
14233
- `);
14234
- }
14235
- function getAdapter(path = getDbPath()) {
14236
- if (path === ":memory:") {
14237
- const memoryAdapter = new SqliteAdapter(path);
14238
- createTables(memoryAdapter.raw);
14239
- return memoryAdapter;
14240
- }
14241
- if (!adapter) {
14242
- ensureParentDir(path);
14243
- adapter = new SqliteAdapter(path);
14244
- createTables(adapter.raw);
14485
+ async function dispatchWebhook(channel, event, message) {
14486
+ const response = await fetch(channel.target, {
14487
+ method: "POST",
14488
+ headers: {
14489
+ "content-type": "application/json"
14490
+ },
14491
+ body: JSON.stringify({
14492
+ channelId: channel.id,
14493
+ event,
14494
+ message,
14495
+ sentAt: new Date().toISOString()
14496
+ })
14497
+ });
14498
+ if (!response.ok) {
14499
+ const text = await response.text();
14500
+ throw new Error(`Webhook responded ${response.status}: ${text || response.statusText}`);
14245
14501
  }
14246
- return adapter;
14502
+ return {
14503
+ channelId: channel.id,
14504
+ event,
14505
+ delivered: true,
14506
+ transport: channel.type,
14507
+ detail: `Webhook accepted with HTTP ${response.status}`
14508
+ };
14509
+ }
14510
+ async function dispatchCommand(channel, event, message) {
14511
+ const result = Bun.spawnSync(["bash", "-lc", channel.target], {
14512
+ stdout: "pipe",
14513
+ stderr: "pipe",
14514
+ env: {
14515
+ ...process.env,
14516
+ HASNA_MACHINES_NOTIFICATION_CHANNEL: channel.id,
14517
+ HASNA_MACHINES_NOTIFICATION_EVENT: event,
14518
+ HASNA_MACHINES_NOTIFICATION_MESSAGE: message
14519
+ }
14520
+ });
14521
+ if (result.exitCode !== 0) {
14522
+ throw new Error(result.stderr.toString().trim() || `command exited with ${result.exitCode}`);
14523
+ }
14524
+ const stdout = result.stdout.toString().trim();
14525
+ return {
14526
+ channelId: channel.id,
14527
+ event,
14528
+ delivered: true,
14529
+ transport: channel.type,
14530
+ detail: stdout || "Command completed successfully"
14531
+ };
14247
14532
  }
14248
- function getDb(path = getDbPath()) {
14249
- return getAdapter(path).raw;
14533
+ async function dispatchChannel(channel, event, message) {
14534
+ if (!channel.enabled) {
14535
+ return {
14536
+ channelId: channel.id,
14537
+ event,
14538
+ delivered: false,
14539
+ transport: channel.type,
14540
+ detail: "Channel is disabled"
14541
+ };
14542
+ }
14543
+ if (channel.type === "email") {
14544
+ return dispatchEmail(channel, event, message);
14545
+ }
14546
+ if (channel.type === "webhook") {
14547
+ return dispatchWebhook(channel, event, message);
14548
+ }
14549
+ return dispatchCommand(channel, event, message);
14250
14550
  }
14251
- function getLocalMachineId() {
14252
- return process.env["HASNA_MACHINES_MACHINE_ID"] || hostname2();
14551
+ function getDefaultNotificationConfig() {
14552
+ return {
14553
+ version: 1,
14554
+ updatedAt: new Date().toISOString(),
14555
+ channels: []
14556
+ };
14253
14557
  }
14254
- function listHeartbeats(machineId) {
14255
- const db = getDb();
14256
- if (machineId) {
14257
- return db.query(`SELECT machine_id, pid, status, updated_at
14258
- FROM agent_heartbeats
14259
- WHERE machine_id = ?
14260
- ORDER BY updated_at DESC`).all(machineId);
14558
+ function readNotificationConfig(path = getNotificationsPath()) {
14559
+ if (!existsSync5(path)) {
14560
+ return getDefaultNotificationConfig();
14261
14561
  }
14262
- return db.query(`SELECT machine_id, pid, status, updated_at
14263
- FROM agent_heartbeats
14264
- ORDER BY updated_at DESC`).all();
14562
+ return notificationConfigSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
14265
14563
  }
14266
- function countRuns(table) {
14267
- const db = getDb();
14268
- const row = db.query(`SELECT COUNT(*) as count FROM ${table}`).get();
14269
- return row.count;
14564
+ function writeNotificationConfig(config, path = getNotificationsPath()) {
14565
+ ensureParentDir(path);
14566
+ const nextConfig = {
14567
+ version: 1,
14568
+ updatedAt: new Date().toISOString(),
14569
+ channels: sortChannels(config.channels)
14570
+ };
14571
+ writeFileSync4(path, `${JSON.stringify(nextConfig, null, 2)}
14572
+ `, "utf8");
14573
+ return nextConfig;
14270
14574
  }
14271
- function recordSetupRun(machineId, status, details) {
14272
- const db = getDb();
14273
- const now = new Date().toISOString();
14274
- db.query(`INSERT INTO setup_runs (id, machine_id, status, details_json, created_at, updated_at)
14275
- VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(details), now, now);
14575
+ function listNotificationChannels() {
14576
+ return readNotificationConfig();
14276
14577
  }
14277
- function recordSyncRun(machineId, status, actions) {
14278
- const db = getDb();
14279
- const now = new Date().toISOString();
14280
- db.query(`INSERT INTO sync_runs (id, machine_id, status, actions_json, created_at, updated_at)
14281
- VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(actions), now, now);
14578
+ function addNotificationChannel(channel) {
14579
+ const config = readNotificationConfig();
14580
+ const channels = config.channels.filter((entry) => entry.id !== channel.id);
14581
+ channels.push({
14582
+ ...channel,
14583
+ events: [...new Set(channel.events)]
14584
+ });
14585
+ return writeNotificationConfig({ ...config, channels });
14282
14586
  }
14283
-
14284
- // src/commands/ssh.ts
14285
- import { spawnSync } from "child_process";
14286
- function envReachableHosts() {
14287
- const raw = process.env["HASNA_MACHINES_REACHABLE_HOSTS"];
14288
- return new Set((raw || "").split(",").map((value) => value.trim()).filter(Boolean));
14587
+ function removeNotificationChannel(channelId) {
14588
+ const config = readNotificationConfig();
14589
+ return writeNotificationConfig({
14590
+ ...config,
14591
+ channels: config.channels.filter((channel) => channel.id !== channelId)
14592
+ });
14289
14593
  }
14290
- function isReachable(host) {
14291
- const overrides = envReachableHosts();
14292
- if (overrides.size > 0) {
14293
- return overrides.has(host);
14294
- }
14295
- const probe = spawnSync("bash", ["-lc", `getent hosts ${host} >/dev/null 2>&1 || ping -c 1 -W 1 ${host} >/dev/null 2>&1`], {
14296
- stdio: "ignore"
14594
+ async function dispatchNotificationEvent(event, message, options = {}) {
14595
+ const channels = readNotificationConfig().channels.filter((channel) => {
14596
+ if (options.channelId && channel.id !== options.channelId) {
14597
+ return false;
14598
+ }
14599
+ return channel.events.includes(event) || event === "manual.test";
14297
14600
  });
14298
- return probe.status === 0;
14601
+ const deliveries = [];
14602
+ for (const channel of channels) {
14603
+ try {
14604
+ deliveries.push(await dispatchChannel(channel, event, message));
14605
+ } catch (error) {
14606
+ deliveries.push({
14607
+ channelId: channel.id,
14608
+ event,
14609
+ delivered: false,
14610
+ transport: channel.type,
14611
+ detail: error instanceof Error ? error.message : String(error)
14612
+ });
14613
+ }
14614
+ }
14615
+ return {
14616
+ event,
14617
+ message,
14618
+ deliveries
14619
+ };
14299
14620
  }
14300
- function resolveSshTarget(machineId) {
14301
- const machine = getManifestMachine(machineId);
14302
- if (!machine) {
14303
- throw new Error(`Machine not found in manifest: ${machineId}`);
14621
+ async function testNotificationChannel(channelId, event = "manual.test", message = "machines notification test", options = {}) {
14622
+ const channel = readNotificationConfig().channels.find((entry) => entry.id === channelId);
14623
+ if (!channel) {
14624
+ throw new Error(`Notification channel not found: ${channelId}`);
14304
14625
  }
14305
- const current = detectCurrentMachineManifest();
14306
- if (machine.id === current.id) {
14626
+ const preview = buildNotificationPreview(channel, event, message);
14627
+ if (!options.apply) {
14307
14628
  return {
14308
- machineId,
14309
- target: "localhost",
14310
- route: "local"
14629
+ channelId,
14630
+ mode: "plan",
14631
+ delivered: false,
14632
+ preview,
14633
+ detail: "Preview only"
14311
14634
  };
14312
14635
  }
14313
- const lanTarget = machine.sshAddress || machine.hostname || machine.id;
14314
- const tailscaleTarget = machine.tailscaleName || machine.hostname || machine.id;
14315
- const route = isReachable(lanTarget) ? "lan" : "tailscale";
14636
+ if (!options.yes) {
14637
+ throw new Error("Notification test execution requires --yes.");
14638
+ }
14639
+ const delivery = await dispatchChannel(channel, event, message);
14316
14640
  return {
14317
- machineId,
14318
- target: route === "lan" ? lanTarget : tailscaleTarget,
14319
- route
14641
+ channelId,
14642
+ mode: "apply",
14643
+ delivered: delivery.delivered,
14644
+ preview,
14645
+ detail: delivery.detail
14320
14646
  };
14321
14647
  }
14322
- function buildSshCommand(machineId, remoteCommand) {
14323
- const resolved = resolveSshTarget(machineId);
14324
- return remoteCommand ? `ssh ${resolved.target} ${JSON.stringify(remoteCommand)}` : `ssh ${resolved.target}`;
14325
- }
14326
14648
 
14327
14649
  // src/commands/ports.ts
14650
+ import { spawnSync as spawnSync3 } from "child_process";
14328
14651
  function parseSsOutput(output) {
14329
14652
  return output.trim().split(`
14330
14653
  `).map((line) => line.trim()).filter(Boolean).map((line) => {
@@ -14366,7 +14689,7 @@ function listPorts(machineId) {
14366
14689
  const isLocal = targetMachineId === getLocalMachineId();
14367
14690
  const localCommand = "if command -v ss >/dev/null 2>&1; then ss -ltnpH; else lsof -nP -iTCP -sTCP:LISTEN; fi";
14368
14691
  const command = isLocal ? localCommand : buildSshCommand(targetMachineId, localCommand);
14369
- const result = spawnSync2("bash", ["-lc", command], { encoding: "utf8" });
14692
+ const result = spawnSync3("bash", ["-lc", command], { encoding: "utf8" });
14370
14693
  if (result.status !== 0) {
14371
14694
  throw new Error(result.stderr || `Failed to list ports for ${targetMachineId}`);
14372
14695
  }
@@ -14377,6 +14700,55 @@ function listPorts(machineId) {
14377
14700
  };
14378
14701
  }
14379
14702
 
14703
+ // src/commands/manifest.ts
14704
+ function manifestList() {
14705
+ return readManifest();
14706
+ }
14707
+ function manifestAdd(machine) {
14708
+ const manifest = readManifest();
14709
+ const nextMachines = manifest.machines.filter((entry) => entry.id !== machine.id);
14710
+ nextMachines.push(machine);
14711
+ const nextManifest = { ...manifest, machines: nextMachines };
14712
+ writeManifest(nextManifest);
14713
+ return nextManifest;
14714
+ }
14715
+ function manifestBootstrapCurrentMachine() {
14716
+ return manifestAdd(detectCurrentMachineManifest());
14717
+ }
14718
+ function manifestGet(machineId) {
14719
+ return getManifestMachine(machineId);
14720
+ }
14721
+ function manifestRemove(machineId) {
14722
+ const manifest = readManifest();
14723
+ const nextManifest = {
14724
+ ...manifest,
14725
+ machines: manifest.machines.filter((machine) => machine.id !== machineId)
14726
+ };
14727
+ writeManifest(nextManifest);
14728
+ return nextManifest;
14729
+ }
14730
+ function manifestValidate() {
14731
+ return validateManifest(getManifestPath());
14732
+ }
14733
+
14734
+ // src/version.ts
14735
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
14736
+ import { dirname as dirname4, join as join8 } from "path";
14737
+ import { fileURLToPath } from "url";
14738
+ function getPackageVersion() {
14739
+ try {
14740
+ const here = dirname4(fileURLToPath(import.meta.url));
14741
+ const candidates = [join8(here, "..", "package.json"), join8(here, "..", "..", "package.json")];
14742
+ const pkgPath = candidates.find((candidate) => existsSync6(candidate));
14743
+ if (!pkgPath) {
14744
+ return "0.0.0";
14745
+ }
14746
+ return JSON.parse(readFileSync5(pkgPath, "utf8")).version || "0.0.0";
14747
+ } catch {
14748
+ return "0.0.0";
14749
+ }
14750
+ }
14751
+
14380
14752
  // src/commands/status.ts
14381
14753
  function getStatus() {
14382
14754
  const manifest = readManifest();
@@ -14409,35 +14781,34 @@ function getStatus() {
14409
14781
  };
14410
14782
  }
14411
14783
 
14412
- // src/commands/manifest.ts
14413
- function manifestList() {
14414
- return readManifest();
14415
- }
14416
- function manifestAdd(machine) {
14417
- const manifest = readManifest();
14418
- const nextMachines = manifest.machines.filter((entry) => entry.id !== machine.id);
14419
- nextMachines.push(machine);
14420
- const nextManifest = { ...manifest, machines: nextMachines };
14421
- writeManifest(nextManifest);
14422
- return nextManifest;
14423
- }
14424
- function manifestBootstrapCurrentMachine() {
14425
- return manifestAdd(detectCurrentMachineManifest());
14426
- }
14427
- function manifestGet(machineId) {
14428
- return getManifestMachine(machineId);
14784
+ // src/commands/self-test.ts
14785
+ function check(id, status, summary, detail) {
14786
+ return { id, status, summary, detail };
14429
14787
  }
14430
- function manifestRemove(machineId) {
14431
- const manifest = readManifest();
14432
- const nextManifest = {
14433
- ...manifest,
14434
- machines: manifest.machines.filter((machine) => machine.id !== machineId)
14788
+ function runSelfTest() {
14789
+ const version = getPackageVersion();
14790
+ const status = getStatus();
14791
+ const doctor = runDoctor();
14792
+ const serveInfo = getServeInfo();
14793
+ const html = renderDashboardHtml();
14794
+ const notifications = listNotificationChannels();
14795
+ const apps = listApps(status.machineId);
14796
+ const appsDiff = diffApps(status.machineId);
14797
+ const cliPlan = buildClaudeInstallPlan(status.machineId);
14798
+ return {
14799
+ machineId: getLocalMachineId(),
14800
+ checks: [
14801
+ check("package-version", version === "0.0.0" ? "fail" : "ok", "Package version resolves", version),
14802
+ check("status", "ok", "Status loads", JSON.stringify({ machines: status.manifestMachineCount, heartbeats: status.heartbeatCount })),
14803
+ check("doctor", doctor.checks.some((entry) => entry.status === "fail") ? "warn" : "ok", "Doctor completed", `${doctor.checks.length} checks`),
14804
+ check("serve-info", "ok", "Dashboard info renders", `${serveInfo.url} routes=${serveInfo.routes.length}`),
14805
+ check("dashboard-html", html.includes("Machines Dashboard") ? "ok" : "fail", "Dashboard HTML renders", html.slice(0, 80)),
14806
+ check("notifications", "ok", "Notifications config loads", `${notifications.channels.length} channels`),
14807
+ check("apps", "ok", "Apps manifest loads", `${apps.apps.length} apps`),
14808
+ check("apps-diff", appsDiff.missing.length === 0 ? "ok" : "warn", "Apps diff computed", `missing=${appsDiff.missing.length} installed=${appsDiff.installed.length}`),
14809
+ check("install-claude-plan", cliPlan.steps.length > 0 ? "ok" : "warn", "Install plan renders", `${cliPlan.steps.length} steps`)
14810
+ ]
14435
14811
  };
14436
- writeManifest(nextManifest);
14437
- return nextManifest;
14438
- }
14439
- function manifestValidate() {
14440
- return validateManifest(getManifestPath());
14441
14812
  }
14442
14813
 
14443
14814
  // src/commands/serve.ts
@@ -14451,13 +14822,27 @@ function getServeInfo(options = {}) {
14451
14822
  host,
14452
14823
  port,
14453
14824
  url: `http://${host}:${port}`,
14454
- routes: ["/", "/health", "/api/status", "/api/manifest", "/api/notifications"]
14825
+ routes: [
14826
+ "/",
14827
+ "/health",
14828
+ "/api/status",
14829
+ "/api/manifest",
14830
+ "/api/notifications",
14831
+ "/api/doctor",
14832
+ "/api/self-test",
14833
+ "/api/apps/status",
14834
+ "/api/apps/diff",
14835
+ "/api/install-claude/status",
14836
+ "/api/install-claude/diff",
14837
+ "/api/notifications/test"
14838
+ ]
14455
14839
  };
14456
14840
  }
14457
14841
  function renderDashboardHtml() {
14458
14842
  const status = getStatus();
14459
14843
  const manifest = manifestList();
14460
14844
  const notifications = listNotificationChannels();
14845
+ const doctor = runDoctor();
14461
14846
  return `<!doctype html>
14462
14847
  <html lang="en">
14463
14848
  <head>
@@ -14473,12 +14858,14 @@ function renderDashboardHtml() {
14473
14858
  .card { background: #121933; border: 1px solid #243057; border-radius: 16px; padding: 20px; }
14474
14859
  .stat { font-size: 32px; font-weight: 700; margin-top: 8px; }
14475
14860
  table { width: 100%; border-collapse: collapse; }
14476
- th, td { text-align: left; padding: 10px 8px; border-bottom: 1px solid #243057; }
14861
+ th, td { text-align: left; padding: 10px 8px; border-bottom: 1px solid #243057; vertical-align: top; }
14477
14862
  code { color: #9ed0ff; }
14478
14863
  .badge { display: inline-block; border-radius: 999px; padding: 4px 10px; font-size: 12px; }
14479
- .online { background: #12351f; color: #74f0a7; }
14480
- .offline { background: #3b1a1a; color: #ff8c8c; }
14481
- .unknown { background: #2f2b16; color: #ffd76a; }
14864
+ .online, .ok { background: #12351f; color: #74f0a7; }
14865
+ .offline, .fail { background: #3b1a1a; color: #ff8c8c; }
14866
+ .unknown, .warn { background: #2f2b16; color: #ffd76a; }
14867
+ ul { margin: 8px 0 0; padding-left: 18px; }
14868
+ .muted { color: #9fb0d9; }
14482
14869
  </style>
14483
14870
  </head>
14484
14871
  <body>
@@ -14488,6 +14875,7 @@ function renderDashboardHtml() {
14488
14875
  <section class="card"><div>Manifest machines</div><div class="stat">${status.manifestMachineCount}</div></section>
14489
14876
  <section class="card"><div>Heartbeats</div><div class="stat">${status.heartbeatCount}</div></section>
14490
14877
  <section class="card"><div>Notification channels</div><div class="stat">${notifications.channels.length}</div></section>
14878
+ <section class="card"><div>Doctor warnings</div><div class="stat">${doctor.checks.filter((entry) => entry.status !== "ok").length}</div></section>
14491
14879
  </div>
14492
14880
 
14493
14881
  <section class="card" style="margin-top:16px">
@@ -14505,6 +14893,25 @@ function renderDashboardHtml() {
14505
14893
  </table>
14506
14894
  </section>
14507
14895
 
14896
+ <section class="card" style="margin-top:16px">
14897
+ <h2>Doctor</h2>
14898
+ <table>
14899
+ <thead><tr><th>Check</th><th>Status</th><th>Detail</th></tr></thead>
14900
+ <tbody>
14901
+ ${doctor.checks.map((entry) => `<tr>
14902
+ <td>${escapeHtml(entry.summary)}</td>
14903
+ <td><span class="badge ${escapeHtml(entry.status)}">${escapeHtml(entry.status)}</span></td>
14904
+ <td class="muted">${escapeHtml(entry.detail)}</td>
14905
+ </tr>`).join("")}
14906
+ </tbody>
14907
+ </table>
14908
+ </section>
14909
+
14910
+ <section class="card" style="margin-top:16px">
14911
+ <h2>Self Test</h2>
14912
+ <p class="muted">Use <code>/api/self-test</code> for the full smoke-check payload.</p>
14913
+ </section>
14914
+
14508
14915
  <section class="card" style="margin-top:16px">
14509
14916
  <h2>Manifest</h2>
14510
14917
  <pre>${escapeHtml(JSON.stringify(manifest, null, 2))}</pre>
@@ -14630,7 +15037,7 @@ function runSetup(machineId, options = {}) {
14630
15037
  }
14631
15038
 
14632
15039
  // src/commands/sync.ts
14633
- import { existsSync as existsSync6, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync as copyFileSync2 } from "fs";
15040
+ import { existsSync as existsSync7, lstatSync, readFileSync as readFileSync6, symlinkSync, copyFileSync as copyFileSync2 } from "fs";
14634
15041
  function quote4(value) {
14635
15042
  return `'${value.replace(/'/g, `'\\''`)}'`;
14636
15043
  }
@@ -14661,12 +15068,12 @@ function packageInstallCommand(machine, packageName, manager = machine.platform
14661
15068
  function detectPackageActions(machine) {
14662
15069
  return (machine.packages || []).map((pkg, index) => {
14663
15070
  const manager = pkg.manager || (machine.platform === "macos" ? "brew" : "apt");
14664
- const check = Bun.spawnSync(["bash", "-lc", packageCheckCommand(machine, pkg.name, manager)], {
15071
+ const check2 = Bun.spawnSync(["bash", "-lc", packageCheckCommand(machine, pkg.name, manager)], {
14665
15072
  stdout: "ignore",
14666
15073
  stderr: "ignore",
14667
15074
  env: process.env
14668
15075
  });
14669
- const installed = check.exitCode === 0;
15076
+ const installed = check2.exitCode === 0;
14670
15077
  return {
14671
15078
  id: `package-${index + 1}`,
14672
15079
  title: `${installed ? "Package present" : "Install package"} ${pkg.name}`,
@@ -14678,15 +15085,15 @@ function detectPackageActions(machine) {
14678
15085
  }
14679
15086
  function detectFileActions(machine) {
14680
15087
  return (machine.files || []).map((file, index) => {
14681
- const sourceExists = existsSync6(file.source);
14682
- const targetExists = existsSync6(file.target);
15088
+ const sourceExists = existsSync7(file.source);
15089
+ const targetExists = existsSync7(file.target);
14683
15090
  let status = "missing";
14684
15091
  if (sourceExists && targetExists) {
14685
15092
  if (file.mode === "symlink") {
14686
15093
  status = lstatSync(file.target).isSymbolicLink() ? "ok" : "drifted";
14687
15094
  } else {
14688
- const source = readFileSync5(file.source, "utf8");
14689
- const target = readFileSync5(file.target, "utf8");
15095
+ const source = readFileSync6(file.source, "utf8");
15096
+ const target = readFileSync6(file.target, "utf8");
14690
15097
  status = source === target ? "ok" : "drifted";
14691
15098
  }
14692
15099
  }
@@ -14793,25 +15200,19 @@ function getAgentStatus(machineId = getLocalMachineId()) {
14793
15200
 
14794
15201
  // src/mcp/server.ts
14795
15202
  function createMcpServer(version) {
14796
- const server = new McpServer({
14797
- name: "machines",
14798
- version
14799
- });
15203
+ const server = new McpServer({ name: "machines", version });
14800
15204
  server.tool("machines_status", "Return local machine fleet status paths and machine identity.", {}, async () => ({
14801
15205
  content: [{ type: "text", text: JSON.stringify(getStatus(), null, 2) }]
14802
15206
  }));
14803
- server.tool("machines_apps_list", "List manifest-managed apps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({
14804
- content: [{ type: "text", text: JSON.stringify(listApps(machine_id), null, 2) }]
14805
- }));
14806
- server.tool("machines_apps_plan", "Preview app install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({
14807
- content: [{ type: "text", text: JSON.stringify(buildAppsPlan(machine_id), null, 2) }]
14808
- }));
14809
- server.tool("machines_apps_apply", "Install manifest-managed apps for a machine.", {
14810
- machine_id: exports_external.string().optional().describe("Machine identifier"),
14811
- yes: exports_external.boolean().describe("Confirmation flag for execution")
14812
- }, async ({ machine_id, yes }) => ({
14813
- content: [{ type: "text", text: JSON.stringify(runAppsInstall(machine_id, { apply: true, yes }), null, 2) }]
15207
+ server.tool("machines_doctor", "Run machine preflight checks.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(runDoctor(machine_id), null, 2) }] }));
15208
+ server.tool("machines_self_test", "Run local package smoke checks.", {}, async () => ({
15209
+ content: [{ type: "text", text: JSON.stringify(runSelfTest(), null, 2) }]
14814
15210
  }));
15211
+ server.tool("machines_apps_list", "List manifest-managed apps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(listApps(machine_id), null, 2) }] }));
15212
+ server.tool("machines_apps_status", "Check installed state for manifest-managed apps.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(getAppsStatus(machine_id), null, 2) }] }));
15213
+ server.tool("machines_apps_diff", "Show missing and installed manifest-managed apps.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(diffApps(machine_id), null, 2) }] }));
15214
+ server.tool("machines_apps_plan", "Preview app install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildAppsPlan(machine_id), null, 2) }] }));
15215
+ server.tool("machines_apps_apply", "Install manifest-managed apps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runAppsInstall(machine_id, { apply: true, yes }), null, 2) }] }));
14815
15216
  server.tool("machines_manifest", "Read the current fleet manifest.", {}, async () => ({
14816
15217
  content: [{ type: "text", text: JSON.stringify(manifestList(), null, 2) }]
14817
15218
  }));
@@ -14821,45 +15222,33 @@ function createMcpServer(version) {
14821
15222
  server.tool("machines_manifest_bootstrap", "Detect and upsert the current machine into the fleet manifest.", {}, async () => ({
14822
15223
  content: [{ type: "text", text: JSON.stringify(manifestBootstrapCurrentMachine(), null, 2) }]
14823
15224
  }));
14824
- server.tool("machines_manifest_get", "Read a single machine from the fleet manifest.", { machine_id: exports_external.string().describe("Machine identifier") }, async ({ machine_id }) => ({
14825
- content: [{ type: "text", text: JSON.stringify(manifestGet(machine_id), null, 2) }]
14826
- }));
14827
- server.tool("machines_manifest_remove", "Remove a single machine from the fleet manifest.", { machine_id: exports_external.string().describe("Machine identifier") }, async ({ machine_id }) => ({
14828
- content: [{ type: "text", text: JSON.stringify(manifestRemove(machine_id), null, 2) }]
14829
- }));
15225
+ server.tool("machines_manifest_get", "Read a single machine from the fleet manifest.", { machine_id: exports_external.string().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(manifestGet(machine_id), null, 2) }] }));
15226
+ server.tool("machines_manifest_remove", "Remove a single machine from the fleet manifest.", { machine_id: exports_external.string().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(manifestRemove(machine_id), null, 2) }] }));
14830
15227
  server.tool("machines_agent_status", "List current machine agent heartbeats.", {}, async () => ({
14831
15228
  content: [{ type: "text", text: JSON.stringify(getAgentStatus(), null, 2) }]
14832
15229
  }));
14833
- server.tool("machines_setup_preview", "Preview setup actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({
14834
- content: [{ type: "text", text: JSON.stringify(buildSetupPlan(machine_id), null, 2) }]
14835
- }));
14836
- server.tool("machines_setup_apply", "Execute setup actions for a machine.", {
14837
- machine_id: exports_external.string().optional().describe("Machine identifier"),
14838
- yes: exports_external.boolean().describe("Confirmation flag for execution")
14839
- }, async ({ machine_id, yes }) => ({
14840
- content: [{ type: "text", text: JSON.stringify(runSetup(machine_id, { apply: true, yes }), null, 2) }]
14841
- }));
14842
- server.tool("machines_sync_preview", "Preview sync actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({
14843
- content: [{ type: "text", text: JSON.stringify(buildSyncPlan(machine_id), null, 2) }]
14844
- }));
14845
- server.tool("machines_sync_apply", "Execute sync actions for a machine.", {
14846
- machine_id: exports_external.string().optional().describe("Machine identifier"),
14847
- yes: exports_external.boolean().describe("Confirmation flag for execution")
14848
- }, async ({ machine_id, yes }) => ({
14849
- content: [{ type: "text", text: JSON.stringify(runSync(machine_id, { apply: true, yes }), null, 2) }]
14850
- }));
15230
+ server.tool("machines_setup_preview", "Preview setup actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildSetupPlan(machine_id), null, 2) }] }));
15231
+ server.tool("machines_setup_apply", "Execute setup actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runSetup(machine_id, { apply: true, yes }), null, 2) }] }));
15232
+ server.tool("machines_sync_preview", "Preview sync actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildSyncPlan(machine_id), null, 2) }] }));
15233
+ server.tool("machines_sync_apply", "Execute sync actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runSync(machine_id, { apply: true, yes }), null, 2) }] }));
14851
15234
  server.tool("machines_diff", "Show manifest differences between two machines.", {
14852
15235
  left_machine_id: exports_external.string().describe("Left machine identifier"),
14853
15236
  right_machine_id: exports_external.string().optional().describe("Right machine identifier")
14854
15237
  }, async ({ left_machine_id, right_machine_id }) => ({
14855
15238
  content: [{ type: "text", text: JSON.stringify(diffMachines(left_machine_id, right_machine_id), null, 2) }]
14856
15239
  }));
15240
+ server.tool("machines_install_claude_status", "Check installed state for Claude, Codex, and Gemini CLIs.", {
15241
+ machine_id: exports_external.string().optional().describe("Machine identifier"),
15242
+ tools: exports_external.array(exports_external.enum(["claude", "codex", "gemini"])).optional().describe("AI CLIs to inspect")
15243
+ }, async ({ machine_id, tools }) => ({ content: [{ type: "text", text: JSON.stringify(getClaudeCliStatus(machine_id, tools), null, 2) }] }));
15244
+ server.tool("machines_install_claude_diff", "Show missing and installed Claude, Codex, and Gemini CLIs.", {
15245
+ machine_id: exports_external.string().optional().describe("Machine identifier"),
15246
+ tools: exports_external.array(exports_external.enum(["claude", "codex", "gemini"])).optional().describe("AI CLIs to inspect")
15247
+ }, async ({ machine_id, tools }) => ({ content: [{ type: "text", text: JSON.stringify(diffClaudeCli(machine_id, tools), null, 2) }] }));
14857
15248
  server.tool("machines_install_claude_preview", "Preview Claude, Codex, and Gemini CLI install steps for a machine.", {
14858
15249
  machine_id: exports_external.string().optional().describe("Machine identifier"),
14859
15250
  tools: exports_external.array(exports_external.enum(["claude", "codex", "gemini"])).optional().describe("AI CLIs to install")
14860
- }, async ({ machine_id, tools }) => ({
14861
- content: [{ type: "text", text: JSON.stringify(buildClaudeInstallPlan(machine_id, tools), null, 2) }]
14862
- }));
15251
+ }, async ({ machine_id, tools }) => ({ content: [{ type: "text", text: JSON.stringify(buildClaudeInstallPlan(machine_id, tools), null, 2) }] }));
14863
15252
  server.tool("machines_install_claude_apply", "Execute Claude, Codex, and Gemini CLI install steps for a machine.", {
14864
15253
  machine_id: exports_external.string().optional().describe("Machine identifier"),
14865
15254
  tools: exports_external.array(exports_external.enum(["claude", "codex", "gemini"])).optional().describe("AI CLIs to install"),
@@ -14867,73 +15256,21 @@ function createMcpServer(version) {
14867
15256
  }, async ({ machine_id, tools, yes }) => ({
14868
15257
  content: [{ type: "text", text: JSON.stringify(runClaudeInstall(machine_id, tools, { apply: true, yes }), null, 2) }]
14869
15258
  }));
14870
- server.tool("machines_install_tailscale_preview", "Preview Tailscale install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({
14871
- content: [{ type: "text", text: JSON.stringify(buildTailscaleInstallPlan(machine_id), null, 2) }]
14872
- }));
14873
- server.tool("machines_install_tailscale_apply", "Execute Tailscale install steps for a machine.", {
14874
- machine_id: exports_external.string().optional().describe("Machine identifier"),
14875
- yes: exports_external.boolean().describe("Confirmation flag for execution")
14876
- }, async ({ machine_id, yes }) => ({
14877
- content: [{ type: "text", text: JSON.stringify(runTailscaleInstall(machine_id, { apply: true, yes }), null, 2) }]
14878
- }));
14879
- server.tool("machines_ssh_resolve", "Resolve the best SSH route for a machine.", {
14880
- machine_id: exports_external.string().describe("Machine identifier"),
14881
- remote_command: exports_external.string().optional().describe("Optional remote command")
14882
- }, async ({ machine_id, remote_command }) => ({
14883
- content: [
14884
- {
14885
- type: "text",
14886
- text: JSON.stringify({
14887
- resolved: resolveSshTarget(machine_id),
14888
- command: buildSshCommand(machine_id, remote_command)
14889
- }, null, 2)
14890
- }
14891
- ]
15259
+ server.tool("machines_install_tailscale_preview", "Preview Tailscale install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildTailscaleInstallPlan(machine_id), null, 2) }] }));
15260
+ server.tool("machines_install_tailscale_apply", "Execute Tailscale install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runTailscaleInstall(machine_id, { apply: true, yes }), null, 2) }] }));
15261
+ server.tool("machines_ssh_resolve", "Resolve the best SSH route for a machine.", { machine_id: exports_external.string().describe("Machine identifier"), remote_command: exports_external.string().optional().describe("Optional remote command") }, async ({ machine_id, remote_command }) => ({
15262
+ content: [{ type: "text", text: JSON.stringify({ resolved: resolveSshTarget(machine_id), command: buildSshCommand(machine_id, remote_command) }, null, 2) }]
14892
15263
  }));
14893
- server.tool("machines_ports", "List listening ports on a machine.", {
14894
- machine_id: exports_external.string().optional().describe("Machine identifier")
14895
- }, async ({ machine_id }) => ({
15264
+ server.tool("machines_ports", "List listening ports on a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({
14896
15265
  content: [{ type: "text", text: JSON.stringify(listPorts(machine_id), null, 2) }]
14897
15266
  }));
14898
- server.tool("machines_backup_preview", "Preview backup steps for the current machine.", {
14899
- bucket: exports_external.string().describe("S3 bucket name"),
14900
- prefix: exports_external.string().optional().describe("S3 key prefix")
14901
- }, async ({ bucket, prefix }) => ({
14902
- content: [{ type: "text", text: JSON.stringify(buildBackupPlan(bucket, prefix), null, 2) }]
14903
- }));
14904
- server.tool("machines_backup_apply", "Execute backup steps for the current machine.", {
14905
- bucket: exports_external.string().describe("S3 bucket name"),
14906
- prefix: exports_external.string().optional().describe("S3 key prefix"),
14907
- yes: exports_external.boolean().describe("Confirmation flag for execution")
14908
- }, async ({ bucket, prefix, yes }) => ({
14909
- content: [{ type: "text", text: JSON.stringify(runBackup(bucket, prefix, { apply: true, yes }), null, 2) }]
14910
- }));
14911
- server.tool("machines_cert_preview", "Preview mkcert steps for one or more domains.", {
14912
- domains: exports_external.array(exports_external.string()).describe("Domains to issue certificates for")
14913
- }, async ({ domains }) => ({
14914
- content: [{ type: "text", text: JSON.stringify(buildCertPlan(domains), null, 2) }]
14915
- }));
14916
- server.tool("machines_cert_apply", "Execute mkcert steps for one or more domains.", {
14917
- domains: exports_external.array(exports_external.string()).describe("Domains to issue certificates for"),
14918
- yes: exports_external.boolean().describe("Confirmation flag for execution")
14919
- }, async ({ domains, yes }) => ({
14920
- content: [{ type: "text", text: JSON.stringify(runCertPlan(domains, { apply: true, yes }), null, 2) }]
14921
- }));
14922
- server.tool("machines_dns_add", "Add or replace a local domain mapping.", {
14923
- domain: exports_external.string().describe("Domain name"),
14924
- port: exports_external.number().describe("Target port"),
14925
- target_host: exports_external.string().optional().describe("Target host")
14926
- }, async ({ domain, port, target_host }) => ({
14927
- content: [{ type: "text", text: JSON.stringify(addDomainMapping(domain, port, target_host), null, 2) }]
14928
- }));
14929
- server.tool("machines_dns_list", "List local domain mappings.", {}, async () => ({
14930
- content: [{ type: "text", text: JSON.stringify(listDomainMappings(), null, 2) }]
14931
- }));
14932
- server.tool("machines_dns_render", "Render hosts/proxy configuration for a domain.", {
14933
- domain: exports_external.string().describe("Domain name")
14934
- }, async ({ domain }) => ({
14935
- content: [{ type: "text", text: JSON.stringify(renderDomainMapping(domain), null, 2) }]
14936
- }));
15267
+ server.tool("machines_backup_preview", "Preview backup steps for the current machine.", { bucket: exports_external.string().describe("S3 bucket name"), prefix: exports_external.string().optional().describe("S3 key prefix") }, async ({ bucket, prefix }) => ({ content: [{ type: "text", text: JSON.stringify(buildBackupPlan(bucket, prefix), null, 2) }] }));
15268
+ server.tool("machines_backup_apply", "Execute backup steps for the current machine.", { bucket: exports_external.string().describe("S3 bucket name"), prefix: exports_external.string().optional().describe("S3 key prefix"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ bucket, prefix, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runBackup(bucket, prefix, { apply: true, yes }), null, 2) }] }));
15269
+ server.tool("machines_cert_preview", "Preview mkcert steps for one or more domains.", { domains: exports_external.array(exports_external.string()).describe("Domains to issue certificates for") }, async ({ domains }) => ({ content: [{ type: "text", text: JSON.stringify(buildCertPlan(domains), null, 2) }] }));
15270
+ server.tool("machines_cert_apply", "Execute mkcert steps for one or more domains.", { domains: exports_external.array(exports_external.string()).describe("Domains to issue certificates for"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ domains, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runCertPlan(domains, { apply: true, yes }), null, 2) }] }));
15271
+ server.tool("machines_dns_add", "Add or replace a local domain mapping.", { domain: exports_external.string().describe("Domain name"), port: exports_external.number().describe("Target port"), target_host: exports_external.string().optional().describe("Target host") }, async ({ domain, port, target_host }) => ({ content: [{ type: "text", text: JSON.stringify(addDomainMapping(domain, port, target_host), null, 2) }] }));
15272
+ server.tool("machines_dns_list", "List local domain mappings.", {}, async () => ({ content: [{ type: "text", text: JSON.stringify(listDomainMappings(), null, 2) }] }));
15273
+ server.tool("machines_dns_render", "Render hosts/proxy configuration for a domain.", { domain: exports_external.string().describe("Domain name") }, async ({ domain }) => ({ content: [{ type: "text", text: JSON.stringify(renderDomainMapping(domain), null, 2) }] }));
14937
15274
  server.tool("machines_notifications_add", "Add or replace a notification channel.", {
14938
15275
  channel_id: exports_external.string().describe("Channel identifier"),
14939
15276
  type: exports_external.enum(["email", "webhook", "command"]).describe("Notification transport"),
@@ -14941,46 +15278,17 @@ function createMcpServer(version) {
14941
15278
  events: exports_external.array(exports_external.string()).describe("Events routed to this channel"),
14942
15279
  enabled: exports_external.boolean().optional().describe("Whether the channel is enabled")
14943
15280
  }, async ({ channel_id, type, target, events, enabled }) => ({
14944
- content: [
14945
- {
14946
- type: "text",
14947
- text: JSON.stringify(addNotificationChannel({
14948
- id: channel_id,
14949
- type,
14950
- target,
14951
- events,
14952
- enabled: enabled ?? true
14953
- }), null, 2)
14954
- }
14955
- ]
15281
+ content: [{ type: "text", text: JSON.stringify(addNotificationChannel({ id: channel_id, type, target, events, enabled: enabled ?? true }), null, 2) }]
14956
15282
  }));
14957
15283
  server.tool("machines_notifications_list", "List notification channels.", {}, async () => ({
14958
15284
  content: [{ type: "text", text: JSON.stringify(listNotificationChannels(), null, 2) }]
14959
15285
  }));
14960
- server.tool("machines_notifications_test", "Preview or execute a notification test.", {
14961
- channel_id: exports_external.string().describe("Channel identifier"),
14962
- event: exports_external.string().optional().describe("Event name"),
14963
- message: exports_external.string().optional().describe("Message body"),
14964
- yes: exports_external.boolean().optional().describe("Execute the test when true")
14965
- }, async ({ channel_id, event, message, yes }) => ({
14966
- content: [
14967
- {
14968
- type: "text",
14969
- text: JSON.stringify(testNotificationChannel(channel_id, event, message, { apply: Boolean(yes), yes }), null, 2)
14970
- }
14971
- ]
14972
- }));
14973
- server.tool("machines_notifications_remove", "Remove a notification channel.", {
14974
- channel_id: exports_external.string().describe("Channel identifier")
14975
- }, async ({ channel_id }) => ({
14976
- content: [{ type: "text", text: JSON.stringify(removeNotificationChannel(channel_id), null, 2) }]
14977
- }));
14978
- server.tool("machines_serve_info", "Preview the dashboard server bind address and routes.", {
14979
- host: exports_external.string().optional().describe("Host interface"),
14980
- port: exports_external.number().optional().describe("Port number")
14981
- }, async ({ host, port }) => ({
14982
- content: [{ type: "text", text: JSON.stringify(getServeInfo({ host, port }), null, 2) }]
15286
+ server.tool("machines_notifications_test", "Preview or execute a notification test.", { channel_id: exports_external.string().describe("Channel identifier"), event: exports_external.string().optional().describe("Event name"), message: exports_external.string().optional().describe("Message body"), yes: exports_external.boolean().optional().describe("Execute the test when true") }, async ({ channel_id, event, message, yes }) => ({
15287
+ content: [{ type: "text", text: JSON.stringify(await testNotificationChannel(channel_id, event, message, { apply: Boolean(yes), yes }), null, 2) }]
14983
15288
  }));
15289
+ 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) }] }));
15290
+ 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) }] }));
15291
+ 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) }] }));
14984
15292
  server.tool("machines_serve_dashboard", "Render the current dashboard HTML.", {}, async () => ({
14985
15293
  content: [{ type: "text", text: renderDashboardHtml() }]
14986
15294
  }));
@@ -14990,8 +15298,8 @@ function createMcpServer(version) {
14990
15298
  // src/mcp/index.ts
14991
15299
  function getPkgVersion() {
14992
15300
  try {
14993
- const pkgPath = join7(dirname4(fileURLToPath(import.meta.url)), "..", "..", "package.json");
14994
- return JSON.parse(readFileSync6(pkgPath, "utf8")).version || "0.0.0";
15301
+ const pkgPath = join9(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
15302
+ return JSON.parse(readFileSync7(pkgPath, "utf8")).version || "0.0.0";
14995
15303
  } catch {
14996
15304
  return "0.0.0";
14997
15305
  }