@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/dist/index.js CHANGED
@@ -10350,7 +10350,7 @@ import { dirname as dirname8, relative as relative3 } from "path";
10350
10350
  // package.json
10351
10351
  var package_default = {
10352
10352
  name: "@hasna/skills",
10353
- version: "0.1.70",
10353
+ version: "0.1.71",
10354
10354
  description: "Skills library for AI coding agents",
10355
10355
  type: "module",
10356
10356
  bin: {
@@ -11990,14 +11990,625 @@ function toArrayBuffer(bytes) {
11990
11990
  new Uint8Array(buffer).set(bytes);
11991
11991
  return buffer;
11992
11992
  }
11993
+ // src/lib/station-snapshot.ts
11994
+ import { createHash as createHash6 } from "crypto";
11995
+ import {
11996
+ copyFileSync as copyFileSync2,
11997
+ mkdirSync as mkdirSync13,
11998
+ readFileSync as readFileSync16,
11999
+ statSync as statSync11,
12000
+ writeFileSync as writeFileSync12
12001
+ } from "fs";
12002
+ import { dirname as dirname11, isAbsolute as isAbsolute3, relative as relative5, resolve, sep as sep4 } from "path";
12003
+
12004
+ // src/lib/portable-snapshot-filter.ts
12005
+ import { readdirSync as readdirSync10, statSync as statSync10 } from "fs";
12006
+ import { homedir as homedir5 } from "os";
12007
+ import { join as join18, sep as sep3 } from "path";
12008
+ var SYNC_HOMES = [
12009
+ { name: "skills", subClass: "skills", agent: null },
12010
+ { name: "custom", subClass: "custom", agent: null },
12011
+ { name: "claude", subClass: "agent-homes", agent: "claude" },
12012
+ { name: "codewith", subClass: "agent-homes", agent: "codewith" },
12013
+ { name: "codex", subClass: "agent-homes", agent: "codex" },
12014
+ { name: "opencode", subClass: "agent-homes", agent: "opencode" },
12015
+ { name: "cursor", subClass: "agent-homes", agent: "cursor" }
12016
+ ];
12017
+ var EXCLUDE_DIR_NAMES = new Set([
12018
+ ".git",
12019
+ "node_modules",
12020
+ "__pycache__",
12021
+ ".cache",
12022
+ ".pytest_cache",
12023
+ ".mypy_cache",
12024
+ ".ruff_cache"
12025
+ ]);
12026
+ var EXCLUDE_DIR_PATTERNS = [
12027
+ /^\.merge-pr\.rollback-/
12028
+ ];
12029
+ var EXCLUDE_FILE_NAMES = new Set([
12030
+ ".DS_Store",
12031
+ "package-lock.json",
12032
+ "pnpm-lock.yaml",
12033
+ "yarn.lock",
12034
+ "Cargo.lock"
12035
+ ]);
12036
+ var EXCLUDE_FILE_PATTERNS = [
12037
+ /^\._/,
12038
+ /\.bak$/,
12039
+ /\.orig$/,
12040
+ /\.rej$/,
12041
+ /~$/,
12042
+ /\.pyc$/,
12043
+ /\.pyo$/,
12044
+ /\.log$/,
12045
+ /\.db$/,
12046
+ /\.sqlite(\d)?$/,
12047
+ /^bun\.lock/,
12048
+ /\.env($|\.)/,
12049
+ /\.pem$/,
12050
+ /\.key$/,
12051
+ /\.p12$/,
12052
+ /\.pfx$/,
12053
+ /\.jks$/,
12054
+ /^id_rsa/,
12055
+ /^id_ed25519/,
12056
+ /^credentials/
12057
+ ];
12058
+ var PORTABLE_TOP_LEVEL = new Set(["SKILL.md", "skill.json"]);
12059
+ var PORTABLE_SUBDIRS = new Set(["scripts", "assets", "references"]);
12060
+ var REFUSED_SCANNER_FLAGGED = new Set([
12061
+ "aws-cross-account-app-migration/SKILL.md",
12062
+ "aws-cross-account-app-migration/scripts/selftest.sh",
12063
+ "gateway-serve/SKILL.md",
12064
+ "infinity-drain/SKILL.md",
12065
+ "infinity-run/SKILL.md",
12066
+ "oss-saas-code-cleanup/SKILL.md",
12067
+ "repo-project-familiarization/scripts/repo_shape.py",
12068
+ "repo-project-familiarization/scripts/session_history.py",
12069
+ "scale-check/SKILL.md",
12070
+ "standard-align-repo/SKILL.md",
12071
+ "standard-build-iapp/SKILL.md",
12072
+ "standard-build-oss/SKILL.md",
12073
+ "skill-image/SKILL.md",
12074
+ "skill-scale-check/SKILL.md",
12075
+ "sqlite-to-rds-parity-migrate/scripts/parity-migrate.ts",
12076
+ "pdf-operations/scripts/pdf_ops.py"
12077
+ ]);
12078
+ function isExcludedSkillFileName(fileName) {
12079
+ if (EXCLUDE_FILE_NAMES.has(fileName)) {
12080
+ return true;
12081
+ }
12082
+ return EXCLUDE_FILE_PATTERNS.some((pattern) => pattern.test(fileName));
12083
+ }
12084
+ function isPortableWithinSkill(relativeParts) {
12085
+ if (relativeParts.length < 2) {
12086
+ return false;
12087
+ }
12088
+ const [, second] = relativeParts;
12089
+ if (PORTABLE_TOP_LEVEL.has(second)) {
12090
+ return relativeParts.length === 2;
12091
+ }
12092
+ if (relativeParts.length < 3) {
12093
+ return false;
12094
+ }
12095
+ return PORTABLE_SUBDIRS.has(second);
12096
+ }
12097
+ function homePathFor(definition, homesRoot) {
12098
+ const home = homesRoot ?? homedir5();
12099
+ if (definition.subClass === "skills" || definition.subClass === "custom") {
12100
+ return join18(home, ".hasna", "skills", definition.name);
12101
+ }
12102
+ if (definition.agent === "opencode") {
12103
+ return join18(home, ".config", "opencode", "skills");
12104
+ }
12105
+ return join18(home, `.${definition.agent}`, "skills");
12106
+ }
12107
+ function destinationFor(definition, stationId, relativePath) {
12108
+ const category = definition.subClass === "agent-homes" ? join18("agent-homes", definition.agent ?? "") : definition.name;
12109
+ return join18("resources", stationId, "skills", category, ...relativePath.split(sep3));
12110
+ }
12111
+ function walkEntries(absoluteRoot) {
12112
+ let entries;
12113
+ try {
12114
+ entries = readdirSync10(absoluteRoot, { withFileTypes: true });
12115
+ } catch {
12116
+ return [];
12117
+ }
12118
+ const output = [];
12119
+ for (const entry of entries) {
12120
+ const childFull = join18(absoluteRoot, entry.name);
12121
+ if (entry.isSymbolicLink()) {
12122
+ output.push({ kind: "symlink", relativePath: entry.name, fullPath: childFull });
12123
+ continue;
12124
+ }
12125
+ if (entry.isDirectory()) {
12126
+ if (EXCLUDE_DIR_NAMES.has(entry.name) || EXCLUDE_DIR_PATTERNS.some((pattern) => pattern.test(entry.name))) {
12127
+ continue;
12128
+ }
12129
+ const nested = walkEntries(childFull);
12130
+ for (const item of nested) {
12131
+ output.push({ ...item, relativePath: join18(entry.name, item.relativePath) });
12132
+ }
12133
+ continue;
12134
+ }
12135
+ if (entry.isFile()) {
12136
+ output.push({ kind: "file", relativePath: entry.name, fullPath: childFull });
12137
+ }
12138
+ }
12139
+ return output;
12140
+ }
12141
+ function isRegularFile(filePath) {
12142
+ try {
12143
+ return statSync10(filePath).isFile();
12144
+ } catch {
12145
+ return false;
12146
+ }
12147
+ }
12148
+
12149
+ // src/lib/station-snapshot.ts
12150
+ var STATION_SYNC_MANIFEST_SCHEMA = "hasna.fleet-resources.skills-sync-manifest/v1";
12151
+ var STATION_SNAPSHOT_PRODUCER = { name: "@hasna/skills", version: package_default.version };
12152
+
12153
+ class StationSnapshotError extends Error {
12154
+ code;
12155
+ detail;
12156
+ constructor(code, message, detail = []) {
12157
+ super(message);
12158
+ this.name = "StationSnapshotError";
12159
+ this.code = code;
12160
+ this.detail = detail;
12161
+ }
12162
+ }
12163
+ function validateStationId(stationId) {
12164
+ if (!/^[a-z0-9-]+$/.test(stationId)) {
12165
+ throw new StationSnapshotError("INVALID_STATION", `station id must be a slug, got: ${stationId}`);
12166
+ }
12167
+ }
12168
+ function sha256File(filePath) {
12169
+ return createHash6("sha256").update(readFileSync16(filePath)).digest("hex");
12170
+ }
12171
+ function scanHome(definition, homesRoot) {
12172
+ const homePath = homePathFor(definition, homesRoot);
12173
+ const entries = walkEntries(homePath);
12174
+ const portable = [];
12175
+ const skipped = [];
12176
+ for (const entry of entries) {
12177
+ const relativeParts = entry.relativePath.split(sep4);
12178
+ const fileName = relativeParts[relativeParts.length - 1];
12179
+ if (entry.kind === "symlink") {
12180
+ skipped.push({ relativePath: entry.relativePath, reason: "symlink" });
12181
+ continue;
12182
+ }
12183
+ if (!isPortableWithinSkill(relativeParts)) {
12184
+ skipped.push({ relativePath: entry.relativePath, reason: "not-portable" });
12185
+ continue;
12186
+ }
12187
+ if (isExcludedSkillFileName(fileName)) {
12188
+ skipped.push({ relativePath: entry.relativePath, reason: "excluded" });
12189
+ continue;
12190
+ }
12191
+ if (REFUSED_SCANNER_FLAGGED.has(entry.relativePath)) {
12192
+ skipped.push({ relativePath: entry.relativePath, reason: "refused-scanner-flagged" });
12193
+ continue;
12194
+ }
12195
+ if (!isRegularFile(entry.fullPath)) {
12196
+ skipped.push({ relativePath: entry.relativePath, reason: "not-regular-file" });
12197
+ continue;
12198
+ }
12199
+ const info = statSync11(entry.fullPath);
12200
+ portable.push({
12201
+ relativePath: entry.relativePath,
12202
+ fullPath: entry.fullPath,
12203
+ size: info.size,
12204
+ mtimeMs: info.mtimeMs,
12205
+ mtimeIso: info.mtime.toISOString()
12206
+ });
12207
+ }
12208
+ return { definition, homePath, portable, skipped };
12209
+ }
12210
+ function planStationSnapshot(options) {
12211
+ validateStationId(options.stationId);
12212
+ const scanned = SYNC_HOMES.map((definition) => scanHome(definition, options.homesRoot));
12213
+ const symlinks = scanned.reduce((sum, item) => sum + item.skipped.filter((entry) => entry.reason === "symlink").length, 0);
12214
+ if (symlinks > 0) {
12215
+ throw new StationSnapshotError("SYMLINKS_REFUSED", `${symlinks} symlink(s) inside skill homes; symlinks are refused (fail closed)`);
12216
+ }
12217
+ const plans = [];
12218
+ for (const item of scanned) {
12219
+ for (const file of item.portable) {
12220
+ plans.push({
12221
+ definition: item.definition,
12222
+ source: file,
12223
+ destination: destinationFor(item.definition, options.stationId, file.relativePath),
12224
+ digest: sha256File(file.fullPath)
12225
+ });
12226
+ }
12227
+ }
12228
+ const totalBytes = plans.reduce((sum, plan) => sum + plan.source.size, 0);
12229
+ return { scanned, plans, totalBytes };
12230
+ }
12231
+ function humanHomes(scanned) {
12232
+ return scanned.map((item) => ({
12233
+ name: item.definition.name,
12234
+ homePath: item.homePath,
12235
+ files: item.portable.length,
12236
+ skipped: item.skipped.length
12237
+ }));
12238
+ }
12239
+ function writeStationSnapshot(options) {
12240
+ const repoRoot = resolve(options.repoRoot ?? process.cwd());
12241
+ const { scanned, plans, totalBytes } = planStationSnapshot(options);
12242
+ const manifestFiles = plans.map((plan) => ({
12243
+ relativePath: plan.source.relativePath,
12244
+ destination: plan.destination,
12245
+ subClass: plan.definition.subClass,
12246
+ agent: plan.definition.agent,
12247
+ sha256: plan.digest,
12248
+ sourceMtimeMs: plan.source.mtimeMs,
12249
+ sourceMtimeIso: plan.source.mtimeIso,
12250
+ size: plan.source.size
12251
+ }));
12252
+ const base = {
12253
+ stationId: options.stationId,
12254
+ mode: "dry-run",
12255
+ repoRoot,
12256
+ stats: { files: plans.length, bytes: totalBytes },
12257
+ homes: humanHomes(scanned),
12258
+ files: manifestFiles
12259
+ };
12260
+ if (options.dryRun !== false) {
12261
+ return base;
12262
+ }
12263
+ const conflicts = [];
12264
+ const untouched = [];
12265
+ for (const plan of plans) {
12266
+ const destination = resolve(repoRoot, plan.destination);
12267
+ const destinationRelative = relative5(repoRoot, destination);
12268
+ if (destinationRelative.startsWith("..") || destinationRelative.startsWith(sep4) || isAbsolute3(destinationRelative)) {
12269
+ throw new StationSnapshotError("DESTINATION_ESCAPE", `destination escapes repo root: ${plan.destination}`);
12270
+ }
12271
+ let existingDigest = null;
12272
+ try {
12273
+ existingDigest = sha256File(destination);
12274
+ } catch {}
12275
+ if (existingDigest !== null) {
12276
+ if (existingDigest === plan.digest) {
12277
+ continue;
12278
+ }
12279
+ conflicts.push(`existing destination differs from staged source: ${plan.destination}`);
12280
+ continue;
12281
+ }
12282
+ untouched.push(plan);
12283
+ }
12284
+ if (conflicts.length > 0) {
12285
+ throw new StationSnapshotError("CONFLICT", `${conflicts.length} conflict(s); terminal non-acceptance, nothing written`, conflicts);
12286
+ }
12287
+ let written = 0;
12288
+ for (const plan of untouched) {
12289
+ const destination = resolve(repoRoot, plan.destination);
12290
+ mkdirSync13(dirname11(destination), { recursive: true });
12291
+ copyFileSync2(plan.source.fullPath, destination);
12292
+ written += 1;
12293
+ }
12294
+ const unchanged = plans.length - untouched.length;
12295
+ const manifest = {
12296
+ schema: STATION_SYNC_MANIFEST_SCHEMA,
12297
+ stationId: options.stationId,
12298
+ syncedAt: new Date().toISOString(),
12299
+ producer: STATION_SNAPSHOT_PRODUCER,
12300
+ stats: {
12301
+ written,
12302
+ unchanged,
12303
+ files: plans.length,
12304
+ bytes: totalBytes
12305
+ },
12306
+ files: manifestFiles
12307
+ };
12308
+ const manifestPath = resolve(repoRoot, "resources", options.stationId, "skills", "sync-manifest.json");
12309
+ mkdirSync13(dirname11(manifestPath), { recursive: true });
12310
+ writeFileSync12(manifestPath, `${JSON.stringify(manifest, null, 2)}
12311
+ `);
12312
+ return {
12313
+ ...base,
12314
+ mode: "populate",
12315
+ stats: { files: plans.length, bytes: totalBytes, written, unchanged },
12316
+ manifestPath
12317
+ };
12318
+ }
12319
+ // src/lib/station-hydrate.ts
12320
+ import { createHash as createHash7 } from "crypto";
12321
+ import {
12322
+ copyFileSync as copyFileSync3,
12323
+ mkdirSync as mkdirSync14,
12324
+ readdirSync as readdirSync11,
12325
+ readFileSync as readFileSync17,
12326
+ statSync as statSync12,
12327
+ writeFileSync as writeFileSync13
12328
+ } from "fs";
12329
+ import { dirname as dirname12, join as join19, resolve as resolve2, sep as sep5 } from "path";
12330
+ var STATION_HYDRATION_MANIFEST_SCHEMA = "hasna.fleet-resources.skills-hydration-manifest/v1";
12331
+ var STATION_HYDRATION_PRODUCER = { name: "@hasna/skills", version: package_default.version };
12332
+ var MANIFEST_HASH_KEY_SEP = String.fromCharCode(0);
12333
+ function fail(code, message, detail = []) {
12334
+ throw new StationSnapshotError(code, message, detail);
12335
+ }
12336
+ function snapshotRootFor(repoRoot, stationId) {
12337
+ return join19(repoRoot, "resources", stationId, "skills");
12338
+ }
12339
+ function readSnapshotManifest(repoRoot, stationId) {
12340
+ const snapshotRoot = snapshotRootFor(repoRoot, stationId);
12341
+ const manifestPath = join19(snapshotRoot, "sync-manifest.json");
12342
+ let manifest;
12343
+ try {
12344
+ manifest = JSON.parse(readFileSync17(manifestPath, "utf8"));
12345
+ } catch (error) {
12346
+ fail("MANIFEST_UNREADABLE", `cannot read snapshot manifest: ${manifestPath}: ${error.message}`);
12347
+ }
12348
+ const sourceSnapshotSha = sha256File(manifestPath);
12349
+ return { manifest, manifestPath, sourceSnapshotSha };
12350
+ }
12351
+ function planStationHydration(stationId, repoRoot) {
12352
+ validateStationId(stationId);
12353
+ const { manifest, sourceSnapshotSha } = readSnapshotManifest(repoRoot, stationId);
12354
+ const snapshotRoot = snapshotRootFor(repoRoot, stationId);
12355
+ const manifestHashes = new Map;
12356
+ for (const file of manifest.files ?? []) {
12357
+ const relativePath = file.relativePath;
12358
+ const agent = file.agent;
12359
+ if (agent && relativePath) {
12360
+ manifestHashes.set(`${agent}${MANIFEST_HASH_KEY_SEP}${relativePath}`, file.sha256);
12361
+ }
12362
+ }
12363
+ const candidates = [];
12364
+ const symlinks = [];
12365
+ const skippedByRule = [];
12366
+ for (const agent of SYNC_AGENTS) {
12367
+ const agentRoot = join19(snapshotRoot, "agent-homes", agent);
12368
+ let identEntries;
12369
+ try {
12370
+ identEntries = readdirSync11(agentRoot, { withFileTypes: true });
12371
+ } catch {
12372
+ continue;
12373
+ }
12374
+ for (const identEntry of identEntries) {
12375
+ if (!identEntry.isDirectory() || identEntry.name.startsWith(".")) {
12376
+ continue;
12377
+ }
12378
+ const identRoot = join19(agentRoot, identEntry.name);
12379
+ const entries = walkEntries(identRoot);
12380
+ for (const entry of entries) {
12381
+ const relativeParts = [identEntry.name, ...entry.relativePath.split(sep5)];
12382
+ if (entry.kind === "symlink") {
12383
+ symlinks.push({ ident: identEntry.name, agent, relativePath: entry.relativePath });
12384
+ continue;
12385
+ }
12386
+ if (!isPortableWithinSkill(relativeParts)) {
12387
+ skippedByRule.push({
12388
+ ident: identEntry.name,
12389
+ agent,
12390
+ relativePath: entry.relativePath,
12391
+ reason: "not-portable"
12392
+ });
12393
+ continue;
12394
+ }
12395
+ const fileName = relativeParts[relativeParts.length - 1];
12396
+ if (isExcludedSkillFileName(fileName)) {
12397
+ skippedByRule.push({
12398
+ ident: identEntry.name,
12399
+ agent,
12400
+ relativePath: entry.relativePath,
12401
+ reason: "excluded"
12402
+ });
12403
+ continue;
12404
+ }
12405
+ const withinIdent = relativeParts.slice(1).join(sep5);
12406
+ const homeRelative = relativeParts.join(sep5);
12407
+ if (REFUSED_SCANNER_FLAGGED.has(homeRelative)) {
12408
+ skippedByRule.push({
12409
+ ident: identEntry.name,
12410
+ agent,
12411
+ relativePath: entry.relativePath,
12412
+ reason: "refused-scanner-flagged"
12413
+ });
12414
+ continue;
12415
+ }
12416
+ if (!isRegularFile(entry.fullPath)) {
12417
+ skippedByRule.push({
12418
+ ident: identEntry.name,
12419
+ agent,
12420
+ relativePath: entry.relativePath,
12421
+ reason: "not-regular-file"
12422
+ });
12423
+ continue;
12424
+ }
12425
+ const info = statSync12(entry.fullPath);
12426
+ candidates.push({
12427
+ ident: identEntry.name,
12428
+ agent,
12429
+ withinIdent,
12430
+ fullPath: entry.fullPath,
12431
+ size: info.size,
12432
+ mtimeMs: info.mtimeMs,
12433
+ manifestHash: manifestHashes.get(`${agent}${MANIFEST_HASH_KEY_SEP}${homeRelative}`) ?? null
12434
+ });
12435
+ }
12436
+ }
12437
+ }
12438
+ if (symlinks.length > 0) {
12439
+ fail("SYMLINKS_REFUSED", `${symlinks.length} symlink(s) inside the snapshot; symlinks are refused (fail closed)`);
12440
+ }
12441
+ const byIdent = new Map;
12442
+ for (const candidate of candidates) {
12443
+ const group = byIdent.get(candidate.ident) ?? [];
12444
+ group.push(candidate);
12445
+ byIdent.set(candidate.ident, group);
12446
+ }
12447
+ const winners = [];
12448
+ for (const [ident, group] of byIdent) {
12449
+ const byFile = new Map;
12450
+ for (const candidate of group) {
12451
+ const copies = byFile.get(candidate.withinIdent) ?? [];
12452
+ copies.push(candidate);
12453
+ byFile.set(candidate.withinIdent, copies);
12454
+ }
12455
+ const files = [];
12456
+ for (const [withinIdent, copies] of byFile) {
12457
+ let eligible = copies;
12458
+ if (withinIdent === "SKILL.md") {
12459
+ const content = [];
12460
+ for (const copy of copies) {
12461
+ let isStub = false;
12462
+ try {
12463
+ isStub = isPointerSkillMd(readFileSync17(copy.fullPath, "utf8"));
12464
+ } catch {
12465
+ isStub = false;
12466
+ }
12467
+ if (!isStub)
12468
+ content.push(copy);
12469
+ }
12470
+ if (content.length > 0) {
12471
+ eligible = content;
12472
+ }
12473
+ }
12474
+ eligible.sort((left, right) => {
12475
+ const leftHash = left.manifestHash !== null;
12476
+ const rightHash = right.manifestHash !== null;
12477
+ if (leftHash !== rightHash) {
12478
+ return leftHash ? -1 : 1;
12479
+ }
12480
+ if (right.mtimeMs !== left.mtimeMs) {
12481
+ return right.mtimeMs - left.mtimeMs;
12482
+ }
12483
+ return SYNC_AGENTS.indexOf(left.agent) - SYNC_AGENTS.indexOf(right.agent);
12484
+ });
12485
+ const winner = eligible[0];
12486
+ files.push({
12487
+ withinIdent,
12488
+ winner,
12489
+ alternates: copies.filter((copy) => copy !== winner).map((copy) => copy.agent)
12490
+ });
12491
+ }
12492
+ files.sort((left, right) => left.withinIdent.localeCompare(right.withinIdent));
12493
+ winners.push({ ident, files });
12494
+ }
12495
+ winners.sort((left, right) => left.ident.localeCompare(right.ident));
12496
+ const totalFiles = winners.reduce((sum, skill) => sum + skill.files.length, 0);
12497
+ const totalBytes = winners.reduce((sum, skill) => sum + skill.files.reduce((inner, file) => inner + file.winner.size, 0), 0);
12498
+ return { manifest, sourceSnapshotSha, winners, skippedByRule, totalFiles, totalBytes };
12499
+ }
12500
+ function skillSha256(skill) {
12501
+ const skillMd = skill.files.find((file) => file.withinIdent === "SKILL.md");
12502
+ if (skillMd) {
12503
+ return sha256File(skillMd.winner.fullPath);
12504
+ }
12505
+ if (skill.files.length === 1) {
12506
+ return sha256File(skill.files[0].winner.fullPath);
12507
+ }
12508
+ const joined = skill.files.map((file) => sha256File(file.winner.fullPath));
12509
+ return createHash7("sha256").update(joined.sort().join(`
12510
+ `)).digest("hex");
12511
+ }
12512
+ function writeStationHydration(options) {
12513
+ const repoRoot = resolve2(options.repoRoot ?? process.cwd());
12514
+ const cacheRoot = resolve2(options.cacheRoot ?? resolveCorpusRoot());
12515
+ const plan = planStationHydration(options.stationId, repoRoot);
12516
+ const resultSkills = plan.winners.map((skill) => ({
12517
+ ident: skill.ident,
12518
+ files: skill.files.map((file) => ({
12519
+ relativePath: file.withinIdent,
12520
+ sourceAgent: file.winner.agent,
12521
+ sourceMtimeMs: file.winner.mtimeMs,
12522
+ size: file.winner.size
12523
+ })),
12524
+ sha256: skillSha256(skill)
12525
+ }));
12526
+ const base = {
12527
+ stationId: options.stationId,
12528
+ mode: "dry-run",
12529
+ cacheRoot,
12530
+ snapshotRoot: snapshotRootFor(repoRoot, options.stationId),
12531
+ sourceSnapshotSha: plan.sourceSnapshotSha,
12532
+ stats: {
12533
+ idents: plan.winners.length,
12534
+ files: plan.totalFiles,
12535
+ bytes: plan.totalBytes
12536
+ },
12537
+ winners: plan.winners,
12538
+ skills: resultSkills
12539
+ };
12540
+ if (options.dryRun !== false) {
12541
+ return base;
12542
+ }
12543
+ const conflicts = [];
12544
+ const toWrite = [];
12545
+ for (const skill of plan.winners) {
12546
+ for (const file of skill.files) {
12547
+ const destination = join19(cacheRoot, skill.ident, file.withinIdent);
12548
+ const digest = sha256File(file.winner.fullPath);
12549
+ let existingDigest = null;
12550
+ try {
12551
+ existingDigest = sha256File(destination);
12552
+ } catch {}
12553
+ if (existingDigest !== null) {
12554
+ if (existingDigest === digest) {
12555
+ continue;
12556
+ }
12557
+ conflicts.push(`existing destination differs from snapshot winner: ${destination}`);
12558
+ continue;
12559
+ }
12560
+ toWrite.push({ destination, fullPath: file.winner.fullPath });
12561
+ }
12562
+ }
12563
+ if (conflicts.length > 0) {
12564
+ fail("CONFLICT", `${conflicts.length} conflict(s); terminal non-acceptance, nothing written`, conflicts);
12565
+ }
12566
+ let written = 0;
12567
+ for (const entry of toWrite) {
12568
+ mkdirSync14(dirname12(entry.destination), { recursive: true });
12569
+ copyFileSync3(entry.fullPath, entry.destination);
12570
+ written += 1;
12571
+ }
12572
+ const unchanged = plan.totalFiles - written;
12573
+ const hydration = {
12574
+ schema: STATION_HYDRATION_MANIFEST_SCHEMA,
12575
+ stationId: options.stationId,
12576
+ hydratedAt: new Date().toISOString(),
12577
+ producer: STATION_HYDRATION_PRODUCER,
12578
+ sourceSnapshotSha: plan.sourceSnapshotSha,
12579
+ cacheRoot,
12580
+ stats: {
12581
+ idents: plan.winners.length,
12582
+ written,
12583
+ unchanged,
12584
+ files: plan.totalFiles,
12585
+ bytes: plan.totalBytes
12586
+ },
12587
+ skills: resultSkills
12588
+ };
12589
+ const hydrationManifestPath = join19(dirname12(cacheRoot), `hydration-${options.stationId}.json`);
12590
+ mkdirSync14(dirname12(hydrationManifestPath), { recursive: true });
12591
+ writeFileSync13(hydrationManifestPath, `${JSON.stringify(hydration, null, 2)}
12592
+ `);
12593
+ return {
12594
+ ...base,
12595
+ mode: "apply",
12596
+ stats: { ...base.stats, written, unchanged },
12597
+ manifestPath: hydrationManifestPath
12598
+ };
12599
+ }
11993
12600
  export {
12601
+ writeStationSnapshot,
12602
+ writeStationHydration,
11994
12603
  writeRunLogs,
11995
12604
  writeRegistrySyncArtifact,
11996
12605
  writeManagedSkillDir,
11997
12606
  writeManagedAgentSkill,
11998
12607
  writeCorpusSkill,
12608
+ walkEntries,
11999
12609
  verifyContentHash,
12000
12610
  validateToolPrimitiveCoverage,
12611
+ validateStationId,
12001
12612
  validateSkillsCliMcpParity,
12002
12613
  validateSkillDirectory,
12003
12614
  validateRegistryConsistency,
@@ -12016,6 +12627,7 @@ export {
12016
12627
  skillsPostgresSyncSchemaSql,
12017
12628
  skillExists,
12018
12629
  signSkillsAwsV4Request,
12630
+ sha256File,
12019
12631
  setSkillDisabled,
12020
12632
  setScheduleEnabled,
12021
12633
  searchSkills,
@@ -12043,6 +12655,8 @@ export {
12043
12655
  portPortableSkillDirectory,
12044
12656
  portPortableSkill,
12045
12657
  pointerSkillMd,
12658
+ planStationSnapshot,
12659
+ planStationHydration,
12046
12660
  planSkillsS3SnapshotUpload,
12047
12661
  pinSkill,
12048
12662
  pinProjectSkill,
@@ -12068,7 +12682,10 @@ export {
12068
12682
  listPinnedSkills,
12069
12683
  listMcpToolContracts,
12070
12684
  isSyncAgent,
12685
+ isRegularFile,
12686
+ isPortableWithinSkill,
12071
12687
  isGatewayBackedSkill,
12688
+ isExcludedSkillFileName,
12072
12689
  isBasicSkillName,
12073
12690
  installSkills,
12074
12691
  installSkillSource,
@@ -12076,6 +12693,7 @@ export {
12076
12693
  installSkillForAgent,
12077
12694
  installSkill,
12078
12695
  importSkillsLocalSnapshot,
12696
+ homePathFor,
12079
12697
  getToolPrimitive,
12080
12698
  getStorageStatus,
12081
12699
  getStorageDatabaseUrl,
@@ -12125,6 +12743,7 @@ export {
12125
12743
  ensureProjectConfig,
12126
12744
  enableSkill,
12127
12745
  disableSkill,
12746
+ destinationFor,
12128
12747
  describeMcpToolContracts,
12129
12748
  createSkillsSnapshotSyncRecord,
12130
12749
  createSkillsS3ObjectStore,
@@ -12148,12 +12767,16 @@ export {
12148
12767
  adaptSkillMdForAgent,
12149
12768
  TOOL_PRIMITIVE_SCHEMA_VERSION,
12150
12769
  TOOL_PRIMITIVES,
12770
+ StationSnapshotError,
12151
12771
  SkillsS3ObjectStore,
12152
12772
  SkillsPostgresSyncStore,
12153
12773
  SYNC_MARKER_MANAGED_BY,
12154
12774
  SYNC_MARKER_FILE,
12775
+ SYNC_HOMES,
12155
12776
  SYNC_AGENTS,
12156
12777
  STORAGE_TABLES,
12778
+ STATION_SYNC_MANIFEST_SCHEMA,
12779
+ STATION_HYDRATION_MANIFEST_SCHEMA,
12157
12780
  SKILL_SYSTEM_DEPS_ALLOWLIST,
12158
12781
  SKILL_SANDBOX_MODES,
12159
12782
  SKILL_RUNTIMES,
@@ -12170,6 +12793,7 @@ export {
12170
12793
  RemoteRouteUnsupportedError,
12171
12794
  RemoteRequestError,
12172
12795
  REMOTE_SKILL_RUN_CONTRACT_VERSION,
12796
+ REFUSED_SCANNER_FLAGGED,
12173
12797
  PullSkillError,
12174
12798
  PROJECT_CONFIG_FILE,
12175
12799
  PORTABLE_SKILL_STANDARD,