@hasna/configs 0.2.36 → 0.2.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,8 +58,8 @@ Data is stored in `~/.hasna/configs/`.
58
58
 
59
59
  `configs init` now seeds two platform profiles:
60
60
 
61
- - `linux-arm64` for `spark01` / `spark02`
62
- - `macos-arm64` for `apple01` / `apple03`
61
+ - `linux-arm64` for `linux-node-a` / `linux-node-b`
62
+ - `macos-arm64` for `macos-node-a` / `macos-node-b`
63
63
 
64
64
  These profiles resolve machine variables like `{{WORKSPACE_ROOT}}`, `{{BUN_BIN_DIR}}`, `{{BUN_PATH}}`, and `{{PATH_PREFIX}}`, so synced configs can be portable across Linux and macOS arm64 machines.
65
65
 
package/dist/cli/index.js CHANGED
@@ -12694,7 +12694,6 @@ var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
12694
12694
  function getEventsDataDir(override) {
12695
12695
  return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
12696
12696
  }
12697
-
12698
12697
  class JsonEventsStore {
12699
12698
  dataDir;
12700
12699
  channelsPath;
@@ -13342,7 +13341,7 @@ var {
13342
13341
  // src/cli/index.tsx
13343
13342
  init_configs();
13344
13343
  import chalk from "chalk";
13345
- import { existsSync as existsSync12, readFileSync as readFileSync6 } from "fs";
13344
+ import { existsSync as existsSync13, readFileSync as readFileSync7 } from "fs";
13346
13345
  import { homedir as homedir11 } from "os";
13347
13346
  import { join as join13, resolve as resolve4 } from "path";
13348
13347
 
@@ -13600,8 +13599,8 @@ init_configs();
13600
13599
  var PLATFORM_PROFILE_PRESETS = [
13601
13600
  {
13602
13601
  name: "linux-arm64",
13603
- description: "Default Linux arm64 profile for spark01/spark02-style machines",
13604
- selectors: { os: ["linux"], arch: ["arm64"], hostnames: ["spark01", "spark02"] },
13602
+ description: "Default Linux arm64 profile for linux-node-a/linux-node-b-style machines",
13603
+ selectors: { os: ["linux"], arch: ["arm64"], hostnames: ["linux-node-a", "linux-node-b"] },
13605
13604
  variables: {
13606
13605
  WORKSPACE_ROOT: "{{HOME_DIR}}/workspace",
13607
13606
  BUN_BIN_DIR: "{{HOME_DIR}}/.bun/bin",
@@ -13611,8 +13610,8 @@ var PLATFORM_PROFILE_PRESETS = [
13611
13610
  },
13612
13611
  {
13613
13612
  name: "macos-arm64",
13614
- description: "Default macOS arm64 profile for apple01/apple03-style machines",
13615
- selectors: { os: ["macos"], arch: ["arm64"], hostnames: ["apple01", "apple03"] },
13613
+ description: "Default macOS arm64 profile for macos-node-a/macos-node-b-style machines",
13614
+ selectors: { os: ["macos"], arch: ["arm64"], hostnames: ["macos-node-a", "macos-node-b"] },
13616
13615
  variables: {
13617
13616
  WORKSPACE_ROOT: "{{HOME_DIR}}/Workspace",
13618
13617
  BUN_BIN_DIR: "{{HOME_DIR}}/.bun/bin",
@@ -13646,9 +13645,144 @@ function ensurePlatformProfiles(db) {
13646
13645
  return ensured;
13647
13646
  }
13648
13647
 
13649
- // src/cli/index.tsx
13648
+ // src/status.ts
13649
+ init_database();
13650
+ init_configs();
13651
+ import { existsSync as existsSync12, readFileSync as readFileSync6 } from "fs";
13650
13652
  import { createRequire as createRequire2 } from "module";
13651
- var pkg = createRequire2(import.meta.url)("../../package.json");
13653
+
13654
+ // src/db/machines.ts
13655
+ init_database();
13656
+ function listMachines(db) {
13657
+ const d = db || getDatabase();
13658
+ return d.query("SELECT * FROM machines ORDER BY last_applied_at DESC NULLS LAST").all();
13659
+ }
13660
+
13661
+ // src/status.ts
13662
+ init_apply();
13663
+ init_redact();
13664
+ var require2 = createRequire2(import.meta.url);
13665
+ var pkg = require2("../package.json");
13666
+ function activeDatabaseEnv() {
13667
+ if (process.env["HASNA_CONFIGS_DB_PATH"])
13668
+ return "HASNA_CONFIGS_DB_PATH";
13669
+ if (process.env["CONFIGS_DB_PATH"])
13670
+ return "CONFIGS_DB_PATH";
13671
+ return null;
13672
+ }
13673
+ function configuredDatabaseKind() {
13674
+ const value = process.env["HASNA_CONFIGS_DB_PATH"] ?? process.env["CONFIGS_DB_PATH"] ?? "";
13675
+ return value === ":memory:" || value.startsWith("file::memory:") ? "memory" : "file";
13676
+ }
13677
+ function countBy(items, getValue) {
13678
+ const counts = {};
13679
+ for (const item of items) {
13680
+ const value = getValue(item);
13681
+ if (!value)
13682
+ continue;
13683
+ counts[value] = (counts[value] ?? 0) + 1;
13684
+ }
13685
+ return counts;
13686
+ }
13687
+ function tableCount(db, table) {
13688
+ try {
13689
+ const row = db.query(`SELECT COUNT(*) AS count FROM ${table}`).get();
13690
+ return Number(row?.count ?? 0);
13691
+ } catch {
13692
+ return 0;
13693
+ }
13694
+ }
13695
+ function getConfigsStatus(db = getDatabase()) {
13696
+ let databaseReachable = true;
13697
+ let configs = [];
13698
+ let categoryStats = { total: 0 };
13699
+ try {
13700
+ configs = listConfigs(undefined, db);
13701
+ categoryStats = getConfigStats(db);
13702
+ } catch {
13703
+ databaseReachable = false;
13704
+ }
13705
+ const fileConfigs = configs.filter((config) => config.kind === "file");
13706
+ let driftedTargets = 0;
13707
+ let missingTargets = 0;
13708
+ let unredactedSecretFindings = 0;
13709
+ let knownTargets = 0;
13710
+ for (const config of fileConfigs) {
13711
+ unredactedSecretFindings += scanSecrets(config.content, config.format).length;
13712
+ if (!config.target_path)
13713
+ continue;
13714
+ knownTargets += 1;
13715
+ const targetPath = expandPath(config.target_path);
13716
+ if (!existsSync12(targetPath)) {
13717
+ missingTargets += 1;
13718
+ continue;
13719
+ }
13720
+ const disk = readFileSync6(targetPath, "utf-8");
13721
+ const { content: redactedDisk } = redactContent(disk, config.format);
13722
+ if (redactedDisk !== config.content) {
13723
+ driftedTargets += 1;
13724
+ }
13725
+ }
13726
+ const profiles = databaseReachable ? listProfiles(db).length : 0;
13727
+ const machines = databaseReachable ? listMachines(db).length : 0;
13728
+ const profileLinks = databaseReachable ? tableCount(db, "profile_configs") : 0;
13729
+ const snapshots = databaseReachable ? tableCount(db, "config_snapshots") : 0;
13730
+ const byCategory = Object.fromEntries(Object.entries(categoryStats).filter(([key]) => key !== "total"));
13731
+ const status = databaseReachable && driftedTargets === 0 && missingTargets === 0 && unredactedSecretFindings === 0 ? "ok" : "warn";
13732
+ return {
13733
+ service: "configs",
13734
+ schemaVersion: "1.0",
13735
+ package: {
13736
+ name: pkg.name,
13737
+ version: pkg.version
13738
+ },
13739
+ env: {
13740
+ database: {
13741
+ primary: "HASNA_CONFIGS_DB_PATH",
13742
+ fallback: "CONFIGS_DB_PATH",
13743
+ active: activeDatabaseEnv(),
13744
+ kind: configuredDatabaseKind()
13745
+ }
13746
+ },
13747
+ counts: {
13748
+ configs: {
13749
+ total: configs.length,
13750
+ file: fileConfigs.length,
13751
+ reference: configs.filter((config) => config.kind === "reference").length,
13752
+ templates: configs.filter((config) => config.is_template).length
13753
+ },
13754
+ byCategory,
13755
+ byAgent: countBy(configs, (config) => config.agent),
13756
+ byFormat: countBy(configs, (config) => config.format),
13757
+ profiles,
13758
+ profileLinks,
13759
+ machines,
13760
+ snapshots,
13761
+ knownTargets
13762
+ },
13763
+ health: {
13764
+ status,
13765
+ databaseReachable,
13766
+ driftedTargets,
13767
+ missingTargets,
13768
+ unredactedSecretFindings,
13769
+ hasDrift: driftedTargets > 0,
13770
+ hasMissingTargets: missingTargets > 0,
13771
+ hasUnredactedSecrets: unredactedSecretFindings > 0
13772
+ },
13773
+ safety: {
13774
+ includesConfigValues: false,
13775
+ includesPrivatePaths: false,
13776
+ includesHostnames: false,
13777
+ includesSecretValues: false,
13778
+ statusOutputIsMetadataOnly: true
13779
+ }
13780
+ };
13781
+ }
13782
+
13783
+ // src/cli/index.tsx
13784
+ import { createRequire as createRequire3 } from "module";
13785
+ var pkg2 = createRequire3(import.meta.url)("../../package.json");
13652
13786
  function fmtConfig(c, format) {
13653
13787
  if (format === "json")
13654
13788
  return JSON.stringify(c, null, 2);
@@ -13759,11 +13893,11 @@ program.command("show <id>").description("Show a config's content and metadata")
13759
13893
  });
13760
13894
  program.command("add <path>").description("Ingest a file into the config DB").option("-n, --name <name>", "config name (defaults to filename)").option("-c, --category <cat>", "category override").option("-a, --agent <agent>", "agent override").option("-k, --kind <kind>", "kind: file|reference", "file").option("--template", "mark as template (has {{VAR}} placeholders)").action(async (filePath, opts) => {
13761
13895
  const abs = resolve4(filePath);
13762
- if (!existsSync12(abs)) {
13896
+ if (!existsSync13(abs)) {
13763
13897
  console.error(chalk.red(`File not found: ${abs}`));
13764
13898
  process.exit(1);
13765
13899
  }
13766
- const rawContent = readFileSync6(abs, "utf-8");
13900
+ const rawContent = readFileSync7(abs, "utf-8");
13767
13901
  const fmt = detectFormat(abs);
13768
13902
  const { content, redacted, isTemplate: isTemplate2 } = redactContent(rawContent, fmt);
13769
13903
  const targetPath = abs.startsWith(homedir11()) ? abs.replace(homedir11(), "~") : abs;
@@ -13850,7 +13984,7 @@ program.command("sync").description("Sync known AI coding configs from disk into
13850
13984
  if (!entry.isDirectory())
13851
13985
  continue;
13852
13986
  const projDir = join13(absDir, entry.name);
13853
- const hasClaude = existsSync12(join13(projDir, "CLAUDE.md")) || existsSync12(join13(projDir, ".mcp.json")) || existsSync12(join13(projDir, ".claude"));
13987
+ const hasClaude = existsSync13(join13(projDir, "CLAUDE.md")) || existsSync13(join13(projDir, ".mcp.json")) || existsSync13(join13(projDir, ".claude"));
13854
13988
  if (!hasClaude)
13855
13989
  continue;
13856
13990
  const result2 = await syncProject({ projectDir: projDir, dryRun: opts.dryRun });
@@ -13900,7 +14034,7 @@ program.command("import <file>").description("Import configs from a tar.gz bundl
13900
14034
  program.command("whoami").description("Show setup summary").action(async () => {
13901
14035
  const dbPath = process.env["CONFIGS_DB_PATH"] || join13(homedir11(), ".hasna", "configs", "configs.db");
13902
14036
  const stats = getConfigStats();
13903
- console.log(chalk.bold("@hasna/configs") + chalk.dim(" v" + pkg.version));
14037
+ console.log(chalk.bold("@hasna/configs") + chalk.dim(" v" + pkg2.version));
13904
14038
  console.log(chalk.cyan("DB:") + " " + dbPath);
13905
14039
  console.log(chalk.cyan("Total configs:") + " " + (stats["total"] || 0));
13906
14040
  console.log();
@@ -14234,7 +14368,7 @@ command = "${mcpBinary}"
14234
14368
  args = []
14235
14369
  `;
14236
14370
  if (ex(configPath)) {
14237
- const content = readFileSync6(configPath, "utf-8");
14371
+ const content = readFileSync7(configPath, "utf-8");
14238
14372
  if (content.includes("[mcp_servers.configs]")) {
14239
14373
  console.log(chalk.dim("= Already installed in Codex"));
14240
14374
  continue;
@@ -14273,7 +14407,7 @@ mcpCmd.command("uninstall").alias("remove").description("Remove configs MCP serv
14273
14407
  });
14274
14408
  program.command("init").description("First-time setup: sync all known configs, create default profile").option("--force", "delete existing DB and start fresh").action(async (opts) => {
14275
14409
  const dbPath = join13(homedir11(), ".hasna", "configs", "configs.db");
14276
- if (opts.force && existsSync12(dbPath)) {
14410
+ if (opts.force && existsSync13(dbPath)) {
14277
14411
  const { rmSync: rmSync3 } = await import("fs");
14278
14412
  rmSync3(dbPath);
14279
14413
  console.log(chalk.dim("Deleted existing DB."));
@@ -14325,41 +14459,20 @@ DB stats:`));
14325
14459
  console.log(chalk.dim(`
14326
14460
  DB: ${dbPath}`));
14327
14461
  });
14328
- program.command("status").description("Health check: total configs, drift from disk, unredacted secrets").action(async () => {
14329
- const dbPath = join13(homedir11(), ".hasna", "configs", "configs.db");
14330
- const stats = getConfigStats();
14331
- const { statSync: st } = await import("fs");
14332
- const dbSize = existsSync12(dbPath) ? st(dbPath).size : 0;
14333
- console.log(chalk.bold("@hasna/configs") + chalk.dim(` v${pkg.version}`));
14334
- console.log(chalk.cyan("DB:") + ` ${dbPath} (${(dbSize / 1024).toFixed(1)}KB)`);
14335
- console.log(chalk.cyan("Total:") + ` ${stats["total"] || 0} configs
14336
- `);
14337
- const allKnown = listConfigs({ kind: "file" });
14338
- let drifted = 0;
14339
- let missing = 0;
14340
- let secrets = 0;
14341
- let templates = 0;
14342
- for (const c of allKnown) {
14343
- if (!c.target_path)
14344
- continue;
14345
- const path = expandPath(c.target_path);
14346
- if (!existsSync12(path)) {
14347
- missing++;
14348
- continue;
14349
- }
14350
- const disk = readFileSync6(path, "utf-8");
14351
- const { content: redactedDisk } = redactContent(disk, c.format);
14352
- if (redactedDisk !== c.content)
14353
- drifted++;
14354
- if (c.is_template)
14355
- templates++;
14356
- const found = scanSecrets(c.content, c.format);
14357
- secrets += found.length;
14462
+ program.command("status").description("Health check: total configs, drift from disk, unredacted secrets").option("--json", "output metadata-only JSON").action(async (opts) => {
14463
+ const status = getConfigsStatus();
14464
+ if (opts.json) {
14465
+ console.log(JSON.stringify(status, null, 2));
14466
+ return;
14358
14467
  }
14359
- console.log(chalk.cyan("Drifted:") + ` ${drifted === 0 ? chalk.green("0") : chalk.yellow(String(drifted))} (stored \u2260 disk)`);
14360
- console.log(chalk.cyan("Missing:") + ` ${missing === 0 ? chalk.green("0") : chalk.yellow(String(missing))} (file not on disk)`);
14361
- console.log(chalk.cyan("Secrets:") + ` ${secrets === 0 ? chalk.green("0 \u2713") : chalk.red(String(secrets) + " \u26A0")} unredacted`);
14362
- console.log(chalk.cyan("Templates:") + ` ${templates} (with {{VAR}} placeholders)`);
14468
+ console.log(chalk.bold("@hasna/configs") + chalk.dim(` v${pkg2.version}`));
14469
+ console.log(chalk.cyan("Database:") + ` ${status.env.database.kind} (${status.env.database.active ?? "default"})`);
14470
+ console.log(chalk.cyan("Total:") + ` ${status.counts.configs.total} configs
14471
+ `);
14472
+ console.log(chalk.cyan("Drifted:") + ` ${status.health.driftedTargets === 0 ? chalk.green("0") : chalk.yellow(String(status.health.driftedTargets))} (stored differs from disk)`);
14473
+ console.log(chalk.cyan("Missing:") + ` ${status.health.missingTargets === 0 ? chalk.green("0") : chalk.yellow(String(status.health.missingTargets))} (file not on disk)`);
14474
+ console.log(chalk.cyan("Secrets:") + ` ${status.health.unredactedSecretFindings === 0 ? chalk.green("0 \u2713") : chalk.red(String(status.health.unredactedSecretFindings) + " \u26A0")} unredacted`);
14475
+ console.log(chalk.cyan("Templates:") + ` ${status.counts.configs.templates} (with {{VAR}} placeholders)`);
14363
14476
  });
14364
14477
  program.command("backup").description("Export configs to a timestamped backup file").action(async () => {
14365
14478
  const { mkdirSync: mk } = await import("fs");
@@ -14393,9 +14506,9 @@ program.command("doctor").description("Validate configs: syntax, permissions, mi
14393
14506
  console.log(chalk.cyan("Known files on disk:"));
14394
14507
  for (const k of KNOWN_CONFIGS) {
14395
14508
  if (k.rulesDir) {
14396
- existsSync12(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) : fail(`${k.rulesDir}/ not found`);
14509
+ existsSync13(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) : fail(`${k.rulesDir}/ not found`);
14397
14510
  } else {
14398
- existsSync12(expandPath(k.path)) ? pass(k.path) : k.optional ? skip(`${k.path} (optional)`) : fail(`${k.path} not found`);
14511
+ existsSync13(expandPath(k.path)) ? pass(k.path) : k.optional ? skip(`${k.path} (optional)`) : fail(`${k.path} not found`);
14399
14512
  }
14400
14513
  }
14401
14514
  const allConfigs = listConfigs();
@@ -14517,7 +14630,7 @@ program.command("watch").description("Watch known config files for changes and a
14517
14630
  for (const k of KNOWN_CONFIGS) {
14518
14631
  if (k.rulesDir) {
14519
14632
  const absDir = expandPath2(k.rulesDir);
14520
- if (!existsSync12(absDir))
14633
+ if (!existsSync13(absDir))
14521
14634
  continue;
14522
14635
  const { readdirSync: readdirSync5 } = await import("fs");
14523
14636
  for (const f of readdirSync5(absDir).filter((f2) => f2.endsWith(".md"))) {
@@ -14526,7 +14639,7 @@ program.command("watch").description("Watch known config files for changes and a
14526
14639
  }
14527
14640
  } else {
14528
14641
  const abs = expandPath2(k.path);
14529
- if (existsSync12(abs))
14642
+ if (existsSync13(abs))
14530
14643
  mtimes.set(abs, st(abs).mtimeMs);
14531
14644
  }
14532
14645
  }
@@ -14534,7 +14647,7 @@ program.command("watch").description("Watch known config files for changes and a
14534
14647
  const tick = async () => {
14535
14648
  let changed = 0;
14536
14649
  for (const [abs, oldMtime] of mtimes) {
14537
- if (!existsSync12(abs))
14650
+ if (!existsSync13(abs))
14538
14651
  continue;
14539
14652
  const newMtime = st(abs).mtimeMs;
14540
14653
  if (newMtime !== oldMtime) {
@@ -14546,7 +14659,7 @@ program.command("watch").description("Watch known config files for changes and a
14546
14659
  for (const k of KNOWN_CONFIGS) {
14547
14660
  if (k.rulesDir) {
14548
14661
  const absDir = expandPath2(k.rulesDir);
14549
- if (!existsSync12(absDir))
14662
+ if (!existsSync13(absDir))
14550
14663
  continue;
14551
14664
  for (const f of rd(absDir).filter((f2) => f2.endsWith(".md"))) {
14552
14665
  const abs = join13(absDir, f);
@@ -14557,7 +14670,7 @@ program.command("watch").description("Watch known config files for changes and a
14557
14670
  }
14558
14671
  } else {
14559
14672
  const abs = expandPath2(k.path);
14560
- if (existsSync12(abs) && !mtimes.has(abs)) {
14673
+ if (existsSync13(abs) && !mtimes.has(abs)) {
14561
14674
  mtimes.set(abs, st(abs).mtimeMs);
14562
14675
  changed++;
14563
14676
  }
@@ -14584,11 +14697,11 @@ program.command("report").description("Summary of stored configs, drift, and eco
14584
14697
  if (!c.target_path)
14585
14698
  continue;
14586
14699
  const abs = expandPath(c.target_path);
14587
- if (!existsSync12(abs)) {
14700
+ if (!existsSync13(abs)) {
14588
14701
  missing++;
14589
14702
  continue;
14590
14703
  }
14591
- const disk = readFileSync6(abs, "utf-8");
14704
+ const disk = readFileSync7(abs, "utf-8");
14592
14705
  const { content: redactedDisk } = redactContent(disk, c.format);
14593
14706
  if (redactedDisk !== c.content)
14594
14707
  drifted++;
@@ -14626,7 +14739,7 @@ program.command("clean").description("Remove configs from DB whose target files
14626
14739
  if (!c.target_path)
14627
14740
  continue;
14628
14741
  const abs = expandPath(c.target_path);
14629
- if (!existsSync12(abs)) {
14742
+ if (!existsSync13(abs)) {
14630
14743
  if (opts.dryRun) {
14631
14744
  console.log(chalk.yellow(" would remove:") + ` ${c.slug} ${chalk.dim(`(${c.target_path})`)}`);
14632
14745
  } else {
@@ -14660,39 +14773,39 @@ program.command("bootstrap").description("Install the full @hasna ecosystem: CLI
14660
14773
  console.log(chalk.bold("@hasna/configs bootstrap") + chalk.dim(` \u2014 installing ${packages.length} ecosystem packages
14661
14774
  `));
14662
14775
  console.log(chalk.cyan("Installing CLI tools:"));
14663
- for (const pkg2 of packages) {
14776
+ for (const pkg3 of packages) {
14664
14777
  if (opts.dryRun) {
14665
- console.log(chalk.dim(` would install: ${pkg2.name}`));
14778
+ console.log(chalk.dim(` would install: ${pkg3.name}`));
14666
14779
  continue;
14667
14780
  }
14668
14781
  try {
14669
- const proc = Bun.spawn(["bun", "install", "-g", pkg2.name], { stdout: "pipe", stderr: "pipe" });
14782
+ const proc = Bun.spawn(["bun", "install", "-g", pkg3.name], { stdout: "pipe", stderr: "pipe" });
14670
14783
  const code = await proc.exited;
14671
14784
  if (code === 0)
14672
- console.log(chalk.green(" \u2713 ") + pkg2.name);
14785
+ console.log(chalk.green(" \u2713 ") + pkg3.name);
14673
14786
  else
14674
- console.log(chalk.yellow(" \u26A0 ") + pkg2.name + chalk.dim(" (may already be installed)"));
14787
+ console.log(chalk.yellow(" \u26A0 ") + pkg3.name + chalk.dim(" (may already be installed)"));
14675
14788
  } catch {
14676
- console.log(chalk.yellow(" \u26A0 ") + pkg2.name + chalk.dim(" (skipped)"));
14789
+ console.log(chalk.yellow(" \u26A0 ") + pkg3.name + chalk.dim(" (skipped)"));
14677
14790
  }
14678
14791
  }
14679
14792
  if (!opts.skipMcp) {
14680
14793
  console.log(chalk.cyan(`
14681
14794
  Registering MCP servers in Claude Code:`));
14682
- for (const pkg2 of packages) {
14795
+ for (const pkg3 of packages) {
14683
14796
  if (opts.dryRun) {
14684
- console.log(chalk.dim(` would register: ${pkg2.mcp}`));
14797
+ console.log(chalk.dim(` would register: ${pkg3.mcp}`));
14685
14798
  continue;
14686
14799
  }
14687
14800
  try {
14688
- const proc = Bun.spawn(["claude", "mcp", "add", "--transport", "stdio", "--scope", "user", pkg2.bin, "--", pkg2.mcp], { stdout: "pipe", stderr: "pipe" });
14801
+ const proc = Bun.spawn(["claude", "mcp", "add", "--transport", "stdio", "--scope", "user", pkg3.bin, "--", pkg3.mcp], { stdout: "pipe", stderr: "pipe" });
14689
14802
  const code = await proc.exited;
14690
14803
  if (code === 0)
14691
- console.log(chalk.green(" \u2713 ") + pkg2.bin);
14804
+ console.log(chalk.green(" \u2713 ") + pkg3.bin);
14692
14805
  else
14693
- console.log(chalk.dim(" = ") + pkg2.bin + chalk.dim(" (already registered)"));
14806
+ console.log(chalk.dim(" = ") + pkg3.bin + chalk.dim(" (already registered)"));
14694
14807
  } catch {
14695
- console.log(chalk.yellow(" \u26A0 ") + pkg2.bin + chalk.dim(" (skipped)"));
14808
+ console.log(chalk.yellow(" \u26A0 ") + pkg3.bin + chalk.dim(" (skipped)"));
14696
14809
  }
14697
14810
  }
14698
14811
  }
@@ -14720,10 +14833,10 @@ program.command("update").description("Check for updates and install latest vers
14720
14833
  const proc = Bun.spawn(["npm", "view", "@hasna/configs", "version"], { stdout: "pipe", stderr: "pipe" });
14721
14834
  const latest = (await new Response(proc.stdout).text()).trim();
14722
14835
  await proc.exited;
14723
- if (latest === pkg.version) {
14724
- console.log(chalk.green("\u2713") + ` Already on latest version (${pkg.version})`);
14836
+ if (latest === pkg2.version) {
14837
+ console.log(chalk.green("\u2713") + ` Already on latest version (${pkg2.version})`);
14725
14838
  } else {
14726
- console.log(`Current: ${chalk.dim(pkg.version)} \u2192 Latest: ${chalk.green(latest)}`);
14839
+ console.log(`Current: ${chalk.dim(pkg2.version)} \u2192 Latest: ${chalk.green(latest)}`);
14727
14840
  if (!opts.check) {
14728
14841
  console.log(chalk.dim("Installing..."));
14729
14842
  const install = Bun.spawn(["bun", "install", "-g", `@hasna/configs@${latest}`], { stdout: "inherit", stderr: "inherit" });
@@ -14737,9 +14850,9 @@ program.command("update").description("Check for updates and install latest vers
14737
14850
  });
14738
14851
  program.command("feedback <message>").description("Send feedback about this service").option("-e, --email <email>", "Contact email").option("-c, --category <cat>", "Category: bug, feature, general", "general").action(async (message, opts) => {
14739
14852
  const db = getDatabase();
14740
- db.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", [message, opts.email || null, opts.category || "general", pkg.version]);
14853
+ db.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", [message, opts.email || null, opts.category || "general", pkg2.version]);
14741
14854
  console.log(chalk.green("\u2713") + " Feedback saved. Thank you!");
14742
14855
  });
14743
- program.version(pkg.version).name("configs");
14856
+ program.version(pkg2.version).name("configs");
14744
14857
  registerEventsCommands(program, { source: "configs" });
14745
14858
  program.parse(process.argv);
package/dist/index.d.ts CHANGED
@@ -4,6 +4,8 @@ export { createSnapshot, listSnapshots, getSnapshot, getSnapshotByVersion, prune
4
4
  export { createProfile, getProfile, listProfiles, updateProfile, deleteProfile, addConfigToProfile, removeConfigFromProfile, getProfileConfigs, profileHasSelectors, profileMatchesMachine, resolveProfileForMachine } from "./db/profiles.js";
5
5
  export { registerMachine, updateMachineApplied, listMachines, currentHostname, currentOs, currentArch } from "./db/machines.js";
6
6
  export { getDatabase, resetDatabase, uuid, now, slugify } from "./db/database.js";
7
+ export { getConfigsStatus } from "./status.js";
8
+ export type { ConfigsStatusContract } from "./status.js";
7
9
  export { PG_MIGRATIONS } from "./db/pg-migrations.js";
8
10
  export { applyConfig, applyConfigs, expandPath } from "./lib/apply.js";
9
11
  export type { ApplyOptions } from "./lib/apply.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,kBAAkB,CAAC;AAGjC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGlI,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,WAAW,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGrH,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAG/O,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGhI,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAGlF,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAGtD,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACvE,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAGnD,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,uBAAuB,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AACrL,YAAY,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAGhE,OAAO,EAAE,wBAAwB,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAG9F,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC/J,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC3D,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC1G,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAG5D,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGnE,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACvG,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGrD,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACzE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,kBAAkB,CAAC;AAGjC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGlI,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,WAAW,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGrH,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAG/O,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGhI,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAGlF,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,YAAY,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAGzD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAGtD,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACvE,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAGnD,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,uBAAuB,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AACrL,YAAY,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAGhE,OAAO,EAAE,wBAAwB,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAG9F,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC/J,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC3D,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC1G,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAG5D,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGnE,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACvG,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGrD,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACzE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -10236,64 +10236,10 @@ function listMachines(db) {
10236
10236
  const d = db || getDatabase();
10237
10237
  return d.query("SELECT * FROM machines ORDER BY last_applied_at DESC NULLS LAST").all();
10238
10238
  }
10239
- // src/db/pg-migrations.ts
10240
- var PG_MIGRATIONS = [
10241
- `CREATE TABLE IF NOT EXISTS configs (
10242
- id TEXT PRIMARY KEY,
10243
- name TEXT NOT NULL,
10244
- slug TEXT NOT NULL UNIQUE,
10245
- kind TEXT NOT NULL DEFAULT 'file',
10246
- category TEXT NOT NULL,
10247
- agent TEXT NOT NULL DEFAULT 'global',
10248
- target_path TEXT,
10249
- format TEXT NOT NULL DEFAULT 'text',
10250
- content TEXT NOT NULL DEFAULT '',
10251
- description TEXT,
10252
- tags TEXT NOT NULL DEFAULT '[]',
10253
- is_template BOOLEAN NOT NULL DEFAULT FALSE,
10254
- version INTEGER NOT NULL DEFAULT 1,
10255
- created_at TEXT NOT NULL,
10256
- updated_at TEXT NOT NULL,
10257
- synced_at TEXT
10258
- )`,
10259
- `CREATE TABLE IF NOT EXISTS config_snapshots (
10260
- id TEXT PRIMARY KEY,
10261
- config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
10262
- content TEXT NOT NULL,
10263
- version INTEGER NOT NULL,
10264
- created_at TEXT NOT NULL
10265
- )`,
10266
- `CREATE TABLE IF NOT EXISTS profiles (
10267
- id TEXT PRIMARY KEY,
10268
- name TEXT NOT NULL,
10269
- slug TEXT NOT NULL UNIQUE,
10270
- description TEXT,
10271
- created_at TEXT NOT NULL,
10272
- updated_at TEXT NOT NULL
10273
- )`,
10274
- `CREATE TABLE IF NOT EXISTS profile_configs (
10275
- profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
10276
- config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
10277
- sort_order INTEGER NOT NULL DEFAULT 0,
10278
- PRIMARY KEY (profile_id, config_id)
10279
- )`,
10280
- `CREATE TABLE IF NOT EXISTS machines (
10281
- id TEXT PRIMARY KEY,
10282
- hostname TEXT NOT NULL UNIQUE,
10283
- os TEXT,
10284
- last_applied_at TEXT,
10285
- created_at TEXT NOT NULL
10286
- )`,
10287
- `CREATE TABLE IF NOT EXISTS feedback (
10288
- id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
10289
- message TEXT NOT NULL,
10290
- email TEXT,
10291
- category TEXT DEFAULT 'general',
10292
- version TEXT,
10293
- machine_id TEXT,
10294
- created_at TEXT NOT NULL DEFAULT NOW()::text
10295
- )`
10296
- ];
10239
+ // src/status.ts
10240
+ import { existsSync as existsSync9, readFileSync as readFileSync3 } from "fs";
10241
+ import { createRequire as createRequire2 } from "module";
10242
+
10297
10243
  // src/lib/apply.ts
10298
10244
  import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
10299
10245
  import { dirname as dirname2, resolve } from "path";
@@ -10344,59 +10290,6 @@ async function applyConfigs(configs, opts = {}) {
10344
10290
  }
10345
10291
  return results;
10346
10292
  }
10347
- // src/lib/platform-profiles.ts
10348
- var PLATFORM_PROFILE_PRESETS = [
10349
- {
10350
- name: "linux-arm64",
10351
- description: "Default Linux arm64 profile for spark01/spark02-style machines",
10352
- selectors: { os: ["linux"], arch: ["arm64"], hostnames: ["spark01", "spark02"] },
10353
- variables: {
10354
- WORKSPACE_ROOT: "{{HOME_DIR}}/workspace",
10355
- BUN_BIN_DIR: "{{HOME_DIR}}/.bun/bin",
10356
- BUN_PATH: "{{BUN_BIN_DIR}}/bun",
10357
- PATH_PREFIX: "{{BUN_BIN_DIR}}"
10358
- }
10359
- },
10360
- {
10361
- name: "macos-arm64",
10362
- description: "Default macOS arm64 profile for apple01/apple03-style machines",
10363
- selectors: { os: ["macos"], arch: ["arm64"], hostnames: ["apple01", "apple03"] },
10364
- variables: {
10365
- WORKSPACE_ROOT: "{{HOME_DIR}}/Workspace",
10366
- BUN_BIN_DIR: "{{HOME_DIR}}/.bun/bin",
10367
- BUN_PATH: "/opt/homebrew/bin/bun",
10368
- PATH_PREFIX: "/opt/homebrew/bin:{{BUN_BIN_DIR}}"
10369
- }
10370
- }
10371
- ];
10372
- function ensurePlatformProfiles(db) {
10373
- const configs = listConfigs(undefined, db);
10374
- const ensured = [];
10375
- for (const preset of PLATFORM_PROFILE_PRESETS) {
10376
- let profile;
10377
- try {
10378
- profile = getProfile(preset.name, db);
10379
- if (!profileHasSelectors(profile) || Object.keys(profile.variables).length === 0) {
10380
- profile = updateProfile(profile.id, {
10381
- description: profile.description ?? preset.description,
10382
- selectors: profileHasSelectors(profile) ? profile.selectors : preset.selectors,
10383
- variables: Object.keys(profile.variables).length > 0 ? profile.variables : preset.variables
10384
- }, db);
10385
- }
10386
- } catch {
10387
- profile = createProfile(preset, db);
10388
- }
10389
- for (const config of configs) {
10390
- addConfigToProfile(profile.id, config.id, db);
10391
- }
10392
- ensured.push(profile);
10393
- }
10394
- return ensured;
10395
- }
10396
- // src/lib/sync.ts
10397
- import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync4 } from "fs";
10398
- import { extname, join as join10 } from "path";
10399
- import { homedir as homedir10 } from "os";
10400
10293
 
10401
10294
  // src/lib/redact.ts
10402
10295
  var SECRET_KEY_PATTERN = /^(.*_?API_?KEY|.*_?TOKEN|.*_?SECRET|.*_?PASSWORD|.*_?PASSWD|.*_?CREDENTIAL|.*_?AUTH(?:_TOKEN|_KEY|ORIZATION)?|.*_?PRIVATE_?KEY|.*_?ACCESS_?KEY|.*_?CLIENT_?SECRET|.*_?SIGNING_?KEY|.*_?ENCRYPTION_?KEY|.*_AUTH_TOKEN)$/i;
@@ -10578,8 +10471,239 @@ function hasSecrets(content, format) {
10578
10471
  return scanSecrets(content, format).length > 0;
10579
10472
  }
10580
10473
 
10474
+ // src/status.ts
10475
+ var require2 = createRequire2(import.meta.url);
10476
+ var pkg = require2("../package.json");
10477
+ function activeDatabaseEnv() {
10478
+ if (process.env["HASNA_CONFIGS_DB_PATH"])
10479
+ return "HASNA_CONFIGS_DB_PATH";
10480
+ if (process.env["CONFIGS_DB_PATH"])
10481
+ return "CONFIGS_DB_PATH";
10482
+ return null;
10483
+ }
10484
+ function configuredDatabaseKind() {
10485
+ const value = process.env["HASNA_CONFIGS_DB_PATH"] ?? process.env["CONFIGS_DB_PATH"] ?? "";
10486
+ return value === ":memory:" || value.startsWith("file::memory:") ? "memory" : "file";
10487
+ }
10488
+ function countBy(items, getValue) {
10489
+ const counts = {};
10490
+ for (const item of items) {
10491
+ const value = getValue(item);
10492
+ if (!value)
10493
+ continue;
10494
+ counts[value] = (counts[value] ?? 0) + 1;
10495
+ }
10496
+ return counts;
10497
+ }
10498
+ function tableCount(db, table) {
10499
+ try {
10500
+ const row = db.query(`SELECT COUNT(*) AS count FROM ${table}`).get();
10501
+ return Number(row?.count ?? 0);
10502
+ } catch {
10503
+ return 0;
10504
+ }
10505
+ }
10506
+ function getConfigsStatus(db = getDatabase()) {
10507
+ let databaseReachable = true;
10508
+ let configs = [];
10509
+ let categoryStats = { total: 0 };
10510
+ try {
10511
+ configs = listConfigs(undefined, db);
10512
+ categoryStats = getConfigStats(db);
10513
+ } catch {
10514
+ databaseReachable = false;
10515
+ }
10516
+ const fileConfigs = configs.filter((config) => config.kind === "file");
10517
+ let driftedTargets = 0;
10518
+ let missingTargets = 0;
10519
+ let unredactedSecretFindings = 0;
10520
+ let knownTargets = 0;
10521
+ for (const config of fileConfigs) {
10522
+ unredactedSecretFindings += scanSecrets(config.content, config.format).length;
10523
+ if (!config.target_path)
10524
+ continue;
10525
+ knownTargets += 1;
10526
+ const targetPath = expandPath(config.target_path);
10527
+ if (!existsSync9(targetPath)) {
10528
+ missingTargets += 1;
10529
+ continue;
10530
+ }
10531
+ const disk = readFileSync3(targetPath, "utf-8");
10532
+ const { content: redactedDisk } = redactContent(disk, config.format);
10533
+ if (redactedDisk !== config.content) {
10534
+ driftedTargets += 1;
10535
+ }
10536
+ }
10537
+ const profiles = databaseReachable ? listProfiles(db).length : 0;
10538
+ const machines = databaseReachable ? listMachines(db).length : 0;
10539
+ const profileLinks = databaseReachable ? tableCount(db, "profile_configs") : 0;
10540
+ const snapshots = databaseReachable ? tableCount(db, "config_snapshots") : 0;
10541
+ const byCategory = Object.fromEntries(Object.entries(categoryStats).filter(([key]) => key !== "total"));
10542
+ const status = databaseReachable && driftedTargets === 0 && missingTargets === 0 && unredactedSecretFindings === 0 ? "ok" : "warn";
10543
+ return {
10544
+ service: "configs",
10545
+ schemaVersion: "1.0",
10546
+ package: {
10547
+ name: pkg.name,
10548
+ version: pkg.version
10549
+ },
10550
+ env: {
10551
+ database: {
10552
+ primary: "HASNA_CONFIGS_DB_PATH",
10553
+ fallback: "CONFIGS_DB_PATH",
10554
+ active: activeDatabaseEnv(),
10555
+ kind: configuredDatabaseKind()
10556
+ }
10557
+ },
10558
+ counts: {
10559
+ configs: {
10560
+ total: configs.length,
10561
+ file: fileConfigs.length,
10562
+ reference: configs.filter((config) => config.kind === "reference").length,
10563
+ templates: configs.filter((config) => config.is_template).length
10564
+ },
10565
+ byCategory,
10566
+ byAgent: countBy(configs, (config) => config.agent),
10567
+ byFormat: countBy(configs, (config) => config.format),
10568
+ profiles,
10569
+ profileLinks,
10570
+ machines,
10571
+ snapshots,
10572
+ knownTargets
10573
+ },
10574
+ health: {
10575
+ status,
10576
+ databaseReachable,
10577
+ driftedTargets,
10578
+ missingTargets,
10579
+ unredactedSecretFindings,
10580
+ hasDrift: driftedTargets > 0,
10581
+ hasMissingTargets: missingTargets > 0,
10582
+ hasUnredactedSecrets: unredactedSecretFindings > 0
10583
+ },
10584
+ safety: {
10585
+ includesConfigValues: false,
10586
+ includesPrivatePaths: false,
10587
+ includesHostnames: false,
10588
+ includesSecretValues: false,
10589
+ statusOutputIsMetadataOnly: true
10590
+ }
10591
+ };
10592
+ }
10593
+ // src/db/pg-migrations.ts
10594
+ var PG_MIGRATIONS = [
10595
+ `CREATE TABLE IF NOT EXISTS configs (
10596
+ id TEXT PRIMARY KEY,
10597
+ name TEXT NOT NULL,
10598
+ slug TEXT NOT NULL UNIQUE,
10599
+ kind TEXT NOT NULL DEFAULT 'file',
10600
+ category TEXT NOT NULL,
10601
+ agent TEXT NOT NULL DEFAULT 'global',
10602
+ target_path TEXT,
10603
+ format TEXT NOT NULL DEFAULT 'text',
10604
+ content TEXT NOT NULL DEFAULT '',
10605
+ description TEXT,
10606
+ tags TEXT NOT NULL DEFAULT '[]',
10607
+ is_template BOOLEAN NOT NULL DEFAULT FALSE,
10608
+ version INTEGER NOT NULL DEFAULT 1,
10609
+ created_at TEXT NOT NULL,
10610
+ updated_at TEXT NOT NULL,
10611
+ synced_at TEXT
10612
+ )`,
10613
+ `CREATE TABLE IF NOT EXISTS config_snapshots (
10614
+ id TEXT PRIMARY KEY,
10615
+ config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
10616
+ content TEXT NOT NULL,
10617
+ version INTEGER NOT NULL,
10618
+ created_at TEXT NOT NULL
10619
+ )`,
10620
+ `CREATE TABLE IF NOT EXISTS profiles (
10621
+ id TEXT PRIMARY KEY,
10622
+ name TEXT NOT NULL,
10623
+ slug TEXT NOT NULL UNIQUE,
10624
+ description TEXT,
10625
+ created_at TEXT NOT NULL,
10626
+ updated_at TEXT NOT NULL
10627
+ )`,
10628
+ `CREATE TABLE IF NOT EXISTS profile_configs (
10629
+ profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
10630
+ config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
10631
+ sort_order INTEGER NOT NULL DEFAULT 0,
10632
+ PRIMARY KEY (profile_id, config_id)
10633
+ )`,
10634
+ `CREATE TABLE IF NOT EXISTS machines (
10635
+ id TEXT PRIMARY KEY,
10636
+ hostname TEXT NOT NULL UNIQUE,
10637
+ os TEXT,
10638
+ last_applied_at TEXT,
10639
+ created_at TEXT NOT NULL
10640
+ )`,
10641
+ `CREATE TABLE IF NOT EXISTS feedback (
10642
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
10643
+ message TEXT NOT NULL,
10644
+ email TEXT,
10645
+ category TEXT DEFAULT 'general',
10646
+ version TEXT,
10647
+ machine_id TEXT,
10648
+ created_at TEXT NOT NULL DEFAULT NOW()::text
10649
+ )`
10650
+ ];
10651
+ // src/lib/platform-profiles.ts
10652
+ var PLATFORM_PROFILE_PRESETS = [
10653
+ {
10654
+ name: "linux-arm64",
10655
+ description: "Default Linux arm64 profile for linux-node-a/linux-node-b-style machines",
10656
+ selectors: { os: ["linux"], arch: ["arm64"], hostnames: ["linux-node-a", "linux-node-b"] },
10657
+ variables: {
10658
+ WORKSPACE_ROOT: "{{HOME_DIR}}/workspace",
10659
+ BUN_BIN_DIR: "{{HOME_DIR}}/.bun/bin",
10660
+ BUN_PATH: "{{BUN_BIN_DIR}}/bun",
10661
+ PATH_PREFIX: "{{BUN_BIN_DIR}}"
10662
+ }
10663
+ },
10664
+ {
10665
+ name: "macos-arm64",
10666
+ description: "Default macOS arm64 profile for macos-node-a/macos-node-b-style machines",
10667
+ selectors: { os: ["macos"], arch: ["arm64"], hostnames: ["macos-node-a", "macos-node-b"] },
10668
+ variables: {
10669
+ WORKSPACE_ROOT: "{{HOME_DIR}}/Workspace",
10670
+ BUN_BIN_DIR: "{{HOME_DIR}}/.bun/bin",
10671
+ BUN_PATH: "/opt/homebrew/bin/bun",
10672
+ PATH_PREFIX: "/opt/homebrew/bin:{{BUN_BIN_DIR}}"
10673
+ }
10674
+ }
10675
+ ];
10676
+ function ensurePlatformProfiles(db) {
10677
+ const configs = listConfigs(undefined, db);
10678
+ const ensured = [];
10679
+ for (const preset of PLATFORM_PROFILE_PRESETS) {
10680
+ let profile;
10681
+ try {
10682
+ profile = getProfile(preset.name, db);
10683
+ if (!profileHasSelectors(profile) || Object.keys(profile.variables).length === 0) {
10684
+ profile = updateProfile(profile.id, {
10685
+ description: profile.description ?? preset.description,
10686
+ selectors: profileHasSelectors(profile) ? profile.selectors : preset.selectors,
10687
+ variables: Object.keys(profile.variables).length > 0 ? profile.variables : preset.variables
10688
+ }, db);
10689
+ }
10690
+ } catch {
10691
+ profile = createProfile(preset, db);
10692
+ }
10693
+ for (const config of configs) {
10694
+ addConfigToProfile(profile.id, config.id, db);
10695
+ }
10696
+ ensured.push(profile);
10697
+ }
10698
+ return ensured;
10699
+ }
10700
+ // src/lib/sync.ts
10701
+ import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync5 } from "fs";
10702
+ import { extname, join as join10 } from "path";
10703
+ import { homedir as homedir10 } from "os";
10704
+
10581
10705
  // src/lib/sync-dir.ts
10582
- import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync } from "fs";
10706
+ import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync } from "fs";
10583
10707
  import { join as join9, relative as relative2 } from "path";
10584
10708
  import { homedir as homedir9 } from "os";
10585
10709
  var SKIP = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
@@ -10589,7 +10713,7 @@ function shouldSkip(p) {
10589
10713
  async function syncFromDir(dir, opts = {}) {
10590
10714
  const d = opts.db || getDatabase();
10591
10715
  const absDir = expandPath(dir);
10592
- if (!existsSync9(absDir))
10716
+ if (!existsSync10(absDir))
10593
10717
  return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
10594
10718
  const files = opts.recursive !== false ? walkDir(absDir) : readdirSync2(absDir).map((f) => join9(absDir, f)).filter((f) => statSync(f).isFile());
10595
10719
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
@@ -10601,7 +10725,7 @@ async function syncFromDir(dir, opts = {}) {
10601
10725
  continue;
10602
10726
  }
10603
10727
  try {
10604
- const content = readFileSync3(file, "utf-8");
10728
+ const content = readFileSync4(file, "utf-8");
10605
10729
  if (content.length > 500000) {
10606
10730
  result.skipped.push(file + " (too large)");
10607
10731
  continue;
@@ -10696,10 +10820,10 @@ async function syncProject(opts) {
10696
10820
  const machine = detectMachineContext();
10697
10821
  for (const pf of PROJECT_CONFIG_FILES) {
10698
10822
  const abs = join10(absDir, pf.file);
10699
- if (!existsSync10(abs))
10823
+ if (!existsSync11(abs))
10700
10824
  continue;
10701
10825
  try {
10702
- const rawContent = readFileSync4(abs, "utf-8");
10826
+ const rawContent = readFileSync5(abs, "utf-8");
10703
10827
  if (rawContent.length > 500000) {
10704
10828
  result.skipped.push(pf.file);
10705
10829
  continue;
@@ -10728,11 +10852,11 @@ async function syncProject(opts) {
10728
10852
  }
10729
10853
  }
10730
10854
  const rulesDir = join10(absDir, ".claude", "rules");
10731
- if (existsSync10(rulesDir)) {
10855
+ if (existsSync11(rulesDir)) {
10732
10856
  const mdFiles = readdirSync4(rulesDir).filter((f) => f.endsWith(".md"));
10733
10857
  for (const f of mdFiles) {
10734
10858
  const abs = join10(rulesDir, f);
10735
- const raw = readFileSync4(abs, "utf-8");
10859
+ const raw = readFileSync5(abs, "utf-8");
10736
10860
  const redacted = redactContent(raw, "markdown");
10737
10861
  const machineAware = templateizeMachineContent(redacted.content, machine);
10738
10862
  const content = machineAware.content;
@@ -10770,7 +10894,7 @@ async function syncKnown(opts = {}) {
10770
10894
  for (const known of targets) {
10771
10895
  if (known.rulesDir) {
10772
10896
  const absDir = expandPath(known.rulesDir);
10773
- if (!existsSync10(absDir)) {
10897
+ if (!existsSync11(absDir)) {
10774
10898
  result.skipped.push(known.rulesDir);
10775
10899
  continue;
10776
10900
  }
@@ -10778,7 +10902,7 @@ async function syncKnown(opts = {}) {
10778
10902
  for (const f of mdFiles) {
10779
10903
  const abs2 = join10(absDir, f);
10780
10904
  const targetPath = abs2.replace(home, "~");
10781
- const raw = readFileSync4(abs2, "utf-8");
10905
+ const raw = readFileSync5(abs2, "utf-8");
10782
10906
  const redacted = redactContent(raw, "markdown");
10783
10907
  const machineAware = templateizeMachineContent(redacted.content, machine);
10784
10908
  const content = machineAware.content;
@@ -10801,12 +10925,12 @@ async function syncKnown(opts = {}) {
10801
10925
  continue;
10802
10926
  }
10803
10927
  const abs = expandPath(known.path);
10804
- if (!existsSync10(abs)) {
10928
+ if (!existsSync11(abs)) {
10805
10929
  result.skipped.push(known.path);
10806
10930
  continue;
10807
10931
  }
10808
10932
  try {
10809
- const rawContent = readFileSync4(abs, "utf-8");
10933
+ const rawContent = readFileSync5(abs, "utf-8");
10810
10934
  if (rawContent.length > 500000) {
10811
10935
  result.skipped.push(known.path + " (too large)");
10812
10936
  continue;
@@ -10866,9 +10990,9 @@ function diffConfig(config) {
10866
10990
  if (!config.target_path)
10867
10991
  return "(reference \u2014 no target path)";
10868
10992
  const path = expandPath(config.target_path);
10869
- if (!existsSync10(path))
10993
+ if (!existsSync11(path))
10870
10994
  return `(file not found on disk: ${path})`;
10871
- const diskContent = readFileSync4(path, "utf-8");
10995
+ const diskContent = readFileSync5(path, "utf-8");
10872
10996
  if (diskContent === config.content)
10873
10997
  return "(no diff \u2014 identical)";
10874
10998
  const stored = config.content.split(`
@@ -10942,7 +11066,7 @@ function detectFormat(filePath) {
10942
11066
  return "text";
10943
11067
  }
10944
11068
  // src/lib/export.ts
10945
- import { existsSync as existsSync11, mkdirSync as mkdirSync6, rmSync, writeFileSync as writeFileSync3 } from "fs";
11069
+ import { existsSync as existsSync12, mkdirSync as mkdirSync6, rmSync, writeFileSync as writeFileSync3 } from "fs";
10946
11070
  import { join as join11, resolve as resolve2 } from "path";
10947
11071
  import { tmpdir } from "os";
10948
11072
  async function exportConfigs(outputPath, opts = {}) {
@@ -10974,13 +11098,13 @@ async function exportConfigs(outputPath, opts = {}) {
10974
11098
  }
10975
11099
  return { path: absOutput, count: configs.length };
10976
11100
  } finally {
10977
- if (existsSync11(tmpDir)) {
11101
+ if (existsSync12(tmpDir)) {
10978
11102
  rmSync(tmpDir, { recursive: true, force: true });
10979
11103
  }
10980
11104
  }
10981
11105
  }
10982
11106
  // src/lib/import.ts
10983
- import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync5, rmSync as rmSync2 } from "fs";
11107
+ import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync6, rmSync as rmSync2 } from "fs";
10984
11108
  import { join as join12, resolve as resolve3 } from "path";
10985
11109
  import { tmpdir as tmpdir2 } from "os";
10986
11110
  async function importConfigs(bundlePath, opts = {}) {
@@ -11001,14 +11125,14 @@ async function importConfigs(bundlePath, opts = {}) {
11001
11125
  throw new Error(`tar extraction failed: ${stderr}`);
11002
11126
  }
11003
11127
  const manifestPath = join12(tmpDir, "manifest.json");
11004
- if (!existsSync12(manifestPath))
11128
+ if (!existsSync13(manifestPath))
11005
11129
  throw new Error("Invalid bundle: missing manifest.json");
11006
- const manifest = JSON.parse(readFileSync5(manifestPath, "utf-8"));
11130
+ const manifest = JSON.parse(readFileSync6(manifestPath, "utf-8"));
11007
11131
  for (const meta of manifest.configs) {
11008
11132
  try {
11009
11133
  const ext = meta.format === "text" ? "txt" : meta.format;
11010
11134
  const contentFile = join12(tmpDir, "contents", `${meta.slug}.${ext}`);
11011
- const content = existsSync12(contentFile) ? readFileSync5(contentFile, "utf-8") : "";
11135
+ const content = existsSync13(contentFile) ? readFileSync6(contentFile, "utf-8") : "";
11012
11136
  let existing = null;
11013
11137
  try {
11014
11138
  existing = getConfig(meta.slug, d);
@@ -11041,7 +11165,7 @@ async function importConfigs(bundlePath, opts = {}) {
11041
11165
  }
11042
11166
  return result;
11043
11167
  } finally {
11044
- if (existsSync12(tmpDir)) {
11168
+ if (existsSync13(tmpDir)) {
11045
11169
  rmSync2(tmpDir, { recursive: true, force: true });
11046
11170
  }
11047
11171
  }
@@ -11086,6 +11210,7 @@ export {
11086
11210
  getProfileConfigs,
11087
11211
  getProfile,
11088
11212
  getDatabase,
11213
+ getConfigsStatus,
11089
11214
  getConfigStats,
11090
11215
  getConfigById,
11091
11216
  getConfig,
package/dist/mcp/index.js CHANGED
@@ -11241,7 +11241,7 @@ var init_sync_dir = __esm(() => {
11241
11241
  var require_package = __commonJS((exports, module) => {
11242
11242
  module.exports = {
11243
11243
  name: "@hasna/configs",
11244
- version: "0.2.36",
11244
+ version: "0.2.37",
11245
11245
  description: "AI coding agent configuration manager \u2014 store, version, apply, and share all your AI coding configs. CLI + MCP + REST API + Dashboard.",
11246
11246
  type: "module",
11247
11247
  main: "dist/index.js",
@@ -17829,7 +17829,7 @@ var require_dist2 = __commonJS((exports, module) => {
17829
17829
  var require_package = __commonJS((exports, module) => {
17830
17830
  module.exports = {
17831
17831
  name: "@hasna/configs",
17832
- version: "0.2.36",
17832
+ version: "0.2.37",
17833
17833
  description: "AI coding agent configuration manager \u2014 store, version, apply, and share all your AI coding configs. CLI + MCP + REST API + Dashboard.",
17834
17834
  type: "module",
17835
17835
  main: "dist/index.js",
@@ -0,0 +1,56 @@
1
+ import type { Database } from "bun:sqlite";
2
+ type ActiveDbEnv = "HASNA_CONFIGS_DB_PATH" | "CONFIGS_DB_PATH" | null;
3
+ type DatabaseKind = "memory" | "file";
4
+ type ContractStatus = "ok" | "warn";
5
+ export interface ConfigsStatusContract {
6
+ service: "configs";
7
+ schemaVersion: "1.0";
8
+ package: {
9
+ name: string;
10
+ version: string;
11
+ };
12
+ env: {
13
+ database: {
14
+ primary: "HASNA_CONFIGS_DB_PATH";
15
+ fallback: "CONFIGS_DB_PATH";
16
+ active: ActiveDbEnv;
17
+ kind: DatabaseKind;
18
+ };
19
+ };
20
+ counts: {
21
+ configs: {
22
+ total: number;
23
+ file: number;
24
+ reference: number;
25
+ templates: number;
26
+ };
27
+ byCategory: Record<string, number>;
28
+ byAgent: Record<string, number>;
29
+ byFormat: Record<string, number>;
30
+ profiles: number;
31
+ profileLinks: number;
32
+ machines: number;
33
+ snapshots: number;
34
+ knownTargets: number;
35
+ };
36
+ health: {
37
+ status: ContractStatus;
38
+ databaseReachable: boolean;
39
+ driftedTargets: number;
40
+ missingTargets: number;
41
+ unredactedSecretFindings: number;
42
+ hasDrift: boolean;
43
+ hasMissingTargets: boolean;
44
+ hasUnredactedSecrets: boolean;
45
+ };
46
+ safety: {
47
+ includesConfigValues: false;
48
+ includesPrivatePaths: false;
49
+ includesHostnames: false;
50
+ includesSecretValues: false;
51
+ statusOutputIsMetadataOnly: true;
52
+ };
53
+ }
54
+ export declare function getConfigsStatus(db?: Database): ConfigsStatusContract;
55
+ export {};
56
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAW3C,KAAK,WAAW,GAAG,uBAAuB,GAAG,iBAAiB,GAAG,IAAI,CAAC;AACtE,KAAK,YAAY,GAAG,QAAQ,GAAG,MAAM,CAAC;AACtC,KAAK,cAAc,GAAG,IAAI,GAAG,MAAM,CAAC;AAEpC,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,SAAS,CAAC;IACnB,aAAa,EAAE,KAAK,CAAC;IACrB,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,GAAG,EAAE;QACH,QAAQ,EAAE;YACR,OAAO,EAAE,uBAAuB,CAAC;YACjC,QAAQ,EAAE,iBAAiB,CAAC;YAC5B,MAAM,EAAE,WAAW,CAAC;YACpB,IAAI,EAAE,YAAY,CAAC;SACpB,CAAC;KACH,CAAC;IACF,MAAM,EAAE;QACN,OAAO,EAAE;YACP,KAAK,EAAE,MAAM,CAAC;YACd,IAAI,EAAE,MAAM,CAAC;YACb,SAAS,EAAE,MAAM,CAAC;YAClB,SAAS,EAAE,MAAM,CAAC;SACnB,CAAC;QACF,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACnC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAChC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACjC,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,MAAM,EAAE;QACN,MAAM,EAAE,cAAc,CAAC;QACvB,iBAAiB,EAAE,OAAO,CAAC;QAC3B,cAAc,EAAE,MAAM,CAAC;QACvB,cAAc,EAAE,MAAM,CAAC;QACvB,wBAAwB,EAAE,MAAM,CAAC;QACjC,QAAQ,EAAE,OAAO,CAAC;QAClB,iBAAiB,EAAE,OAAO,CAAC;QAC3B,oBAAoB,EAAE,OAAO,CAAC;KAC/B,CAAC;IACF,MAAM,EAAE;QACN,oBAAoB,EAAE,KAAK,CAAC;QAC5B,oBAAoB,EAAE,KAAK,CAAC;QAC5B,iBAAiB,EAAE,KAAK,CAAC;QACzB,oBAAoB,EAAE,KAAK,CAAC;QAC5B,0BAA0B,EAAE,IAAI,CAAC;KAClC,CAAC;CACH;AAgCD,wBAAgB,gBAAgB,CAAC,EAAE,GAAE,QAAwB,GAAG,qBAAqB,CAmGpF"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=status.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.test.d.ts","sourceRoot":"","sources":["../src/status.test.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/configs",
3
- "version": "0.2.36",
3
+ "version": "0.2.37",
4
4
  "description": "AI coding agent configuration manager — store, version, apply, and share all your AI coding configs. CLI + MCP + REST API + Dashboard.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",