@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/dist/index.js CHANGED
@@ -488,6 +488,14 @@ var DEVELOPMENT_TOOLS_SKILLS = [
488
488
  category: "Development Tools",
489
489
  tags: ["config", "validation", "schema", "linting"]
490
490
  },
491
+ {
492
+ name: "oss-app-two-backend-storage",
493
+ displayName: "OSS App Two-Backend Storage",
494
+ 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",
495
+ category: "Development Tools",
496
+ tags: ["storage", "backend", "postgresql", "sqlite", "two-backend", "oss-app"],
497
+ kind: "instruction"
498
+ },
491
499
  {
492
500
  name: "session-inject-monitor",
493
501
  displayName: "Session Inject Monitor",
@@ -10342,7 +10350,7 @@ import { dirname as dirname8, relative as relative3 } from "path";
10342
10350
  // package.json
10343
10351
  var package_default = {
10344
10352
  name: "@hasna/skills",
10345
- version: "0.1.69",
10353
+ version: "0.1.71",
10346
10354
  description: "Skills library for AI coding agents",
10347
10355
  type: "module",
10348
10356
  bin: {
@@ -10394,6 +10402,7 @@ var package_default = {
10394
10402
  "dev:mcp": "bun --watch run ./src/mcp/index.ts",
10395
10403
  "dev:server": "bun --watch run ./src/server/index.ts",
10396
10404
  "dev:worker": "bun --watch run ./src/server/worker.ts",
10405
+ "docker:check": "bash scripts/docker-build-check.sh",
10397
10406
  migrate: "bun run ./src/server/migrate.ts",
10398
10407
  typecheck: "tsc --noEmit",
10399
10408
  "verify:release": "bun run scripts/release-guard.ts",
@@ -10426,7 +10435,8 @@ var package_default = {
10426
10435
  "@types/react": "^18.2.0",
10427
10436
  "bun-types": "1.3.14",
10428
10437
  "react-devtools-core": "^7.0.1",
10429
- typescript: "^5"
10438
+ typescript: "^5",
10439
+ yaml: "^2.9.0"
10430
10440
  },
10431
10441
  dependencies: {
10432
10442
  "@aws-sdk/client-ecs": "^3.1079.0",
@@ -11980,14 +11990,625 @@ function toArrayBuffer(bytes) {
11980
11990
  new Uint8Array(buffer).set(bytes);
11981
11991
  return buffer;
11982
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
+ }
11983
12600
  export {
12601
+ writeStationSnapshot,
12602
+ writeStationHydration,
11984
12603
  writeRunLogs,
11985
12604
  writeRegistrySyncArtifact,
11986
12605
  writeManagedSkillDir,
11987
12606
  writeManagedAgentSkill,
11988
12607
  writeCorpusSkill,
12608
+ walkEntries,
11989
12609
  verifyContentHash,
11990
12610
  validateToolPrimitiveCoverage,
12611
+ validateStationId,
11991
12612
  validateSkillsCliMcpParity,
11992
12613
  validateSkillDirectory,
11993
12614
  validateRegistryConsistency,
@@ -12006,6 +12627,7 @@ export {
12006
12627
  skillsPostgresSyncSchemaSql,
12007
12628
  skillExists,
12008
12629
  signSkillsAwsV4Request,
12630
+ sha256File,
12009
12631
  setSkillDisabled,
12010
12632
  setScheduleEnabled,
12011
12633
  searchSkills,
@@ -12033,6 +12655,8 @@ export {
12033
12655
  portPortableSkillDirectory,
12034
12656
  portPortableSkill,
12035
12657
  pointerSkillMd,
12658
+ planStationSnapshot,
12659
+ planStationHydration,
12036
12660
  planSkillsS3SnapshotUpload,
12037
12661
  pinSkill,
12038
12662
  pinProjectSkill,
@@ -12058,7 +12682,10 @@ export {
12058
12682
  listPinnedSkills,
12059
12683
  listMcpToolContracts,
12060
12684
  isSyncAgent,
12685
+ isRegularFile,
12686
+ isPortableWithinSkill,
12061
12687
  isGatewayBackedSkill,
12688
+ isExcludedSkillFileName,
12062
12689
  isBasicSkillName,
12063
12690
  installSkills,
12064
12691
  installSkillSource,
@@ -12066,6 +12693,7 @@ export {
12066
12693
  installSkillForAgent,
12067
12694
  installSkill,
12068
12695
  importSkillsLocalSnapshot,
12696
+ homePathFor,
12069
12697
  getToolPrimitive,
12070
12698
  getStorageStatus,
12071
12699
  getStorageDatabaseUrl,
@@ -12115,6 +12743,7 @@ export {
12115
12743
  ensureProjectConfig,
12116
12744
  enableSkill,
12117
12745
  disableSkill,
12746
+ destinationFor,
12118
12747
  describeMcpToolContracts,
12119
12748
  createSkillsSnapshotSyncRecord,
12120
12749
  createSkillsS3ObjectStore,
@@ -12138,12 +12767,16 @@ export {
12138
12767
  adaptSkillMdForAgent,
12139
12768
  TOOL_PRIMITIVE_SCHEMA_VERSION,
12140
12769
  TOOL_PRIMITIVES,
12770
+ StationSnapshotError,
12141
12771
  SkillsS3ObjectStore,
12142
12772
  SkillsPostgresSyncStore,
12143
12773
  SYNC_MARKER_MANAGED_BY,
12144
12774
  SYNC_MARKER_FILE,
12775
+ SYNC_HOMES,
12145
12776
  SYNC_AGENTS,
12146
12777
  STORAGE_TABLES,
12778
+ STATION_SYNC_MANIFEST_SCHEMA,
12779
+ STATION_HYDRATION_MANIFEST_SCHEMA,
12147
12780
  SKILL_SYSTEM_DEPS_ALLOWLIST,
12148
12781
  SKILL_SANDBOX_MODES,
12149
12782
  SKILL_RUNTIMES,
@@ -12160,6 +12793,7 @@ export {
12160
12793
  RemoteRouteUnsupportedError,
12161
12794
  RemoteRequestError,
12162
12795
  REMOTE_SKILL_RUN_CONTRACT_VERSION,
12796
+ REFUSED_SCANNER_FLAGGED,
12163
12797
  PullSkillError,
12164
12798
  PROJECT_CONFIG_FILE,
12165
12799
  PORTABLE_SKILL_STANDARD,