@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/README.md +1 -1
- package/bin/index.js +825 -72
- package/bin/mcp.js +13 -3
- package/bin/server.js +12 -2
- package/bin/worker.js +8 -0
- package/dist/cli/commands/hydrate.d.ts +2 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +636 -2
- package/dist/lib/portable-snapshot-filter.d.ts +48 -0
- package/dist/lib/station-hydrate.d.ts +103 -0
- package/dist/lib/station-snapshot.d.ts +96 -0
- package/dist/sdk/index.js +8 -0
- package/package.json +4 -2
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.
|
|
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,
|