@hasna/skills 0.1.69 → 0.1.71

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/bin/index.js CHANGED
@@ -36860,7 +36860,7 @@ var package_default;
36860
36860
  var init_package = __esm(() => {
36861
36861
  package_default = {
36862
36862
  name: "@hasna/skills",
36863
- version: "0.1.69",
36863
+ version: "0.1.71",
36864
36864
  description: "Skills library for AI coding agents",
36865
36865
  type: "module",
36866
36866
  bin: {
@@ -36912,6 +36912,7 @@ var init_package = __esm(() => {
36912
36912
  "dev:mcp": "bun --watch run ./src/mcp/index.ts",
36913
36913
  "dev:server": "bun --watch run ./src/server/index.ts",
36914
36914
  "dev:worker": "bun --watch run ./src/server/worker.ts",
36915
+ "docker:check": "bash scripts/docker-build-check.sh",
36915
36916
  migrate: "bun run ./src/server/migrate.ts",
36916
36917
  typecheck: "tsc --noEmit",
36917
36918
  "verify:release": "bun run scripts/release-guard.ts",
@@ -36944,7 +36945,8 @@ var init_package = __esm(() => {
36944
36945
  "@types/react": "^18.2.0",
36945
36946
  "bun-types": "1.3.14",
36946
36947
  "react-devtools-core": "^7.0.1",
36947
- typescript: "^5"
36948
+ typescript: "^5",
36949
+ yaml: "^2.9.0"
36948
36950
  },
36949
36951
  dependencies: {
36950
36952
  "@aws-sdk/client-ecs": "^3.1079.0",
@@ -38320,6 +38322,14 @@ var init_development_tools = __esm(() => {
38320
38322
  category: "Development Tools",
38321
38323
  tags: ["config", "validation", "schema", "linting"]
38322
38324
  },
38325
+ {
38326
+ name: "oss-app-two-backend-storage",
38327
+ displayName: "OSS App Two-Backend Storage",
38328
+ description: "Recipe for the Hasna two-backend storage contract: client transport + HTTP store, server PG/SQLite backend, pg-migrations + apply script, fail-closed URL-without-key, bun bins, contract manifest, Dockerfile",
38329
+ category: "Development Tools",
38330
+ tags: ["storage", "backend", "postgresql", "sqlite", "two-backend", "oss-app"],
38331
+ kind: "instruction"
38332
+ },
38323
38333
  {
38324
38334
  name: "session-inject-monitor",
38325
38335
  displayName: "Session Inject Monitor",
@@ -69906,7 +69916,7 @@ var init_server3 = __esm(() => {
69906
69916
  server = buildServer();
69907
69917
  });
69908
69918
 
69909
- // ../../node_modules/.bun/@hono+node-server@1.19.17+804d2c5c04916069/node_modules/@hono/node-server/dist/index.mjs
69919
+ // ../../node_modules/.bun/@hono+node-server@1.19.17+2145b681a064c8e9/node_modules/@hono/node-server/dist/index.mjs
69910
69920
  import { Readable } from "stream";
69911
69921
  import crypto2 from "crypto";
69912
69922
  var GlobalRequest, Request, newHeadersFromIncoming = (incoming) => {
@@ -71385,13 +71395,344 @@ var init_completion = __esm(() => {
71385
71395
  categoryNames = CATEGORIES.map((c) => c);
71386
71396
  });
71387
71397
 
71398
+ // src/lib/portable-snapshot-filter.ts
71399
+ import { readdirSync as readdirSync15, statSync as statSync15 } from "fs";
71400
+ import { homedir as homedir9 } from "os";
71401
+ import { join as join28, sep as sep3 } from "path";
71402
+ function isExcludedSkillFileName(fileName) {
71403
+ if (EXCLUDE_FILE_NAMES.has(fileName)) {
71404
+ return true;
71405
+ }
71406
+ return EXCLUDE_FILE_PATTERNS.some((pattern) => pattern.test(fileName));
71407
+ }
71408
+ function isPortableWithinSkill(relativeParts) {
71409
+ if (relativeParts.length < 2) {
71410
+ return false;
71411
+ }
71412
+ const [, second] = relativeParts;
71413
+ if (PORTABLE_TOP_LEVEL.has(second)) {
71414
+ return relativeParts.length === 2;
71415
+ }
71416
+ if (relativeParts.length < 3) {
71417
+ return false;
71418
+ }
71419
+ return PORTABLE_SUBDIRS.has(second);
71420
+ }
71421
+ function homePathFor(definition, homesRoot) {
71422
+ const home = homesRoot ?? homedir9();
71423
+ if (definition.subClass === "skills" || definition.subClass === "custom") {
71424
+ return join28(home, ".hasna", "skills", definition.name);
71425
+ }
71426
+ if (definition.agent === "opencode") {
71427
+ return join28(home, ".config", "opencode", "skills");
71428
+ }
71429
+ return join28(home, `.${definition.agent}`, "skills");
71430
+ }
71431
+ function destinationFor(definition, stationId, relativePath) {
71432
+ const category = definition.subClass === "agent-homes" ? join28("agent-homes", definition.agent ?? "") : definition.name;
71433
+ return join28("resources", stationId, "skills", category, ...relativePath.split(sep3));
71434
+ }
71435
+ function walkEntries(absoluteRoot) {
71436
+ let entries;
71437
+ try {
71438
+ entries = readdirSync15(absoluteRoot, { withFileTypes: true });
71439
+ } catch {
71440
+ return [];
71441
+ }
71442
+ const output = [];
71443
+ for (const entry of entries) {
71444
+ const childFull = join28(absoluteRoot, entry.name);
71445
+ if (entry.isSymbolicLink()) {
71446
+ output.push({ kind: "symlink", relativePath: entry.name, fullPath: childFull });
71447
+ continue;
71448
+ }
71449
+ if (entry.isDirectory()) {
71450
+ if (EXCLUDE_DIR_NAMES.has(entry.name) || EXCLUDE_DIR_PATTERNS.some((pattern) => pattern.test(entry.name))) {
71451
+ continue;
71452
+ }
71453
+ const nested = walkEntries(childFull);
71454
+ for (const item of nested) {
71455
+ output.push({ ...item, relativePath: join28(entry.name, item.relativePath) });
71456
+ }
71457
+ continue;
71458
+ }
71459
+ if (entry.isFile()) {
71460
+ output.push({ kind: "file", relativePath: entry.name, fullPath: childFull });
71461
+ }
71462
+ }
71463
+ return output;
71464
+ }
71465
+ function isRegularFile(filePath) {
71466
+ try {
71467
+ return statSync15(filePath).isFile();
71468
+ } catch {
71469
+ return false;
71470
+ }
71471
+ }
71472
+ var SYNC_HOMES, EXCLUDE_DIR_NAMES, EXCLUDE_DIR_PATTERNS, EXCLUDE_FILE_NAMES, EXCLUDE_FILE_PATTERNS, PORTABLE_TOP_LEVEL, PORTABLE_SUBDIRS, REFUSED_SCANNER_FLAGGED;
71473
+ var init_portable_snapshot_filter = __esm(() => {
71474
+ SYNC_HOMES = [
71475
+ { name: "skills", subClass: "skills", agent: null },
71476
+ { name: "custom", subClass: "custom", agent: null },
71477
+ { name: "claude", subClass: "agent-homes", agent: "claude" },
71478
+ { name: "codewith", subClass: "agent-homes", agent: "codewith" },
71479
+ { name: "codex", subClass: "agent-homes", agent: "codex" },
71480
+ { name: "opencode", subClass: "agent-homes", agent: "opencode" },
71481
+ { name: "cursor", subClass: "agent-homes", agent: "cursor" }
71482
+ ];
71483
+ EXCLUDE_DIR_NAMES = new Set([
71484
+ ".git",
71485
+ "node_modules",
71486
+ "__pycache__",
71487
+ ".cache",
71488
+ ".pytest_cache",
71489
+ ".mypy_cache",
71490
+ ".ruff_cache"
71491
+ ]);
71492
+ EXCLUDE_DIR_PATTERNS = [
71493
+ /^\.merge-pr\.rollback-/
71494
+ ];
71495
+ EXCLUDE_FILE_NAMES = new Set([
71496
+ ".DS_Store",
71497
+ "package-lock.json",
71498
+ "pnpm-lock.yaml",
71499
+ "yarn.lock",
71500
+ "Cargo.lock"
71501
+ ]);
71502
+ EXCLUDE_FILE_PATTERNS = [
71503
+ /^\._/,
71504
+ /\.bak$/,
71505
+ /\.orig$/,
71506
+ /\.rej$/,
71507
+ /~$/,
71508
+ /\.pyc$/,
71509
+ /\.pyo$/,
71510
+ /\.log$/,
71511
+ /\.db$/,
71512
+ /\.sqlite(\d)?$/,
71513
+ /^bun\.lock/,
71514
+ /\.env($|\.)/,
71515
+ /\.pem$/,
71516
+ /\.key$/,
71517
+ /\.p12$/,
71518
+ /\.pfx$/,
71519
+ /\.jks$/,
71520
+ /^id_rsa/,
71521
+ /^id_ed25519/,
71522
+ /^credentials/
71523
+ ];
71524
+ PORTABLE_TOP_LEVEL = new Set(["SKILL.md", "skill.json"]);
71525
+ PORTABLE_SUBDIRS = new Set(["scripts", "assets", "references"]);
71526
+ REFUSED_SCANNER_FLAGGED = new Set([
71527
+ "aws-cross-account-app-migration/SKILL.md",
71528
+ "aws-cross-account-app-migration/scripts/selftest.sh",
71529
+ "gateway-serve/SKILL.md",
71530
+ "infinity-drain/SKILL.md",
71531
+ "infinity-run/SKILL.md",
71532
+ "oss-saas-code-cleanup/SKILL.md",
71533
+ "repo-project-familiarization/scripts/repo_shape.py",
71534
+ "repo-project-familiarization/scripts/session_history.py",
71535
+ "scale-check/SKILL.md",
71536
+ "standard-align-repo/SKILL.md",
71537
+ "standard-build-iapp/SKILL.md",
71538
+ "standard-build-oss/SKILL.md",
71539
+ "skill-image/SKILL.md",
71540
+ "skill-scale-check/SKILL.md",
71541
+ "sqlite-to-rds-parity-migrate/scripts/parity-migrate.ts",
71542
+ "pdf-operations/scripts/pdf_ops.py"
71543
+ ]);
71544
+ });
71545
+
71546
+ // src/lib/station-snapshot.ts
71547
+ import { createHash as createHash4 } from "crypto";
71548
+ import {
71549
+ copyFileSync as copyFileSync2,
71550
+ mkdirSync as mkdirSync15,
71551
+ readFileSync as readFileSync22,
71552
+ statSync as statSync16,
71553
+ writeFileSync as writeFileSync16
71554
+ } from "fs";
71555
+ import { dirname as dirname11, isAbsolute as isAbsolute3, relative as relative4, resolve, sep as sep4 } from "path";
71556
+ function validateStationId(stationId) {
71557
+ if (!/^[a-z0-9-]+$/.test(stationId)) {
71558
+ throw new StationSnapshotError("INVALID_STATION", `station id must be a slug, got: ${stationId}`);
71559
+ }
71560
+ }
71561
+ function sha256File(filePath) {
71562
+ return createHash4("sha256").update(readFileSync22(filePath)).digest("hex");
71563
+ }
71564
+ function scanHome(definition, homesRoot) {
71565
+ const homePath = homePathFor(definition, homesRoot);
71566
+ const entries = walkEntries(homePath);
71567
+ const portable = [];
71568
+ const skipped = [];
71569
+ for (const entry of entries) {
71570
+ const relativeParts = entry.relativePath.split(sep4);
71571
+ const fileName = relativeParts[relativeParts.length - 1];
71572
+ if (entry.kind === "symlink") {
71573
+ skipped.push({ relativePath: entry.relativePath, reason: "symlink" });
71574
+ continue;
71575
+ }
71576
+ if (!isPortableWithinSkill(relativeParts)) {
71577
+ skipped.push({ relativePath: entry.relativePath, reason: "not-portable" });
71578
+ continue;
71579
+ }
71580
+ if (isExcludedSkillFileName(fileName)) {
71581
+ skipped.push({ relativePath: entry.relativePath, reason: "excluded" });
71582
+ continue;
71583
+ }
71584
+ if (REFUSED_SCANNER_FLAGGED.has(entry.relativePath)) {
71585
+ skipped.push({ relativePath: entry.relativePath, reason: "refused-scanner-flagged" });
71586
+ continue;
71587
+ }
71588
+ if (!isRegularFile(entry.fullPath)) {
71589
+ skipped.push({ relativePath: entry.relativePath, reason: "not-regular-file" });
71590
+ continue;
71591
+ }
71592
+ const info = statSync16(entry.fullPath);
71593
+ portable.push({
71594
+ relativePath: entry.relativePath,
71595
+ fullPath: entry.fullPath,
71596
+ size: info.size,
71597
+ mtimeMs: info.mtimeMs,
71598
+ mtimeIso: info.mtime.toISOString()
71599
+ });
71600
+ }
71601
+ return { definition, homePath, portable, skipped };
71602
+ }
71603
+ function planStationSnapshot(options) {
71604
+ validateStationId(options.stationId);
71605
+ const scanned = SYNC_HOMES.map((definition) => scanHome(definition, options.homesRoot));
71606
+ const symlinks = scanned.reduce((sum2, item) => sum2 + item.skipped.filter((entry) => entry.reason === "symlink").length, 0);
71607
+ if (symlinks > 0) {
71608
+ throw new StationSnapshotError("SYMLINKS_REFUSED", `${symlinks} symlink(s) inside skill homes; symlinks are refused (fail closed)`);
71609
+ }
71610
+ const plans = [];
71611
+ for (const item of scanned) {
71612
+ for (const file of item.portable) {
71613
+ plans.push({
71614
+ definition: item.definition,
71615
+ source: file,
71616
+ destination: destinationFor(item.definition, options.stationId, file.relativePath),
71617
+ digest: sha256File(file.fullPath)
71618
+ });
71619
+ }
71620
+ }
71621
+ const totalBytes = plans.reduce((sum2, plan) => sum2 + plan.source.size, 0);
71622
+ return { scanned, plans, totalBytes };
71623
+ }
71624
+ function humanHomes(scanned) {
71625
+ return scanned.map((item) => ({
71626
+ name: item.definition.name,
71627
+ homePath: item.homePath,
71628
+ files: item.portable.length,
71629
+ skipped: item.skipped.length
71630
+ }));
71631
+ }
71632
+ function writeStationSnapshot(options) {
71633
+ const repoRoot = resolve(options.repoRoot ?? process.cwd());
71634
+ const { scanned, plans, totalBytes } = planStationSnapshot(options);
71635
+ const manifestFiles = plans.map((plan) => ({
71636
+ relativePath: plan.source.relativePath,
71637
+ destination: plan.destination,
71638
+ subClass: plan.definition.subClass,
71639
+ agent: plan.definition.agent,
71640
+ sha256: plan.digest,
71641
+ sourceMtimeMs: plan.source.mtimeMs,
71642
+ sourceMtimeIso: plan.source.mtimeIso,
71643
+ size: plan.source.size
71644
+ }));
71645
+ const base2 = {
71646
+ stationId: options.stationId,
71647
+ mode: "dry-run",
71648
+ repoRoot,
71649
+ stats: { files: plans.length, bytes: totalBytes },
71650
+ homes: humanHomes(scanned),
71651
+ files: manifestFiles
71652
+ };
71653
+ if (options.dryRun !== false) {
71654
+ return base2;
71655
+ }
71656
+ const conflicts = [];
71657
+ const untouched = [];
71658
+ for (const plan of plans) {
71659
+ const destination = resolve(repoRoot, plan.destination);
71660
+ const destinationRelative = relative4(repoRoot, destination);
71661
+ if (destinationRelative.startsWith("..") || destinationRelative.startsWith(sep4) || isAbsolute3(destinationRelative)) {
71662
+ throw new StationSnapshotError("DESTINATION_ESCAPE", `destination escapes repo root: ${plan.destination}`);
71663
+ }
71664
+ let existingDigest = null;
71665
+ try {
71666
+ existingDigest = sha256File(destination);
71667
+ } catch {}
71668
+ if (existingDigest !== null) {
71669
+ if (existingDigest === plan.digest) {
71670
+ continue;
71671
+ }
71672
+ conflicts.push(`existing destination differs from staged source: ${plan.destination}`);
71673
+ continue;
71674
+ }
71675
+ untouched.push(plan);
71676
+ }
71677
+ if (conflicts.length > 0) {
71678
+ throw new StationSnapshotError("CONFLICT", `${conflicts.length} conflict(s); terminal non-acceptance, nothing written`, conflicts);
71679
+ }
71680
+ let written = 0;
71681
+ for (const plan of untouched) {
71682
+ const destination = resolve(repoRoot, plan.destination);
71683
+ mkdirSync15(dirname11(destination), { recursive: true });
71684
+ copyFileSync2(plan.source.fullPath, destination);
71685
+ written += 1;
71686
+ }
71687
+ const unchanged = plans.length - untouched.length;
71688
+ const manifest = {
71689
+ schema: STATION_SYNC_MANIFEST_SCHEMA,
71690
+ stationId: options.stationId,
71691
+ syncedAt: new Date().toISOString(),
71692
+ producer: STATION_SNAPSHOT_PRODUCER,
71693
+ stats: {
71694
+ written,
71695
+ unchanged,
71696
+ files: plans.length,
71697
+ bytes: totalBytes
71698
+ },
71699
+ files: manifestFiles
71700
+ };
71701
+ const manifestPath = resolve(repoRoot, "resources", options.stationId, "skills", "sync-manifest.json");
71702
+ mkdirSync15(dirname11(manifestPath), { recursive: true });
71703
+ writeFileSync16(manifestPath, `${JSON.stringify(manifest, null, 2)}
71704
+ `);
71705
+ return {
71706
+ ...base2,
71707
+ mode: "populate",
71708
+ stats: { files: plans.length, bytes: totalBytes, written, unchanged },
71709
+ manifestPath
71710
+ };
71711
+ }
71712
+ var STATION_SYNC_MANIFEST_SCHEMA = "hasna.fleet-resources.skills-sync-manifest/v1", STATION_SNAPSHOT_PRODUCER, StationSnapshotError;
71713
+ var init_station_snapshot = __esm(() => {
71714
+ init_package();
71715
+ init_portable_snapshot_filter();
71716
+ STATION_SNAPSHOT_PRODUCER = { name: "@hasna/skills", version: package_default.version };
71717
+ StationSnapshotError = class StationSnapshotError extends Error {
71718
+ code;
71719
+ detail;
71720
+ constructor(code, message, detail = []) {
71721
+ super(message);
71722
+ this.name = "StationSnapshotError";
71723
+ this.code = code;
71724
+ this.detail = detail;
71725
+ }
71726
+ };
71727
+ });
71728
+
71388
71729
  // src/cli/commands/create-sync-config.ts
71389
71730
  var exports_create_sync_config = {};
71390
71731
  __export(exports_create_sync_config, {
71391
71732
  registerCreateSync: () => registerCreateSync
71392
71733
  });
71393
- import { existsSync as existsSync27, writeFileSync as writeFileSync16, mkdirSync as mkdirSync15 } from "fs";
71394
- import { join as join28 } from "path";
71734
+ import { existsSync as existsSync27, writeFileSync as writeFileSync17, mkdirSync as mkdirSync16 } from "fs";
71735
+ import { join as join29 } from "path";
71395
71736
  function registerCreateSync(parent) {
71396
71737
  const configCmd = parent.command("config").description("Manage skills configuration");
71397
71738
  configCmd.command("show", { isDefault: true }).option("--json", "Output as JSON", false).description("Show current merged configuration").action((options) => {
@@ -71466,13 +71807,13 @@ function registerCreateSync(parent) {
71466
71807
  console.log(`${source_default.cyan("project")}: ${pp}${existsSync27(pp) ? source_default.green(" (exists)") : source_default.dim(" (not found)")}`);
71467
71808
  });
71468
71809
  parent.command("create").argument("<name>", "Skill name (e.g. my-tool)").option("--category <category>", "Skill category", "Development Tools").option("--description <description>", "Short description of what the skill does").option("--tags <tags>", "Comma-separated tags (e.g. api,testing,automation)").option("--global", "Deprecated; custom skills are always global", false).option("--json", "Output result as JSON", false).description("Scaffold a new custom skill directory").action((name, options) => handleCreate(name, options));
71469
- parent.command("sync").alias("render").argument("[names...]", "Skills to sync (default: every skill in this machine's corpus)").option("--for <agent>", `Target one agent (${SYNC_AGENTS.join(", ")}, or all)`, "all").option("--all", "Sync every corpus skill (the default)", false).option("--source <path>", "Canonical corpus source: a directory of skill folders, or a package root with skills/ (overrides $SKILLS_SOURCE)").option("--dry-run", "Show what would be written without touching any agent folder", false).option("--force", "Adopt an unmanaged skill that already has SKILL.md; other unmarked directories are never overwritten", false).option("--check", "Home drift census (missing-from-home / stray-in-home / diverged); exits non-zero on drift, writes nothing", false).option("--adopt", "Unmarked-home adoption mode: hash unmarked home skills against the corpus; exact matches are marked (dry-run by default)", false).option("--prune", "Prune mode: list (or with --apply, remove) marked home skill dirs that have no canonical corpus entry", false).option("--apply", "Write adoption markers / conflicts ledger, or perform prune removals", false).option("--json", "Output as JSON", false).description("Write corpus skills into each coding agent's global skills folder, per-tool adapted").action((names, options) => handleSync(names, options));
71810
+ parent.command("sync").alias("render").argument("[names...]", "Skills to sync (default: every skill in this machine's corpus)").option("--for <agent>", `Target one agent (${SYNC_AGENTS.join(", ")}, or all)`, "all").option("--all", "Sync every corpus skill (the default)", false).option("--source <path>", "Canonical corpus source: a directory of skill folders, or a package root with skills/ (overrides $SKILLS_SOURCE)").option("--dry-run", "Show what would be written without touching any agent folder", false).option("--force", "Adopt an unmanaged skill that already has SKILL.md; other unmarked directories are never overwritten", false).option("--check", "Home drift census (missing-from-home / stray-in-home / diverged); exits non-zero on drift, writes nothing", false).option("--adopt", "Unmarked-home adoption mode: hash unmarked home skills against the corpus; exact matches are marked (dry-run by default)", false).option("--prune", "Prune mode: list (or with --apply, remove) marked home skill dirs that have no canonical corpus entry", false).option("--apply", "Write adoption markers / conflicts ledger, or perform prune removals", false).option("--json", "Output as JSON", false).option("--station <id>", "Per-station snapshot mode: snapshot the installed skill homes into resources/<station>/skills with a v3 sync-manifest (dry-run by default; --populate writes)").option("--populate", "Write the per-station snapshot (station mode; the default is dry-run)", false).option("--repo-root <path>", "Station snapshot destination repo root (default: cwd)").option("--homes-root <dir>", "Build the station snapshot from a staged mirror of the skill homes instead of this machine's $HOME").description("Write corpus skills into each coding agent's global skills folder, per-tool adapted; with --station, snapshot the homes into a reviewed snapshot repo instead").action((names, options) => handleSync(names, options));
71470
71811
  }
71471
71812
  function handleCreate(name, options) {
71472
71813
  const bare = name.trim();
71473
71814
  const dirName = bare;
71474
71815
  const baseDir = getPortableSkillsRoot();
71475
- const skillDir = join28(baseDir, dirName);
71816
+ const skillDir = join29(baseDir, dirName);
71476
71817
  if (existsSync27(skillDir)) {
71477
71818
  console.log(options.json ? JSON.stringify({ error: `Skill '${bare}' already exists at ${skillDir}` }) : source_default.red(`Skill '${bare}' already exists at ${skillDir}`));
71478
71819
  process.exitCode = 1;
@@ -71481,8 +71822,8 @@ function handleCreate(name, options) {
71481
71822
  const description = options.description || `${bare} skill`;
71482
71823
  const tags = options.tags ? options.tags.split(",").map((t) => t.trim()).filter(Boolean) : [bare];
71483
71824
  const displayName2 = bare.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
71484
- mkdirSync15(join28(skillDir, "src"), { recursive: true });
71485
- writeFileSync16(join28(skillDir, "SKILL.md"), [
71825
+ mkdirSync16(join29(skillDir, "src"), { recursive: true });
71826
+ writeFileSync17(join29(skillDir, "SKILL.md"), [
71486
71827
  "---",
71487
71828
  `name: ${bare}`,
71488
71829
  `description: ${description}`,
@@ -71502,11 +71843,11 @@ function handleCreate(name, options) {
71502
71843
  ""
71503
71844
  ].join(`
71504
71845
  `));
71505
- writeFileSync16(join28(skillDir, "src", "index.ts"), [`#!/usr/bin/env bun`, `/**`, ` * ${displayName2} \u2014 ${description}`, ` */`, "", `console.log("${displayName2}");`, ""].join(`
71846
+ writeFileSync17(join29(skillDir, "src", "index.ts"), [`#!/usr/bin/env bun`, `/**`, ` * ${displayName2} \u2014 ${description}`, ` */`, "", `console.log("${displayName2}");`, ""].join(`
71506
71847
  `));
71507
- writeFileSync16(join28(skillDir, "package.json"), JSON.stringify({ name: bare, version: "0.1.0", description, bin: { [bare]: "./src/index.ts" }, scripts: { dev: `bun src/index.ts` }, dependencies: {} }, null, 2) + `
71848
+ writeFileSync17(join29(skillDir, "package.json"), JSON.stringify({ name: bare, version: "0.1.0", description, bin: { [bare]: "./src/index.ts" }, scripts: { dev: `bun src/index.ts` }, dependencies: {} }, null, 2) + `
71508
71849
  `);
71509
- writeFileSync16(join28(skillDir, "tsconfig.json"), JSON.stringify({ compilerOptions: { target: "ES2022", module: "ESNext", moduleResolution: "bundler", strict: true, outDir: "dist" }, include: ["src/**/*.ts"] }, null, 2) + `
71850
+ writeFileSync17(join29(skillDir, "tsconfig.json"), JSON.stringify({ compilerOptions: { target: "ES2022", module: "ESNext", moduleResolution: "bundler", strict: true, outDir: "dist" }, include: ["src/**/*.ts"] }, null, 2) + `
71510
71851
  `);
71511
71852
  clearRegistryCache();
71512
71853
  if (options.json)
@@ -71515,11 +71856,15 @@ function handleCreate(name, options) {
71515
71856
  console.log(source_default.green(`\u2713 Created custom skill '${bare}' at ${skillDir}`));
71516
71857
  console.log(source_default.dim(` Category: ${options.category}`));
71517
71858
  console.log(source_default.dim(` Tags: ${tags.join(", ")}`));
71518
- console.log(` ${source_default.cyan("Edit:")} ${join28(skillDir, "src", "index.ts")}`);
71519
- console.log(` ${source_default.cyan("Run:")} bun ${join28(skillDir, "src", "index.ts")}`);
71859
+ console.log(` ${source_default.cyan("Edit:")} ${join29(skillDir, "src", "index.ts")}`);
71860
+ console.log(` ${source_default.cyan("Run:")} bun ${join29(skillDir, "src", "index.ts")}`);
71520
71861
  }
71521
71862
  }
71522
71863
  function handleSync(names, options) {
71864
+ if (options.station) {
71865
+ handleStationSnapshot(names, options);
71866
+ return;
71867
+ }
71523
71868
  const modes = [options.check, options.adopt, options.prune].filter(Boolean).length;
71524
71869
  if (modes > 1) {
71525
71870
  const message = "--check, --adopt, and --prune are mutually exclusive";
@@ -71580,6 +71925,68 @@ function handleSync(names, options) {
71580
71925
  process.exitCode = 1;
71581
71926
  }
71582
71927
  }
71928
+ function handleStationSnapshot(names, options) {
71929
+ const station = options.station;
71930
+ if (!station)
71931
+ return;
71932
+ if (options.populate && options.dryRun) {
71933
+ const message = "--populate and --dry-run are mutually exclusive";
71934
+ if (options.json)
71935
+ console.log(JSON.stringify({ error: message }));
71936
+ else
71937
+ console.error(source_default.red(message));
71938
+ process.exitCode = 1;
71939
+ return;
71940
+ }
71941
+ const incompatible = names.length > 0 || options.check || options.adopt || options.prune || options.force || options.all || options.source !== undefined;
71942
+ if (incompatible) {
71943
+ const message = "--station (per-station snapshot mode) cannot be combined with corpus->home sync names, --check, --adopt, --prune, --force, --all, or --source";
71944
+ if (options.json)
71945
+ console.log(JSON.stringify({ error: message }));
71946
+ else
71947
+ console.error(source_default.red(message));
71948
+ process.exitCode = 1;
71949
+ return;
71950
+ }
71951
+ try {
71952
+ const result2 = writeStationSnapshot({
71953
+ stationId: station,
71954
+ repoRoot: options.repoRoot,
71955
+ homesRoot: options.homesRoot,
71956
+ dryRun: !options.populate
71957
+ });
71958
+ if (result2.mode === "dry-run") {
71959
+ if (options.json) {
71960
+ console.log(JSON.stringify({
71961
+ stationId: result2.stationId,
71962
+ mode: "dry-run",
71963
+ stats: { files: result2.stats.files, bytes: result2.stats.bytes },
71964
+ homes: Object.fromEntries(result2.homes.map((home) => [
71965
+ home.name,
71966
+ { homePath: home.homePath, files: home.files, skipped: home.skipped }
71967
+ ]))
71968
+ }, null, 2));
71969
+ return;
71970
+ }
71971
+ console.log(`DRY-RUN station=${result2.stationId} files=${result2.stats.files} bytes=${result2.stats.bytes}`);
71972
+ for (const home of result2.homes) {
71973
+ console.log(` ${home.name}: ${home.files} files, ${home.skipped} skipped`);
71974
+ }
71975
+ return;
71976
+ }
71977
+ console.log(`POPULATE station=${result2.stationId} written=${result2.stats.written} unchanged=${result2.stats.unchanged} total=${result2.stats.files} bytes=${result2.stats.bytes}`);
71978
+ } catch (error2) {
71979
+ if (error2 instanceof StationSnapshotError) {
71980
+ for (const line of error2.detail)
71981
+ console.error(`CONFLICT ${line}`);
71982
+ console.error(`FAIL ${error2.message}`);
71983
+ process.exitCode = 2;
71984
+ } else {
71985
+ console.error(`FAIL ${error2.stack ?? error2.message}`);
71986
+ process.exitCode = 1;
71987
+ }
71988
+ }
71989
+ }
71583
71990
  function handleSyncCheck(json) {
71584
71991
  const census = censusHomeDrift();
71585
71992
  if (json) {
@@ -71691,6 +72098,350 @@ var init_create_sync_config = __esm(() => {
71691
72098
  init_agent_sync();
71692
72099
  init_home_adoption();
71693
72100
  init_home_census();
72101
+ init_station_snapshot();
72102
+ });
72103
+
72104
+ // src/lib/station-hydrate.ts
72105
+ import { createHash as createHash5 } from "crypto";
72106
+ import {
72107
+ copyFileSync as copyFileSync3,
72108
+ mkdirSync as mkdirSync17,
72109
+ readdirSync as readdirSync16,
72110
+ readFileSync as readFileSync23,
72111
+ statSync as statSync17,
72112
+ writeFileSync as writeFileSync18
72113
+ } from "fs";
72114
+ import { dirname as dirname12, join as join30, resolve as resolve2, sep as sep5 } from "path";
72115
+ function fail2(code, message, detail = []) {
72116
+ throw new StationSnapshotError(code, message, detail);
72117
+ }
72118
+ function snapshotRootFor(repoRoot, stationId) {
72119
+ return join30(repoRoot, "resources", stationId, "skills");
72120
+ }
72121
+ function readSnapshotManifest(repoRoot, stationId) {
72122
+ const snapshotRoot = snapshotRootFor(repoRoot, stationId);
72123
+ const manifestPath = join30(snapshotRoot, "sync-manifest.json");
72124
+ let manifest;
72125
+ try {
72126
+ manifest = JSON.parse(readFileSync23(manifestPath, "utf8"));
72127
+ } catch (error2) {
72128
+ fail2("MANIFEST_UNREADABLE", `cannot read snapshot manifest: ${manifestPath}: ${error2.message}`);
72129
+ }
72130
+ const sourceSnapshotSha = sha256File(manifestPath);
72131
+ return { manifest, manifestPath, sourceSnapshotSha };
72132
+ }
72133
+ function planStationHydration(stationId, repoRoot) {
72134
+ validateStationId(stationId);
72135
+ const { manifest, sourceSnapshotSha } = readSnapshotManifest(repoRoot, stationId);
72136
+ const snapshotRoot = snapshotRootFor(repoRoot, stationId);
72137
+ const manifestHashes = new Map;
72138
+ for (const file of manifest.files ?? []) {
72139
+ const relativePath = file.relativePath;
72140
+ const agent = file.agent;
72141
+ if (agent && relativePath) {
72142
+ manifestHashes.set(`${agent}${MANIFEST_HASH_KEY_SEP}${relativePath}`, file.sha256);
72143
+ }
72144
+ }
72145
+ const candidates = [];
72146
+ const symlinks = [];
72147
+ const skippedByRule = [];
72148
+ for (const agent of SYNC_AGENTS) {
72149
+ const agentRoot = join30(snapshotRoot, "agent-homes", agent);
72150
+ let identEntries;
72151
+ try {
72152
+ identEntries = readdirSync16(agentRoot, { withFileTypes: true });
72153
+ } catch {
72154
+ continue;
72155
+ }
72156
+ for (const identEntry of identEntries) {
72157
+ if (!identEntry.isDirectory() || identEntry.name.startsWith(".")) {
72158
+ continue;
72159
+ }
72160
+ const identRoot = join30(agentRoot, identEntry.name);
72161
+ const entries = walkEntries(identRoot);
72162
+ for (const entry of entries) {
72163
+ const relativeParts = [identEntry.name, ...entry.relativePath.split(sep5)];
72164
+ if (entry.kind === "symlink") {
72165
+ symlinks.push({ ident: identEntry.name, agent, relativePath: entry.relativePath });
72166
+ continue;
72167
+ }
72168
+ if (!isPortableWithinSkill(relativeParts)) {
72169
+ skippedByRule.push({
72170
+ ident: identEntry.name,
72171
+ agent,
72172
+ relativePath: entry.relativePath,
72173
+ reason: "not-portable"
72174
+ });
72175
+ continue;
72176
+ }
72177
+ const fileName = relativeParts[relativeParts.length - 1];
72178
+ if (isExcludedSkillFileName(fileName)) {
72179
+ skippedByRule.push({
72180
+ ident: identEntry.name,
72181
+ agent,
72182
+ relativePath: entry.relativePath,
72183
+ reason: "excluded"
72184
+ });
72185
+ continue;
72186
+ }
72187
+ const withinIdent = relativeParts.slice(1).join(sep5);
72188
+ const homeRelative = relativeParts.join(sep5);
72189
+ if (REFUSED_SCANNER_FLAGGED.has(homeRelative)) {
72190
+ skippedByRule.push({
72191
+ ident: identEntry.name,
72192
+ agent,
72193
+ relativePath: entry.relativePath,
72194
+ reason: "refused-scanner-flagged"
72195
+ });
72196
+ continue;
72197
+ }
72198
+ if (!isRegularFile(entry.fullPath)) {
72199
+ skippedByRule.push({
72200
+ ident: identEntry.name,
72201
+ agent,
72202
+ relativePath: entry.relativePath,
72203
+ reason: "not-regular-file"
72204
+ });
72205
+ continue;
72206
+ }
72207
+ const info = statSync17(entry.fullPath);
72208
+ candidates.push({
72209
+ ident: identEntry.name,
72210
+ agent,
72211
+ withinIdent,
72212
+ fullPath: entry.fullPath,
72213
+ size: info.size,
72214
+ mtimeMs: info.mtimeMs,
72215
+ manifestHash: manifestHashes.get(`${agent}${MANIFEST_HASH_KEY_SEP}${homeRelative}`) ?? null
72216
+ });
72217
+ }
72218
+ }
72219
+ }
72220
+ if (symlinks.length > 0) {
72221
+ fail2("SYMLINKS_REFUSED", `${symlinks.length} symlink(s) inside the snapshot; symlinks are refused (fail closed)`);
72222
+ }
72223
+ const byIdent = new Map;
72224
+ for (const candidate of candidates) {
72225
+ const group = byIdent.get(candidate.ident) ?? [];
72226
+ group.push(candidate);
72227
+ byIdent.set(candidate.ident, group);
72228
+ }
72229
+ const winners = [];
72230
+ for (const [ident, group] of byIdent) {
72231
+ const byFile = new Map;
72232
+ for (const candidate of group) {
72233
+ const copies = byFile.get(candidate.withinIdent) ?? [];
72234
+ copies.push(candidate);
72235
+ byFile.set(candidate.withinIdent, copies);
72236
+ }
72237
+ const files = [];
72238
+ for (const [withinIdent, copies] of byFile) {
72239
+ let eligible = copies;
72240
+ if (withinIdent === "SKILL.md") {
72241
+ const content = [];
72242
+ for (const copy of copies) {
72243
+ let isStub = false;
72244
+ try {
72245
+ isStub = isPointerSkillMd(readFileSync23(copy.fullPath, "utf8"));
72246
+ } catch {
72247
+ isStub = false;
72248
+ }
72249
+ if (!isStub)
72250
+ content.push(copy);
72251
+ }
72252
+ if (content.length > 0) {
72253
+ eligible = content;
72254
+ }
72255
+ }
72256
+ eligible.sort((left, right) => {
72257
+ const leftHash = left.manifestHash !== null;
72258
+ const rightHash = right.manifestHash !== null;
72259
+ if (leftHash !== rightHash) {
72260
+ return leftHash ? -1 : 1;
72261
+ }
72262
+ if (right.mtimeMs !== left.mtimeMs) {
72263
+ return right.mtimeMs - left.mtimeMs;
72264
+ }
72265
+ return SYNC_AGENTS.indexOf(left.agent) - SYNC_AGENTS.indexOf(right.agent);
72266
+ });
72267
+ const winner = eligible[0];
72268
+ files.push({
72269
+ withinIdent,
72270
+ winner,
72271
+ alternates: copies.filter((copy) => copy !== winner).map((copy) => copy.agent)
72272
+ });
72273
+ }
72274
+ files.sort((left, right) => left.withinIdent.localeCompare(right.withinIdent));
72275
+ winners.push({ ident, files });
72276
+ }
72277
+ winners.sort((left, right) => left.ident.localeCompare(right.ident));
72278
+ const totalFiles = winners.reduce((sum2, skill) => sum2 + skill.files.length, 0);
72279
+ const totalBytes = winners.reduce((sum2, skill) => sum2 + skill.files.reduce((inner, file) => inner + file.winner.size, 0), 0);
72280
+ return { manifest, sourceSnapshotSha, winners, skippedByRule, totalFiles, totalBytes };
72281
+ }
72282
+ function skillSha256(skill) {
72283
+ const skillMd = skill.files.find((file) => file.withinIdent === "SKILL.md");
72284
+ if (skillMd) {
72285
+ return sha256File(skillMd.winner.fullPath);
72286
+ }
72287
+ if (skill.files.length === 1) {
72288
+ return sha256File(skill.files[0].winner.fullPath);
72289
+ }
72290
+ const joined = skill.files.map((file) => sha256File(file.winner.fullPath));
72291
+ return createHash5("sha256").update(joined.sort().join(`
72292
+ `)).digest("hex");
72293
+ }
72294
+ function writeStationHydration(options) {
72295
+ const repoRoot = resolve2(options.repoRoot ?? process.cwd());
72296
+ const cacheRoot = resolve2(options.cacheRoot ?? resolveCorpusRoot());
72297
+ const plan = planStationHydration(options.stationId, repoRoot);
72298
+ const resultSkills = plan.winners.map((skill) => ({
72299
+ ident: skill.ident,
72300
+ files: skill.files.map((file) => ({
72301
+ relativePath: file.withinIdent,
72302
+ sourceAgent: file.winner.agent,
72303
+ sourceMtimeMs: file.winner.mtimeMs,
72304
+ size: file.winner.size
72305
+ })),
72306
+ sha256: skillSha256(skill)
72307
+ }));
72308
+ const base2 = {
72309
+ stationId: options.stationId,
72310
+ mode: "dry-run",
72311
+ cacheRoot,
72312
+ snapshotRoot: snapshotRootFor(repoRoot, options.stationId),
72313
+ sourceSnapshotSha: plan.sourceSnapshotSha,
72314
+ stats: {
72315
+ idents: plan.winners.length,
72316
+ files: plan.totalFiles,
72317
+ bytes: plan.totalBytes
72318
+ },
72319
+ winners: plan.winners,
72320
+ skills: resultSkills
72321
+ };
72322
+ if (options.dryRun !== false) {
72323
+ return base2;
72324
+ }
72325
+ const conflicts = [];
72326
+ const toWrite = [];
72327
+ for (const skill of plan.winners) {
72328
+ for (const file of skill.files) {
72329
+ const destination = join30(cacheRoot, skill.ident, file.withinIdent);
72330
+ const digest = sha256File(file.winner.fullPath);
72331
+ let existingDigest = null;
72332
+ try {
72333
+ existingDigest = sha256File(destination);
72334
+ } catch {}
72335
+ if (existingDigest !== null) {
72336
+ if (existingDigest === digest) {
72337
+ continue;
72338
+ }
72339
+ conflicts.push(`existing destination differs from snapshot winner: ${destination}`);
72340
+ continue;
72341
+ }
72342
+ toWrite.push({ destination, fullPath: file.winner.fullPath });
72343
+ }
72344
+ }
72345
+ if (conflicts.length > 0) {
72346
+ fail2("CONFLICT", `${conflicts.length} conflict(s); terminal non-acceptance, nothing written`, conflicts);
72347
+ }
72348
+ let written = 0;
72349
+ for (const entry of toWrite) {
72350
+ mkdirSync17(dirname12(entry.destination), { recursive: true });
72351
+ copyFileSync3(entry.fullPath, entry.destination);
72352
+ written += 1;
72353
+ }
72354
+ const unchanged = plan.totalFiles - written;
72355
+ const hydration = {
72356
+ schema: STATION_HYDRATION_MANIFEST_SCHEMA,
72357
+ stationId: options.stationId,
72358
+ hydratedAt: new Date().toISOString(),
72359
+ producer: STATION_HYDRATION_PRODUCER,
72360
+ sourceSnapshotSha: plan.sourceSnapshotSha,
72361
+ cacheRoot,
72362
+ stats: {
72363
+ idents: plan.winners.length,
72364
+ written,
72365
+ unchanged,
72366
+ files: plan.totalFiles,
72367
+ bytes: plan.totalBytes
72368
+ },
72369
+ skills: resultSkills
72370
+ };
72371
+ const hydrationManifestPath = join30(dirname12(cacheRoot), `hydration-${options.stationId}.json`);
72372
+ mkdirSync17(dirname12(hydrationManifestPath), { recursive: true });
72373
+ writeFileSync18(hydrationManifestPath, `${JSON.stringify(hydration, null, 2)}
72374
+ `);
72375
+ return {
72376
+ ...base2,
72377
+ mode: "apply",
72378
+ stats: { ...base2.stats, written, unchanged },
72379
+ manifestPath: hydrationManifestPath
72380
+ };
72381
+ }
72382
+ var STATION_HYDRATION_MANIFEST_SCHEMA = "hasna.fleet-resources.skills-hydration-manifest/v1", STATION_HYDRATION_PRODUCER, MANIFEST_HASH_KEY_SEP;
72383
+ var init_station_hydrate = __esm(() => {
72384
+ init_package();
72385
+ init_agent_sync();
72386
+ init_home_migration();
72387
+ init_portable_snapshot_filter();
72388
+ init_station_snapshot();
72389
+ STATION_HYDRATION_PRODUCER = { name: "@hasna/skills", version: package_default.version };
72390
+ MANIFEST_HASH_KEY_SEP = String.fromCharCode(0);
72391
+ });
72392
+
72393
+ // src/cli/commands/hydrate.ts
72394
+ var exports_hydrate = {};
72395
+ __export(exports_hydrate, {
72396
+ registerHydrate: () => registerHydrate
72397
+ });
72398
+ function registerHydrate(parent) {
72399
+ parent.command("hydrate").description("Hydrate the canonical dedup corpus cache from a reviewed per-station skills snapshot").requiredOption("--station <id>", "Station id (slug) naming the snapshot under resources/<id>/skills").option("--apply", "Write into the corpus cache (the default is dry-run)", false).option("--dry-run", "Report without writing anything (the default)", false).option("--cache-root <dir>", "Override the destination corpus cache (used to stage another station's cache before rsync)").option("--repo-root <path>", "Repo root holding resources/<station>/skills (default: cwd)").action((options) => {
72400
+ handleHydrate(options);
72401
+ });
72402
+ }
72403
+ function handleHydrate(options) {
72404
+ if (options.apply && options.dryRun) {
72405
+ console.error(source_default.red("--apply and --dry-run are mutually exclusive"));
72406
+ process.exitCode = 2;
72407
+ return;
72408
+ }
72409
+ let result2;
72410
+ try {
72411
+ result2 = writeStationHydration({
72412
+ stationId: options.station,
72413
+ repoRoot: options.repoRoot,
72414
+ cacheRoot: options.cacheRoot,
72415
+ dryRun: !options.apply
72416
+ });
72417
+ } catch (error2) {
72418
+ if (error2 instanceof StationSnapshotError) {
72419
+ for (const line of error2.detail)
72420
+ console.error(`CONFLICT ${line}`);
72421
+ console.error(`FAIL ${error2.message}`);
72422
+ process.exitCode = 2;
72423
+ } else {
72424
+ console.error(`FAIL ${error2.stack ?? error2.message}`);
72425
+ process.exitCode = 1;
72426
+ }
72427
+ return;
72428
+ }
72429
+ if (result2.mode === "dry-run") {
72430
+ console.log(`DRY-RUN station=${result2.stationId} idents=${result2.stats.idents} files=${result2.stats.files} bytes=${result2.stats.bytes}`);
72431
+ console.log(` cache-root=${result2.cacheRoot} snapshot-sha=${result2.sourceSnapshotSha.slice(0, 12)}`);
72432
+ for (const skill of result2.winners) {
72433
+ const merged = skill.files.length > 1 ? ` (${skill.files.length} files, alternates: ${skill.files.map((file) => file.alternates.length > 0 ? `${file.withinIdent}<-${file.alternates.join(",")}` : null).filter(Boolean).join("; ") || "none"})` : "";
72434
+ console.log(` ${skill.ident}${merged}`);
72435
+ }
72436
+ return;
72437
+ }
72438
+ console.log(`HYDRATE station=${result2.stationId} idents=${result2.stats.idents} written=${result2.stats.written} unchanged=${result2.stats.unchanged} files=${result2.stats.files} bytes=${result2.stats.bytes}`);
72439
+ console.log(` manifest=${result2.manifestPath} snapshot-sha=${result2.sourceSnapshotSha}`);
72440
+ }
72441
+ var init_hydrate = __esm(() => {
72442
+ init_source();
72443
+ init_station_snapshot();
72444
+ init_station_hydrate();
71694
72445
  });
71695
72446
 
71696
72447
  // src/cli/commands/portable-skills.ts
@@ -72021,8 +72772,8 @@ var init_schedule = __esm(() => {
72021
72772
  });
72022
72773
 
72023
72774
  // src/lib/registry-sync.ts
72024
- import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync17 } from "fs";
72025
- import { dirname as dirname11, relative as relative4 } from "path";
72775
+ import { mkdirSync as mkdirSync18, writeFileSync as writeFileSync19 } from "fs";
72776
+ import { dirname as dirname13, relative as relative5 } from "path";
72026
72777
  function createRegistrySyncArtifact(options = {}) {
72027
72778
  const profile = options.profile ?? "all";
72028
72779
  const includeDocs = options.includeDocs ?? true;
@@ -72034,7 +72785,7 @@ function createRegistrySyncArtifact(options = {}) {
72034
72785
  const registry2 = [...loadRegistryProfile(profile)].sort((a, b) => a.name.localeCompare(b.name));
72035
72786
  const skills = registry2.map((skill) => {
72036
72787
  const skillPath = getSkillPath(skill.name);
72037
- const directory = relative4(process.cwd(), skillPath) || skillPath;
72788
+ const directory = relative5(process.cwd(), skillPath) || skillPath;
72038
72789
  const validation = includeValidation ? validateSkillDirectory(skill.name, skillPath, skill) : undefined;
72039
72790
  const docs = includeDocs ? buildDocs(skill.name) : undefined;
72040
72791
  return {
@@ -72079,8 +72830,8 @@ function createRegistrySyncArtifact(options = {}) {
72079
72830
  };
72080
72831
  }
72081
72832
  function writeRegistrySyncArtifact(path, artifact) {
72082
- mkdirSync16(dirname11(path), { recursive: true });
72083
- writeFileSync17(path, `${JSON.stringify(artifact, null, 2)}
72833
+ mkdirSync18(dirname13(path), { recursive: true });
72834
+ writeFileSync19(path, `${JSON.stringify(artifact, null, 2)}
72084
72835
  `);
72085
72836
  }
72086
72837
  function buildDocs(name) {
@@ -72099,7 +72850,7 @@ var init_registry_sync = __esm(() => {
72099
72850
  });
72100
72851
 
72101
72852
  // src/lib/revision.ts
72102
- import { createHash as createHash4 } from "crypto";
72853
+ import { createHash as createHash6 } from "crypto";
72103
72854
  function revisionIdOf(content) {
72104
72855
  const canonical = JSON.stringify({
72105
72856
  slug: content.slug,
@@ -72114,7 +72865,7 @@ function revisionIdOf(content) {
72114
72865
  bundleSha256: content.bundleSha256 ?? null,
72115
72866
  bundleByteSize: content.bundleByteSize ?? null
72116
72867
  });
72117
- return createHash4("sha256").update(canonical).digest("hex");
72868
+ return createHash6("sha256").update(canonical).digest("hex");
72118
72869
  }
72119
72870
  var REVISION_ID_PATTERN;
72120
72871
  var init_revision = __esm(() => {
@@ -72122,9 +72873,9 @@ var init_revision = __esm(() => {
72122
72873
  });
72123
72874
 
72124
72875
  // src/lib/skill-bundle.ts
72125
- import { createHash as createHash5 } from "crypto";
72126
- import { readFileSync as readFileSync22, readdirSync as readdirSync15, statSync as statSync15 } from "fs";
72127
- import { join as join29, relative as relative5 } from "path";
72876
+ import { createHash as createHash7 } from "crypto";
72877
+ import { readFileSync as readFileSync24, readdirSync as readdirSync17, statSync as statSync18 } from "fs";
72878
+ import { join as join31, relative as relative6 } from "path";
72128
72879
  function isDotenvFile(lower) {
72129
72880
  if (lower === ".env" || lower.startsWith(".env."))
72130
72881
  return true;
@@ -72153,7 +72904,7 @@ function ownBytes(view) {
72153
72904
  return out;
72154
72905
  }
72155
72906
  function sha256Hex(bytes) {
72156
- return createHash5("sha256").update(bytes).digest("hex");
72907
+ return createHash7("sha256").update(bytes).digest("hex");
72157
72908
  }
72158
72909
  function collectSkillBundleEntries(dir) {
72159
72910
  const entries = [];
@@ -72161,9 +72912,9 @@ function collectSkillBundleEntries(dir) {
72161
72912
  return entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
72162
72913
  }
72163
72914
  function walk(root, current, out) {
72164
- for (const entry of readdirSync15(current, { withFileTypes: true })) {
72165
- const absolute = join29(current, entry.name);
72166
- const rel = relative5(root, absolute).split("\\").join("/");
72915
+ for (const entry of readdirSync17(current, { withFileTypes: true })) {
72916
+ const absolute = join31(current, entry.name);
72917
+ const rel = relative6(root, absolute).split("\\").join("/");
72167
72918
  const isRootLevel = !rel.includes("/");
72168
72919
  if (ANY_SEGMENT_EXCLUDES.has(entry.name.toLowerCase()))
72169
72920
  continue;
@@ -72183,10 +72934,10 @@ function walk(root, current, out) {
72183
72934
  continue;
72184
72935
  if (isCredentialFile(entry.name))
72185
72936
  continue;
72186
- const stats = statSync15(absolute);
72937
+ const stats = statSync18(absolute);
72187
72938
  out.push({
72188
72939
  path: rel,
72189
- bytes: ownBytes(readFileSync22(absolute)),
72940
+ bytes: ownBytes(readFileSync24(absolute)),
72190
72941
  mode: stats.mode & 64 ? 493 : 420
72191
72942
  });
72192
72943
  }
@@ -72472,8 +73223,8 @@ var init_skill_bundles = __esm(() => {
72472
73223
  });
72473
73224
 
72474
73225
  // src/lib/pull.ts
72475
- import { existsSync as existsSync28, mkdirSync as mkdirSync17, mkdtempSync as mkdtempSync3, readFileSync as readFileSync23, renameSync as renameSync4, rmSync as rmSync6, writeFileSync as writeFileSync18 } from "fs";
72476
- import { dirname as dirname12, join as join30 } from "path";
73226
+ import { existsSync as existsSync28, mkdirSync as mkdirSync19, mkdtempSync as mkdtempSync3, readFileSync as readFileSync25, renameSync as renameSync4, rmSync as rmSync6, writeFileSync as writeFileSync20 } from "fs";
73227
+ import { dirname as dirname14, join as join32 } from "path";
72477
73228
  async function pullSkills(options = {}) {
72478
73229
  const client = options.client !== undefined ? options.client : createRemoteSkillsClient();
72479
73230
  if (!client) {
@@ -72518,7 +73269,7 @@ async function pullOne(client, rawName, corpusOptions, verify) {
72518
73269
  return reconcileTombstone(slug, corpusOptions);
72519
73270
  }
72520
73271
  if (bundleResponse.status === 404) {
72521
- const marker = readPullMarker(join30(getPortableSkillsRoot(corpusOptions), slug));
73272
+ const marker = readPullMarker(join32(getPortableSkillsRoot(corpusOptions), slug));
72522
73273
  if (marker && typeof marker.revisionId === "string" && marker.revisionId) {
72523
73274
  return { name: slug, success: true, purged: true, removed: false };
72524
73275
  }
@@ -72549,7 +73300,7 @@ async function pullOne(client, rawName, corpusOptions, verify) {
72549
73300
  return { name: slug, success: false, error: `Skill '${slug}' was not found on the configured Skills instance.` };
72550
73301
  }
72551
73302
  if (!meta?.revisionId) {
72552
- const marker = readPullMarker(join30(getPortableSkillsRoot(corpusOptions), slug));
73303
+ const marker = readPullMarker(join32(getPortableSkillsRoot(corpusOptions), slug));
72553
73304
  if (marker && typeof marker.revisionId === "string" && marker.revisionId) {
72554
73305
  return { name: slug, success: true, purged: true, removed: false };
72555
73306
  }
@@ -72606,8 +73357,8 @@ function provenRevision(meta, slug, bundle) {
72606
73357
  return declared;
72607
73358
  }
72608
73359
  function reconcileTombstone(slug, corpusOptions) {
72609
- const target = join30(getPortableSkillsRoot(corpusOptions), slug);
72610
- if (!existsSync28(join30(target, PULL_MARKER_FILE))) {
73360
+ const target = join32(getPortableSkillsRoot(corpusOptions), slug);
73361
+ if (!existsSync28(join32(target, PULL_MARKER_FILE))) {
72611
73362
  return { name: slug, success: true, tombstoned: true, removed: false, leftInPlace: true };
72612
73363
  }
72613
73364
  rmSync6(target, { recursive: true, force: true });
@@ -72615,7 +73366,7 @@ function reconcileTombstone(slug, corpusOptions) {
72615
73366
  }
72616
73367
  function readPullMarker(dir) {
72617
73368
  try {
72618
- return JSON.parse(readFileSync23(join30(dir, PULL_MARKER_FILE), "utf-8"));
73369
+ return JSON.parse(readFileSync25(join32(dir, PULL_MARKER_FILE), "utf-8"));
72619
73370
  } catch {
72620
73371
  return null;
72621
73372
  }
@@ -72735,17 +73486,17 @@ function verifyBundleResponseBytes(buffer, response, verify = {}) {
72735
73486
  }
72736
73487
  function installBundleAtomically(name, entries, options = {}, marker = {}) {
72737
73488
  const root = getPortableSkillsRoot(options);
72738
- mkdirSync17(root, { recursive: true });
72739
- const target = join30(root, name);
73489
+ mkdirSync19(root, { recursive: true });
73490
+ const target = join32(root, name);
72740
73491
  const created = !existsSync28(target);
72741
- const staging = mkdtempSync3(join30(root, `.pull-${name}-`));
73492
+ const staging = mkdtempSync3(join32(root, `.pull-${name}-`));
72742
73493
  let moved = false;
72743
73494
  let backup = null;
72744
73495
  try {
72745
73496
  for (const entry of entries) {
72746
- const destination = join30(staging, entry.path);
72747
- mkdirSync17(dirname12(destination), { recursive: true });
72748
- writeFileSync18(destination, entry.bytes, { mode: entry.mode });
73497
+ const destination = join32(staging, entry.path);
73498
+ mkdirSync19(dirname14(destination), { recursive: true });
73499
+ writeFileSync20(destination, entry.bytes, { mode: entry.mode });
72749
73500
  }
72750
73501
  writePullMarker(staging, {
72751
73502
  skill: name,
@@ -72756,8 +73507,8 @@ function installBundleAtomically(name, entries, options = {}, marker = {}) {
72756
73507
  ...marker.revisionId ? { revisionId: marker.revisionId } : {}
72757
73508
  });
72758
73509
  if (existsSync28(target)) {
72759
- backup = mkdtempSync3(join30(root, `.pull-backup-${name}-`));
72760
- renameSync4(target, join30(backup, name));
73510
+ backup = mkdtempSync3(join32(root, `.pull-backup-${name}-`));
73511
+ renameSync4(target, join32(backup, name));
72761
73512
  moved = true;
72762
73513
  }
72763
73514
  renameSync4(staging, target);
@@ -72765,9 +73516,9 @@ function installBundleAtomically(name, entries, options = {}, marker = {}) {
72765
73516
  rmSync6(backup, { recursive: true, force: true });
72766
73517
  } catch (error2) {
72767
73518
  rmSync6(staging, { recursive: true, force: true });
72768
- if (moved && backup && existsSync28(join30(backup, name))) {
73519
+ if (moved && backup && existsSync28(join32(backup, name))) {
72769
73520
  try {
72770
- renameSync4(join30(backup, name), target);
73521
+ renameSync4(join32(backup, name), target);
72771
73522
  } catch {}
72772
73523
  }
72773
73524
  throw error2;
@@ -72786,7 +73537,7 @@ function writePullMarker(dir, record3) {
72786
73537
  ...record3.revisionId ? { revisionId: record3.revisionId } : {},
72787
73538
  syncedAt: new Date().toISOString()
72788
73539
  };
72789
- writeFileSync18(join30(dir, PULL_MARKER_FILE), `${JSON.stringify(marker, null, 2)}
73540
+ writeFileSync20(join32(dir, PULL_MARKER_FILE), `${JSON.stringify(marker, null, 2)}
72790
73541
  `);
72791
73542
  }
72792
73543
  async function safeMeta(client, slug) {
@@ -72860,12 +73611,12 @@ function registerRegistry(parent) {
72860
73611
  async function writeJson2(value, space) {
72861
73612
  const text = `${JSON.stringify(value, null, space)}
72862
73613
  `;
72863
- await new Promise((resolve, reject2) => {
73614
+ await new Promise((resolve3, reject2) => {
72864
73615
  process.stdout.write(text, (error2) => {
72865
73616
  if (error2)
72866
73617
  reject2(error2);
72867
73618
  else
72868
- resolve();
73619
+ resolve3();
72869
73620
  });
72870
73621
  });
72871
73622
  }
@@ -72957,8 +73708,8 @@ __export(exports_publish, {
72957
73708
  pushSkill: () => pushSkill,
72958
73709
  PushSkillError: () => PushSkillError
72959
73710
  });
72960
- import { existsSync as existsSync29, readFileSync as readFileSync24 } from "fs";
72961
- import { join as join31 } from "path";
73711
+ import { existsSync as existsSync29, readFileSync as readFileSync26 } from "fs";
73712
+ import { join as join33 } from "path";
72962
73713
  function registerPublish(parent) {
72963
73714
  parent.command("push").argument("<name>", "Name of a skill in the local corpus (~/.hasna/skills/installed or the migrated ~/.hasna/skills/skills)").option("--version <version>", "Override the version recorded on the instance").option("--dry-run", "Pack and validate without uploading", false).option("--json", "Output result as JSON", false).description("Publish a local skill to the configured skills instance").action(async (name, options) => {
72964
73715
  try {
@@ -72999,8 +73750,8 @@ async function pushSkill(name, options = {}) {
72999
73750
  }
73000
73751
  const manifest = readPortableSkillManifest(skill.path, skill.name);
73001
73752
  const packed = packSkillBundle(skill.path, { maxUnpackedBytes: MAX_UNPACKED_BYTES });
73002
- const skillMdPath = join31(skill.path, "SKILL.md");
73003
- const skillMd = existsSync29(skillMdPath) ? readFileSync24(skillMdPath, "utf-8") : undefined;
73753
+ const skillMdPath = join33(skill.path, "SKILL.md");
73754
+ const skillMd = existsSync29(skillMdPath) ? readFileSync26(skillMdPath, "utf-8") : undefined;
73004
73755
  const base2 = {
73005
73756
  slug: skill.name,
73006
73757
  path: skill.path,
@@ -73117,10 +73868,10 @@ __export(exports_auth, {
73117
73868
  import { createInterface as createInterface2 } from "readline";
73118
73869
  function prompt(question) {
73119
73870
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
73120
- return new Promise((resolve) => {
73871
+ return new Promise((resolve3) => {
73121
73872
  rl.question(question, (answer) => {
73122
73873
  rl.close();
73123
- resolve(answer.trim());
73874
+ resolve3(answer.trim());
73124
73875
  });
73125
73876
  });
73126
73877
  }
@@ -73270,7 +74021,7 @@ function printWhoami(payload) {
73270
74021
  console.log(source_default.dim("(offline \u2014 showing cached info)"));
73271
74022
  }
73272
74023
  function sleep(ms) {
73273
- return new Promise((resolve) => setTimeout(resolve, ms));
74024
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
73274
74025
  }
73275
74026
  function browserCommand(url) {
73276
74027
  if (process.platform === "darwin")
@@ -73771,11 +74522,11 @@ var init_storage = __esm(() => {
73771
74522
  });
73772
74523
 
73773
74524
  // src/lib/registry-reconcile.ts
73774
- import { existsSync as existsSync30, readFileSync as readFileSync25, statSync as statSync16, writeFileSync as writeFileSync19 } from "fs";
73775
- import { join as join32 } from "path";
74525
+ import { existsSync as existsSync30, readFileSync as readFileSync27, statSync as statSync19, writeFileSync as writeFileSync21 } from "fs";
74526
+ import { join as join34 } from "path";
73776
74527
  function isDirectory2(path) {
73777
74528
  try {
73778
- return statSync16(path).isDirectory();
74529
+ return statSync19(path).isDirectory();
73779
74530
  } catch {
73780
74531
  return false;
73781
74532
  }
@@ -73783,15 +74534,15 @@ function isDirectory2(path) {
73783
74534
  function migrationNeeded(options) {
73784
74535
  if (options.rootDir)
73785
74536
  return false;
73786
- const appDir = options.homeDir ? join32(options.homeDir, ".hasna", "skills") : getDataDir();
73787
- return !(isOwnerLayoutMigrated(appDir) && isDirectory2(join32(appDir, SKILLS_CACHE_DIRNAME)));
74537
+ const appDir = options.homeDir ? join34(options.homeDir, ".hasna", "skills") : getDataDir();
74538
+ return !(isOwnerLayoutMigrated(appDir) && isDirectory2(join34(appDir, SKILLS_CACHE_DIRNAME)));
73788
74539
  }
73789
74540
  function readBaseline(skillDir) {
73790
- const markerPath = join32(skillDir, PULL_MARKER_FILE);
74541
+ const markerPath = join34(skillDir, PULL_MARKER_FILE);
73791
74542
  if (!existsSync30(markerPath))
73792
74543
  return;
73793
74544
  try {
73794
- const marker = JSON.parse(readFileSync25(markerPath, "utf-8"));
74545
+ const marker = JSON.parse(readFileSync27(markerPath, "utf-8"));
73795
74546
  return {
73796
74547
  ...typeof marker.contentHash === "string" && marker.contentHash ? { contentHash: marker.contentHash } : {},
73797
74548
  ...typeof marker.version === "string" && marker.version ? { version: marker.version } : {}
@@ -73801,11 +74552,11 @@ function readBaseline(skillDir) {
73801
74552
  }
73802
74553
  }
73803
74554
  function readCursor(root) {
73804
- const path = join32(root, SYNC_CURSOR_FILE);
74555
+ const path = join34(root, SYNC_CURSOR_FILE);
73805
74556
  if (!existsSync30(path))
73806
74557
  return { runCount: 0 };
73807
74558
  try {
73808
- const cursor = JSON.parse(readFileSync25(path, "utf-8"));
74559
+ const cursor = JSON.parse(readFileSync27(path, "utf-8"));
73809
74560
  return { runCount: typeof cursor.runCount === "number" ? cursor.runCount : 0 };
73810
74561
  } catch {
73811
74562
  return { runCount: 0 };
@@ -73814,12 +74565,12 @@ function readCursor(root) {
73814
74565
  function resolveCorpusRootReadOnly(options) {
73815
74566
  if (options.rootDir)
73816
74567
  return { root: options.rootDir, migrationPending: false };
73817
- const appDir = options.homeDir ? join32(options.homeDir, ".hasna", "skills") : getDataDirReadOnly();
73818
- const cache3 = join32(appDir, SKILLS_CACHE_DIRNAME);
74568
+ const appDir = options.homeDir ? join34(options.homeDir, ".hasna", "skills") : getDataDirReadOnly();
74569
+ const cache3 = join34(appDir, SKILLS_CACHE_DIRNAME);
73819
74570
  if (isOwnerLayoutMigrated(appDir) && isDirectory2(cache3)) {
73820
74571
  return { root: cache3, migrationPending: false };
73821
74572
  }
73822
- return { root: join32(appDir, INSTALLED_SKILLS_DIRNAME), migrationPending: true };
74573
+ return { root: join34(appDir, INSTALLED_SKILLS_DIRNAME), migrationPending: true };
73823
74574
  }
73824
74575
  function remoteRowToSkill(record3) {
73825
74576
  const slug = typeof record3.slug === "string" ? record3.slug : typeof record3.name === "string" ? record3.name : undefined;
@@ -73951,7 +74702,7 @@ async function reconcileRegistry(options = {}) {
73951
74702
  for (const slug of allSlugs) {
73952
74703
  const local = locals.get(slug);
73953
74704
  const remote = remotes.get(slug);
73954
- const baseline = local ? readBaseline(join32(root, slug)) : undefined;
74705
+ const baseline = local ? readBaseline(join34(root, slug)) : undefined;
73955
74706
  const { state, reason } = classifySkill(local, remote, baseline);
73956
74707
  let { action, reason: actionReason } = resolveAction(state, direction, conflict);
73957
74708
  if (state === "remote-only" && isDigestless(remote)) {
@@ -74010,7 +74761,7 @@ async function reconcileRegistry(options = {}) {
74010
74761
  try {
74011
74762
  await pushSkill(slug, { rootDir: root, client });
74012
74763
  const pushed = locals.get(slug);
74013
- writePullMarker(join32(root, slug), {
74764
+ writePullMarker(join34(root, slug), {
74014
74765
  skill: slug,
74015
74766
  ...pushed?.version ? { version: pushed.version } : {},
74016
74767
  ...pushed?.sha256 ? { contentHash: pushed.sha256 } : {},
@@ -74082,7 +74833,7 @@ async function reconcileRegistry(options = {}) {
74082
74833
  runCount: readCursor(root).runCount + 1,
74083
74834
  summary
74084
74835
  };
74085
- writeFileSync19(join32(root, SYNC_CURSOR_FILE), `${JSON.stringify(cursor, null, 2)}
74836
+ writeFileSync21(join34(root, SYNC_CURSOR_FILE), `${JSON.stringify(cursor, null, 2)}
74086
74837
  `);
74087
74838
  return {
74088
74839
  corpusRoot: root,
@@ -81894,6 +82645,8 @@ var { registerCompletion: registerCompletion2 } = await Promise.resolve().then((
81894
82645
  registerCompletion2(program2);
81895
82646
  var { registerCreateSync: registerCreateSync2 } = await Promise.resolve().then(() => (init_create_sync_config(), exports_create_sync_config));
81896
82647
  registerCreateSync2(program2);
82648
+ var { registerHydrate: registerHydrate2 } = await Promise.resolve().then(() => (init_hydrate(), exports_hydrate));
82649
+ registerHydrate2(program2);
81897
82650
  var { registerPortableSkillCommands: registerPortableSkillCommands2 } = await Promise.resolve().then(() => (init_portable_skills2(), exports_portable_skills));
81898
82651
  registerPortableSkillCommands2(program2);
81899
82652
  var { registerSchedule: registerSchedule2 } = await Promise.resolve().then(() => (init_schedule(), exports_schedule));