@hasna/skills 0.1.70 → 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.70",
36863
+ version: "0.1.71",
36864
36864
  description: "Skills library for AI coding agents",
36865
36865
  type: "module",
36866
36866
  bin: {
@@ -71395,13 +71395,344 @@ var init_completion = __esm(() => {
71395
71395
  categoryNames = CATEGORIES.map((c) => c);
71396
71396
  });
71397
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
+
71398
71729
  // src/cli/commands/create-sync-config.ts
71399
71730
  var exports_create_sync_config = {};
71400
71731
  __export(exports_create_sync_config, {
71401
71732
  registerCreateSync: () => registerCreateSync
71402
71733
  });
71403
- import { existsSync as existsSync27, writeFileSync as writeFileSync16, mkdirSync as mkdirSync15 } from "fs";
71404
- 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";
71405
71736
  function registerCreateSync(parent) {
71406
71737
  const configCmd = parent.command("config").description("Manage skills configuration");
71407
71738
  configCmd.command("show", { isDefault: true }).option("--json", "Output as JSON", false).description("Show current merged configuration").action((options) => {
@@ -71476,13 +71807,13 @@ function registerCreateSync(parent) {
71476
71807
  console.log(`${source_default.cyan("project")}: ${pp}${existsSync27(pp) ? source_default.green(" (exists)") : source_default.dim(" (not found)")}`);
71477
71808
  });
71478
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));
71479
- 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));
71480
71811
  }
71481
71812
  function handleCreate(name, options) {
71482
71813
  const bare = name.trim();
71483
71814
  const dirName = bare;
71484
71815
  const baseDir = getPortableSkillsRoot();
71485
- const skillDir = join28(baseDir, dirName);
71816
+ const skillDir = join29(baseDir, dirName);
71486
71817
  if (existsSync27(skillDir)) {
71487
71818
  console.log(options.json ? JSON.stringify({ error: `Skill '${bare}' already exists at ${skillDir}` }) : source_default.red(`Skill '${bare}' already exists at ${skillDir}`));
71488
71819
  process.exitCode = 1;
@@ -71491,8 +71822,8 @@ function handleCreate(name, options) {
71491
71822
  const description = options.description || `${bare} skill`;
71492
71823
  const tags = options.tags ? options.tags.split(",").map((t) => t.trim()).filter(Boolean) : [bare];
71493
71824
  const displayName2 = bare.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
71494
- mkdirSync15(join28(skillDir, "src"), { recursive: true });
71495
- writeFileSync16(join28(skillDir, "SKILL.md"), [
71825
+ mkdirSync16(join29(skillDir, "src"), { recursive: true });
71826
+ writeFileSync17(join29(skillDir, "SKILL.md"), [
71496
71827
  "---",
71497
71828
  `name: ${bare}`,
71498
71829
  `description: ${description}`,
@@ -71512,11 +71843,11 @@ function handleCreate(name, options) {
71512
71843
  ""
71513
71844
  ].join(`
71514
71845
  `));
71515
- 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(`
71516
71847
  `));
71517
- 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) + `
71518
71849
  `);
71519
- 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) + `
71520
71851
  `);
71521
71852
  clearRegistryCache();
71522
71853
  if (options.json)
@@ -71525,11 +71856,15 @@ function handleCreate(name, options) {
71525
71856
  console.log(source_default.green(`\u2713 Created custom skill '${bare}' at ${skillDir}`));
71526
71857
  console.log(source_default.dim(` Category: ${options.category}`));
71527
71858
  console.log(source_default.dim(` Tags: ${tags.join(", ")}`));
71528
- console.log(` ${source_default.cyan("Edit:")} ${join28(skillDir, "src", "index.ts")}`);
71529
- 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")}`);
71530
71861
  }
71531
71862
  }
