@hasna/cloud 0.1.34 → 0.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -994,7 +994,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
994
994
  this._exitCallback = (err) => {
995
995
  if (err.code !== "commander.executeSubCommandAsync") {
996
996
  throw err;
997
- } else {}
997
+ }
998
998
  };
999
999
  }
1000
1000
  return this;
@@ -6158,7 +6158,7 @@ var require_arrayParser = __commonJS((exports, module) => {
6158
6158
  };
6159
6159
  });
6160
6160
 
6161
- // node_modules/pg-types/node_modules/postgres-date/index.js
6161
+ // node_modules/postgres-date/index.js
6162
6162
  var require_postgres_date = __commonJS((exports, module) => {
6163
6163
  var DATE_TIME = /(\d{1,})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})(\.\d{1,})?.*?( BC)?$/;
6164
6164
  var DATE = /^(\d{1,})-(\d{2})-(\d{2})( BC)?$/;
@@ -6260,7 +6260,7 @@ var require_mutable = __commonJS((exports, module) => {
6260
6260
  }
6261
6261
  });
6262
6262
 
6263
- // node_modules/pg-types/node_modules/postgres-interval/index.js
6263
+ // node_modules/postgres-interval/index.js
6264
6264
  var require_postgres_interval = __commonJS((exports, module) => {
6265
6265
  var extend = require_mutable();
6266
6266
  module.exports = PostgresInterval;
@@ -6352,7 +6352,7 @@ var require_postgres_interval = __commonJS((exports, module) => {
6352
6352
  }
6353
6353
  });
6354
6354
 
6355
- // node_modules/pg-types/node_modules/postgres-bytea/index.js
6355
+ // node_modules/postgres-bytea/index.js
6356
6356
  var require_postgres_bytea = __commonJS((exports, module) => {
6357
6357
  var bufferFrom = Buffer.from || Buffer;
6358
6358
  module.exports = function parseBytea(input) {
@@ -11238,28 +11238,28 @@ var init_adapter = __esm(() => {
11238
11238
 
11239
11239
  // src/dotfile.ts
11240
11240
  import {
11241
- existsSync,
11241
+ existsSync as existsSync2,
11242
11242
  mkdirSync,
11243
11243
  readdirSync,
11244
11244
  copyFileSync
11245
11245
  } from "fs";
11246
- import { homedir } from "os";
11247
- import { join, relative } from "path";
11246
+ import { homedir as homedir2 } from "os";
11247
+ import { join as join2, relative } from "path";
11248
11248
  function getDataDir(serviceName) {
11249
- const dir = join(HASNA_DIR, serviceName);
11249
+ const dir = join2(HASNA_DIR, serviceName);
11250
11250
  mkdirSync(dir, { recursive: true });
11251
11251
  return dir;
11252
11252
  }
11253
11253
  function getDbPath(serviceName) {
11254
11254
  const dir = getDataDir(serviceName);
11255
- return join(dir, `${serviceName}.db`);
11255
+ return join2(dir, `${serviceName}.db`);
11256
11256
  }
11257
11257
  function migrateDotfile(serviceName) {
11258
- const legacyDir = join(homedir(), `.${serviceName}`);
11259
- const newDir = join(HASNA_DIR, serviceName);
11260
- if (!existsSync(legacyDir))
11258
+ const legacyDir = join2(homedir2(), `.${serviceName}`);
11259
+ const newDir = join2(HASNA_DIR, serviceName);
11260
+ if (!existsSync2(legacyDir))
11261
11261
  return [];
11262
- if (existsSync(newDir))
11262
+ if (existsSync2(newDir))
11263
11263
  return [];
11264
11264
  mkdirSync(newDir, { recursive: true });
11265
11265
  const migrated = [];
@@ -11269,8 +11269,8 @@ function migrateDotfile(serviceName) {
11269
11269
  function copyDirRecursive(src, dest, root, migrated) {
11270
11270
  const entries = readdirSync(src, { withFileTypes: true });
11271
11271
  for (const entry of entries) {
11272
- const srcPath = join(src, entry.name);
11273
- const destPath = join(dest, entry.name);
11272
+ const srcPath = join2(src, entry.name);
11273
+ const destPath = join2(dest, entry.name);
11274
11274
  if (entry.isDirectory()) {
11275
11275
  mkdirSync(destPath, { recursive: true });
11276
11276
  copyDirRecursive(srcPath, destPath, root, migrated);
@@ -11286,7 +11286,7 @@ function getHasnaDir() {
11286
11286
  }
11287
11287
  var HASNA_DIR;
11288
11288
  var init_dotfile = __esm(() => {
11289
- HASNA_DIR = join(homedir(), ".hasna");
11289
+ HASNA_DIR = join2(homedir2(), ".hasna");
11290
11290
  });
11291
11291
 
11292
11292
  // src/discover.ts
@@ -11299,15 +11299,15 @@ __export(exports_discover, {
11299
11299
  SYNC_EXCLUDED_TABLE_PATTERNS: () => SYNC_EXCLUDED_TABLE_PATTERNS,
11300
11300
  KNOWN_PG_SERVICES: () => KNOWN_PG_SERVICES
11301
11301
  });
11302
- import { readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
11303
- import { join as join2 } from "path";
11304
- import { homedir as homedir2 } from "os";
11302
+ import { readdirSync as readdirSync2, existsSync as existsSync3 } from "fs";
11303
+ import { join as join3 } from "path";
11304
+ import { homedir as homedir3 } from "os";
11305
11305
  function isSyncExcludedTable(table) {
11306
11306
  return SYNC_EXCLUDED_TABLE_PATTERNS.some((p) => p.test(table));
11307
11307
  }
11308
11308
  function discoverServices() {
11309
- const dataDir = join2(homedir2(), ".hasna");
11310
- if (!existsSync2(dataDir))
11309
+ const dataDir = join3(homedir3(), ".hasna");
11310
+ if (!existsSync3(dataDir))
11311
11311
  return [];
11312
11312
  try {
11313
11313
  const entries = readdirSync2(dataDir, { withFileTypes: true });
@@ -11328,24 +11328,24 @@ function discoverSyncableServices() {
11328
11328
  return local.filter((s) => pgSet.has(s));
11329
11329
  }
11330
11330
  function getServiceDbPath(service) {
11331
- const dataDir = join2(homedir2(), ".hasna", service);
11332
- if (!existsSync2(dataDir))
11331
+ const dataDir = join3(homedir3(), ".hasna", service);
11332
+ if (!existsSync3(dataDir))
11333
11333
  return null;
11334
11334
  const candidates = [
11335
- join2(dataDir, `${service}.db`),
11336
- join2(dataDir, "data.db"),
11337
- join2(dataDir, "database.db")
11335
+ join3(dataDir, `${service}.db`),
11336
+ join3(dataDir, "data.db"),
11337
+ join3(dataDir, "database.db")
11338
11338
  ];
11339
11339
  try {
11340
11340
  const files = readdirSync2(dataDir);
11341
11341
  for (const f of files) {
11342
11342
  if (f.endsWith(".db") && !f.endsWith("-wal") && !f.endsWith("-shm")) {
11343
- candidates.push(join2(dataDir, f));
11343
+ candidates.push(join3(dataDir, f));
11344
11344
  }
11345
11345
  }
11346
11346
  } catch {}
11347
11347
  for (const p of candidates) {
11348
- if (existsSync2(p))
11348
+ if (existsSync3(p))
11349
11349
  return p;
11350
11350
  }
11351
11351
  return null;
@@ -11400,9 +11400,9 @@ var init_discover = __esm(() => {
11400
11400
 
11401
11401
  // src/machines.ts
11402
11402
  import { spawnSync } from "child_process";
11403
- import { existsSync as existsSync3 } from "fs";
11404
- import { homedir as homedir3, hostname, platform, arch, userInfo } from "os";
11405
- import { dirname, join as join3 } from "path";
11403
+ import { existsSync as existsSync4 } from "fs";
11404
+ import { homedir as homedir4, hostname, platform, arch, userInfo } from "os";
11405
+ import { dirname, join as join4 } from "path";
11406
11406
  function quoteSqlString(value) {
11407
11407
  return `'${value.replace(/'/g, "''")}'`;
11408
11408
  }
@@ -11414,10 +11414,10 @@ function normalizePlatform(value) {
11414
11414
  return value;
11415
11415
  }
11416
11416
  function detectWorkspacePath() {
11417
- const home = homedir3();
11418
- const candidates = [join3(home, "workspace"), join3(home, "Workspace")];
11417
+ const home = homedir4();
11418
+ const candidates = [join4(home, "workspace"), join4(home, "Workspace")];
11419
11419
  for (const candidate of candidates) {
11420
- if (existsSync3(candidate))
11420
+ if (existsSync4(candidate))
11421
11421
  return candidate;
11422
11422
  }
11423
11423
  const cwd = process.cwd();
@@ -11470,7 +11470,7 @@ function registerMachine(db, opts = {}) {
11470
11470
  ensureMachinesTable(db);
11471
11471
  const detected = detectCurrentMachine(opts);
11472
11472
  const id = detected.id ?? getCurrentMachineId();
11473
- const now = new Date().toISOString();
11473
+ const now2 = new Date().toISOString();
11474
11474
  const existing = getMachineRecord(db, id);
11475
11475
  const isPrimary = toFlag(detected.is_primary, existing?.is_primary ?? 0);
11476
11476
  const archived = toFlag(detected.archived, existing?.archived ?? 0);
@@ -11484,8 +11484,8 @@ function registerMachine(db, opts = {}) {
11484
11484
  workspace_path: detected.workspace_path ?? existing?.workspace_path ?? "",
11485
11485
  bun_path: detected.bun_path ?? existing?.bun_path ?? "",
11486
11486
  is_primary: isPrimary,
11487
- last_seen_at: detected.last_seen_at ?? now,
11488
- registered_at: existing?.registered_at ?? detected.registered_at ?? now,
11487
+ last_seen_at: detected.last_seen_at ?? now2,
11488
+ registered_at: existing?.registered_at ?? detected.registered_at ?? now2,
11489
11489
  archived
11490
11490
  };
11491
11491
  db.run(`INSERT INTO machines (
@@ -11814,9 +11814,9 @@ __export(exports_config, {
11814
11814
  createDatabase: () => createDatabase,
11815
11815
  CloudConfigSchema: () => CloudConfigSchema
11816
11816
  });
11817
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
11818
- import { homedir as homedir4 } from "os";
11819
- import { join as join4 } from "path";
11817
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
11818
+ import { homedir as homedir5 } from "os";
11819
+ import { join as join5 } from "path";
11820
11820
  function getConfigDir() {
11821
11821
  return CONFIG_DIR;
11822
11822
  }
@@ -11824,7 +11824,7 @@ function getConfigPath() {
11824
11824
  return CONFIG_PATH;
11825
11825
  }
11826
11826
  function getCloudConfig() {
11827
- if (!existsSync4(CONFIG_PATH)) {
11827
+ if (!existsSync5(CONFIG_PATH)) {
11828
11828
  return CloudConfigSchema.parse({});
11829
11829
  }
11830
11830
  try {
@@ -11899,8 +11899,8 @@ var init_config = __esm(() => {
11899
11899
  }).default({}),
11900
11900
  daemon: DaemonConfigSchema
11901
11901
  });
11902
- CONFIG_DIR = join4(homedir4(), ".hasna", "cloud");
11903
- CONFIG_PATH = join4(CONFIG_DIR, "config.json");
11902
+ CONFIG_DIR = join5(homedir5(), ".hasna", "cloud");
11903
+ CONFIG_PATH = join5(CONFIG_DIR, "config.json");
11904
11904
  });
11905
11905
 
11906
11906
  // node_modules/commander/esm.mjs
@@ -11919,6 +11919,690 @@ var {
11919
11919
  Help
11920
11920
  } = import__.default;
11921
11921
 
11922
+ // node_modules/@hasna/events/dist/commander.js
11923
+ import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
11924
+ import { existsSync } from "fs";
11925
+ import { homedir } from "os";
11926
+ import { join } from "path";
11927
+ import { createHmac, timingSafeEqual } from "crypto";
11928
+ import { randomUUID } from "crypto";
11929
+ import { spawn } from "child_process";
11930
+ import { randomUUID as randomUUID2 } from "crypto";
11931
+ function getPathValue(input, path) {
11932
+ return path.split(".").reduce((value, part) => {
11933
+ if (value && typeof value === "object" && part in value) {
11934
+ return value[part];
11935
+ }
11936
+ return;
11937
+ }, input);
11938
+ }
11939
+ function wildcardToRegExp(pattern) {
11940
+ const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
11941
+ return new RegExp(`^${escaped}$`);
11942
+ }
11943
+ function matchString(value, matcher) {
11944
+ if (matcher === undefined)
11945
+ return true;
11946
+ if (value === undefined)
11947
+ return false;
11948
+ const matchers = Array.isArray(matcher) ? matcher : [matcher];
11949
+ return matchers.some((item) => wildcardToRegExp(item).test(value));
11950
+ }
11951
+ function matchRecord(input, matcher) {
11952
+ if (!matcher)
11953
+ return true;
11954
+ return Object.entries(matcher).every(([path, expected]) => {
11955
+ const actual = getPathValue(input, path);
11956
+ if (typeof expected === "string" || Array.isArray(expected)) {
11957
+ return matchString(actual === undefined ? undefined : String(actual), expected);
11958
+ }
11959
+ return actual === expected;
11960
+ });
11961
+ }
11962
+ function eventMatchesFilter(event, filter) {
11963
+ return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
11964
+ }
11965
+ function channelMatchesEvent(channel, event) {
11966
+ if (!channel.enabled)
11967
+ return false;
11968
+ if (!channel.filters || channel.filters.length === 0)
11969
+ return true;
11970
+ return channel.filters.some((filter) => eventMatchesFilter(event, filter));
11971
+ }
11972
+ var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
11973
+ var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
11974
+ function getEventsDataDir(override) {
11975
+ return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
11976
+ }
11977
+
11978
+ class JsonEventsStore {
11979
+ dataDir;
11980
+ channelsPath;
11981
+ eventsPath;
11982
+ deliveriesPath;
11983
+ constructor(dataDir = getEventsDataDir()) {
11984
+ this.dataDir = dataDir;
11985
+ this.channelsPath = join(dataDir, "channels.json");
11986
+ this.eventsPath = join(dataDir, "events.json");
11987
+ this.deliveriesPath = join(dataDir, "deliveries.json");
11988
+ }
11989
+ async init() {
11990
+ await mkdir(this.dataDir, { recursive: true, mode: 448 });
11991
+ await chmod(this.dataDir, 448).catch(() => {
11992
+ return;
11993
+ });
11994
+ await this.ensureArrayFile(this.channelsPath);
11995
+ await this.ensureArrayFile(this.eventsPath);
11996
+ await this.ensureArrayFile(this.deliveriesPath);
11997
+ }
11998
+ async addChannel(channel) {
11999
+ await this.init();
12000
+ const channels = await this.readJson(this.channelsPath, []);
12001
+ const index = channels.findIndex((item) => item.id === channel.id);
12002
+ if (index >= 0) {
12003
+ channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
12004
+ } else {
12005
+ channels.push(channel);
12006
+ }
12007
+ await this.writeJson(this.channelsPath, channels);
12008
+ return index >= 0 ? channels[index] : channel;
12009
+ }
12010
+ async listChannels() {
12011
+ await this.init();
12012
+ return this.readJson(this.channelsPath, []);
12013
+ }
12014
+ async getChannel(id) {
12015
+ const channels = await this.listChannels();
12016
+ return channels.find((channel) => channel.id === id);
12017
+ }
12018
+ async removeChannel(id) {
12019
+ await this.init();
12020
+ const channels = await this.readJson(this.channelsPath, []);
12021
+ const next = channels.filter((channel) => channel.id !== id);
12022
+ await this.writeJson(this.channelsPath, next);
12023
+ return next.length !== channels.length;
12024
+ }
12025
+ async appendEvent(event) {
12026
+ await this.init();
12027
+ const events = await this.readJson(this.eventsPath, []);
12028
+ events.push(event);
12029
+ await this.writeJson(this.eventsPath, events);
12030
+ return event;
12031
+ }
12032
+ async listEvents() {
12033
+ await this.init();
12034
+ return this.readJson(this.eventsPath, []);
12035
+ }
12036
+ async findEventByIdentity(identity) {
12037
+ const events = await this.listEvents();
12038
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
12039
+ }
12040
+ async appendDelivery(result) {
12041
+ await this.init();
12042
+ const deliveries = await this.readJson(this.deliveriesPath, []);
12043
+ deliveries.push(result);
12044
+ await this.writeJson(this.deliveriesPath, deliveries);
12045
+ return result;
12046
+ }
12047
+ async listDeliveries() {
12048
+ await this.init();
12049
+ return this.readJson(this.deliveriesPath, []);
12050
+ }
12051
+ async exportData() {
12052
+ return {
12053
+ channels: await this.listChannels(),
12054
+ events: await this.listEvents(),
12055
+ deliveries: await this.listDeliveries()
12056
+ };
12057
+ }
12058
+ async ensureArrayFile(path) {
12059
+ if (!existsSync(path)) {
12060
+ await writeFile(path, `[]
12061
+ `, { encoding: "utf-8", mode: 384 });
12062
+ }
12063
+ await chmod(path, 384).catch(() => {
12064
+ return;
12065
+ });
12066
+ }
12067
+ async readJson(path, fallback) {
12068
+ try {
12069
+ const raw = await readFile(path, "utf-8");
12070
+ if (!raw.trim())
12071
+ return fallback;
12072
+ return JSON.parse(raw);
12073
+ } catch (error) {
12074
+ if (error.code === "ENOENT")
12075
+ return fallback;
12076
+ throw error;
12077
+ }
12078
+ }
12079
+ async writeJson(path, value) {
12080
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
12081
+ await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
12082
+ `, { encoding: "utf-8", mode: 384 });
12083
+ await rename(tempPath, path);
12084
+ await chmod(path, 384).catch(() => {
12085
+ return;
12086
+ });
12087
+ }
12088
+ }
12089
+ var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
12090
+ function buildSignatureBase(timestamp, body) {
12091
+ return `${timestamp}.${body}`;
12092
+ }
12093
+ function signPayload(secret, timestamp, body) {
12094
+ const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
12095
+ return `sha256=${digest}`;
12096
+ }
12097
+ function now() {
12098
+ return new Date().toISOString();
12099
+ }
12100
+ function truncate(value, max = 4096) {
12101
+ return value.length > max ? `${value.slice(0, max)}...` : value;
12102
+ }
12103
+ function buildWebhookRequest(event, channel) {
12104
+ if (!channel.webhook)
12105
+ throw new Error(`Channel ${channel.id} has no webhook config`);
12106
+ const body = JSON.stringify(event);
12107
+ const timestamp = event.time;
12108
+ const headers = {
12109
+ "Content-Type": "application/json",
12110
+ "User-Agent": "@hasna/events",
12111
+ "X-Hasna-Event-Id": event.id,
12112
+ "X-Hasna-Event-Type": event.type,
12113
+ "X-Hasna-Timestamp": timestamp,
12114
+ ...channel.webhook.headers
12115
+ };
12116
+ if (channel.webhook.secret) {
12117
+ headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
12118
+ }
12119
+ return { body, headers };
12120
+ }
12121
+ async function dispatchWebhook(event, channel, options = {}) {
12122
+ if (!channel.webhook)
12123
+ throw new Error(`Channel ${channel.id} has no webhook config`);
12124
+ const startedAt = now();
12125
+ const { body, headers } = buildWebhookRequest(event, channel);
12126
+ const controller = new AbortController;
12127
+ const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
12128
+ try {
12129
+ const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
12130
+ method: "POST",
12131
+ headers,
12132
+ body,
12133
+ signal: controller.signal
12134
+ });
12135
+ const responseBody = truncate(await response.text());
12136
+ return {
12137
+ attempt: 1,
12138
+ status: response.ok ? "success" : "failed",
12139
+ startedAt,
12140
+ completedAt: now(),
12141
+ responseStatus: response.status,
12142
+ responseBody,
12143
+ error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
12144
+ };
12145
+ } catch (error) {
12146
+ return {
12147
+ attempt: 1,
12148
+ status: "failed",
12149
+ startedAt,
12150
+ completedAt: now(),
12151
+ error: error instanceof Error ? error.message : String(error)
12152
+ };
12153
+ } finally {
12154
+ clearTimeout(timeout);
12155
+ }
12156
+ }
12157
+ async function dispatchCommand(event, channel) {
12158
+ if (!channel.command)
12159
+ throw new Error(`Channel ${channel.id} has no command config`);
12160
+ const startedAt = now();
12161
+ const eventJson = JSON.stringify(event);
12162
+ const env = {
12163
+ ...process.env,
12164
+ ...channel.command.env,
12165
+ HASNA_CHANNEL_ID: channel.id,
12166
+ HASNA_EVENT_ID: event.id,
12167
+ HASNA_EVENT_TYPE: event.type,
12168
+ HASNA_EVENT_SOURCE: event.source,
12169
+ HASNA_EVENT_SUBJECT: event.subject ?? "",
12170
+ HASNA_EVENT_SEVERITY: event.severity,
12171
+ HASNA_EVENT_TIME: event.time,
12172
+ HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
12173
+ HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
12174
+ HASNA_EVENT_JSON: eventJson
12175
+ };
12176
+ return new Promise((resolve) => {
12177
+ const child = spawn(channel.command.command, channel.command.args ?? [], {
12178
+ cwd: channel.command.cwd,
12179
+ env,
12180
+ stdio: ["pipe", "pipe", "pipe"]
12181
+ });
12182
+ let stdout = "";
12183
+ let stderr = "";
12184
+ const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
12185
+ child.stdin.end(eventJson);
12186
+ child.stdout.on("data", (chunk) => {
12187
+ stdout += chunk.toString();
12188
+ });
12189
+ child.stderr.on("data", (chunk) => {
12190
+ stderr += chunk.toString();
12191
+ });
12192
+ child.on("error", (error) => {
12193
+ clearTimeout(timeout);
12194
+ resolve({
12195
+ attempt: 1,
12196
+ status: "failed",
12197
+ startedAt,
12198
+ completedAt: now(),
12199
+ stdout: truncate(stdout),
12200
+ stderr: truncate(stderr),
12201
+ error: error.message
12202
+ });
12203
+ });
12204
+ child.on("close", (code, signal) => {
12205
+ clearTimeout(timeout);
12206
+ const success = code === 0;
12207
+ resolve({
12208
+ attempt: 1,
12209
+ status: success ? "success" : "failed",
12210
+ startedAt,
12211
+ completedAt: now(),
12212
+ stdout: truncate(stdout),
12213
+ stderr: truncate(stderr),
12214
+ error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
12215
+ });
12216
+ });
12217
+ });
12218
+ }
12219
+ async function dispatchChannel(event, channel, options = {}) {
12220
+ if (channel.transport === "webhook")
12221
+ return dispatchWebhook(event, channel, options);
12222
+ if (channel.transport === "command")
12223
+ return dispatchCommand(event, channel);
12224
+ return {
12225
+ attempt: 1,
12226
+ status: "skipped",
12227
+ startedAt: now(),
12228
+ completedAt: now(),
12229
+ error: `Unsupported transport: ${channel.transport}`
12230
+ };
12231
+ }
12232
+ function createDeliveryResult(event, channel, attempts) {
12233
+ const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
12234
+ return {
12235
+ id: randomUUID(),
12236
+ eventId: event.id,
12237
+ channelId: channel.id,
12238
+ transport: channel.transport,
12239
+ status,
12240
+ attempts,
12241
+ createdAt: attempts[0]?.startedAt ?? now(),
12242
+ completedAt: attempts.at(-1)?.completedAt ?? now()
12243
+ };
12244
+ }
12245
+ function createEvent(input) {
12246
+ return {
12247
+ id: input.id ?? randomUUID2(),
12248
+ source: input.source,
12249
+ type: input.type,
12250
+ time: normalizeTime(input.time),
12251
+ subject: input.subject,
12252
+ severity: input.severity ?? "info",
12253
+ data: input.data ?? {},
12254
+ message: input.message,
12255
+ dedupeKey: input.dedupeKey,
12256
+ schemaVersion: input.schemaVersion ?? "1.0",
12257
+ metadata: input.metadata ?? {}
12258
+ };
12259
+ }
12260
+
12261
+ class EventsClient {
12262
+ store;
12263
+ redactors;
12264
+ transportOptions;
12265
+ constructor(options = {}) {
12266
+ this.store = options.store ?? new JsonEventsStore(options.dataDir);
12267
+ this.redactors = options.redactors ?? [];
12268
+ this.transportOptions = { fetchImpl: options.fetchImpl };
12269
+ }
12270
+ async addChannel(input) {
12271
+ const timestamp = new Date().toISOString();
12272
+ return this.store.addChannel({
12273
+ ...input,
12274
+ createdAt: input.createdAt ?? timestamp,
12275
+ updatedAt: input.updatedAt ?? timestamp
12276
+ });
12277
+ }
12278
+ async listChannels() {
12279
+ return this.store.listChannels();
12280
+ }
12281
+ async removeChannel(id) {
12282
+ return this.store.removeChannel(id);
12283
+ }
12284
+ async emit(input, options = {}) {
12285
+ const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
12286
+ if (options.dedupe !== false) {
12287
+ const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
12288
+ if (existing) {
12289
+ return { event: existing, deliveries: [], deduped: true };
12290
+ }
12291
+ }
12292
+ await this.store.appendEvent(event);
12293
+ const deliveries = options.deliver === false ? [] : await this.deliver(event);
12294
+ return { event, deliveries, deduped: false };
12295
+ }
12296
+ async listEvents() {
12297
+ return this.store.listEvents();
12298
+ }
12299
+ async listDeliveries() {
12300
+ return this.store.listDeliveries();
12301
+ }
12302
+ async deliver(event) {
12303
+ const channels = await this.store.listChannels();
12304
+ const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
12305
+ const deliveries = [];
12306
+ for (const channel of selected) {
12307
+ const eventForChannel = await this.applyRedaction(event, channel);
12308
+ const result = await this.deliverWithRetry(eventForChannel, channel);
12309
+ await this.store.appendDelivery(result);
12310
+ deliveries.push(result);
12311
+ }
12312
+ return deliveries;
12313
+ }
12314
+ async testChannel(id, input = {}) {
12315
+ const channel = await this.store.getChannel(id);
12316
+ if (!channel)
12317
+ throw new Error(`Channel not found: ${id}`);
12318
+ const event = createEvent({
12319
+ source: input.source ?? "hasna.events",
12320
+ type: input.type ?? "events.test",
12321
+ subject: input.subject ?? id,
12322
+ severity: input.severity ?? "info",
12323
+ data: input.data ?? { test: true },
12324
+ message: input.message ?? "Hasna events test delivery",
12325
+ dedupeKey: input.dedupeKey,
12326
+ schemaVersion: input.schemaVersion,
12327
+ metadata: input.metadata,
12328
+ time: input.time,
12329
+ id: input.id
12330
+ });
12331
+ const eventForChannel = await this.applyRedaction(event, channel);
12332
+ const result = await this.deliverWithRetry(eventForChannel, channel);
12333
+ await this.store.appendDelivery(result);
12334
+ return result;
12335
+ }
12336
+ async replay(options = {}) {
12337
+ const events = (await this.store.listEvents()).filter((event) => {
12338
+ if (options.eventId && event.id !== options.eventId)
12339
+ return false;
12340
+ if (options.source && event.source !== options.source)
12341
+ return false;
12342
+ if (options.type && event.type !== options.type)
12343
+ return false;
12344
+ return true;
12345
+ });
12346
+ if (options.dryRun)
12347
+ return { events, deliveries: [] };
12348
+ const deliveries = [];
12349
+ for (const event of events) {
12350
+ deliveries.push(...await this.deliver(event));
12351
+ }
12352
+ return { events, deliveries };
12353
+ }
12354
+ async applyRedaction(event, channel) {
12355
+ let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
12356
+ for (const redactor of this.redactors) {
12357
+ next = await redactor(next, channel);
12358
+ }
12359
+ return next;
12360
+ }
12361
+ async deliverWithRetry(event, channel) {
12362
+ const policy = normalizeRetryPolicy(channel.retry);
12363
+ const attempts = [];
12364
+ for (let index = 0;index < policy.maxAttempts; index += 1) {
12365
+ const attempt = await dispatchChannel(event, channel, this.transportOptions);
12366
+ attempt.attempt = index + 1;
12367
+ if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
12368
+ attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
12369
+ }
12370
+ attempts.push(attempt);
12371
+ if (attempt.status !== "failed")
12372
+ break;
12373
+ if (attempt.nextBackoffMs)
12374
+ await Bun.sleep(attempt.nextBackoffMs);
12375
+ }
12376
+ return createDeliveryResult(event, channel, attempts);
12377
+ }
12378
+ }
12379
+ function redactPaths(event, paths, replacement = "[REDACTED]") {
12380
+ if (paths.length === 0)
12381
+ return event;
12382
+ const copy = structuredClone(event);
12383
+ for (const path of paths) {
12384
+ setPath(copy, path, replacement);
12385
+ }
12386
+ return copy;
12387
+ }
12388
+ function sanitizeChannelForOutput(channel) {
12389
+ const copy = structuredClone(channel);
12390
+ if (copy.webhook?.secret)
12391
+ copy.webhook.secret = "[REDACTED]";
12392
+ if (copy.command?.env) {
12393
+ copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey(key) ? "[REDACTED]" : value]));
12394
+ }
12395
+ return copy;
12396
+ }
12397
+ function sanitizeChannelsForOutput(channels) {
12398
+ return channels.map(sanitizeChannelForOutput);
12399
+ }
12400
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
12401
+ return redactValue(event, replacement);
12402
+ }
12403
+ function shouldRedactKey(key) {
12404
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
12405
+ }
12406
+ function redactValue(value, replacement) {
12407
+ if (Array.isArray(value))
12408
+ return value.map((item) => redactValue(item, replacement));
12409
+ if (!value || typeof value !== "object")
12410
+ return value;
12411
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
12412
+ key,
12413
+ shouldRedactKey(key) ? replacement : redactValue(item, replacement)
12414
+ ]));
12415
+ }
12416
+ function setPath(input, path, replacement) {
12417
+ const parts = path.split(".");
12418
+ let cursor = input;
12419
+ for (const part of parts.slice(0, -1)) {
12420
+ const next = cursor[part];
12421
+ if (!next || typeof next !== "object")
12422
+ return;
12423
+ cursor = next;
12424
+ }
12425
+ const last = parts.at(-1);
12426
+ if (last && last in cursor)
12427
+ cursor[last] = replacement;
12428
+ }
12429
+ function normalizeTime(value) {
12430
+ if (!value)
12431
+ return new Date().toISOString();
12432
+ return value instanceof Date ? value.toISOString() : value;
12433
+ }
12434
+ function normalizeRetryPolicy(policy) {
12435
+ return {
12436
+ maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
12437
+ backoffMs: Math.max(0, policy?.backoffMs ?? 250),
12438
+ multiplier: Math.max(1, policy?.multiplier ?? 2)
12439
+ };
12440
+ }
12441
+ function parseJsonObject(value, fallback) {
12442
+ if (!value)
12443
+ return fallback;
12444
+ const parsed = JSON.parse(value);
12445
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
12446
+ throw new Error("Expected a JSON object");
12447
+ }
12448
+ return parsed;
12449
+ }
12450
+ function parseHeaders(values) {
12451
+ if (!values?.length)
12452
+ return;
12453
+ const headers = {};
12454
+ for (const value of values) {
12455
+ const separator = value.indexOf("=");
12456
+ if (separator === -1)
12457
+ throw new Error(`Invalid header, expected name=value: ${value}`);
12458
+ headers[value.slice(0, separator)] = value.slice(separator + 1);
12459
+ }
12460
+ return headers;
12461
+ }
12462
+ function parseFilter(options) {
12463
+ const filter2 = {};
12464
+ if (options.source)
12465
+ filter2.source = options.source;
12466
+ if (options.type)
12467
+ filter2.type = options.type;
12468
+ if (options.subject)
12469
+ filter2.subject = options.subject;
12470
+ if (options.severity)
12471
+ filter2.severity = options.severity;
12472
+ return Object.keys(filter2).length > 0 ? [filter2] : undefined;
12473
+ }
12474
+ function createClient(options) {
12475
+ if (options.createClient)
12476
+ return options.createClient();
12477
+ return new EventsClient({ store: new JsonEventsStore(options.dataDir) });
12478
+ }
12479
+ function print(value, json, text) {
12480
+ if (json)
12481
+ console.log(JSON.stringify(value, null, 2));
12482
+ else
12483
+ console.log(text);
12484
+ }
12485
+ function hasJsonOption(options) {
12486
+ return Boolean(options?.json || options?.opts?.().json || options?.optsWithGlobals?.().json || options?.parent?.opts?.().json || options?.parent?.optsWithGlobals?.().json);
12487
+ }
12488
+ function wantsJson(actionOptions, command) {
12489
+ return hasJsonOption(actionOptions) || hasJsonOption(command);
12490
+ }
12491
+ function registerWebhookCommands(program2, options) {
12492
+ const webhooks = program2.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
12493
+ webhooks.command("add").description("Add or replace a webhook or command subscription").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Subscription/channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions, command) => {
12494
+ const timestamp = new Date().toISOString();
12495
+ const channel = {
12496
+ id: actionOptions.id,
12497
+ name: actionOptions.name,
12498
+ enabled: !actionOptions.disabled,
12499
+ transport: actionOptions.transport,
12500
+ filters: parseFilter(actionOptions),
12501
+ retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
12502
+ redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
12503
+ createdAt: timestamp,
12504
+ updatedAt: timestamp
12505
+ };
12506
+ if (actionOptions.transport === "webhook") {
12507
+ channel.webhook = { url: target, secret: actionOptions.secret, headers: parseHeaders(actionOptions.header), timeoutMs: actionOptions.timeoutMs };
12508
+ } else if (actionOptions.transport === "command") {
12509
+ channel.command = { command: target, args: actionOptions.arg ?? [], timeoutMs: actionOptions.timeoutMs };
12510
+ } else {
12511
+ throw new Error(`Transport ${actionOptions.transport} is reserved for future use and cannot be added yet`);
12512
+ }
12513
+ const saved = await createClient(options).addChannel(channel);
12514
+ print(sanitizeChannelForOutput(saved), wantsJson(actionOptions, command), `Added ${saved.transport} channel ${saved.id}`);
12515
+ });
12516
+ webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
12517
+ const channels = await createClient(options).listChannels();
12518
+ if (wantsJson(actionOptions, command)) {
12519
+ console.log(JSON.stringify(sanitizeChannelsForOutput(channels), null, 2));
12520
+ return;
12521
+ }
12522
+ if (!channels.length) {
12523
+ console.log("No channels configured.");
12524
+ return;
12525
+ }
12526
+ for (const channel of channels) {
12527
+ console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
12528
+ }
12529
+ });
12530
+ webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
12531
+ const removed = await createClient(options).removeChannel(id);
12532
+ print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
12533
+ });
12534
+ webhooks.command("test").description("Send a test event to one subscription").argument("<id>", "Subscription/channel identifier").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
12535
+ const result = await createClient(options).testChannel(id, {
12536
+ source: options.source,
12537
+ type: actionOptions.type,
12538
+ subject: actionOptions.subject ?? id,
12539
+ message: actionOptions.message,
12540
+ data: parseJsonObject(actionOptions.data, { test: true })
12541
+ });
12542
+ print(result, wantsJson(actionOptions, command), `${result.status}: ${result.channelId}`);
12543
+ });
12544
+ return webhooks;
12545
+ }
12546
+ function registerEventCommands(program2, options) {
12547
+ const events = program2.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
12548
+ events.command("emit").description("Emit an event from this app").argument("<type>", "Event type").option("--source <source>", "Event source override").option("--subject <subject>", "Event subject").option("--severity <severity>", "Event severity", "info").option("--message <message>", "Event message").option("--dedupe-key <key>", "Dedupe key").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--no-deliver", "Record without delivering").option("--no-dedupe", "Allow duplicate id/dedupeKey events").option("-j, --json", "Print JSON output", false).action(async (type, actionOptions, command) => {
12549
+ const result = await createClient(options).emit({
12550
+ source: actionOptions.source ?? options.source,
12551
+ type,
12552
+ subject: actionOptions.subject,
12553
+ severity: actionOptions.severity,
12554
+ message: actionOptions.message,
12555
+ dedupeKey: actionOptions.dedupeKey,
12556
+ data: parseJsonObject(actionOptions.data, {}),
12557
+ metadata: parseJsonObject(actionOptions.metadata, {})
12558
+ }, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
12559
+ print(result, wantsJson(actionOptions, command), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
12560
+ });
12561
+ events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", "Limit results", parseNumber).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
12562
+ let rows = await createClient(options).listEvents();
12563
+ if (actionOptions.source)
12564
+ rows = rows.filter((event) => event.source === actionOptions.source);
12565
+ if (actionOptions.type)
12566
+ rows = rows.filter((event) => event.type === actionOptions.type);
12567
+ if (actionOptions.limit)
12568
+ rows = rows.slice(-actionOptions.limit);
12569
+ if (wantsJson(actionOptions, command)) {
12570
+ console.log(JSON.stringify(rows, null, 2));
12571
+ return;
12572
+ }
12573
+ if (!rows.length) {
12574
+ console.log("No events recorded.");
12575
+ return;
12576
+ }
12577
+ for (const event of rows)
12578
+ console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
12579
+ });
12580
+ events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
12581
+ const result = await createClient(options).replay({
12582
+ eventId: actionOptions.id,
12583
+ source: actionOptions.source,
12584
+ type: actionOptions.type,
12585
+ dryRun: actionOptions.dryRun
12586
+ });
12587
+ print(result, wantsJson(actionOptions, command), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
12588
+ });
12589
+ return events;
12590
+ }
12591
+ function registerEventsCommands(program2, options) {
12592
+ registerWebhookCommands(program2, options);
12593
+ registerEventCommands(program2, options);
12594
+ }
12595
+ function parseNumber(value) {
12596
+ const parsed = Number(value);
12597
+ if (!Number.isFinite(parsed))
12598
+ throw new Error(`Expected a number, got ${value}`);
12599
+ return parsed;
12600
+ }
12601
+ function collectValues(value, previous) {
12602
+ previous.push(value);
12603
+ return previous;
12604
+ }
12605
+
11922
12606
  // src/cli/cmd-setup.ts
