@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 +813 -70
- package/bin/mcp.js +1 -1
- package/bin/server.js +1 -1
- package/dist/cli/commands/hydrate.d.ts +2 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +625 -1
- 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/package.json +1 -1
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.
|
|
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,
|