71532
71863
  function handleSync(names, options) {
71864
+ if (options.station) {
71865
+ handleStationSnapshot(names, options);
71866
+ return;
71867
+ }
71533
71868
  const modes = [options.check, options.adopt, options.prune].filter(Boolean).length;
71534
71869
  if (modes > 1) {
71535
71870
  const message = "--check, --adopt, and --prune are mutually exclusive";
@@ -71590,6 +71925,68 @@ function handleSync(names, options) {
71590
71925
  process.exitCode = 1;
71591
71926
  }
71592
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
+ }
71593
71990
  function handleSyncCheck(json) {
71594
71991
  const census = censusHomeDrift();
71595
71992
  if (json) {
@@ -71701,6 +72098,350 @@ var init_create_sync_config = __esm(() => {
71701
72098
  init_agent_sync();
71702
72099
  init_home_adoption();
71703
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();
71704
72445
  });
71705
72446
 
71706
72447
  // src/cli/commands/portable-skills.ts
@@ -72031,8 +72772,8 @@ var init_schedule = __esm(() => {
72031
72772
  });
72032
72773
 
72033
72774
  // src/lib/registry-sync.ts
72034
- import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync17 } from "fs";
72035
- 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";
72036
72777
  function createRegistrySyncArtifact(options = {}) {
72037
72778
  const profile = options.profile ?? "all";
72038
72779
  const includeDocs = options.includeDocs ?? true;
@@ -72044,7 +72785,7 @@ function createRegistrySyncArtifact(options = {}) {
72044
72785
  const registry2 = [...loadRegistryProfile(profile)].sort((a, b) => a.name.localeCompare(b.name));
72045
72786
  const skills = registry2.map((skill) => {
72046
72787
  const skillPath = getSkillPath(skill.name);
72047
- const directory = relative4(process.cwd(), skillPath) || skillPath;
72788
+ const directory = relative5(process.cwd(), skillPath) || skillPath;
72048
72789
  const validation = includeValidation ? validateSkillDirectory(skill.name, skillPath, skill) : undefined;
72049
72790
  const docs = includeDocs ? buildDocs(skill.name) : undefined;
72050
72791
  return {
@@ -72089,8 +72830,8 @@ function createRegistrySyncArtifact(options = {}) {
72089
72830
  };
72090
72831
  }
72091
72832
  function writeRegistrySyncArtifact(path, artifact) {
72092
- mkdirSync16(dirname11(path), { recursive: true });
72093
- writeFileSync17(path, `${JSON.stringify(artifact, null, 2)}
72833
+ mkdirSync18(dirname13(path), { recursive: true });
72834
+ writeFileSync19(path, `${JSON.stringify(artifact, null, 2)}
72094
72835
  `);
72095
72836
  }
72096
72837
  function buildDocs(name) {
@@ -72109,7 +72850,7 @@ var init_registry_sync = __esm(() => {
72109
72850
  });
72110
72851
 
72111
72852
  // src/lib/revision.ts
72112
- import { createHash as createHash4 } from "crypto";
72853
+ import { createHash as createHash6 } from "crypto";
72113
72854
  function revisionIdOf(content) {
72114
72855
  const canonical = JSON.stringify({
72115
72856
  slug: content.slug,
@@ -72124,7 +72865,7 @@ function revisionIdOf(content) {
72124
72865
  bundleSha256: content.bundleSha256 ?? null,
72125
72866
  bundleByteSize: content.bundleByteSize ?? null
72126
72867
  });
72127
- return createHash4("sha256").update(canonical).digest("hex");
72868
+ return createHash6("sha256").update(canonical).digest("hex");
72128
72869
  }
72129
72870
  var REVISION_ID_PATTERN;
72130
72871
  var init_revision = __esm(() => {
@@ -72132,9 +72873,9 @@ var init_revision = __esm(() => {
72132
72873
  });
72133
72874
 
72134
72875
  // src/lib/skill-bundle.ts
72135
- import { createHash as createHash5 } from "crypto";
72136
- import { readFileSync as readFileSync22, readdirSync as readdirSync15, statSync as statSync15 } from "fs";
72137
- 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";
72138
72879
  function isDotenvFile(lower) {
72139
72880
  if (lower === ".env" || lower.startsWith(".env."))
72140
72881
  return true;
@@ -72163,7 +72904,7 @@ function ownBytes(view) {
72163
72904
  return out;
72164
72905
  }
72165
72906
  function sha256Hex(bytes) {
72166
- return createHash5("sha256").update(bytes).digest("hex");
72907
+ return createHash7("sha256").update(bytes).digest("hex");
72167
72908
  }
72168
72909
  function collectSkillBundleEntries(dir) {
72169
72910
  const entries = [];
@@ -72171,9 +72912,9 @@ function collectSkillBundleEntries(dir) {
72171
72912
  return entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
72172
72913
  }
72173
72914
  function walk(root, current, out) {
72174
- for (const entry of readdirSync15(current, { withFileTypes: true })) {
72175
- const absolute = join29(current, entry.name);
72176
- 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("/");
72177
72918
  const isRootLevel = !rel.includes("/");
72178
72919
  if (ANY_SEGMENT_EXCLUDES.has(entry.name.toLowerCase()))
72179
72920
  continue;
@@ -72193,10 +72934,10 @@ function walk(root, current, out) {
72193
72934
  continue;
72194
72935
  if (isCredentialFile(entry.name))
72195
72936
  continue;
72196
- const stats = statSync15(absolute);
72937
+ const stats = statSync18(absolute);
72197
72938
  out.push({
72198
72939
  path: rel,
72199
- bytes: ownBytes(readFileSync22(absolute)),
72940
+ bytes: ownBytes(readFileSync24(absolute)),
72200
72941
  mode: stats.mode & 64 ? 493 : 420
72201
72942
  });
72202
72943
  }
@@ -72482,8 +73223,8 @@ var init_skill_bundles = __esm(() => {
72482
73223
  });
72483
73224
 
72484
73225
  // src/lib/pull.ts
72485
- import { existsSync as existsSync28, mkdirSync as mkdirSync17, mkdtempSync as mkdtempSync3, readFileSync as readFileSync23, renameSync as renameSync4, rmSync as rmSync6, writeFileSync as writeFileSync18 } from "fs";
72486
- 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";
72487
73228
  async function pullSkills(options = {}) {
72488
73229
  const client = options.client !== undefined ? options.client : createRemoteSkillsClient();
72489
73230
  if (!client) {
@@ -72528,7 +73269,7 @@ async function pullOne(client, rawName, corpusOptions, verify) {
72528
73269
  return reconcileTombstone(slug, corpusOptions);
72529
73270
  }
72530
73271
  if (bundleResponse.status === 404) {
72531
- const marker = readPullMarker(join30(getPortableSkillsRoot(corpusOptions), slug));
73272
+ const marker = readPullMarker(join32(getPortableSkillsRoot(corpusOptions), slug));
72532
73273
  if (marker && typeof marker.revisionId === "string" && marker.revisionId) {
72533
73274
  return { name: slug, success: true, purged: true, removed: false };
72534
73275
  }
@@ -72559,7 +73300,7 @@ async function pullOne(client, rawName, corpusOptions, verify) {
72559
73300
  return { name: slug, success: false, error: `Skill '${slug}' was not found on the configured Skills instance.` };
72560
73301
  }
72561
73302
  if (!meta?.revisionId) {
72562
- const marker = readPullMarker(join30(getPortableSkillsRoot(corpusOptions), slug));
73303
+ const marker = readPullMarker(join32(getPortableSkillsRoot(corpusOptions), slug));
72563
73304
  if (marker && typeof marker.revisionId === "string" && marker.revisionId) {
72564
73305
  return { name: slug, success: true, purged: true, removed: false };
72565
73306
  }
@@ -72616,8 +73357,8 @@ function provenRevision(meta, slug, bundle) {
72616
73357
  return declared;
72617
73358
  }
72618
73359
  function reconcileTombstone(slug, corpusOptions) {
72619
- const target = join30(getPortableSkillsRoot(corpusOptions), slug);
72620
- if (!existsSync28(join30(target, PULL_MARKER_FILE))) {
73360
+ const target = join32(getPortableSkillsRoot(corpusOptions), slug);
73361
+ if (!existsSync28(join32(target, PULL_MARKER_FILE))) {
72621
73362
  return { name: slug, success: true, tombstoned: true, removed: false, leftInPlace: true };
72622
73363
  }
72623
73364
  rmSync6(target, { recursive: true, force: true });
@@ -72625,7 +73366,7 @@ function reconcileTombstone(slug, corpusOptions) {
72625
73366
  }
72626
73367
  function readPullMarker(dir) {
72627
73368
  try {
72628
- return JSON.parse(readFileSync23(join30(dir, PULL_MARKER_FILE), "utf-8"));
73369
+ return JSON.parse(readFileSync25(join32(dir, PULL_MARKER_FILE), "utf-8"));
72629
73370
  } catch {
72630
73371
  return null;
72631
73372
  }
@@ -72745,17 +73486,17 @@ function verifyBundleResponseBytes(buffer, response, verify = {}) {
72745
73486
  }
72746
73487
  function installBundleAtomically(name, entries, options = {}, marker = {}) {
72747
73488
  const root = getPortableSkillsRoot(options);
72748
- mkdirSync17(root, { recursive: true });
72749
- const target = join30(root, name);
73489
+ mkdirSync19(root, { recursive: true });
73490
+ const target = join32(root, name);
72750
73491
  const created = !existsSync28(target);
72751
- const staging = mkdtempSync3(join30(root, `.pull-${name}-`));
73492
+ const staging = mkdtempSync3(join32(root, `.pull-${name}-`));
72752
73493
  let moved = false;
72753
73494
  let backup = null;
72754
73495
  try {
72755
73496
  for (const entry of entries) {
72756
- const destination = join30(staging, entry.path);
72757
- mkdirSync17(dirname12(destination), { recursive: true });
72758
- 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 });
72759
73500
  }
72760
73501
  writePullMarker(staging, {
72761
73502
  skill: name,
@@ -72766,8 +73507,8 @@ function installBundleAtomically(name, entries, options = {}, marker = {}) {
72766
73507
  ...marker.revisionId ? { revisionId: marker.revisionId } : {}
72767
73508
  });
72768
73509
  if (existsSync28(target)) {
72769
- backup = mkdtempSync3(join30(root, `.pull-backup-${name}-`));
72770
- renameSync4(target, join30(backup, name));
73510
+ backup = mkdtempSync3(join32(root, `.pull-backup-${name}-`));
73511
+ renameSync4(target, join32(backup, name));
72771
73512
  moved = true;
72772
73513
  }
72773
73514
  renameSync4(staging, target);
@@ -72775,9 +73516,9 @@ function installBundleAtomically(name, entries, options = {}, marker = {}) {
72775
73516
  rmSync6(backup, { recursive: true, force: true });
72776
73517
  } catch (error2) {
72777
73518
  rmSync6(staging, { recursive: true, force: true });
72778
- if (moved && backup && existsSync28(join30(backup, name))) {
73519
+ if (moved && backup && existsSync28(join32(backup, name))) {
72779
73520
  try {
72780
- renameSync4(join30(backup, name), target);
73521
+ renameSync4(join32(backup, name), target);
72781
73522
  } catch {}
72782
73523
  }
72783
73524
  throw error2;
@@ -72796,7 +73537,7 @@ function writePullMarker(dir, record3) {
72796
73537
  ...record3.revisionId ? { revisionId: record3.revisionId } : {},
72797
73538
  syncedAt: new Date().toISOString()
72798
73539
  };
72799
- writeFileSync18(join30(dir, PULL_MARKER_FILE), `${JSON.stringify(marker, null, 2)}
73540
+ writeFileSync20(join32(dir, PULL_MARKER_FILE), `${JSON.stringify(marker, null, 2)}
72800
73541
  `);
72801
73542
  }
72802
73543
  async function safeMeta(client, slug) {
@@ -72870,12 +73611,12 @@ function registerRegistry(parent) {
72870
73611
  async function writeJson2(value, space) {
72871
73612
  const text = `${JSON.stringify(value, null, space)}
72872
73613
  `;
72873
- await new Promise((resolve, reject2) => {
73614
+ await new Promise((resolve3, reject2) => {
72874
73615
  process.stdout.write(text, (error2) => {
72875
73616
  if (error2)
72876
73617
  reject2(error2);
72877
73618
  else
72878
- resolve();
73619
+ resolve3();
72879
73620
  });
72880
73621
  });
72881
73622
  }
@@ -72967,8 +73708,8 @@ __export(exports_publish, {
72967
73708
  pushSkill: () => pushSkill,
72968
73709
  PushSkillError: () => PushSkillError
72969
73710
  });
72970
- import { existsSync as existsSync29, readFileSync as readFileSync24 } from "fs";
72971
- import { join as join31 } from "path";
73711
+ import { existsSync as existsSync29, readFileSync as readFileSync26 } from "fs";
73712
+ import { join as join33 } from "path";
72972
73713
  function registerPublish(parent) {
72973
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) => {
72974
73715
  try {
@@ -73009,8 +73750,8 @@ async function pushSkill(name, options = {}) {
73009
73750
  }
73010
73751
  const manifest = readPortableSkillManifest(skill.path, skill.name);
73011
73752
  const packed = packSkillBundle(skill.path, { maxUnpackedBytes: MAX_UNPACKED_BYTES });
73012
- const skillMdPath = join31(skill.path, "SKILL.md");
73013
- 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;
73014
73755
  const base2 = {
73015
73756
  slug: skill.name,
73016
73757
  path: skill.path,
@@ -73127,10 +73868,10 @@ __export(exports_auth, {
73127
73868
  import { createInterface as createInterface2 } from "readline";
73128
73869
  function prompt(question) {
73129
73870
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
73130
- return new Promise((resolve) => {
73871
+ return new Promise((resolve3) => {
73131
73872
  rl.question(question, (answer) => {
73132
73873
  rl.close();
73133
- resolve(answer.trim());
73874
+ resolve3(answer.trim());
73134
73875
  });
73135
73876
  });
73136
73877
  }
@@ -73280,7 +74021,7 @@ function printWhoami(payload) {
73280
74021
  console.log(source_default.dim("(offline \u2014 showing cached info)"));
73281
74022
  }
73282
74023
  function sleep(ms) {
73283
- return new Promise((resolve) => setTimeout(resolve, ms));
74024
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
73284
74025
  }
73285
74026
  function browserCommand(url) {
73286
74027
  if (process.platform === "darwin")
@@ -73781,11 +74522,11 @@ var init_storage = __esm(() => {
73781
74522
  });
73782
74523
 
73783
74524
  // src/lib/registry-reconcile.ts
73784
- import { existsSync as existsSync30, readFileSync as readFileSync25, statSync as statSync16, writeFileSync as writeFileSync19 } from "fs";
73785
- 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";
73786
74527
  function isDirectory2(path) {
73787
74528
  try {
73788
- return statSync16(path).isDirectory();
74529
+ return statSync19(path).isDirectory();
73789
74530
  } catch {
73790
74531
  return false;
73791
74532
  }
@@ -73793,15 +74534,15 @@ function isDirectory2(path) {
73793
74534
  function migrationNeeded(options) {
73794
74535
  if (options.rootDir)
73795
74536
  return false;
73796
- const appDir = options.homeDir ? join32(options.homeDir, ".hasna", "skills") : getDataDir();
73797
- 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)));
73798
74539
  }
73799
74540
  function readBaseline(skillDir) {
73800
- const markerPath = join32(skillDir, PULL_MARKER_FILE);
74541
+ const markerPath = join34(skillDir, PULL_MARKER_FILE);
73801
74542
  if (!existsSync30(markerPath))
73802
74543
  return;
73803
74544
  try {
73804
- const marker = JSON.parse(readFileSync25(markerPath, "utf-8"));
74545
+ const marker = JSON.parse(readFileSync27(markerPath, "utf-8"));
73805
74546
  return {
73806
74547
  ...typeof marker.contentHash === "string" && marker.contentHash ? { contentHash: marker.contentHash } : {},
73807
74548
  ...typeof marker.version === "string" && marker.version ? { version: marker.version } : {}
@@ -73811,11 +74552,11 @@ function readBaseline(skillDir) {
73811
74552
  }
73812
74553
  }
73813
74554
  function readCursor(root) {
73814
- const path = join32(root, SYNC_CURSOR_FILE);
74555
+ const path = join34(root, SYNC_CURSOR_FILE);
73815
74556
  if (!existsSync30(path))
73816
74557
  return { runCount: 0 };
73817
74558
  try {
73818
- const cursor = JSON.parse(readFileSync25(path, "utf-8"));
74559
+ const cursor = JSON.parse(readFileSync27(path, "utf-8"));
73819
74560
  return { runCount: typeof cursor.runCount === "number" ? cursor.runCount : 0 };
73820
74561
  } catch {
73821
74562
  return { runCount: 0 };
@@ -73824,12 +74565,12 @@ function readCursor(root) {
73824
74565
  function resolveCorpusRootReadOnly(options) {
73825
74566
  if (options.rootDir)
73826
74567
  return { root: options.rootDir, migrationPending: false };
73827
- const appDir = options.homeDir ? join32(options.homeDir, ".hasna", "skills") : getDataDirReadOnly();
73828
- 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);
73829
74570
  if (isOwnerLayoutMigrated(appDir) && isDirectory2(cache3)) {
73830
74571
  return { root: cache3, migrationPending: false };
73831
74572
  }
73832
- return { root: join32(appDir, INSTALLED_SKILLS_DIRNAME), migrationPending: true };
74573
+ return { root: join34(appDir, INSTALLED_SKILLS_DIRNAME), migrationPending: true };
73833
74574
  }
73834
74575
  function remoteRowToSkill(record3) {
73835
74576
  const slug = typeof record3.slug === "string" ? record3.slug : typeof record3.name === "string" ? record3.name : undefined;
@@ -73961,7 +74702,7 @@ async function reconcileRegistry(options = {}) {
73961
74702
  for (const slug of allSlugs) {
73962
74703
  const local = locals.get(slug);
73963
74704
  const remote = remotes.get(slug);
73964
- const baseline = local ? readBaseline(join32(root, slug)) : undefined;
74705
+ const baseline = local ? readBaseline(join34(root, slug)) : undefined;
73965
74706
  const { state, reason } = classifySkill(local, remote, baseline);
73966
74707
  let { action, reason: actionReason } = resolveAction(state, direction, conflict);
73967
74708
  if (state === "remote-only" && isDigestless(remote)) {
@@ -74020,7 +74761,7 @@ async function reconcileRegistry(options = {}) {
74020
74761
  try {
74021
74762
  await pushSkill(slug, { rootDir: root, client });
74022
74763
  const pushed = locals.get(slug);
74023
- writePullMarker(join32(root, slug), {
74764
+ writePullMarker(join34(root, slug), {
74024
74765
  skill: slug,
74025
74766
  ...pushed?.version ? { version: pushed.version } : {},
74026
74767
  ...pushed?.sha256 ? { contentHash: pushed.sha256 } : {},
@@ -74092,7 +74833,7 @@ async function reconcileRegistry(options = {}) {
74092
74833
  runCount: readCursor(root).runCount + 1,
74093
74834
  summary
74094
74835
  };
74095
- writeFileSync19(join32(root, SYNC_CURSOR_FILE), `${JSON.stringify(cursor, null, 2)}
74836
+ writeFileSync21(join34(root, SYNC_CURSOR_FILE), `${JSON.stringify(cursor, null, 2)}
74096
74837
  `);
74097
74838
  return {
74098
74839
  corpusRoot: root,
@@ -81904,6 +82645,8 @@ var { registerCompletion: registerCompletion2 } = await Promise.resolve().then((
81904
82645
  registerCompletion2(program2);
81905
82646
  var { registerCreateSync: registerCreateSync2 } = await Promise.resolve().then(() => (init_create_sync_config(), exports_create_sync_config));
81906
82647
  registerCreateSync2(program2);
82648
+ var { registerHydrate: registerHydrate2 } = await Promise.resolve().then(() => (init_hydrate(), exports_hydrate));
82649
+ registerHydrate2(program2);
81907
82650
  var { registerPortableSkillCommands: registerPortableSkillCommands2 } = await Promise.resolve().then(() => (init_portable_skills2(), exports_portable_skills));
81908
82651
  registerPortableSkillCommands2(program2);
81909
82652
  var { registerSchedule: registerSchedule2 } = await Promise.resolve().then(() => (init_schedule(), exports_schedule));