11923
12607
  init_config();
11924
12608
 
@@ -12510,11 +13194,11 @@ async function ensureAllPgDatabases() {
12510
13194
 
12511
13195
  // src/sync-schedule.ts
12512
13196
  init_config();
12513
- import { join as join5, dirname as dirname2 } from "path";
12514
- import { existsSync as existsSync5, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
12515
- import { homedir as homedir5, platform as platform2 } from "os";
13197
+ import { join as join6, dirname as dirname2 } from "path";
13198
+ import { existsSync as existsSync6, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
13199
+ import { homedir as homedir6, platform as platform2 } from "os";
12516
13200
  var SERVICE_NAME = "hasna-cloud-sync";
12517
- var CONFIG_DIR2 = join5(homedir5(), ".hasna", "cloud");
13201
+ var CONFIG_DIR2 = join6(homedir6(), ".hasna", "cloud");
12518
13202
  function parseInterval(input) {
12519
13203
  const trimmed = input.trim().toLowerCase();
12520
13204
  const hourMatch = trimmed.match(/^(\d+)\s*h$/);
@@ -12555,34 +13239,34 @@ function minutesToCron(minutes) {
12555
13239
  }
12556
13240
  function getWorkerPath() {
12557
13241
  const dir = typeof import.meta.dir === "string" ? import.meta.dir : dirname2(import.meta.url.replace("file://", ""));
12558
- const tsPath = join5(dir, "scheduled-sync.ts");
12559
- const jsPath = join5(dir, "scheduled-sync.js");
13242
+ const tsPath = join6(dir, "scheduled-sync.ts");
13243
+ const jsPath = join6(dir, "scheduled-sync.js");
12560
13244
  try {
12561
- if (existsSync5(tsPath))
13245
+ if (existsSync6(tsPath))
12562
13246
  return tsPath;
12563
13247
  } catch {}
12564
13248
  return jsPath;
12565
13249
  }
12566
13250
  function getBunPath() {
12567
13251
  const candidates = [
12568
- join5(homedir5(), ".bun", "bin", "bun"),
13252
+ join6(homedir6(), ".bun", "bin", "bun"),
12569
13253
  "/usr/local/bin/bun",
12570
13254
  "/usr/bin/bun"
12571
13255
  ];
12572
13256
  for (const p of candidates) {
12573
- if (existsSync5(p))
13257
+ if (existsSync6(p))
12574
13258
  return p;
12575
13259
  }
12576
13260
  return "bun";
12577
13261
  }
12578
13262
  function getLaunchdPlistPath() {
12579
- return join5(homedir5(), "Library", "LaunchAgents", `com.hasna.cloud-sync.plist`);
13263
+ return join6(homedir6(), "Library", "LaunchAgents", `com.hasna.cloud-sync.plist`);
12580
13264
  }
12581
13265
  function createLaunchdPlist(intervalMinutes) {
12582
13266
  const workerPath = getWorkerPath();
12583
13267
  const bunPath = getBunPath();
12584
- const logPath = join5(CONFIG_DIR2, "sync.log");
12585
- const errorLogPath = join5(CONFIG_DIR2, "sync-error.log");
13268
+ const logPath = join6(CONFIG_DIR2, "sync.log");
13269
+ const errorLogPath = join6(CONFIG_DIR2, "sync-error.log");
12586
13270
  return `<?xml version="1.0" encoding="UTF-8"?>
12587
13271
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
12588
13272
  <plist version="1.0">
@@ -12608,7 +13292,7 @@ function createLaunchdPlist(intervalMinutes) {
12608
13292
  <key>PATH</key>
12609
13293
  <string>${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}</string>
12610
13294
  <key>HOME</key>
12611
- <string>${homedir5()}</string>
13295
+ <string>${homedir6()}</string>
12612
13296
  </dict>
12613
13297
  </dict>
12614
13298
  </plist>`;
@@ -12633,7 +13317,7 @@ async function removeLaunchd() {
12633
13317
  } catch {}
12634
13318
  }
12635
13319
  function getSystemdDir() {
12636
- return join5(homedir5(), ".config", "systemd", "user");
13320
+ return join6(homedir6(), ".config", "systemd", "user");
12637
13321
  }
12638
13322
  function createSystemdService() {
12639
13323
  const workerPath = getWorkerPath();
@@ -12645,7 +13329,7 @@ After=network.target
12645
13329
  [Service]
12646
13330
  Type=oneshot
12647
13331
  ExecStart=${bunPath} run ${workerPath}
12648
- Environment=HOME=${homedir5()}
13332
+ Environment=HOME=${homedir6()}
12649
13333
  Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
12650
13334
 
12651
13335
  [Install]
@@ -12668,8 +13352,8 @@ WantedBy=timers.target
12668
13352
  async function registerSystemd(intervalMinutes) {
12669
13353
  const dir = getSystemdDir();
12670
13354
  mkdirSync3(dir, { recursive: true });
12671
- writeFileSync2(join5(dir, `${SERVICE_NAME}.service`), createSystemdService());
12672
- writeFileSync2(join5(dir, `${SERVICE_NAME}.timer`), createSystemdTimer(intervalMinutes));
13355
+ writeFileSync2(join6(dir, `${SERVICE_NAME}.service`), createSystemdService());
13356
+ writeFileSync2(join6(dir, `${SERVICE_NAME}.timer`), createSystemdTimer(intervalMinutes));
12673
13357
  await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
12674
13358
  await Bun.spawn(["systemctl", "--user", "enable", "--now", `${SERVICE_NAME}.timer`]).exited;
12675
13359
  }
@@ -12679,10 +13363,10 @@ async function removeSystemd() {
12679
13363
  } catch {}
12680
13364
  const dir = getSystemdDir();
12681
13365
  try {
12682
- unlinkSync(join5(dir, `${SERVICE_NAME}.service`));
13366
+ unlinkSync(join6(dir, `${SERVICE_NAME}.service`));
12683
13367
  } catch {}
12684
13368
  try {
12685
- unlinkSync(join5(dir, `${SERVICE_NAME}.timer`));
13369
+ unlinkSync(join6(dir, `${SERVICE_NAME}.timer`));
12686
13370
  } catch {}
12687
13371
  try {
12688
13372
  await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
@@ -12719,9 +13403,9 @@ function getSyncScheduleStatus() {
12719
13403
  let mechanism = "none";
12720
13404
  if (registered) {
12721
13405
  if (platform2() === "darwin") {
12722
- mechanism = existsSync5(getLaunchdPlistPath()) ? "launchd" : "none";
13406
+ mechanism = existsSync6(getLaunchdPlistPath()) ? "launchd" : "none";
12723
13407
  } else {
12724
- mechanism = existsSync5(join5(getSystemdDir(), `${SERVICE_NAME}.timer`)) ? "systemd" : "none";
13408
+ mechanism = existsSync6(join6(getSystemdDir(), `${SERVICE_NAME}.timer`)) ? "systemd" : "none";
12725
13409
  }
12726
13410
  }
12727
13411
  return {
@@ -12913,8 +13597,8 @@ function purgeResolvedConflicts(db) {
12913
13597
  // src/scheduled-sync.ts
12914
13598
  init_config();
12915
13599
  init_adapter();
12916
- import { existsSync as existsSync6, readdirSync as readdirSync3 } from "fs";
12917
- import { join as join6 } from "path";
13600
+ import { existsSync as existsSync7, readdirSync as readdirSync3 } from "fs";
13601
+ import { join as join7 } from "path";
12918
13602
 
12919
13603
  // src/sync-incremental.ts
12920
13604
  init_machines();
@@ -13040,10 +13724,10 @@ function incrementalSyncPush(local, remote, tables, options = {}) {
13040
13724
  if (rows.length === 0) {
13041
13725
  stat.skipped_rows = stat.total_rows;
13042
13726
  }
13043
- const now = new Date().toISOString();
13727
+ const now2 = new Date().toISOString();
13044
13728
  upsertSyncMeta(local, {
13045
13729
  table_name: table,
13046
- last_synced_at: now,
13730
+ last_synced_at: now2,
13047
13731
  last_synced_row_count: stat.synced_rows,
13048
13732
  direction: "push"
13049
13733
  });
@@ -13096,10 +13780,10 @@ function incrementalSyncPull(remote, local, tables, options = {}) {
13096
13780
  if (rows.length === 0) {
13097
13781
  stat.skipped_rows = stat.total_rows;
13098
13782
  }
13099
- const now = new Date().toISOString();
13783
+ const now2 = new Date().toISOString();
13100
13784
  upsertSyncMeta(local, {
13101
13785
  table_name: table,
13102
- last_synced_at: now,
13786
+ last_synced_at: now2,
13103
13787
  last_synced_row_count: stat.synced_rows,
13104
13788
  direction: "pull"
13105
13789
  });
@@ -13121,8 +13805,8 @@ function discoverSyncableServices2() {
13121
13805
  for (const entry of entries) {
13122
13806
  if (!entry.isDirectory())
13123
13807
  continue;
13124
- const dbPath = join6(hasnaDir, entry.name, `${entry.name}.db`);
13125
- if (existsSync6(dbPath)) {
13808
+ const dbPath = join7(hasnaDir, entry.name, `${entry.name}.db`);
13809
+ if (existsSync7(dbPath)) {
13126
13810
  services.push(entry.name);
13127
13811
  }
13128
13812
  }
@@ -13144,8 +13828,8 @@ async function runScheduledSync() {
13144
13828
  errors: []
13145
13829
  };
13146
13830
  try {
13147
- const dbPath = join6(getDataDir(service), `${service}.db`);
13148
- if (!existsSync6(dbPath)) {
13831
+ const dbPath = join7(getDataDir(service), `${service}.db`);
13832
+ if (!existsSync7(dbPath)) {
13149
13833
  continue;
13150
13834
  }
13151
13835
  const local = new SqliteAdapter(dbPath);
@@ -13188,26 +13872,26 @@ async function runScheduledSync() {
13188
13872
  }
13189
13873
 
13190
13874
  // src/cli/cmd-sync.ts
13191
- import { existsSync as existsSync8, statSync as statSync5 } from "fs";
13192
- import { join as join8 } from "path";
13193
- import { homedir as homedir7 } from "os";
13875
+ import { existsSync as existsSync9, statSync as statSync5 } from "fs";
13876
+ import { join as join9 } from "path";
13877
+ import { homedir as homedir8 } from "os";
13194
13878
 
13195
13879
  // src/daemon-sync.ts
13196
13880
  init_adapter();
13197
13881
  init_config();
13198
13882
  init_discover();
13199
13883
  init_dotfile();
13200
- import { spawn } from "child_process";
13884
+ import { spawn as spawn2 } from "child_process";
13201
13885
  import {
13202
- existsSync as existsSync7,
13886
+ existsSync as existsSync8,
13203
13887
  mkdirSync as mkdirSync4,
13204
13888
  readFileSync as readFileSync3,
13205
13889
  statSync as statSync4,
13206
13890
  writeFileSync as writeFileSync3
13207
13891
  } from "fs";
13208
- import { homedir as homedir6 } from "os";
13209
- import { join as join7 } from "path";
13210
- var DAEMON_STATE_PATH = join7(homedir6(), ".hasna", "cloud", "daemon-state.json");
13892
+ import { homedir as homedir7 } from "os";
13893
+ import { join as join8 } from "path";
13894
+ var DAEMON_STATE_PATH = join8(homedir7(), ".hasna", "cloud", "daemon-state.json");
13211
13895
  var defaultDaemonAdapterFactory = {
13212
13896
  getLocalDbPath: (service) => getDbPath(service),
13213
13897
  openLocal: (service) => new SqliteAdapter(getDbPath(service)),
@@ -13275,7 +13959,7 @@ function createDefaultDaemonState() {
13275
13959
  };
13276
13960
  }
13277
13961
  function readDaemonState() {
13278
- if (!existsSync7(DAEMON_STATE_PATH)) {
13962
+ if (!existsSync8(DAEMON_STATE_PATH)) {
13279
13963
  return createDefaultDaemonState();
13280
13964
  }
13281
13965
  try {
@@ -13292,7 +13976,7 @@ function readDaemonState() {
13292
13976
  }
13293
13977
  }
13294
13978
  function writeDaemonState(state) {
13295
- mkdirSync4(join7(homedir6(), ".hasna", "cloud"), { recursive: true });
13979
+ mkdirSync4(join8(homedir7(), ".hasna", "cloud"), { recursive: true });
13296
13980
  writeFileSync3(DAEMON_STATE_PATH, JSON.stringify(state, null, 2) + `
13297
13981
  `, "utf-8");
13298
13982
  }
@@ -13430,8 +14114,8 @@ function runDaemonPass(config, state = createDefaultDaemonState(), options = {})
13430
14114
  const nextState = cloneState(state);
13431
14115
  const adapterFactory = options.adapterFactory ?? defaultDaemonAdapterFactory;
13432
14116
  const services = options.services ?? resolveDaemonServices(config);
13433
- const now = nowIso();
13434
- const nowMs = Date.parse(now);
14117
+ const now2 = nowIso();
14118
+ const nowMs = Date.parse(now2);
13435
14119
  const summary = {
13436
14120
  services,
13437
14121
  pushed_services: 0,
@@ -13440,7 +14124,7 @@ function runDaemonPass(config, state = createDefaultDaemonState(), options = {})
13440
14124
  pulled_rows: 0,
13441
14125
  errors: []
13442
14126
  };
13443
- nextState.updated_at = now;
14127
+ nextState.updated_at = now2;
13444
14128
  nextState.status = config.paused ? "paused" : "running";
13445
14129
  if (config.paused) {
13446
14130
  scanConfiguredFiles(config, nextState);
@@ -13448,7 +14132,7 @@ function runDaemonPass(config, state = createDefaultDaemonState(), options = {})
13448
14132
  }
13449
14133
  for (const service of services) {
13450
14134
  const dbPath = adapterFactory.getLocalDbPath(service);
13451
- if (!existsSync7(dbPath))
14135
+ if (!existsSync8(dbPath))
13452
14136
  continue;
13453
14137
  const serviceState = getServiceState(nextState, service);
13454
14138
  let local = null;
@@ -13468,10 +14152,10 @@ function runDaemonPass(config, state = createDefaultDaemonState(), options = {})
13468
14152
  });
13469
14153
  const pushSummary = summarizeStats(pushStats);
13470
14154
  serviceState.last_local_db_mtime_ms = localStats.mtimeMs;
13471
- serviceState.last_push_at = now;
14155
+ serviceState.last_push_at = now2;
13472
14156
  serviceState.last_error = pushSummary.errors[0] ?? null;
13473
- recordTableRuns(serviceState, duePushTables, "push", now);
13474
- nextState.last_push_at = now;
14157
+ recordTableRuns(serviceState, duePushTables, "push", now2);
14158
+ nextState.last_push_at = now2;
13475
14159
  summary.pushed_services++;
13476
14160
  summary.pushed_rows += pushSummary.rows;
13477
14161
  summary.errors.push(...pushSummary.errors.map((error) => `[${service}] ${error}`));
@@ -13486,10 +14170,10 @@ function runDaemonPass(config, state = createDefaultDaemonState(), options = {})
13486
14170
  conflictStrategy: config.conflict_strategy
13487
14171
  });
13488
14172
  const pullSummary = summarizeStats(pullStats);
13489
- serviceState.last_pull_at = now;
14173
+ serviceState.last_pull_at = now2;
13490
14174
  serviceState.last_error = pullSummary.errors[0] ?? null;
13491
- recordTableRuns(serviceState, duePullTables, "pull", now);
13492
- nextState.last_pull_at = now;
14175
+ recordTableRuns(serviceState, duePullTables, "pull", now2);
14176
+ nextState.last_pull_at = now2;
13493
14177
  summary.pulled_services++;
13494
14178
  summary.pulled_rows += pullSummary.rows;
13495
14179
  summary.errors.push(...pullSummary.errors.map((error) => `[${service}] ${error}`));
@@ -13555,7 +14239,7 @@ function startDaemon(overrides = {}) {
13555
14239
  if (currentStatus.running) {
13556
14240
  return currentStatus;
13557
14241
  }
13558
- const child = spawn(process.execPath, getDaemonArgs(), {
14242
+ const child = spawn2(process.execPath, getDaemonArgs(), {
13559
14243
  detached: true,
13560
14244
  stdio: "ignore",
13561
14245
  env: {
@@ -13684,8 +14368,8 @@ function normalizeConflictStrategy(strategy) {
13684
14368
  // src/cli/cmd-sync.ts
13685
14369
  function logSync(direction, service, rows, errors2) {
13686
14370
  try {
13687
- const logDir = join8(homedir7(), ".hasna", "cloud");
13688
- const logPath = join8(logDir, "sync.log");
14371
+ const logDir = join9(homedir8(), ".hasna", "cloud");
14372
+ const logPath = join9(logDir, "sync.log");
13689
14373
  const { mkdirSync: mkdirSync5, appendFileSync } = __require("fs");
13690
14374
  mkdirSync5(logDir, { recursive: true });
13691
14375
  const ts = new Date().toISOString();
@@ -13702,7 +14386,7 @@ function registerSyncCommands(syncCmd) {
13702
14386
  registerScheduleCommand(syncCmd);
13703
14387
  registerDaemonCommand(syncCmd);
13704
14388
  }
13705
- function collectValues(value, previous = []) {
14389
+ function collectValues2(value, previous = []) {
13706
14390
  previous.push(value);
13707
14391
  return previous;
13708
14392
  }
@@ -13948,7 +14632,7 @@ function registerStatusCommand(syncCmd) {
13948
14632
  const statuses = [];
13949
14633
  for (const service of services) {
13950
14634
  const dbPath = getDbPath(service);
13951
- const localExists = existsSync8(dbPath);
14635
+ const localExists = existsSync9(dbPath);
13952
14636
  let localSize = "—";
13953
14637
  let tableCount = 0;
13954
14638
  if (localExists) {
@@ -14194,7 +14878,7 @@ No syncable services found (no .db files in ~/.hasna/).`);
14194
14878
  });
14195
14879
  }
14196
14880
  function registerDaemonCommand(syncCmd) {
14197
- syncCmd.command("daemon").description("Manage continuous background sync daemon").option("--start", "Start the daemon").option("--stop", "Stop the daemon").option("--pause", "Pause the daemon without stopping it").option("--resume", "Resume a paused daemon").option("--status", "Show daemon status").option("--now", "Run one daemon pass immediately").option("--foreground", "Run the daemon in the foreground").option("--watch <seconds>", "Local database scan interval in seconds").option("--pull <seconds>", "Remote pull interval in seconds").option("--push-debounce <seconds>", "Minimum seconds between auto-pushes").option("--conflict-strategy <strategy>", "newest-wins, local-wins, or remote-wins").option("--service <name>", "Restrict daemon to a service (repeatable)", collectValues, []).option("--table-interval <rule>", "Per-table interval, e.g. todos.tasks=30 or todos:tasks=30", collectValues, []).option("--file <rule>", "Track a file path for daemon status, e.g. ~/.claude/agents=30", collectValues, []).addOption(new Option2("--run", "Internal daemon worker").hideHelp()).action((opts) => {
14881
+ syncCmd.command("daemon").description("Manage continuous background sync daemon").option("--start", "Start the daemon").option("--stop", "Stop the daemon").option("--pause", "Pause the daemon without stopping it").option("--resume", "Resume a paused daemon").option("--status", "Show daemon status").option("--now", "Run one daemon pass immediately").option("--foreground", "Run the daemon in the foreground").option("--watch <seconds>", "Local database scan interval in seconds").option("--pull <seconds>", "Remote pull interval in seconds").option("--push-debounce <seconds>", "Minimum seconds between auto-pushes").option("--conflict-strategy <strategy>", "newest-wins, local-wins, or remote-wins").option("--service <name>", "Restrict daemon to a service (repeatable)", collectValues2, []).option("--table-interval <rule>", "Per-table interval, e.g. todos.tasks=30 or todos:tasks=30", collectValues2, []).option("--file <rule>", "Track a file path for daemon status, e.g. ~/.claude/agents=30", collectValues2, []).addOption(new Option2("--run", "Internal daemon worker").hideHelp()).action((opts) => {
14198
14882
  const currentConfig = getDaemonConfig();
14199
14883
  const selectedServices = opts.service.length > 0 ? Array.from(new Set(opts.service)) : currentConfig.services;
14200
14884
  const tableIntervalRules = opts.tableInterval;
@@ -14370,17 +15054,17 @@ function ensureFeedbackTable(db) {
14370
15054
  function saveFeedback(db, feedback) {
14371
15055
  ensureFeedbackTable(db);
14372
15056
  const id = feedback.id ?? Math.random().toString(36).slice(2) + Date.now().toString(36);
14373
- const now = new Date().toISOString();
15057
+ const now2 = new Date().toISOString();
14374
15058
  const machineId = feedback.machine_id ?? hostname2();
14375
15059
  db.run(`INSERT INTO feedback (id, service, version, message, email, machine_id, created_at)
14376
- VALUES (?, ?, ?, ?, ?, ?, ?)`, id, feedback.service, feedback.version ?? "", feedback.message, feedback.email ?? "", machineId, feedback.created_at ?? now);
15060
+ VALUES (?, ?, ?, ?, ?, ?, ?)`, id, feedback.service, feedback.version ?? "", feedback.message, feedback.email ?? "", machineId, feedback.created_at ?? now2);
14377
15061
  return id;
14378
15062
  }
14379
15063
  async function sendFeedback(feedback, db) {
14380
15064
  const config = getCloudConfig();
14381
15065
  const id = feedback.id ?? Math.random().toString(36).slice(2) + Date.now().toString(36);
14382
15066
  const machineId = feedback.machine_id ?? hostname2();
14383
- const now = new Date().toISOString();
15067
+ const now2 = new Date().toISOString();
14384
15068
  const payload = {
14385
15069
  id,
14386
15070
  service: feedback.service,
@@ -14388,7 +15072,7 @@ async function sendFeedback(feedback, db) {
14388
15072
  message: feedback.message,
14389
15073
  email: feedback.email ?? "",
14390
15074
  machine_id: machineId,
14391
- created_at: feedback.created_at ?? now
15075
+ created_at: feedback.created_at ?? now2
14392
15076
  };
14393
15077
  try {
14394
15078
  const res = await fetch(config.feedback_endpoint, {
@@ -14440,14 +15124,14 @@ function registerFeedbackCommand(program3) {
14440
15124
  init_config();
14441
15125
  init_adapter();
14442
15126
  init_discover();
14443
- import { existsSync as existsSync9 } from "fs";
14444
- import { join as join9 } from "path";
14445
- import { homedir as homedir8 } from "os";
15127
+ import { existsSync as existsSync10 } from "fs";
15128
+ import { join as join10 } from "path";
15129
+ import { homedir as homedir9 } from "os";
14446
15130
  function registerDoctorCommand(program3) {
14447
15131
  program3.command("doctor").description("Comprehensive health check for cloud sync setup").action(async () => {
14448
15132
  const checks = [];
14449
- const configPath = join9(homedir8(), ".hasna", "cloud", "config.json");
14450
- checks.push(existsSync9(configPath) ? { name: "Config file", status: "pass", detail: configPath } : { name: "Config file", status: "fail", detail: "Missing. Run `cloud setup`." });
15133
+ const configPath = join10(homedir9(), ".hasna", "cloud", "config.json");
15134
+ checks.push(existsSync10(configPath) ? { name: "Config file", status: "pass", detail: configPath } : { name: "Config file", status: "fail", detail: "Missing. Run `cloud setup`." });
14451
15135
  const config = getCloudConfig();
14452
15136
  checks.push(config.mode === "hybrid" || config.mode === "cloud" ? { name: "Sync mode", status: "pass", detail: config.mode } : { name: "Sync mode", status: "fail", detail: `"${config.mode}" — sync disabled. Run \`cloud setup --mode hybrid\`.` });
14453
15137
  checks.push(config.rds.host ? { name: "RDS host", status: "pass", detail: config.rds.host } : { name: "RDS host", status: "fail", detail: "Not configured. Run `cloud setup`." });
@@ -14466,7 +15150,7 @@ function registerDoctorCommand(program3) {
14466
15150
  checks.push({ name: "PG connection", status: "fail", detail: "Skipped — missing host or password" });
14467
15151
  }
14468
15152
  const caPath = process.env.NODE_EXTRA_CA_CERTS;
14469
- if (caPath && existsSync9(caPath)) {
15153
+ if (caPath && existsSync10(caPath)) {
14470
15154
  checks.push({ name: "SSL CA cert", status: "pass", detail: caPath });
14471
15155
  } else if (caPath) {
14472
15156
  checks.push({ name: "SSL CA cert", status: "warn", detail: `NODE_EXTRA_CA_CERTS set but file missing: ${caPath}` });
@@ -14507,9 +15191,9 @@ init_zod();
14507
15191
  init_adapter();
14508
15192
  init_dotfile();
14509
15193
  init_machines();
14510
- import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
14511
- import { homedir as homedir9 } from "os";
14512
- import { join as join10 } from "path";
15194
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
15195
+ import { homedir as homedir10 } from "os";
15196
+ import { join as join11 } from "path";
14513
15197
  var DaemonConfigSchema2 = exports_external.object({
14514
15198
  enabled: exports_external.boolean().default(false),
14515
15199
  paused: exports_external.boolean().default(false),
@@ -14541,10 +15225,10 @@ var CloudConfigSchema2 = exports_external.object({
14541
15225
  }).default({}),
14542
15226
  daemon: DaemonConfigSchema2
14543
15227
  });
14544
- var CONFIG_DIR3 = join10(homedir9(), ".hasna", "cloud");
14545
- var CONFIG_PATH2 = join10(CONFIG_DIR3, "config.json");
15228
+ var CONFIG_DIR3 = join11(homedir10(), ".hasna", "cloud");
15229
+ var CONFIG_PATH2 = join11(CONFIG_DIR3, "config.json");
14546
15230
  function getCloudConfig2() {
14547
- if (!existsSync10(CONFIG_PATH2)) {
15231
+ if (!existsSync11(CONFIG_PATH2)) {
14548
15232
  return CloudConfigSchema2.parse({});
14549
15233
  }
14550
15234
  try {
@@ -14630,6 +15314,7 @@ class PgAdapterAsync2 {
14630
15314
  // src/cli/index.ts
14631
15315
  var program3 = new Command;
14632
15316
  program3.name("cloud").description("Shared cloud infrastructure \u2014 database adapter, sync engine, feedback, dotfile migration").version("0.1.13");
15317
+ registerEventsCommands(program3, { source: "cloud" });
14633
15318
  registerSetupCommand(program3);
14634
15319
  program3.command("status").description("Show current cloud configuration and connection health").action(async () => {
14635
15320
  const config = getCloudConfig2();