@hasna/skills 0.1.70 → 0.1.72
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 +1376 -852
- package/bin/mcp.js +342 -232
- package/bin/migrate.js +149 -40
- package/bin/server.js +212 -100
- package/bin/worker.js +151 -45
- package/dist/cli/commands/hydrate.d.ts +2 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1058 -304
- package/dist/lib/app-home.d.ts +85 -0
- package/dist/lib/config.d.ts +9 -10
- package/dist/lib/portable-snapshot-filter.d.ts +48 -0
- package/dist/lib/station-hydrate.d.ts +105 -0
- package/dist/lib/station-snapshot.d.ts +96 -0
- package/dist/sdk/index.js +518 -630
- package/dist/storage.js +155 -43
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -47,13 +47,12 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
|
47
47
|
var __require = import.meta.require;
|
|
48
48
|
|
|
49
49
|
// src/lib/registry.ts
|
|
50
|
-
import { existsSync as
|
|
51
|
-
import { join as
|
|
50
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6, readdirSync as readdirSync6 } from "fs";
|
|
51
|
+
import { join as join9 } from "path";
|
|
52
52
|
|
|
53
53
|
// src/lib/config.ts
|
|
54
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
|
|
55
|
-
import { join, dirname } from "path";
|
|
56
|
-
import { homedir } from "os";
|
|
54
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
|
|
55
|
+
import { join as join3, dirname } from "path";
|
|
57
56
|
|
|
58
57
|
// src/lib/retired-settings.ts
|
|
59
58
|
var RETIRED_ENV_SUFFIXES = ["_STORAGE_MODE", "_DEPLOYMENT_MODE", "_CLOUD_MODE"];
|
|
@@ -101,6 +100,123 @@ function assertNoRetiredConfigKeys(config, source) {
|
|
|
101
100
|
}
|
|
102
101
|
}
|
|
103
102
|
|
|
103
|
+
// src/lib/app-home.ts
|
|
104
|
+
import { existsSync } from "fs";
|
|
105
|
+
import { homedir as homedir2 } from "os";
|
|
106
|
+
import { join as join2, resolve } from "path";
|
|
107
|
+
|
|
108
|
+
// ../../node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
|
|
109
|
+
import { homedir } from "os";
|
|
110
|
+
import { join } from "path";
|
|
111
|
+
var KIND_ENV = {
|
|
112
|
+
config: "HASNA_CONFIG_HOME",
|
|
113
|
+
data: "HASNA_DATA_HOME",
|
|
114
|
+
state: "HASNA_STATE_HOME",
|
|
115
|
+
cache: "HASNA_CACHE_HOME"
|
|
116
|
+
};
|
|
117
|
+
var APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
118
|
+
function assertApp(app) {
|
|
119
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
120
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
121
|
+
}
|
|
122
|
+
if (!APP_SLUG_RE.test(app)) {
|
|
123
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function envOf(options) {
|
|
127
|
+
return options.env ?? process.env;
|
|
128
|
+
}
|
|
129
|
+
function envValue(options, kind) {
|
|
130
|
+
const value = envOf(options)[KIND_ENV[kind]];
|
|
131
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
132
|
+
}
|
|
133
|
+
function isMacOS(platform) {
|
|
134
|
+
return platform === "darwin";
|
|
135
|
+
}
|
|
136
|
+
function baseDir(kind, options) {
|
|
137
|
+
const override = envValue(options, kind);
|
|
138
|
+
if (override)
|
|
139
|
+
return override;
|
|
140
|
+
const home = options.home ?? homedir();
|
|
141
|
+
const platform = options.platform ?? process.platform;
|
|
142
|
+
if (isMacOS(platform)) {
|
|
143
|
+
switch (kind) {
|
|
144
|
+
case "config":
|
|
145
|
+
case "data":
|
|
146
|
+
return join(home, "Library", "Application Support", "Hasna");
|
|
147
|
+
case "cache":
|
|
148
|
+
return join(home, "Library", "Caches", "Hasna");
|
|
149
|
+
case "state":
|
|
150
|
+
return join(home, "Library", "Logs", "Hasna");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
switch (kind) {
|
|
154
|
+
case "config":
|
|
155
|
+
return join(home, ".config", "hasna");
|
|
156
|
+
case "data":
|
|
157
|
+
return join(home, ".local", "share", "hasna");
|
|
158
|
+
case "state":
|
|
159
|
+
return join(home, ".local", "state", "hasna");
|
|
160
|
+
case "cache":
|
|
161
|
+
return join(home, ".cache", "hasna");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function resolvePath(kind, options) {
|
|
165
|
+
assertApp(options.app);
|
|
166
|
+
const appSegment = options.internal === true ? join("internal", options.app) : options.app;
|
|
167
|
+
return join(baseDir(kind, options), appSegment);
|
|
168
|
+
}
|
|
169
|
+
function dataDir(options) {
|
|
170
|
+
return resolvePath("data", options);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/lib/app-home.ts
|
|
174
|
+
var DATA_DIR_ENV = "HASNA_SKILLS_DIR";
|
|
175
|
+
var HASNA_SKILLS_HOME_ENV = "HASNA_SKILLS_HOME";
|
|
176
|
+
var SKILLS_HOME_ENV = "SKILLS_HOME";
|
|
177
|
+
var DEFAULT_SQLITE_FILENAME = "server.db";
|
|
178
|
+
var GLOBAL_CONFIG_FILENAME = "config.json";
|
|
179
|
+
function effectiveHome() {
|
|
180
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir2() || "/tmp";
|
|
181
|
+
}
|
|
182
|
+
function legacyDataRoot() {
|
|
183
|
+
return join2(effectiveHome(), ".hasna", "skills");
|
|
184
|
+
}
|
|
185
|
+
function resolverDataRoot(home = effectiveHome()) {
|
|
186
|
+
return dataDir({ app: "skills", home });
|
|
187
|
+
}
|
|
188
|
+
function adoptResolverDataRoot(resolved, env = process.env) {
|
|
189
|
+
const dataOverride = env.HASNA_DATA_HOME;
|
|
190
|
+
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
191
|
+
return true;
|
|
192
|
+
return existsSync(join2(resolved, DEFAULT_SQLITE_FILENAME)) || existsSync(join2(resolved, GLOBAL_CONFIG_FILENAME));
|
|
193
|
+
}
|
|
194
|
+
function exactDataRoot() {
|
|
195
|
+
for (const key of [DATA_DIR_ENV, HASNA_SKILLS_HOME_ENV, SKILLS_HOME_ENV]) {
|
|
196
|
+
const dir = process.env[key]?.trim();
|
|
197
|
+
if (dir)
|
|
198
|
+
return resolve(dir);
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
function hasExactOverride(env = process.env) {
|
|
203
|
+
return Boolean(env[DATA_DIR_ENV]?.trim()) || Boolean(env[HASNA_SKILLS_HOME_ENV]?.trim()) || Boolean(env[SKILLS_HOME_ENV]?.trim());
|
|
204
|
+
}
|
|
205
|
+
function hasOperatorOverride(env = process.env) {
|
|
206
|
+
return hasExactOverride(env) || Boolean(env.HASNA_DATA_HOME?.trim());
|
|
207
|
+
}
|
|
208
|
+
function getDataRoot() {
|
|
209
|
+
const exact = exactDataRoot();
|
|
210
|
+
if (exact)
|
|
211
|
+
return exact;
|
|
212
|
+
const resolved = resolverDataRoot();
|
|
213
|
+
return adoptResolverDataRoot(resolved) ? resolve(resolved) : resolve(legacyDataRoot());
|
|
214
|
+
}
|
|
215
|
+
function skillsDataRootForHome(home) {
|
|
216
|
+
const resolved = resolverDataRoot(home);
|
|
217
|
+
return adoptResolverDataRoot(resolved) ? resolve(resolved) : resolve(join2(home, ".hasna", "skills"));
|
|
218
|
+
}
|
|
219
|
+
|
|
104
220
|
// src/lib/config.ts
|
|
105
221
|
var ENUM_KEYS = {
|
|
106
222
|
defaultAgent: ["claude", "codex", "gemini", "pi", "opencode", "all"],
|
|
@@ -115,19 +231,19 @@ function allowedValues(key) {
|
|
|
115
231
|
return ENUM_KEYS[key];
|
|
116
232
|
}
|
|
117
233
|
function mergeDirectoryContents(sourceDir, targetDir) {
|
|
118
|
-
if (!
|
|
234
|
+
if (!existsSync2(sourceDir))
|
|
119
235
|
return;
|
|
120
236
|
mkdirSync(targetDir, { recursive: true });
|
|
121
237
|
for (const entry of readdirSync(sourceDir)) {
|
|
122
|
-
const sourcePath =
|
|
123
|
-
const targetPath =
|
|
238
|
+
const sourcePath = join3(sourceDir, entry);
|
|
239
|
+
const targetPath = join3(targetDir, entry);
|
|
124
240
|
try {
|
|
125
241
|
const sourceStat = statSync(sourcePath);
|
|
126
242
|
if (sourceStat.isDirectory()) {
|
|
127
243
|
mergeDirectoryContents(sourcePath, targetPath);
|
|
128
244
|
continue;
|
|
129
245
|
}
|
|
130
|
-
if (!
|
|
246
|
+
if (!existsSync2(targetPath))
|
|
131
247
|
copyFileSync(sourcePath, targetPath);
|
|
132
248
|
} catch {}
|
|
133
249
|
}
|
|
@@ -152,44 +268,40 @@ function normalizeConfigValue(key, value) {
|
|
|
152
268
|
return value.trim() ? value : undefined;
|
|
153
269
|
return;
|
|
154
270
|
}
|
|
155
|
-
var DATA_DIR_ENV = "HASNA_SKILLS_DIR";
|
|
156
271
|
var INSTALLED_SKILLS_DIRNAME = "installed";
|
|
157
272
|
var SKILLS_CACHE_DIRNAME = "skills";
|
|
158
273
|
var LAYOUT_MIGRATION_RECORD = ".layout-migration.json";
|
|
159
274
|
function isOwnerLayoutMigrated(appDir) {
|
|
160
|
-
return
|
|
275
|
+
return existsSync2(join3(appDir, SKILLS_CACHE_DIRNAME, LAYOUT_MIGRATION_RECORD));
|
|
161
276
|
}
|
|
162
277
|
function getDataDir() {
|
|
163
|
-
const
|
|
164
|
-
if (override) {
|
|
165
|
-
try {
|
|
166
|
-
mkdirSync(override, { recursive: true });
|
|
167
|
-
} catch {}
|
|
168
|
-
return override;
|
|
169
|
-
}
|
|
170
|
-
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
171
|
-
const newDir = join(home, ".hasna", "skills");
|
|
172
|
-
const oldDir = join(home, ".skills");
|
|
173
|
-
const oldConfigFile = join(home, ".skillsrc");
|
|
174
|
-
mkdirSync(newDir, { recursive: true });
|
|
278
|
+
const root = getDataRoot();
|
|
175
279
|
try {
|
|
176
|
-
|
|
280
|
+
mkdirSync(root, { recursive: true });
|
|
177
281
|
} catch {}
|
|
178
|
-
if (
|
|
282
|
+
if (hasOperatorOverride())
|
|
283
|
+
return root;
|
|
284
|
+
const home = effectiveHome();
|
|
285
|
+
const oldDir = join3(home, ".skills");
|
|
286
|
+
const oldConfigFile = join3(home, ".skillsrc");
|
|
287
|
+
try {
|
|
288
|
+
mergeDirectoryContents(oldDir, root);
|
|
289
|
+
} catch {}
|
|
290
|
+
if (existsSync2(oldConfigFile) && !existsSync2(join3(root, "config.json"))) {
|
|
179
291
|
try {
|
|
180
|
-
copyFileSync(oldConfigFile,
|
|
292
|
+
copyFileSync(oldConfigFile, join3(root, "config.json"));
|
|
181
293
|
} catch {}
|
|
182
294
|
}
|
|
183
|
-
return
|
|
295
|
+
return root;
|
|
184
296
|
}
|
|
185
297
|
function getConfigPath(scope) {
|
|
186
298
|
if (scope === "global") {
|
|
187
|
-
return
|
|
299
|
+
return join3(getDataDir(), "config.json");
|
|
188
300
|
}
|
|
189
|
-
return
|
|
301
|
+
return join3(process.cwd(), "skills.config.json");
|
|
190
302
|
}
|
|
191
303
|
function readConfigFile(path) {
|
|
192
|
-
if (!
|
|
304
|
+
if (!existsSync2(path))
|
|
193
305
|
return {};
|
|
194
306
|
let parsed;
|
|
195
307
|
try {
|
|
@@ -225,7 +337,7 @@ function saveConfig(key, value, scope = "project") {
|
|
|
225
337
|
}
|
|
226
338
|
const filePath = getConfigPath(scope);
|
|
227
339
|
let existing = {};
|
|
228
|
-
if (
|
|
340
|
+
if (existsSync2(filePath)) {
|
|
229
341
|
try {
|
|
230
342
|
existing = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
231
343
|
if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
|
|
@@ -236,7 +348,7 @@ function saveConfig(key, value, scope = "project") {
|
|
|
236
348
|
}
|
|
237
349
|
} else {
|
|
238
350
|
const dir = dirname(filePath);
|
|
239
|
-
if (!
|
|
351
|
+
if (!existsSync2(dir)) {
|
|
240
352
|
mkdirSync(dir, { recursive: true });
|
|
241
353
|
}
|
|
242
354
|
}
|
|
@@ -250,7 +362,7 @@ function unsetConfig(key, scope = "project") {
|
|
|
250
362
|
throw new Error(`Unknown config key: ${key}. Valid keys: ${validKeys().join(", ")}`);
|
|
251
363
|
}
|
|
252
364
|
const filePath = getConfigPath(scope);
|
|
253
|
-
if (!
|
|
365
|
+
if (!existsSync2(filePath))
|
|
254
366
|
return false;
|
|
255
367
|
let existing;
|
|
256
368
|
try {
|
|
@@ -272,7 +384,7 @@ function unsetConfig(key, scope = "project") {
|
|
|
272
384
|
// src/lib/portable-skills.ts
|
|
273
385
|
import {
|
|
274
386
|
cpSync as cpSync2,
|
|
275
|
-
existsSync as
|
|
387
|
+
existsSync as existsSync7,
|
|
276
388
|
mkdirSync as mkdirSync3,
|
|
277
389
|
mkdtempSync,
|
|
278
390
|
readdirSync as readdirSync5,
|
|
@@ -281,7 +393,7 @@ import {
|
|
|
281
393
|
statSync as statSync6,
|
|
282
394
|
writeFileSync as writeFileSync3
|
|
283
395
|
} from "fs";
|
|
284
|
-
import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, join as
|
|
396
|
+
import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, join as join8, normalize as normalize2 } from "path";
|
|
285
397
|
|
|
286
398
|
// src/lib/registry-data/development-tools.ts
|
|
287
399
|
var DEVELOPMENT_TOOLS_SKILLS = [
|
|
@@ -1001,8 +1113,8 @@ var SKILLS = [
|
|
|
1001
1113
|
];
|
|
1002
1114
|
|
|
1003
1115
|
// src/lib/hosted-skill-set.ts
|
|
1004
|
-
import { existsSync as
|
|
1005
|
-
import { join as
|
|
1116
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
1117
|
+
import { join as join4 } from "path";
|
|
1006
1118
|
var HOSTED_RUNTIMES = new Set(["hosted"]);
|
|
1007
1119
|
var HOSTED_SOURCES = new Set(["remote", "private-hosted"]);
|
|
1008
1120
|
function normalizeMarker(value) {
|
|
@@ -1015,8 +1127,8 @@ function isHostedMetadataPackage(pkg) {
|
|
|
1015
1127
|
return HOSTED_RUNTIMES.has(normalizeMarker(skills.runtime)) || HOSTED_SOURCES.has(normalizeMarker(skills.source));
|
|
1016
1128
|
}
|
|
1017
1129
|
function isHostedMetadataSkillDir(skillDir) {
|
|
1018
|
-
const pkgPath =
|
|
1019
|
-
if (!
|
|
1130
|
+
const pkgPath = join4(skillDir, "package.json");
|
|
1131
|
+
if (!existsSync3(pkgPath))
|
|
1020
1132
|
return false;
|
|
1021
1133
|
try {
|
|
1022
1134
|
return isHostedMetadataPackage(JSON.parse(readFileSync2(pkgPath, "utf8")));
|
|
@@ -1040,8 +1152,8 @@ var BRACE_SOURCE_EXCLUSION = new RegExp(`^!skills/\\{(${SLUG}(?:,${SLUG})+)\\}/s
|
|
|
1040
1152
|
var SINGLE_SOURCE_EXCLUSION = new RegExp(`^!skills/(${SLUG})/src$`);
|
|
1041
1153
|
|
|
1042
1154
|
// src/lib/skill-validation.ts
|
|
1043
|
-
import { existsSync as
|
|
1044
|
-
import { isAbsolute, join as
|
|
1155
|
+
import { existsSync as existsSync4, lstatSync, readFileSync as readFileSync3, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
|
|
1156
|
+
import { isAbsolute, join as join5, normalize } from "path";
|
|
1045
1157
|
var VALID_SKILL_KINDS = ["executable", "instruction"];
|
|
1046
1158
|
var DOC_FILES = ["SKILL.md", "README.md", "CLAUDE.md"];
|
|
1047
1159
|
var RESERVED_SKILL_ENTRIES = new Set([
|
|
@@ -1179,7 +1291,7 @@ function validateSkillDirectory(name, skillPath, registryMeta) {
|
|
|
1179
1291
|
binCommands: [],
|
|
1180
1292
|
docFiles: []
|
|
1181
1293
|
};
|
|
1182
|
-
if (!
|
|
1294
|
+
if (!existsSync4(skillPath)) {
|
|
1183
1295
|
add(issues, "skill.dir_missing", `Skill directory not found: ${skillPath}`);
|
|
1184
1296
|
return {
|
|
1185
1297
|
name: bareName,
|
|
@@ -1194,7 +1306,7 @@ function validateSkillDirectory(name, skillPath, registryMeta) {
|
|
|
1194
1306
|
add(issues, "skill.name_invalid", `Skill name '${bareName}' must use lowercase letters, numbers, dots, underscores, or hyphens`);
|
|
1195
1307
|
}
|
|
1196
1308
|
for (const entry of readdirSync3(skillPath).sort()) {
|
|
1197
|
-
const entryPath =
|
|
1309
|
+
const entryPath = join5(skillPath, entry);
|
|
1198
1310
|
if (RESERVED_SKILL_ENTRIES.has(entry)) {
|
|
1199
1311
|
add(issues, "skill.reserved_file", `Reserved file '${entry}' is not allowed in skill packages`);
|
|
1200
1312
|
}
|
|
@@ -1206,14 +1318,14 @@ function validateSkillDirectory(name, skillPath, registryMeta) {
|
|
|
1206
1318
|
}
|
|
1207
1319
|
}
|
|
1208
1320
|
for (const docFile of DOC_FILES) {
|
|
1209
|
-
if (
|
|
1321
|
+
if (existsSync4(join5(skillPath, docFile)))
|
|
1210
1322
|
metadata.docFiles.push(docFile);
|
|
1211
1323
|
}
|
|
1212
1324
|
if (metadata.docFiles.length === 0) {
|
|
1213
1325
|
add(issues, "skill.docs_missing", "Missing documentation file: expected SKILL.md, README.md, or CLAUDE.md");
|
|
1214
1326
|
}
|
|
1215
|
-
const skillMdPath =
|
|
1216
|
-
if (
|
|
1327
|
+
const skillMdPath = join5(skillPath, "SKILL.md");
|
|
1328
|
+
if (existsSync4(skillMdPath)) {
|
|
1217
1329
|
const frontmatter = parseSkillFrontmatter(readFileSync3(skillMdPath, "utf-8"));
|
|
1218
1330
|
if (!frontmatter) {
|
|
1219
1331
|
add(warnings, "skill.frontmatter_missing", "SKILL.md has no YAML frontmatter");
|
|
@@ -1256,8 +1368,8 @@ function validateSkillDirectory(name, skillPath, registryMeta) {
|
|
|
1256
1368
|
}
|
|
1257
1369
|
metadata.kind = resolvedKind;
|
|
1258
1370
|
const isInstruction = resolvedKind === "instruction";
|
|
1259
|
-
const pkgPath =
|
|
1260
|
-
if (!
|
|
1371
|
+
const pkgPath = join5(skillPath, "package.json");
|
|
1372
|
+
if (!existsSync4(pkgPath)) {
|
|
1261
1373
|
if (!isInstruction)
|
|
1262
1374
|
add(issues, "package.missing", "Missing package.json");
|
|
1263
1375
|
} else {
|
|
@@ -1315,8 +1427,8 @@ function validateSkillDirectory(name, skillPath, registryMeta) {
|
|
|
1315
1427
|
add(issues, "package.bin_target_unsafe", `package.json bin '${command}' target '${target}' must stay inside the skill directory`);
|
|
1316
1428
|
continue;
|
|
1317
1429
|
}
|
|
1318
|
-
const targetPath =
|
|
1319
|
-
if (!
|
|
1430
|
+
const targetPath = join5(skillPath, target);
|
|
1431
|
+
if (!existsSync4(targetPath)) {
|
|
1320
1432
|
add(warnings, "package.bin_target_missing", `package.json bin '${command}' target '${target}' is not present before build`);
|
|
1321
1433
|
} else if (statSync3(targetPath).isDirectory()) {
|
|
1322
1434
|
add(issues, "package.bin_target_directory", `package.json bin '${command}' target '${target}' must point to a file, not a directory`);
|
|
@@ -1333,17 +1445,17 @@ function validateSkillDirectory(name, skillPath, registryMeta) {
|
|
|
1333
1445
|
metadata.runtime = "none";
|
|
1334
1446
|
} else {
|
|
1335
1447
|
metadata.runtime = hostedMetadata ? "hosted" : "local";
|
|
1336
|
-
const srcDir =
|
|
1448
|
+
const srcDir = join5(skillPath, "src");
|
|
1337
1449
|
if (hostedMetadata) {
|
|
1338
|
-
if (
|
|
1450
|
+
if (existsSync4(srcDir)) {
|
|
1339
1451
|
add(issues, "skill.hosted_source_forbidden", "Hosted metadata skills must not include local implementation source");
|
|
1340
1452
|
}
|
|
1341
|
-
} else if (!
|
|
1453
|
+
} else if (!existsSync4(srcDir)) {
|
|
1342
1454
|
add(issues, "skill.src_missing", "Missing src/ directory");
|
|
1343
|
-
} else if (!
|
|
1455
|
+
} else if (!existsSync4(join5(srcDir, "index.ts")) && !existsSync4(join5(srcDir, "index.js"))) {
|
|
1344
1456
|
add(issues, "skill.src_index_missing", "Missing src/index.ts or src/index.js");
|
|
1345
1457
|
} else {
|
|
1346
|
-
const indexPath =
|
|
1458
|
+
const indexPath = existsSync4(join5(srcDir, "index.ts")) ? join5(srcDir, "index.ts") : join5(srcDir, "index.js");
|
|
1347
1459
|
const size = statSync3(indexPath).size;
|
|
1348
1460
|
if (size < 50)
|
|
1349
1461
|
add(warnings, "skill.src_index_minimal", `Source entry point is very small (${size}B)`);
|
|
@@ -1367,8 +1479,8 @@ function validateRegistryConsistency(registry, skillsDir) {
|
|
|
1367
1479
|
seen.add(name);
|
|
1368
1480
|
return false;
|
|
1369
1481
|
})));
|
|
1370
|
-
const skillDirs =
|
|
1371
|
-
const fullPath =
|
|
1482
|
+
const skillDirs = existsSync4(skillsDir) ? readdirSync3(skillsDir).filter((entry) => {
|
|
1483
|
+
const fullPath = join5(skillsDir, entry);
|
|
1372
1484
|
return !entry.startsWith(".") && entry !== "_common" && statSync3(fullPath).isDirectory();
|
|
1373
1485
|
}) : [];
|
|
1374
1486
|
const directoryNames = new Set(skillDirs);
|
|
@@ -1385,8 +1497,8 @@ function validateRegistryConsistency(registry, skillsDir) {
|
|
|
1385
1497
|
|
|
1386
1498
|
// src/lib/skill-hash.ts
|
|
1387
1499
|
import { createHash } from "crypto";
|
|
1388
|
-
import { existsSync as
|
|
1389
|
-
import { join as
|
|
1500
|
+
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
|
|
1501
|
+
import { join as join6, sep } from "path";
|
|
1390
1502
|
var CONTENT_HASH_ALGORITHM = "sha256";
|
|
1391
1503
|
var HASH_EXCLUDE_DIRS = new Set([".git", "node_modules", "dist", "build", ".turbo"]);
|
|
1392
1504
|
var HASH_COVERAGE = [
|
|
@@ -1445,8 +1557,8 @@ function collectBundleFiles(skillPath) {
|
|
|
1445
1557
|
if (seen.has(entry))
|
|
1446
1558
|
continue;
|
|
1447
1559
|
seen.add(entry);
|
|
1448
|
-
const absolute =
|
|
1449
|
-
if (!
|
|
1560
|
+
const absolute = join6(skillPath, entry);
|
|
1561
|
+
if (!existsSync5(absolute))
|
|
1450
1562
|
continue;
|
|
1451
1563
|
if (statSync4(absolute).isDirectory())
|
|
1452
1564
|
collectDirectory(files, absolute, entry);
|
|
@@ -1459,7 +1571,7 @@ function collectDirectory(files, dir, rel) {
|
|
|
1459
1571
|
for (const entry of readdirSync4(dir).sort()) {
|
|
1460
1572
|
if (entry.startsWith("."))
|
|
1461
1573
|
continue;
|
|
1462
|
-
const absolute =
|
|
1574
|
+
const absolute = join6(dir, entry);
|
|
1463
1575
|
const childRel = `${rel}/${entry}`;
|
|
1464
1576
|
let stats;
|
|
1465
1577
|
try {
|
|
@@ -1679,14 +1791,14 @@ function validateRuntimeContract(manifest, issues, strict) {
|
|
|
1679
1791
|
// src/lib/portable-skills-files.ts
|
|
1680
1792
|
import {
|
|
1681
1793
|
cpSync,
|
|
1682
|
-
existsSync as
|
|
1794
|
+
existsSync as existsSync6,
|
|
1683
1795
|
lstatSync as lstatSync2,
|
|
1684
1796
|
mkdirSync as mkdirSync2,
|
|
1685
1797
|
readFileSync as readFileSync5,
|
|
1686
1798
|
realpathSync,
|
|
1687
1799
|
writeFileSync as writeFileSync2
|
|
1688
1800
|
} from "fs";
|
|
1689
|
-
import { basename, dirname as dirname2, join as
|
|
1801
|
+
import { basename, dirname as dirname2, join as join7, relative } from "path";
|
|
1690
1802
|
var ANY_SEGMENT_COPY_EXCLUDES = new Set([
|
|
1691
1803
|
".git",
|
|
1692
1804
|
".DS_Store",
|
|
@@ -1729,12 +1841,12 @@ function normalizePortableSkillName(name) {
|
|
|
1729
1841
|
return normalized;
|
|
1730
1842
|
}
|
|
1731
1843
|
function readPortableSkillManifest(skillPath, fallbackName = basename(skillPath)) {
|
|
1732
|
-
const skillJsonPath =
|
|
1733
|
-
const skillMdPath =
|
|
1734
|
-
const pkgPath =
|
|
1735
|
-
const jsonManifest =
|
|
1736
|
-
const frontmatter =
|
|
1737
|
-
const pkg =
|
|
1844
|
+
const skillJsonPath = join7(skillPath, "skill.json");
|
|
1845
|
+
const skillMdPath = join7(skillPath, "SKILL.md");
|
|
1846
|
+
const pkgPath = join7(skillPath, "package.json");
|
|
1847
|
+
const jsonManifest = existsSync6(skillJsonPath) ? readJsonObject(skillJsonPath) : undefined;
|
|
1848
|
+
const frontmatter = existsSync6(skillMdPath) ? parseSkillFrontmatter(readFileSync5(skillMdPath, "utf-8")) ?? undefined : undefined;
|
|
1849
|
+
const pkg = existsSync6(pkgPath) ? readJsonObject(pkgPath) : undefined;
|
|
1738
1850
|
const name = normalizePortableSkillName(stringField(jsonManifest, "name") ?? frontmatter?.name ?? stringValue(pkg?.name) ?? fallbackName);
|
|
1739
1851
|
const description = stringField(jsonManifest, "description") ?? frontmatter?.description ?? stringValue(pkg?.description) ?? `${name} skill`;
|
|
1740
1852
|
const version = stringField(jsonManifest, "version") ?? frontmatter?.version ?? stringValue(pkg?.version) ?? PORTABLE_SKILL_DEFAULT_VERSION;
|
|
@@ -1780,7 +1892,7 @@ function createInstructionManifest(name, options) {
|
|
|
1780
1892
|
}
|
|
1781
1893
|
function writeInstructionSkillTemplate(skillPath, manifest) {
|
|
1782
1894
|
mkdirSync2(skillPath, { recursive: true });
|
|
1783
|
-
writeFileSync2(
|
|
1895
|
+
writeFileSync2(join7(skillPath, "SKILL.md"), renderInstructionSkillMd(manifest));
|
|
1784
1896
|
writeSkillJsonWithHash(skillPath, manifest);
|
|
1785
1897
|
}
|
|
1786
1898
|
function renderInstructionSkillMd(manifest) {
|
|
@@ -1829,12 +1941,12 @@ function createPortableManifest(name, options) {
|
|
|
1829
1941
|
};
|
|
1830
1942
|
}
|
|
1831
1943
|
function writePortableSkillTemplate(skillPath, manifest) {
|
|
1832
|
-
mkdirSync2(
|
|
1833
|
-
writeFileSync2(
|
|
1834
|
-
writeFileSync2(
|
|
1835
|
-
writeFileSync2(
|
|
1836
|
-
writeFileSync2(
|
|
1837
|
-
writeFileSync2(
|
|
1944
|
+
mkdirSync2(join7(skillPath, "src"), { recursive: true });
|
|
1945
|
+
writeFileSync2(join7(skillPath, "SKILL.md"), renderSkillMd(manifest));
|
|
1946
|
+
writeFileSync2(join7(skillPath, "AGENTS.md"), renderAgentsMd(manifest));
|
|
1947
|
+
writeFileSync2(join7(skillPath, "package.json"), renderPackageJson(manifest));
|
|
1948
|
+
writeFileSync2(join7(skillPath, "tsconfig.json"), renderTsconfig());
|
|
1949
|
+
writeFileSync2(join7(skillPath, "src", "index.ts"), renderEntrypoint(manifest));
|
|
1838
1950
|
writeSkillJsonWithHash(skillPath, manifest);
|
|
1839
1951
|
}
|
|
1840
1952
|
function fillContractDefaults(manifest, entrypoint) {
|
|
@@ -1859,7 +1971,7 @@ function writeSkillJsonWithHash(skillPath, manifest) {
|
|
|
1859
1971
|
content_hash: undefined
|
|
1860
1972
|
}
|
|
1861
1973
|
};
|
|
1862
|
-
writeFileSync2(
|
|
1974
|
+
writeFileSync2(join7(skillPath, "skill.json"), `${JSON.stringify({ ...existing, ...renderSkillJsonObject(withoutHash) }, null, 2)}
|
|
1863
1975
|
`);
|
|
1864
1976
|
const hash = computeContentHash(skillPath);
|
|
1865
1977
|
const withHash = {
|
|
@@ -1869,13 +1981,13 @@ function writeSkillJsonWithHash(skillPath, manifest) {
|
|
|
1869
1981
|
content_hash: hash
|
|
1870
1982
|
}
|
|
1871
1983
|
};
|
|
1872
|
-
writeFileSync2(
|
|
1984
|
+
writeFileSync2(join7(skillPath, "skill.json"), `${JSON.stringify({ ...existing, ...renderSkillJsonObject(withHash) }, null, 2)}
|
|
1873
1985
|
`);
|
|
1874
1986
|
return withHash;
|
|
1875
1987
|
}
|
|
1876
1988
|
function readExistingSkillJson(skillPath) {
|
|
1877
|
-
const path =
|
|
1878
|
-
if (!
|
|
1989
|
+
const path = join7(skillPath, "skill.json");
|
|
1990
|
+
if (!existsSync6(path))
|
|
1879
1991
|
return {};
|
|
1880
1992
|
try {
|
|
1881
1993
|
const parsed = JSON.parse(readFileSync5(path, "utf-8"));
|
|
@@ -1908,28 +2020,28 @@ function ensurePortableSkillFiles(skillPath, manifest) {
|
|
|
1908
2020
|
tags: next.tags?.length ? next.tags : ["custom", next.name]
|
|
1909
2021
|
};
|
|
1910
2022
|
const entry = next.commands[0]?.entry ?? "src/index.ts";
|
|
1911
|
-
if (entry && !
|
|
1912
|
-
mkdirSync2(dirname2(
|
|
1913
|
-
writeFileSync2(
|
|
2023
|
+
if (entry && !existsSync6(join7(skillPath, entry))) {
|
|
2024
|
+
mkdirSync2(dirname2(join7(skillPath, entry)), { recursive: true });
|
|
2025
|
+
writeFileSync2(join7(skillPath, entry), renderEntrypoint(next));
|
|
1914
2026
|
}
|
|
1915
|
-
if (!
|
|
1916
|
-
writeFileSync2(
|
|
2027
|
+
if (!existsSync6(join7(skillPath, "SKILL.md")))
|
|
2028
|
+
writeFileSync2(join7(skillPath, "SKILL.md"), renderSkillMd(next));
|
|
1917
2029
|
else
|
|
1918
|
-
writeFileSync2(
|
|
1919
|
-
if (!
|
|
1920
|
-
writeFileSync2(
|
|
2030
|
+
writeFileSync2(join7(skillPath, "SKILL.md"), ensureSkillMdFrontmatter(readFileSync5(join7(skillPath, "SKILL.md"), "utf-8"), next));
|
|
2031
|
+
if (!existsSync6(join7(skillPath, "AGENTS.md")))
|
|
2032
|
+
writeFileSync2(join7(skillPath, "AGENTS.md"), renderAgentsMd(next));
|
|
1921
2033
|
ensurePackageJson(skillPath, next);
|
|
1922
|
-
if (!
|
|
1923
|
-
writeFileSync2(
|
|
2034
|
+
if (!existsSync6(join7(skillPath, "tsconfig.json")))
|
|
2035
|
+
writeFileSync2(join7(skillPath, "tsconfig.json"), renderTsconfig());
|
|
1924
2036
|
writeSkillJsonWithHash(skillPath, next);
|
|
1925
2037
|
return readPortableSkillManifest(skillPath, next.name);
|
|
1926
2038
|
}
|
|
1927
2039
|
function ensurePackageJson(skillPath, manifest) {
|
|
1928
|
-
const pkgPath =
|
|
2040
|
+
const pkgPath = join7(skillPath, "package.json");
|
|
1929
2041
|
const first = manifest.commands[0] ?? { name: manifest.name, entry: "src/index.ts" };
|
|
1930
2042
|
const commandName = normalizePortableSkillName(first.name || manifest.name);
|
|
1931
2043
|
const entry = (first.entry ?? "src/index.ts").replace(/^\.\//, "");
|
|
1932
|
-
if (!
|
|
2044
|
+
if (!existsSync6(pkgPath)) {
|
|
1933
2045
|
writeFileSync2(pkgPath, renderPackageJson(manifest));
|
|
1934
2046
|
return;
|
|
1935
2047
|
}
|
|
@@ -1972,8 +2084,8 @@ function ensureInstructionSkillFiles(skillPath, manifest) {
|
|
|
1972
2084
|
inputs: [],
|
|
1973
2085
|
commands: []
|
|
1974
2086
|
};
|
|
1975
|
-
if (!
|
|
1976
|
-
writeFileSync2(
|
|
2087
|
+
if (!existsSync6(join7(skillPath, "SKILL.md"))) {
|
|
2088
|
+
writeFileSync2(join7(skillPath, "SKILL.md"), renderSkillMd(next));
|
|
1977
2089
|
}
|
|
1978
2090
|
writeSkillJsonWithHash(skillPath, next);
|
|
1979
2091
|
return readPortableSkillManifest(skillPath, next.name);
|
|
@@ -2265,18 +2377,18 @@ var LEGACY_CUSTOM_DIRNAME = "custom";
|
|
|
2265
2377
|
function getPortableSkillsRoot(options = {}) {
|
|
2266
2378
|
if (options.rootDir)
|
|
2267
2379
|
return options.rootDir;
|
|
2268
|
-
const appDir = options.homeDir ?
|
|
2269
|
-
const cache =
|
|
2380
|
+
const appDir = options.homeDir ? join8(options.homeDir, ".hasna", "skills") : getDataDir();
|
|
2381
|
+
const cache = join8(appDir, SKILLS_CACHE_DIRNAME);
|
|
2270
2382
|
if (isOwnerLayoutMigrated(appDir) && safeIsDirectory(cache))
|
|
2271
2383
|
return cache;
|
|
2272
|
-
const installed =
|
|
2384
|
+
const installed = join8(appDir, INSTALLED_SKILLS_DIRNAME);
|
|
2273
2385
|
migrateLegacySkillLayout(appDir, installed);
|
|
2274
2386
|
return installed;
|
|
2275
2387
|
}
|
|
2276
2388
|
function looksLikeSkillDirectory(path) {
|
|
2277
2389
|
if (!safeIsDirectory(path))
|
|
2278
2390
|
return false;
|
|
2279
|
-
return
|
|
2391
|
+
return existsSync7(join8(path, "SKILL.md")) || existsSync7(join8(path, "skill.json")) || existsSync7(join8(path, "package.json"));
|
|
2280
2392
|
}
|
|
2281
2393
|
function migrateLegacySkillLayout(appDir, installed) {
|
|
2282
2394
|
if (!safeIsDirectory(appDir))
|
|
@@ -2286,7 +2398,7 @@ function migrateLegacySkillLayout(appDir, installed) {
|
|
|
2286
2398
|
for (const entry of readdirSync5(appDir)) {
|
|
2287
2399
|
if (entry.startsWith(".") || entry === INSTALLED_SKILLS_DIRNAME)
|
|
2288
2400
|
continue;
|
|
2289
|
-
const path =
|
|
2401
|
+
const path = join8(appDir, entry);
|
|
2290
2402
|
if (entry === LEGACY_CUSTOM_DIRNAME) {
|
|
2291
2403
|
if (!safeIsDirectory(path))
|
|
2292
2404
|
continue;
|
|
@@ -2294,7 +2406,7 @@ function migrateLegacySkillLayout(appDir, installed) {
|
|
|
2294
2406
|
for (const nested of readdirSync5(path)) {
|
|
2295
2407
|
if (nested.startsWith("."))
|
|
2296
2408
|
continue;
|
|
2297
|
-
const nestedPath =
|
|
2409
|
+
const nestedPath = join8(path, nested);
|
|
2298
2410
|
if (looksLikeSkillDirectory(nestedPath))
|
|
2299
2411
|
candidates.push({ from: nestedPath, name: nested });
|
|
2300
2412
|
}
|
|
@@ -2308,10 +2420,10 @@ function migrateLegacySkillLayout(appDir, installed) {
|
|
|
2308
2420
|
return;
|
|
2309
2421
|
}
|
|
2310
2422
|
for (const { from, name } of candidates) {
|
|
2311
|
-
const target =
|
|
2312
|
-
if (
|
|
2423
|
+
const target = join8(installed, name);
|
|
2424
|
+
if (existsSync7(target))
|
|
2313
2425
|
continue;
|
|
2314
|
-
const staging =
|
|
2426
|
+
const staging = join8(installed, `.migrating-${name}-${process.pid}`);
|
|
2315
2427
|
try {
|
|
2316
2428
|
rmSync(staging, { recursive: true, force: true });
|
|
2317
2429
|
cpSync2(from, staging, { recursive: true, errorOnExist: false });
|
|
@@ -2324,7 +2436,7 @@ function migrateLegacySkillLayout(appDir, installed) {
|
|
|
2324
2436
|
}
|
|
2325
2437
|
}
|
|
2326
2438
|
function getPortableSkillPath(name, options = {}) {
|
|
2327
|
-
return
|
|
2439
|
+
return join8(getPortableSkillsRoot(options), normalizePortableSkillName(name));
|
|
2328
2440
|
}
|
|
2329
2441
|
function findPortableSkill(name, options = {}) {
|
|
2330
2442
|
let normalized;
|
|
@@ -2334,7 +2446,7 @@ function findPortableSkill(name, options = {}) {
|
|
|
2334
2446
|
return null;
|
|
2335
2447
|
}
|
|
2336
2448
|
const path = getPortableSkillPath(normalized, options);
|
|
2337
|
-
if (!
|
|
2449
|
+
if (!existsSync7(path) || !statSync6(path).isDirectory())
|
|
2338
2450
|
return null;
|
|
2339
2451
|
try {
|
|
2340
2452
|
return summarizePortableSkill(path, normalized);
|
|
@@ -2350,7 +2462,7 @@ function listPortableSkills(options = {}) {
|
|
|
2350
2462
|
for (const entry of readdirSync5(root).sort()) {
|
|
2351
2463
|
if (entry.startsWith("."))
|
|
2352
2464
|
continue;
|
|
2353
|
-
const path =
|
|
2465
|
+
const path = join8(root, entry);
|
|
2354
2466
|
if (!safeIsDirectory(path))
|
|
2355
2467
|
continue;
|
|
2356
2468
|
try {
|
|
@@ -2384,8 +2496,8 @@ function isOfficialSkillName(name) {
|
|
|
2384
2496
|
function scaffoldPortableSkill(name, options = {}) {
|
|
2385
2497
|
const skillName = normalizePortableSkillName(name);
|
|
2386
2498
|
const root = getPortableSkillsRoot(options);
|
|
2387
|
-
const skillPath =
|
|
2388
|
-
if (
|
|
2499
|
+
const skillPath = join8(root, skillName);
|
|
2500
|
+
if (existsSync7(skillPath)) {
|
|
2389
2501
|
if (!options.overwrite)
|
|
2390
2502
|
throw new Error(`Skill '${skillName}' already exists at ${skillPath}`);
|
|
2391
2503
|
rmSync(skillPath, { recursive: true, force: true });
|
|
@@ -2403,7 +2515,7 @@ function scaffoldPortableSkill(name, options = {}) {
|
|
|
2403
2515
|
}
|
|
2404
2516
|
function portPortableSkillDirectory(sourceDir, options = {}) {
|
|
2405
2517
|
const absoluteSource = normalize2(sourceDir);
|
|
2406
|
-
if (!
|
|
2518
|
+
if (!existsSync7(absoluteSource) || !statSync6(absoluteSource).isDirectory()) {
|
|
2407
2519
|
throw new Error(`Import directory not found: ${sourceDir}`);
|
|
2408
2520
|
}
|
|
2409
2521
|
const continueOnError = options.continueOnError ?? true;
|
|
@@ -2416,7 +2528,7 @@ function portPortableSkillDirectory(sourceDir, options = {}) {
|
|
|
2416
2528
|
const skipped = [];
|
|
2417
2529
|
const entries = readdirSync5(absoluteSource, { withFileTypes: true }).map((entry) => entry.name).filter((entryName) => !entryName.startsWith(".")).sort();
|
|
2418
2530
|
for (const entryName of entries) {
|
|
2419
|
-
const childPath =
|
|
2531
|
+
const childPath = join8(absoluteSource, entryName);
|
|
2420
2532
|
if (!safeIsDirectory(childPath))
|
|
2421
2533
|
continue;
|
|
2422
2534
|
if (!isSkillCandidate(childPath)) {
|
|
@@ -2445,11 +2557,11 @@ function portPortableSkillDirectory(sourceDir, options = {}) {
|
|
|
2445
2557
|
};
|
|
2446
2558
|
}
|
|
2447
2559
|
function isSkillCandidate(dir) {
|
|
2448
|
-
return
|
|
2560
|
+
return existsSync7(join8(dir, "SKILL.md")) || existsSync7(join8(dir, "skill.json")) || existsSync7(join8(dir, "package.json"));
|
|
2449
2561
|
}
|
|
2450
2562
|
function portPortableSkill(sourcePath, options = {}) {
|
|
2451
2563
|
const absoluteSource = normalize2(sourcePath);
|
|
2452
|
-
if (!
|
|
2564
|
+
if (!existsSync7(absoluteSource) || !statSync6(absoluteSource).isDirectory()) {
|
|
2453
2565
|
throw new Error(`Skill source directory not found: ${sourcePath}`);
|
|
2454
2566
|
}
|
|
2455
2567
|
const inferred = readPortableSkillManifest(absoluteSource, basename2(absoluteSource));
|
|
@@ -2461,8 +2573,8 @@ function portPortableSkill(sourcePath, options = {}) {
|
|
|
2461
2573
|
throw new Error(`${via} Importing it would shadow the official '${skillName}'. ` + `Pass --name to choose a different name, or --allow-shadow to override deliberately.`);
|
|
2462
2574
|
}
|
|
2463
2575
|
const root = getPortableSkillsRoot(options);
|
|
2464
|
-
const destination =
|
|
2465
|
-
if (
|
|
2576
|
+
const destination = join8(root, skillName);
|
|
2577
|
+
if (existsSync7(destination)) {
|
|
2466
2578
|
if (!options.overwrite)
|
|
2467
2579
|
throw new Error(`Skill '${skillName}' already exists at ${destination}`);
|
|
2468
2580
|
rmSync(destination, { recursive: true, force: true });
|
|
@@ -2504,10 +2616,10 @@ function buildCorpusManifest(input, name) {
|
|
|
2504
2616
|
function writeCorpusSkill(input, options = {}) {
|
|
2505
2617
|
const name = normalizePortableSkillName(input.name);
|
|
2506
2618
|
const root = getPortableSkillsRoot(options);
|
|
2507
|
-
const skillPath =
|
|
2508
|
-
const created = !
|
|
2619
|
+
const skillPath = join8(root, name);
|
|
2620
|
+
const created = !existsSync7(skillPath);
|
|
2509
2621
|
mkdirSync3(skillPath, { recursive: true });
|
|
2510
|
-
writeFileSync3(
|
|
2622
|
+
writeFileSync3(join8(skillPath, "SKILL.md"), input.skillMd);
|
|
2511
2623
|
const manifest = buildCorpusManifest(input, name);
|
|
2512
2624
|
writeSkillJsonWithHash(skillPath, manifest);
|
|
2513
2625
|
return { name, path: skillPath, manifest, created };
|
|
@@ -2515,19 +2627,19 @@ function writeCorpusSkill(input, options = {}) {
|
|
|
2515
2627
|
function installCorpusSkillAtomically(input, options = {}) {
|
|
2516
2628
|
const name = normalizePortableSkillName(input.name);
|
|
2517
2629
|
const root = getPortableSkillsRoot(options);
|
|
2518
|
-
const target =
|
|
2519
|
-
const created = !
|
|
2630
|
+
const target = join8(root, name);
|
|
2631
|
+
const created = !existsSync7(target);
|
|
2520
2632
|
mkdirSync3(root, { recursive: true });
|
|
2521
|
-
const staging = mkdtempSync(
|
|
2633
|
+
const staging = mkdtempSync(join8(root, `.pull-${name}-`));
|
|
2522
2634
|
let moved = false;
|
|
2523
2635
|
let backup = null;
|
|
2524
2636
|
try {
|
|
2525
|
-
writeFileSync3(
|
|
2637
|
+
writeFileSync3(join8(staging, "SKILL.md"), input.skillMd);
|
|
2526
2638
|
const manifest = buildCorpusManifest(input, name);
|
|
2527
2639
|
writeSkillJsonWithHash(staging, manifest);
|
|
2528
|
-
if (
|
|
2529
|
-
backup = mkdtempSync(
|
|
2530
|
-
renameSync(target,
|
|
2640
|
+
if (existsSync7(target)) {
|
|
2641
|
+
backup = mkdtempSync(join8(root, `.pull-backup-${name}-`));
|
|
2642
|
+
renameSync(target, join8(backup, name));
|
|
2531
2643
|
moved = true;
|
|
2532
2644
|
}
|
|
2533
2645
|
renameSync(staging, target);
|
|
@@ -2536,9 +2648,9 @@ function installCorpusSkillAtomically(input, options = {}) {
|
|
|
2536
2648
|
return { name, path: target, manifest, created };
|
|
2537
2649
|
} catch (error) {
|
|
2538
2650
|
rmSync(staging, { recursive: true, force: true });
|
|
2539
|
-
if (moved && backup &&
|
|
2651
|
+
if (moved && backup && existsSync7(join8(backup, name))) {
|
|
2540
2652
|
try {
|
|
2541
|
-
renameSync(
|
|
2653
|
+
renameSync(join8(backup, name), target);
|
|
2542
2654
|
} catch {}
|
|
2543
2655
|
}
|
|
2544
2656
|
throw error;
|
|
@@ -2550,10 +2662,10 @@ function validatePortableSkillDirectory(name, skillPath) {
|
|
|
2550
2662
|
const issues = [...base.issues];
|
|
2551
2663
|
const warnings = [...base.warnings];
|
|
2552
2664
|
let manifest;
|
|
2553
|
-
if (
|
|
2554
|
-
const skillJsonPath =
|
|
2555
|
-
const skillMdPath =
|
|
2556
|
-
if (!
|
|
2665
|
+
if (existsSync7(skillPath)) {
|
|
2666
|
+
const skillJsonPath = join8(skillPath, "skill.json");
|
|
2667
|
+
const skillMdPath = join8(skillPath, "SKILL.md");
|
|
2668
|
+
if (!existsSync7(skillJsonPath) && !existsSync7(skillMdPath)) {
|
|
2557
2669
|
add3(issues, "portable.manifest_missing", "Missing portable manifest: expected SKILL.md frontmatter and/or skill.json");
|
|
2558
2670
|
}
|
|
2559
2671
|
try {
|
|
@@ -2572,7 +2684,7 @@ function validatePortableSkillDirectory(name, skillPath) {
|
|
|
2572
2684
|
add3(issues, "portable.version_missing", "Portable manifest missing version");
|
|
2573
2685
|
}
|
|
2574
2686
|
const contractIssues = validatePortableManifestContract(manifest, {
|
|
2575
|
-
strict:
|
|
2687
|
+
strict: existsSync7(join8(skillPath, "skill.json")),
|
|
2576
2688
|
skillPath
|
|
2577
2689
|
});
|
|
2578
2690
|
for (const issue of contractIssues)
|
|
@@ -2608,8 +2720,8 @@ function validatePortableSkillDirectory(name, skillPath) {
|
|
|
2608
2720
|
add3(issues, "portable.command_entry_unsafe", `Command '${command.name}' entry '${command.entry}' must stay inside the skill directory`);
|
|
2609
2721
|
continue;
|
|
2610
2722
|
}
|
|
2611
|
-
const entryPath =
|
|
2612
|
-
if (!
|
|
2723
|
+
const entryPath = join8(skillPath, command.entry);
|
|
2724
|
+
if (!existsSync7(entryPath))
|
|
2613
2725
|
add3(issues, "portable.command_entry_missing", `Command '${command.name}' entry '${command.entry}' is missing`);
|
|
2614
2726
|
else if (statSync6(entryPath).isDirectory())
|
|
2615
2727
|
add3(issues, "portable.command_entry_directory", `Command '${command.name}' entry '${command.entry}' must be a file`);
|
|
@@ -2619,7 +2731,7 @@ function validatePortableSkillDirectory(name, skillPath) {
|
|
|
2619
2731
|
} catch (error) {
|
|
2620
2732
|
add3(issues, "portable.manifest_invalid", error.message);
|
|
2621
2733
|
}
|
|
2622
|
-
if (manifest?.kind !== "instruction" && !
|
|
2734
|
+
if (manifest?.kind !== "instruction" && !existsSync7(join8(skillPath, "AGENTS.md"))) {
|
|
2623
2735
|
add3(issues, "portable.agents_missing", "Missing AGENTS.md with build-out instructions for coding agents");
|
|
2624
2736
|
}
|
|
2625
2737
|
}
|
|
@@ -2655,13 +2767,13 @@ async function runPortableSkill(name, args, options = {}) {
|
|
|
2655
2767
|
if (!isSafeRelativePath2(command.entry)) {
|
|
2656
2768
|
return { exitCode: 1, error: `Portable skill '${name}' command entry is unsafe` };
|
|
2657
2769
|
}
|
|
2658
|
-
const entryPath =
|
|
2659
|
-
if (!
|
|
2770
|
+
const entryPath = join8(skill.path, command.entry);
|
|
2771
|
+
if (!existsSync7(entryPath)) {
|
|
2660
2772
|
return { exitCode: 1, error: `Entry point '${command.entry}' not found in portable skill '${name}'` };
|
|
2661
2773
|
}
|
|
2662
|
-
const pkgPath =
|
|
2663
|
-
const nodeModules =
|
|
2664
|
-
if (
|
|
2774
|
+
const pkgPath = join8(skill.path, "package.json");
|
|
2775
|
+
const nodeModules = join8(skill.path, "node_modules");
|
|
2776
|
+
if (existsSync7(pkgPath) && !existsSync7(nodeModules) && hasPackageDependencies(pkgPath)) {
|
|
2665
2777
|
const install = Bun.spawn(["bun", "install", "--no-save"], {
|
|
2666
2778
|
cwd: skill.path,
|
|
2667
2779
|
stdout: "pipe",
|
|
@@ -2930,7 +3042,7 @@ function parseSkillMdFrontmatter(content) {
|
|
|
2930
3042
|
return Object.keys(result).length > 0 ? result : null;
|
|
2931
3043
|
}
|
|
2932
3044
|
function discoverSkillsInDir(dir, source = "custom") {
|
|
2933
|
-
if (!
|
|
3045
|
+
if (!existsSync8(dir))
|
|
2934
3046
|
return [];
|
|
2935
3047
|
const result = [];
|
|
2936
3048
|
try {
|
|
@@ -2938,8 +3050,8 @@ function discoverSkillsInDir(dir, source = "custom") {
|
|
|
2938
3050
|
for (const entry of entries) {
|
|
2939
3051
|
if (!entry.isDirectory())
|
|
2940
3052
|
continue;
|
|
2941
|
-
const skillMdPath =
|
|
2942
|
-
if (!
|
|
3053
|
+
const skillMdPath = join9(dir, entry.name, "SKILL.md");
|
|
3054
|
+
if (!existsSync8(skillMdPath))
|
|
2943
3055
|
continue;
|
|
2944
3056
|
let content;
|
|
2945
3057
|
try {
|
|
@@ -2958,7 +3070,7 @@ function discoverSkillsInDir(dir, source = "custom") {
|
|
|
2958
3070
|
category: fm.category || "Development Tools",
|
|
2959
3071
|
tags: fm.tags || [],
|
|
2960
3072
|
...fm.kind ? { kind: fm.kind } : {},
|
|
2961
|
-
...isHostedMetadataSkillDir(
|
|
3073
|
+
...isHostedMetadataSkillDir(join9(dir, entry.name)) ? { serverOwned: true } : {},
|
|
2962
3074
|
source
|
|
2963
3075
|
});
|
|
2964
3076
|
}
|
|
@@ -2967,16 +3079,16 @@ function discoverSkillsInDir(dir, source = "custom") {
|
|
|
2967
3079
|
}
|
|
2968
3080
|
function findExtensionSkillPath(name) {
|
|
2969
3081
|
const config = loadConfig();
|
|
2970
|
-
if (!config.extensionsDir || !
|
|
3082
|
+
if (!config.extensionsDir || !existsSync8(config.extensionsDir))
|
|
2971
3083
|
return null;
|
|
2972
3084
|
try {
|
|
2973
3085
|
const entries = readdirSync6(config.extensionsDir, { withFileTypes: true });
|
|
2974
3086
|
for (const entry of entries) {
|
|
2975
3087
|
if (!entry.isDirectory())
|
|
2976
3088
|
continue;
|
|
2977
|
-
const skillDir =
|
|
2978
|
-
const skillMdPath =
|
|
2979
|
-
if (!
|
|
3089
|
+
const skillDir = join9(config.extensionsDir, entry.name);
|
|
3090
|
+
const skillMdPath = join9(skillDir, "SKILL.md");
|
|
3091
|
+
if (!existsSync8(skillMdPath))
|
|
2980
3092
|
continue;
|
|
2981
3093
|
let content;
|
|
2982
3094
|
try {
|
|
@@ -3008,12 +3120,12 @@ function loadRegistry(cwd) {
|
|
|
3008
3120
|
if (registryCache && registryCacheKey === rootKey && now - registryCacheTime < REGISTRY_CACHE_TTL) {
|
|
3009
3121
|
return registryCache;
|
|
3010
3122
|
}
|
|
3011
|
-
const
|
|
3123
|
+
const dataDir2 = getDataDir();
|
|
3012
3124
|
const config = loadConfig();
|
|
3013
3125
|
const official = SKILLS.map((s) => ({ ...s, source: "official" }));
|
|
3014
3126
|
const extensions = config.extensionsDir ? discoverSkillsInDir(config.extensionsDir, "extension") : [];
|
|
3015
3127
|
const portableCustom = listPortableSkillMetas();
|
|
3016
|
-
const legacyCustom = discoverSkillsInDir(
|
|
3128
|
+
const legacyCustom = discoverSkillsInDir(join9(dataDir2, "custom"));
|
|
3017
3129
|
const globalCustom = mergeCustomSkills([...legacyCustom, ...portableCustom]);
|
|
3018
3130
|
registryCache = mergeSkillRegistryLists(official, extensions, globalCustom);
|
|
3019
3131
|
registryCacheTime = now;
|
|
@@ -3060,15 +3172,15 @@ function getAllTags() {
|
|
|
3060
3172
|
return Array.from(tagSet).sort();
|
|
3061
3173
|
}
|
|
3062
3174
|
// src/lib/installer.ts
|
|
3063
|
-
import { existsSync as
|
|
3064
|
-
import { dirname as dirname5, join as
|
|
3065
|
-
import { homedir as
|
|
3175
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9, rmSync as rmSync3 } from "fs";
|
|
3176
|
+
import { dirname as dirname5, join as join12 } from "path";
|
|
3177
|
+
import { homedir as homedir4 } from "os";
|
|
3066
3178
|
import { fileURLToPath } from "url";
|
|
3067
3179
|
|
|
3068
3180
|
// src/lib/agent-sync.ts
|
|
3069
3181
|
import {
|
|
3070
3182
|
cpSync as cpSync3,
|
|
3071
|
-
existsSync as
|
|
3183
|
+
existsSync as existsSync9,
|
|
3072
3184
|
mkdirSync as mkdirSync4,
|
|
3073
3185
|
mkdtempSync as mkdtempSync2,
|
|
3074
3186
|
readFileSync as readFileSync7,
|
|
@@ -3078,8 +3190,8 @@ import {
|
|
|
3078
3190
|
statSync as statSync7,
|
|
3079
3191
|
writeFileSync as writeFileSync4
|
|
3080
3192
|
} from "fs";
|
|
3081
|
-
import { homedir as
|
|
3082
|
-
import { basename as basename3, dirname as dirname4, join as
|
|
3193
|
+
import { homedir as homedir3 } from "os";
|
|
3194
|
+
import { basename as basename3, dirname as dirname4, join as join10 } from "path";
|
|
3083
3195
|
// src/lib/home-migration.ts
|
|
3084
3196
|
function resolveCorpusRoot(options = {}) {
|
|
3085
3197
|
return getPortableSkillsRoot(options);
|
|
@@ -3101,12 +3213,12 @@ function resolveSyncAgents(arg) {
|
|
|
3101
3213
|
}
|
|
3102
3214
|
return [arg];
|
|
3103
3215
|
}
|
|
3104
|
-
function agentGlobalSkillsDir(agent, homeDir =
|
|
3216
|
+
function agentGlobalSkillsDir(agent, homeDir = homedir3()) {
|
|
3105
3217
|
switch (agent) {
|
|
3106
3218
|
case "opencode":
|
|
3107
|
-
return
|
|
3219
|
+
return join10(homeDir, ".config", "opencode", "skills");
|
|
3108
3220
|
default:
|
|
3109
|
-
return
|
|
3221
|
+
return join10(homeDir, `.${agent}`, "skills");
|
|
3110
3222
|
}
|
|
3111
3223
|
}
|
|
3112
3224
|
function adaptSkillMdForAgent(skillMd, agent) {
|
|
@@ -3165,8 +3277,8 @@ function resolveSyncCorpus(options = {}) {
|
|
|
3165
3277
|
function packageSourceRoots(source) {
|
|
3166
3278
|
const roots = [];
|
|
3167
3279
|
for (const sub of ["skills"]) {
|
|
3168
|
-
const candidate =
|
|
3169
|
-
if (
|
|
3280
|
+
const candidate = join10(source, sub);
|
|
3281
|
+
if (existsSync9(candidate) && isDirectory(candidate))
|
|
3170
3282
|
roots.push(candidate);
|
|
3171
3283
|
}
|
|
3172
3284
|
if (roots.length > 0)
|
|
@@ -3181,10 +3293,10 @@ function containsSkillDirectories(path) {
|
|
|
3181
3293
|
return false;
|
|
3182
3294
|
}
|
|
3183
3295
|
return entries.some((entry) => {
|
|
3184
|
-
const candidate =
|
|
3296
|
+
const candidate = join10(path, entry);
|
|
3185
3297
|
if (!isDirectory(candidate))
|
|
3186
3298
|
return false;
|
|
3187
|
-
return
|
|
3299
|
+
return existsSync9(join10(candidate, "SKILL.md")) || existsSync9(join10(candidate, "skill.json")) || existsSync9(join10(candidate, "package.json"));
|
|
3188
3300
|
});
|
|
3189
3301
|
}
|
|
3190
3302
|
function isDirectory(path) {
|
|
@@ -3197,7 +3309,7 @@ function isDirectory(path) {
|
|
|
3197
3309
|
function syncSkillsToAgents(options = {}) {
|
|
3198
3310
|
const requested = normalizeRequested(options.names);
|
|
3199
3311
|
const agents = options.agents?.length ? options.agents : [...SYNC_AGENTS];
|
|
3200
|
-
const homeDir = options.homeDir ??
|
|
3312
|
+
const homeDir = options.homeDir ?? homedir3();
|
|
3201
3313
|
const { roots, source } = resolveSyncCorpus(options);
|
|
3202
3314
|
const corpus = listPortableSkillsAcrossRoots(roots);
|
|
3203
3315
|
const byName = new Map(corpus.map((skill) => [skill.name, skill]));
|
|
@@ -3223,7 +3335,7 @@ function syncSkillsToAgents(options = {}) {
|
|
|
3223
3335
|
actions.push({
|
|
3224
3336
|
skill: name,
|
|
3225
3337
|
agent,
|
|
3226
|
-
path:
|
|
3338
|
+
path: join10(agentGlobalSkillsDir(agent, homeDir), name, "SKILL.md"),
|
|
3227
3339
|
action: "skip",
|
|
3228
3340
|
reason: "not found in this machine's corpus"
|
|
3229
3341
|
});
|
|
@@ -3252,8 +3364,8 @@ function syncSkillsToAgents(options = {}) {
|
|
|
3252
3364
|
return { actions };
|
|
3253
3365
|
}
|
|
3254
3366
|
function writeManagedAgentSkill(params) {
|
|
3255
|
-
const homeDir = params.homeDir ??
|
|
3256
|
-
const dir =
|
|
3367
|
+
const homeDir = params.homeDir ?? homedir3();
|
|
3368
|
+
const dir = join10(agentGlobalSkillsDir(params.agent, homeDir), params.skill);
|
|
3257
3369
|
const result = writeManagedSkillDir(dir, params.skillMd, {
|
|
3258
3370
|
skill: params.skill,
|
|
3259
3371
|
source: params.source,
|
|
@@ -3270,11 +3382,11 @@ function writeManagedAgentSkill(params) {
|
|
|
3270
3382
|
};
|
|
3271
3383
|
}
|
|
3272
3384
|
function writeManagedSkillDir(dir, skillMd, options) {
|
|
3273
|
-
const skillMdPath =
|
|
3274
|
-
const markerPath =
|
|
3275
|
-
const dirExists =
|
|
3276
|
-
const managed =
|
|
3277
|
-
const hasSkillMd =
|
|
3385
|
+
const skillMdPath = join10(dir, "SKILL.md");
|
|
3386
|
+
const markerPath = join10(dir, SYNC_MARKER_FILE);
|
|
3387
|
+
const dirExists = existsSync9(dir);
|
|
3388
|
+
const managed = existsSync9(markerPath);
|
|
3389
|
+
const hasSkillMd = existsSync9(skillMdPath);
|
|
3278
3390
|
if (dirExists && !managed && !hasSkillMd) {
|
|
3279
3391
|
return {
|
|
3280
3392
|
action: "skip",
|
|
@@ -3309,11 +3421,11 @@ function writeManagedSkillDir(dir, skillMd, options) {
|
|
|
3309
3421
|
return { action, path: skillMdPath };
|
|
3310
3422
|
const parentDir = dirname4(dir);
|
|
3311
3423
|
mkdirSync4(parentDir, { recursive: true });
|
|
3312
|
-
const transactionDir = mkdtempSync2(
|
|
3313
|
-
const candidateDir =
|
|
3314
|
-
const backupDir =
|
|
3315
|
-
const candidateSkillMdPath =
|
|
3316
|
-
const candidateMarkerPath =
|
|
3424
|
+
const transactionDir = mkdtempSync2(join10(parentDir, `.hasna-skills-write-${basename3(dir)}-`));
|
|
3425
|
+
const candidateDir = join10(transactionDir, "candidate");
|
|
3426
|
+
const backupDir = join10(transactionDir, "backup");
|
|
3427
|
+
const candidateSkillMdPath = join10(candidateDir, "SKILL.md");
|
|
3428
|
+
const candidateMarkerPath = join10(candidateDir, SYNC_MARKER_FILE);
|
|
3317
3429
|
const marker = {
|
|
3318
3430
|
managedBy: SYNC_MARKER_MANAGED_BY,
|
|
3319
3431
|
skill: options.skill,
|
|
@@ -3340,9 +3452,9 @@ function writeManagedSkillDir(dir, skillMd, options) {
|
|
|
3340
3452
|
}
|
|
3341
3453
|
renameDirectory(candidateDir, dir);
|
|
3342
3454
|
} catch (error) {
|
|
3343
|
-
if (originalMoved &&
|
|
3455
|
+
if (originalMoved && existsSync9(backupDir)) {
|
|
3344
3456
|
try {
|
|
3345
|
-
if (
|
|
3457
|
+
if (existsSync9(dir))
|
|
3346
3458
|
rmSync2(dir, { recursive: true, force: true });
|
|
3347
3459
|
renameDirectory(backupDir, dir);
|
|
3348
3460
|
originalMoved = false;
|
|
@@ -3361,17 +3473,17 @@ function writeManagedSkillDir(dir, skillMd, options) {
|
|
|
3361
3473
|
}
|
|
3362
3474
|
return { action, path: skillMdPath };
|
|
3363
3475
|
}
|
|
3364
|
-
function removeManagedAgentSkill(skill, agent, homeDir =
|
|
3365
|
-
const dir =
|
|
3366
|
-
if (!
|
|
3476
|
+
function removeManagedAgentSkill(skill, agent, homeDir = homedir3()) {
|
|
3477
|
+
const dir = join10(agentGlobalSkillsDir(agent, homeDir), skill);
|
|
3478
|
+
if (!existsSync9(join10(dir, SYNC_MARKER_FILE)))
|
|
3367
3479
|
return false;
|
|
3368
3480
|
rmSync2(dir, { recursive: true, force: true });
|
|
3369
3481
|
return true;
|
|
3370
3482
|
}
|
|
3371
3483
|
function sourceSkillMd(skillPath, name, description, kind, preferBundledDocs = false) {
|
|
3372
3484
|
if (kind === undefined || kind === "instruction" || preferBundledDocs) {
|
|
3373
|
-
const skillMdPath =
|
|
3374
|
-
if (
|
|
3485
|
+
const skillMdPath = join10(skillPath, "SKILL.md");
|
|
3486
|
+
if (existsSync9(skillMdPath))
|
|
3375
3487
|
return readFileSync7(skillMdPath, "utf-8");
|
|
3376
3488
|
}
|
|
3377
3489
|
return pointerSkillMd(name, description);
|
|
@@ -3402,8 +3514,8 @@ function normalizeSkillName(name) {
|
|
|
3402
3514
|
}
|
|
3403
3515
|
|
|
3404
3516
|
// src/lib/project-state.ts
|
|
3405
|
-
import { existsSync as
|
|
3406
|
-
import { join as
|
|
3517
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
3518
|
+
import { join as join11 } from "path";
|
|
3407
3519
|
var VALID_PIN_SOURCES = [
|
|
3408
3520
|
"official",
|
|
3409
3521
|
"custom",
|
|
@@ -3418,14 +3530,14 @@ var SKILLS_PROJECT_DIR = ".skills";
|
|
|
3418
3530
|
var PROJECT_CONFIG_FILE = "project.json";
|
|
3419
3531
|
var DEFAULT_EXPORT_DIR = ".skills/exports";
|
|
3420
3532
|
function getProjectStateDir(targetDir = process.cwd()) {
|
|
3421
|
-
return
|
|
3533
|
+
return join11(targetDir, SKILLS_PROJECT_DIR);
|
|
3422
3534
|
}
|
|
3423
3535
|
function getProjectConfigPath(targetDir = process.cwd()) {
|
|
3424
|
-
return
|
|
3536
|
+
return join11(getProjectStateDir(targetDir), PROJECT_CONFIG_FILE);
|
|
3425
3537
|
}
|
|
3426
3538
|
function loadProjectConfig(targetDir = process.cwd()) {
|
|
3427
3539
|
const path = getProjectConfigPath(targetDir);
|
|
3428
|
-
if (!
|
|
3540
|
+
if (!existsSync10(path))
|
|
3429
3541
|
return null;
|
|
3430
3542
|
try {
|
|
3431
3543
|
return normalizeProjectConfig(JSON.parse(readFileSync8(path, "utf-8")));
|
|
@@ -3546,12 +3658,12 @@ var __dirname2 = dirname5(fileURLToPath(import.meta.url));
|
|
|
3546
3658
|
function findSkillsDir() {
|
|
3547
3659
|
let dir = __dirname2;
|
|
3548
3660
|
for (let i = 0;i < 5; i++) {
|
|
3549
|
-
const candidate =
|
|
3550
|
-
if (
|
|
3661
|
+
const candidate = join12(dir, "skills");
|
|
3662
|
+
if (existsSync11(candidate) && !dir.includes(".skills"))
|
|
3551
3663
|
return candidate;
|
|
3552
3664
|
dir = dirname5(dir);
|
|
3553
3665
|
}
|
|
3554
|
-
return
|
|
3666
|
+
return join12(__dirname2, "..", "skills");
|
|
3555
3667
|
}
|
|
3556
3668
|
var SKILLS_DIR = findSkillsDir();
|
|
3557
3669
|
function getSkillPath(name) {
|
|
@@ -3559,25 +3671,25 @@ function getSkillPath(name) {
|
|
|
3559
3671
|
const portable = findPortableSkill(skillName);
|
|
3560
3672
|
if (portable)
|
|
3561
3673
|
return portable.path;
|
|
3562
|
-
const legacyCustomPath =
|
|
3563
|
-
if (
|
|
3674
|
+
const legacyCustomPath = join12(getDataDir(), "custom", skillName);
|
|
3675
|
+
if (existsSync11(legacyCustomPath))
|
|
3564
3676
|
return legacyCustomPath;
|
|
3565
3677
|
const extensionPath = findExtensionSkillPath(skillName);
|
|
3566
3678
|
if (extensionPath)
|
|
3567
3679
|
return extensionPath;
|
|
3568
|
-
return
|
|
3680
|
+
return join12(SKILLS_DIR, skillName);
|
|
3569
3681
|
}
|
|
3570
3682
|
function getCanonicalSkillName(name) {
|
|
3571
3683
|
return getSkill(name)?.name ?? resolveSkillAlias(normalizeSkillSlug(name));
|
|
3572
3684
|
}
|
|
3573
3685
|
function skillExists(name) {
|
|
3574
|
-
return
|
|
3686
|
+
return existsSync11(getSkillPath(name));
|
|
3575
3687
|
}
|
|
3576
3688
|
function installSkill(name, options = {}) {
|
|
3577
3689
|
const { targetDir = process.cwd(), overwrite = false } = options;
|
|
3578
3690
|
const canonicalName = getCanonicalSkillName(name);
|
|
3579
3691
|
const skillName = normalizeSkillName(canonicalName);
|
|
3580
|
-
if (!
|
|
3692
|
+
if (!existsSync11(getSkillPath(name))) {
|
|
3581
3693
|
const knownOfficial = Boolean(getSkill(name));
|
|
3582
3694
|
return {
|
|
3583
3695
|
skill: canonicalName,
|
|
@@ -3604,7 +3716,7 @@ function installSkill(name, options = {}) {
|
|
|
3604
3716
|
}
|
|
3605
3717
|
function installSkillSource(name, _options = {}) {
|
|
3606
3718
|
const canonicalName = getCanonicalSkillName(name);
|
|
3607
|
-
if (!
|
|
3719
|
+
if (!existsSync11(getSkillPath(name))) {
|
|
3608
3720
|
return { skill: canonicalName, success: false, error: `Skill '${name}' not found`, mode: "source" };
|
|
3609
3721
|
}
|
|
3610
3722
|
return {
|
|
@@ -3625,11 +3737,11 @@ function installSkillManifest(manifest, _options = {}) {
|
|
|
3625
3737
|
}
|
|
3626
3738
|
function createLocalSkillManifest(name, generateSkillMd) {
|
|
3627
3739
|
const sourcePath = getSkillPath(name);
|
|
3628
|
-
if (!
|
|
3740
|
+
if (!existsSync11(sourcePath))
|
|
3629
3741
|
return null;
|
|
3630
3742
|
let skillMd = "";
|
|
3631
|
-
const skillMdPath =
|
|
3632
|
-
if (
|
|
3743
|
+
const skillMdPath = join12(sourcePath, "SKILL.md");
|
|
3744
|
+
if (existsSync11(skillMdPath)) {
|
|
3633
3745
|
skillMd = readFileSync9(skillMdPath, "utf-8");
|
|
3634
3746
|
} else if (generateSkillMd) {
|
|
3635
3747
|
skillMd = generateSkillMd(name) ?? "";
|
|
@@ -3702,20 +3814,20 @@ function getAgentSkillsDir(agent, scope = "global", projectDir) {
|
|
|
3702
3814
|
const base = projectDir || process.cwd();
|
|
3703
3815
|
switch (agent) {
|
|
3704
3816
|
case "pi":
|
|
3705
|
-
return scope === "project" ?
|
|
3817
|
+
return scope === "project" ? join12(base, ".pi", "skills") : join12(homedir4(), ".pi", "agent", "skills");
|
|
3706
3818
|
case "opencode":
|
|
3707
|
-
return scope === "project" ?
|
|
3819
|
+
return scope === "project" ? join12(base, ".opencode", "skills") : join12(homedir4(), ".config", "opencode", "skills");
|
|
3708
3820
|
default:
|
|
3709
|
-
return scope === "project" ?
|
|
3821
|
+
return scope === "project" ? join12(base, `.${agent}`, "skills") : join12(homedir4(), `.${agent}`, "skills");
|
|
3710
3822
|
}
|
|
3711
3823
|
}
|
|
3712
3824
|
function getAgentSkillPath(name, agent, scope = "global", projectDir) {
|
|
3713
3825
|
const skillName = normalizeSkillName(getCanonicalSkillName(name));
|
|
3714
|
-
return
|
|
3826
|
+
return join12(getAgentSkillsDir(agent, scope, projectDir), skillName);
|
|
3715
3827
|
}
|
|
3716
3828
|
function installSkillForAgent(name, options, generateSkillMd) {
|
|
3717
3829
|
const canonicalName = getCanonicalSkillName(name);
|
|
3718
|
-
if (!
|
|
3830
|
+
if (!existsSync11(getSkillPath(name))) {
|
|
3719
3831
|
return { skill: canonicalName, success: false, error: `Skill '${name}' not found` };
|
|
3720
3832
|
}
|
|
3721
3833
|
const scope = options.scope ?? "global";
|
|
@@ -3739,15 +3851,15 @@ function removeSkillForAgent(name, options) {
|
|
|
3739
3851
|
const canonicalName = getCanonicalSkillName(name);
|
|
3740
3852
|
const scope = options.scope ?? "global";
|
|
3741
3853
|
const dir = getAgentSkillPath(canonicalName, options.agent, scope, options.projectDir);
|
|
3742
|
-
if (!
|
|
3854
|
+
if (!existsSync11(join12(dir, SYNC_MARKER_FILE)))
|
|
3743
3855
|
return false;
|
|
3744
3856
|
rmSync3(dir, { recursive: true, force: true });
|
|
3745
3857
|
return true;
|
|
3746
3858
|
}
|
|
3747
3859
|
function resolveAgentSkillMd(name, generateSkillMd) {
|
|
3748
3860
|
const sourcePath = getSkillPath(name);
|
|
3749
|
-
const skillMdPath =
|
|
3750
|
-
if (
|
|
3861
|
+
const skillMdPath = join12(sourcePath, "SKILL.md");
|
|
3862
|
+
if (existsSync11(skillMdPath))
|
|
3751
3863
|
return readFileSync9(skillMdPath, "utf-8");
|
|
3752
3864
|
if (generateSkillMd)
|
|
3753
3865
|
return generateSkillMd(name);
|
|
@@ -3766,7 +3878,7 @@ function warnMissingDependencies(name, targetDir) {
|
|
|
3766
3878
|
}
|
|
3767
3879
|
function generateMinimalSkillMd(name) {
|
|
3768
3880
|
const sourcePath = getSkillPath(name);
|
|
3769
|
-
if (!
|
|
3881
|
+
if (!existsSync11(sourcePath))
|
|
3770
3882
|
return null;
|
|
3771
3883
|
const canonicalName = getCanonicalSkillName(name);
|
|
3772
3884
|
const meta = getSkill(canonicalName);
|
|
@@ -3781,7 +3893,7 @@ function generateMinimalSkillMd(name) {
|
|
|
3781
3893
|
"---",
|
|
3782
3894
|
""
|
|
3783
3895
|
].filter(Boolean);
|
|
3784
|
-
const fallbackDoc = readFileIfExists(
|
|
3896
|
+
const fallbackDoc = readFileIfExists(join12(sourcePath, "README.md")) || readFileIfExists(join12(sourcePath, "CLAUDE.md"));
|
|
3785
3897
|
if (fallbackDoc)
|
|
3786
3898
|
return `${frontmatter.join(`
|
|
3787
3899
|
`)}${fallbackDoc.trim()}
|
|
@@ -3800,8 +3912,8 @@ skills run ${canonicalName}
|
|
|
3800
3912
|
`;
|
|
3801
3913
|
}
|
|
3802
3914
|
function readBundledSkillVersion(name) {
|
|
3803
|
-
const pkgPath =
|
|
3804
|
-
if (!
|
|
3915
|
+
const pkgPath = join12(getSkillPath(name), "package.json");
|
|
3916
|
+
if (!existsSync11(pkgPath))
|
|
3805
3917
|
return "unknown";
|
|
3806
3918
|
try {
|
|
3807
3919
|
const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
|
|
@@ -3811,27 +3923,27 @@ function readBundledSkillVersion(name) {
|
|
|
3811
3923
|
}
|
|
3812
3924
|
}
|
|
3813
3925
|
function readFileIfExists(path) {
|
|
3814
|
-
return
|
|
3926
|
+
return existsSync11(path) ? readFileSync9(path, "utf-8") : null;
|
|
3815
3927
|
}
|
|
3816
3928
|
function loadProjectConfigCompat(targetDir) {
|
|
3817
3929
|
return loadProjectConfig(targetDir);
|
|
3818
3930
|
}
|
|
3819
3931
|
// src/lib/run-state.ts
|
|
3820
3932
|
import { createHash as createHash2, randomBytes } from "crypto";
|
|
3821
|
-
import { existsSync as
|
|
3822
|
-
import { extname, join as
|
|
3933
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync10, readdirSync as readdirSync8, statSync as statSync8, writeFileSync as writeFileSync6 } from "fs";
|
|
3934
|
+
import { extname, join as join13, relative as relative2 } from "path";
|
|
3823
3935
|
function createSkillRun(params, targetDir = process.cwd()) {
|
|
3824
3936
|
const now = new Date;
|
|
3825
3937
|
const id = createRunId(now);
|
|
3826
3938
|
const day = now.toISOString().slice(0, 10);
|
|
3827
3939
|
const skillName = normalizeSkillName(params.skill);
|
|
3828
3940
|
const root = getProjectStateDir(targetDir);
|
|
3829
|
-
const runDir =
|
|
3830
|
-
const logsDir =
|
|
3831
|
-
const exportDir =
|
|
3941
|
+
const runDir = join13(root, "runs", day, id);
|
|
3942
|
+
const logsDir = join13(runDir, "logs");
|
|
3943
|
+
const exportDir = join13(root, "exports", skillName, id);
|
|
3832
3944
|
mkdirSync6(logsDir, { recursive: true });
|
|
3833
3945
|
mkdirSync6(exportDir, { recursive: true });
|
|
3834
|
-
mkdirSync6(
|
|
3946
|
+
mkdirSync6(join13(root, "tmp"), { recursive: true });
|
|
3835
3947
|
const record = {
|
|
3836
3948
|
id,
|
|
3837
3949
|
skill: skillName,
|
|
@@ -3882,27 +3994,27 @@ function updateSkillRun(context, patch) {
|
|
|
3882
3994
|
return context.record;
|
|
3883
3995
|
}
|
|
3884
3996
|
function writeRunLogs(context, stdout = "", stderr = "") {
|
|
3885
|
-
writeFileSync6(
|
|
3886
|
-
writeFileSync6(
|
|
3997
|
+
writeFileSync6(join13(context.logsDir, "stdout.log"), stdout);
|
|
3998
|
+
writeFileSync6(join13(context.logsDir, "stderr.log"), stderr);
|
|
3887
3999
|
}
|
|
3888
4000
|
function appendRunEvent(context, event, data = {}) {
|
|
3889
4001
|
const line = JSON.stringify({ ts: new Date().toISOString(), event, ...data }) + `
|
|
3890
4002
|
`;
|
|
3891
|
-
const path =
|
|
3892
|
-
const previous =
|
|
4003
|
+
const path = join13(context.runDir, "events.ndjson");
|
|
4004
|
+
const previous = existsSync12(path) ? readFileSync10(path, "utf-8") : "";
|
|
3893
4005
|
writeFileSync6(path, previous + line);
|
|
3894
4006
|
}
|
|
3895
4007
|
function listSkillRuns(targetDir = process.cwd(), limit = 50) {
|
|
3896
|
-
const runsRoot =
|
|
3897
|
-
if (!
|
|
4008
|
+
const runsRoot = join13(getProjectStateDir(targetDir), "runs");
|
|
4009
|
+
if (!existsSync12(runsRoot))
|
|
3898
4010
|
return [];
|
|
3899
4011
|
const records = [];
|
|
3900
4012
|
for (const day of readdirSync8(runsRoot).sort().reverse()) {
|
|
3901
|
-
const dayDir =
|
|
4013
|
+
const dayDir = join13(runsRoot, day);
|
|
3902
4014
|
if (!statSync8(dayDir).isDirectory())
|
|
3903
4015
|
continue;
|
|
3904
4016
|
for (const runId of readdirSync8(dayDir).sort().reverse()) {
|
|
3905
|
-
const record = readRunRecord(
|
|
4017
|
+
const record = readRunRecord(join13(dayDir, runId));
|
|
3906
4018
|
if (record)
|
|
3907
4019
|
records.push(record);
|
|
3908
4020
|
if (records.length >= limit)
|
|
@@ -3912,29 +4024,29 @@ function listSkillRuns(targetDir = process.cwd(), limit = 50) {
|
|
|
3912
4024
|
return records;
|
|
3913
4025
|
}
|
|
3914
4026
|
function findSkillRun(runId, targetDir = process.cwd()) {
|
|
3915
|
-
const runsRoot =
|
|
3916
|
-
if (!
|
|
4027
|
+
const runsRoot = join13(getProjectStateDir(targetDir), "runs");
|
|
4028
|
+
if (!existsSync12(runsRoot))
|
|
3917
4029
|
return null;
|
|
3918
4030
|
for (const day of readdirSync8(runsRoot)) {
|
|
3919
|
-
const record = readRunRecord(
|
|
4031
|
+
const record = readRunRecord(join13(runsRoot, day, runId));
|
|
3920
4032
|
if (record)
|
|
3921
4033
|
return record;
|
|
3922
4034
|
}
|
|
3923
4035
|
return null;
|
|
3924
4036
|
}
|
|
3925
4037
|
function getRunExportDir(runId, skill, targetDir = process.cwd()) {
|
|
3926
|
-
return
|
|
4038
|
+
return join13(getProjectStateDir(targetDir), "exports", normalizeSkillName(skill), runId);
|
|
3927
4039
|
}
|
|
3928
4040
|
function writeRunRecord(context) {
|
|
3929
|
-
writeFileSync6(
|
|
4041
|
+
writeFileSync6(join13(context.runDir, "run.json"), JSON.stringify(context.record, null, 2) + `
|
|
3930
4042
|
`);
|
|
3931
4043
|
}
|
|
3932
4044
|
function writeArtifactsManifest(context, artifacts) {
|
|
3933
|
-
writeFileSync6(
|
|
4045
|
+
writeFileSync6(join13(context.runDir, "artifacts.json"), JSON.stringify({ runId: context.record.id, artifacts }, null, 2) + `
|
|
3934
4046
|
`);
|
|
3935
4047
|
}
|
|
3936
4048
|
function collectRunArtifacts(context) {
|
|
3937
|
-
if (!
|
|
4049
|
+
if (!existsSync12(context.exportDir))
|
|
3938
4050
|
return [];
|
|
3939
4051
|
const artifacts = [];
|
|
3940
4052
|
for (const path of walkFiles(context.exportDir)) {
|
|
@@ -3950,8 +4062,8 @@ function collectRunArtifacts(context) {
|
|
|
3950
4062
|
return artifacts.sort((a, b) => a.path.localeCompare(b.path));
|
|
3951
4063
|
}
|
|
3952
4064
|
function readRunRecord(runDir) {
|
|
3953
|
-
const path =
|
|
3954
|
-
if (!
|
|
4065
|
+
const path = join13(runDir, "run.json");
|
|
4066
|
+
if (!existsSync12(path))
|
|
3955
4067
|
return null;
|
|
3956
4068
|
try {
|
|
3957
4069
|
return JSON.parse(readFileSync10(path, "utf-8"));
|
|
@@ -3962,7 +4074,7 @@ function readRunRecord(runDir) {
|
|
|
3962
4074
|
function walkFiles(dir) {
|
|
3963
4075
|
const files = [];
|
|
3964
4076
|
for (const entry of readdirSync8(dir)) {
|
|
3965
|
-
const full =
|
|
4077
|
+
const full = join13(dir, entry);
|
|
3966
4078
|
if (statSync8(full).isDirectory())
|
|
3967
4079
|
files.push(...walkFiles(full));
|
|
3968
4080
|
else
|
|
@@ -4008,13 +4120,13 @@ function mimeForPath(path) {
|
|
|
4008
4120
|
}
|
|
4009
4121
|
}
|
|
4010
4122
|
// src/lib/skillinfo.ts
|
|
4011
|
-
import { existsSync as
|
|
4012
|
-
import { join as
|
|
4123
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
|
|
4124
|
+
import { join as join14 } from "path";
|
|
4013
4125
|
function isInstructionSkillDir(skillPath, meta) {
|
|
4014
4126
|
if (meta?.kind === "instruction")
|
|
4015
4127
|
return true;
|
|
4016
|
-
const skillMdPath =
|
|
4017
|
-
if (!
|
|
4128
|
+
const skillMdPath = join14(skillPath, "SKILL.md");
|
|
4129
|
+
if (!existsSync13(skillMdPath))
|
|
4018
4130
|
return false;
|
|
4019
4131
|
try {
|
|
4020
4132
|
return parseSkillFrontmatter(readFileSync11(skillMdPath, "utf-8"))?.kind === "instruction";
|
|
@@ -4040,12 +4152,12 @@ var HOSTED_PROVIDER_ENV_PREFIXES = [
|
|
|
4040
4152
|
];
|
|
4041
4153
|
function getSkillDocs(name) {
|
|
4042
4154
|
const skillPath = getSkillPath(name);
|
|
4043
|
-
if (!
|
|
4155
|
+
if (!existsSync13(skillPath))
|
|
4044
4156
|
return null;
|
|
4045
4157
|
return {
|
|
4046
|
-
skillMd: readIfExists(
|
|
4047
|
-
readme: readIfExists(
|
|
4048
|
-
claudeMd: readIfExists(
|
|
4158
|
+
skillMd: readIfExists(join14(skillPath, "SKILL.md")),
|
|
4159
|
+
readme: readIfExists(join14(skillPath, "README.md")),
|
|
4160
|
+
claudeMd: readIfExists(join14(skillPath, "CLAUDE.md"))
|
|
4049
4161
|
};
|
|
4050
4162
|
}
|
|
4051
4163
|
function getSkillBestDoc(name) {
|
|
@@ -4056,11 +4168,11 @@ function getSkillBestDoc(name) {
|
|
|
4056
4168
|
}
|
|
4057
4169
|
function getSkillRequirements(name) {
|
|
4058
4170
|
const skillPath = getSkillPath(name);
|
|
4059
|
-
if (!
|
|
4171
|
+
if (!existsSync13(skillPath))
|
|
4060
4172
|
return null;
|
|
4061
4173
|
const texts = [];
|
|
4062
4174
|
for (const file of ["SKILL.md", "README.md", "CLAUDE.md", ".env.example", ".env.local.example"]) {
|
|
4063
|
-
const content = readIfExists(
|
|
4175
|
+
const content = readIfExists(join14(skillPath, file));
|
|
4064
4176
|
if (content)
|
|
4065
4177
|
texts.push(content);
|
|
4066
4178
|
}
|
|
@@ -4099,8 +4211,8 @@ function getSkillRequirements(name) {
|
|
|
4099
4211
|
const skillName = normalizeSkillName(name);
|
|
4100
4212
|
let cliCommand = `skills run ${skillName}`;
|
|
4101
4213
|
let dependencies = {};
|
|
4102
|
-
const pkgPath =
|
|
4103
|
-
if (
|
|
4214
|
+
const pkgPath = join14(skillPath, "package.json");
|
|
4215
|
+
if (existsSync13(pkgPath)) {
|
|
4104
4216
|
try {
|
|
4105
4217
|
const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
|
|
4106
4218
|
dependencies = pkg.dependencies || {};
|
|
@@ -4120,7 +4232,7 @@ async function runSkill(name, args, options = {}) {
|
|
|
4120
4232
|
const meta = getSkill(name);
|
|
4121
4233
|
const canonicalName = meta?.name ?? name;
|
|
4122
4234
|
const skillPath = getSkillPath(canonicalName);
|
|
4123
|
-
if (!
|
|
4235
|
+
if (!existsSync13(skillPath)) {
|
|
4124
4236
|
return { exitCode: 1, error: `Skill '${name}' not found` };
|
|
4125
4237
|
}
|
|
4126
4238
|
if (isInstructionSkillDir(skillPath, meta)) {
|
|
@@ -4129,8 +4241,8 @@ async function runSkill(name, args, options = {}) {
|
|
|
4129
4241
|
error: `Skill '${name}' is an instruction skill (kind: instruction) and is not runnable. Instruction skills are consumed by coding agents via SKILL.md, not executed with 'skills run'.`
|
|
4130
4242
|
};
|
|
4131
4243
|
}
|
|
4132
|
-
const pkgPath =
|
|
4133
|
-
if (!
|
|
4244
|
+
const pkgPath = join14(skillPath, "package.json");
|
|
4245
|
+
if (!existsSync13(pkgPath)) {
|
|
4134
4246
|
return { exitCode: 1, error: `No package.json in skill '${name}'` };
|
|
4135
4247
|
}
|
|
4136
4248
|
let entryPoint;
|
|
@@ -4149,12 +4261,12 @@ async function runSkill(name, args, options = {}) {
|
|
|
4149
4261
|
} catch {
|
|
4150
4262
|
return { exitCode: 1, error: `Failed to parse package.json for skill '${name}'` };
|
|
4151
4263
|
}
|
|
4152
|
-
const entryPath =
|
|
4153
|
-
if (!
|
|
4264
|
+
const entryPath = join14(skillPath, entryPoint);
|
|
4265
|
+
if (!existsSync13(entryPath)) {
|
|
4154
4266
|
return { exitCode: 1, error: `Entry point '${entryPoint}' not found in skill '${name}'` };
|
|
4155
4267
|
}
|
|
4156
|
-
const nodeModules =
|
|
4157
|
-
if (!
|
|
4268
|
+
const nodeModules = join14(skillPath, "node_modules");
|
|
4269
|
+
if (!existsSync13(nodeModules)) {
|
|
4158
4270
|
const install = Bun.spawn(["bun", "install", "--no-save"], {
|
|
4159
4271
|
cwd: skillPath,
|
|
4160
4272
|
stdout: "pipe",
|
|
@@ -4226,7 +4338,7 @@ function generateSkillMd(name) {
|
|
|
4226
4338
|
if (!meta)
|
|
4227
4339
|
return null;
|
|
4228
4340
|
const skillPath = getSkillPath(name);
|
|
4229
|
-
if (!
|
|
4341
|
+
if (!existsSync13(skillPath))
|
|
4230
4342
|
return null;
|
|
4231
4343
|
const frontmatter = [
|
|
4232
4344
|
"---",
|
|
@@ -4235,11 +4347,11 @@ function generateSkillMd(name) {
|
|
|
4235
4347
|
"---"
|
|
4236
4348
|
].join(`
|
|
4237
4349
|
`);
|
|
4238
|
-
const readme = readIfExists(
|
|
4239
|
-
const claudeMd = readIfExists(
|
|
4350
|
+
const readme = readIfExists(join14(skillPath, "README.md"));
|
|
4351
|
+
const claudeMd = readIfExists(join14(skillPath, "CLAUDE.md"));
|
|
4240
4352
|
let cliCommand = null;
|
|
4241
|
-
const pkgPath =
|
|
4242
|
-
if (
|
|
4353
|
+
const pkgPath = join14(skillPath, "package.json");
|
|
4354
|
+
if (existsSync13(pkgPath)) {
|
|
4243
4355
|
try {
|
|
4244
4356
|
const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
|
|
4245
4357
|
if (pkg.bin) {
|
|
@@ -4316,7 +4428,7 @@ function extractEnvVars(text) {
|
|
|
4316
4428
|
}
|
|
4317
4429
|
function readIfExists(path) {
|
|
4318
4430
|
try {
|
|
4319
|
-
if (
|
|
4431
|
+
if (existsSync13(path)) {
|
|
4320
4432
|
return readFileSync11(path, "utf-8");
|
|
4321
4433
|
}
|
|
4322
4434
|
} catch {}
|
|
@@ -8320,21 +8432,21 @@ function requireApiUrl(action = "This command", config, env) {
|
|
|
8320
8432
|
}
|
|
8321
8433
|
|
|
8322
8434
|
// src/lib/auth-store.ts
|
|
8323
|
-
import { existsSync as
|
|
8324
|
-
import { dirname as dirname6, join as
|
|
8325
|
-
import { homedir as
|
|
8435
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync7, unlinkSync } from "fs";
|
|
8436
|
+
import { dirname as dirname6, join as join15 } from "path";
|
|
8437
|
+
import { homedir as homedir5 } from "os";
|
|
8326
8438
|
function getAuthFilePath() {
|
|
8327
|
-
return
|
|
8439
|
+
return join15(getDataDir(), "auth.json");
|
|
8328
8440
|
}
|
|
8329
8441
|
function legacyAuthFilePath() {
|
|
8330
|
-
return
|
|
8442
|
+
return join15(process.env["HOME"] || process.env["USERPROFILE"] || homedir5(), ".skills", "auth.json");
|
|
8331
8443
|
}
|
|
8332
8444
|
var cachedConfig;
|
|
8333
8445
|
function getAuthConfig() {
|
|
8334
8446
|
if (cachedConfig !== undefined)
|
|
8335
8447
|
return cachedConfig;
|
|
8336
8448
|
try {
|
|
8337
|
-
const file =
|
|
8449
|
+
const file = existsSync14(getAuthFilePath()) ? getAuthFilePath() : legacyAuthFilePath();
|
|
8338
8450
|
const raw = readFileSync12(file, "utf-8");
|
|
8339
8451
|
const config = JSON.parse(raw);
|
|
8340
8452
|
if (!config.apiKey) {
|
|
@@ -9435,14 +9547,14 @@ function createRemoteSkillsClient() {
|
|
|
9435
9547
|
return new RemoteSkillsClient(apiKey);
|
|
9436
9548
|
}
|
|
9437
9549
|
// src/lib/scheduler.ts
|
|
9438
|
-
import { existsSync as
|
|
9439
|
-
import { join as
|
|
9550
|
+
import { existsSync as existsSync15, readFileSync as readFileSync13, writeFileSync as writeFileSync8, mkdirSync as mkdirSync8 } from "fs";
|
|
9551
|
+
import { join as join16 } from "path";
|
|
9440
9552
|
function getSchedulesPath(targetDir = process.cwd()) {
|
|
9441
|
-
return
|
|
9553
|
+
return join16(targetDir, ".skills", "schedules.json");
|
|
9442
9554
|
}
|
|
9443
9555
|
function loadSchedules(targetDir = process.cwd()) {
|
|
9444
9556
|
const path = getSchedulesPath(targetDir);
|
|
9445
|
-
if (
|
|
9557
|
+
if (existsSync15(path)) {
|
|
9446
9558
|
try {
|
|
9447
9559
|
return JSON.parse(readFileSync13(path, "utf-8"));
|
|
9448
9560
|
} catch {}
|
|
@@ -9451,8 +9563,8 @@ function loadSchedules(targetDir = process.cwd()) {
|
|
|
9451
9563
|
}
|
|
9452
9564
|
function saveSchedules(data, targetDir = process.cwd()) {
|
|
9453
9565
|
const path = getSchedulesPath(targetDir);
|
|
9454
|
-
const dir =
|
|
9455
|
-
if (!
|
|
9566
|
+
const dir = join16(targetDir, ".skills");
|
|
9567
|
+
if (!existsSync15(dir))
|
|
9456
9568
|
mkdirSync8(dir, { recursive: true });
|
|
9457
9569
|
writeFileSync8(path, JSON.stringify(data, null, 2));
|
|
9458
9570
|
}
|
|
@@ -9639,8 +9751,8 @@ function recordScheduleRun(id, status, targetDir) {
|
|
|
9639
9751
|
saveSchedules(data, targetDir);
|
|
9640
9752
|
}
|
|
9641
9753
|
// src/lib/pull.ts
|
|
9642
|
-
import { existsSync as
|
|
9643
|
-
import { dirname as dirname7, join as
|
|
9754
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync3, readFileSync as readFileSync14, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "fs";
|
|
9755
|
+
import { dirname as dirname7, join as join17 } from "path";
|
|
9644
9756
|
|
|
9645
9757
|
// src/lib/revision.ts
|
|
9646
9758
|
import { createHash as createHash3 } from "crypto";
|
|
@@ -9931,7 +10043,7 @@ async function pullOne(client, rawName, corpusOptions, verify) {
|
|
|
9931
10043
|
return reconcileTombstone(slug, corpusOptions);
|
|
9932
10044
|
}
|
|
9933
10045
|
if (bundleResponse.status === 404) {
|
|
9934
|
-
const marker = readPullMarker(
|
|
10046
|
+
const marker = readPullMarker(join17(getPortableSkillsRoot(corpusOptions), slug));
|
|
9935
10047
|
if (marker && typeof marker.revisionId === "string" && marker.revisionId) {
|
|
9936
10048
|
return { name: slug, success: true, purged: true, removed: false };
|
|
9937
10049
|
}
|
|
@@ -9962,7 +10074,7 @@ async function pullOne(client, rawName, corpusOptions, verify) {
|
|
|
9962
10074
|
return { name: slug, success: false, error: `Skill '${slug}' was not found on the configured Skills instance.` };
|
|
9963
10075
|
}
|
|
9964
10076
|
if (!meta?.revisionId) {
|
|
9965
|
-
const marker = readPullMarker(
|
|
10077
|
+
const marker = readPullMarker(join17(getPortableSkillsRoot(corpusOptions), slug));
|
|
9966
10078
|
if (marker && typeof marker.revisionId === "string" && marker.revisionId) {
|
|
9967
10079
|
return { name: slug, success: true, purged: true, removed: false };
|
|
9968
10080
|
}
|
|
@@ -10019,8 +10131,8 @@ function provenRevision(meta, slug, bundle) {
|
|
|
10019
10131
|
return declared;
|
|
10020
10132
|
}
|
|
10021
10133
|
function reconcileTombstone(slug, corpusOptions) {
|
|
10022
|
-
const target =
|
|
10023
|
-
if (!
|
|
10134
|
+
const target = join17(getPortableSkillsRoot(corpusOptions), slug);
|
|
10135
|
+
if (!existsSync16(join17(target, PULL_MARKER_FILE))) {
|
|
10024
10136
|
return { name: slug, success: true, tombstoned: true, removed: false, leftInPlace: true };
|
|
10025
10137
|
}
|
|
10026
10138
|
rmSync4(target, { recursive: true, force: true });
|
|
@@ -10028,7 +10140,7 @@ function reconcileTombstone(slug, corpusOptions) {
|
|
|
10028
10140
|
}
|
|
10029
10141
|
function readPullMarker(dir) {
|
|
10030
10142
|
try {
|
|
10031
|
-
return JSON.parse(readFileSync14(
|
|
10143
|
+
return JSON.parse(readFileSync14(join17(dir, PULL_MARKER_FILE), "utf-8"));
|
|
10032
10144
|
} catch {
|
|
10033
10145
|
return null;
|
|
10034
10146
|
}
|
|
@@ -10149,14 +10261,14 @@ function verifyBundleResponseBytes(buffer, response, verify = {}) {
|
|
|
10149
10261
|
function installBundleAtomically(name, entries, options = {}, marker = {}) {
|
|
10150
10262
|
const root = getPortableSkillsRoot(options);
|
|
10151
10263
|
mkdirSync9(root, { recursive: true });
|
|
10152
|
-
const target =
|
|
10153
|
-
const created = !
|
|
10154
|
-
const staging = mkdtempSync3(
|
|
10264
|
+
const target = join17(root, name);
|
|
10265
|
+
const created = !existsSync16(target);
|
|
10266
|
+
const staging = mkdtempSync3(join17(root, `.pull-${name}-`));
|
|
10155
10267
|
let moved = false;
|
|
10156
10268
|
let backup = null;
|
|
10157
10269
|
try {
|
|
10158
10270
|
for (const entry of entries) {
|
|
10159
|
-
const destination =
|
|
10271
|
+
const destination = join17(staging, entry.path);
|
|
10160
10272
|
mkdirSync9(dirname7(destination), { recursive: true });
|
|
10161
10273
|
writeFileSync9(destination, entry.bytes, { mode: entry.mode });
|
|
10162
10274
|
}
|
|
@@ -10168,9 +10280,9 @@ function installBundleAtomically(name, entries, options = {}, marker = {}) {
|
|
|
10168
10280
|
...marker.signature ? { signature: marker.signature } : {},
|
|
10169
10281
|
...marker.revisionId ? { revisionId: marker.revisionId } : {}
|
|
10170
10282
|
});
|
|
10171
|
-
if (
|
|
10172
|
-
backup = mkdtempSync3(
|
|
10173
|
-
renameSync3(target,
|
|
10283
|
+
if (existsSync16(target)) {
|
|
10284
|
+
backup = mkdtempSync3(join17(root, `.pull-backup-${name}-`));
|
|
10285
|
+
renameSync3(target, join17(backup, name));
|
|
10174
10286
|
moved = true;
|
|
10175
10287
|
}
|
|
10176
10288
|
renameSync3(staging, target);
|
|
@@ -10178,9 +10290,9 @@ function installBundleAtomically(name, entries, options = {}, marker = {}) {
|
|
|
10178
10290
|
rmSync4(backup, { recursive: true, force: true });
|
|
10179
10291
|
} catch (error) {
|
|
10180
10292
|
rmSync4(staging, { recursive: true, force: true });
|
|
10181
|
-
if (moved && backup &&
|
|
10293
|
+
if (moved && backup && existsSync16(join17(backup, name))) {
|
|
10182
10294
|
try {
|
|
10183
|
-
renameSync3(
|
|
10295
|
+
renameSync3(join17(backup, name), target);
|
|
10184
10296
|
} catch {}
|
|
10185
10297
|
}
|
|
10186
10298
|
throw error;
|
|
@@ -10199,7 +10311,7 @@ function writePullMarker(dir, record) {
|
|
|
10199
10311
|
...record.revisionId ? { revisionId: record.revisionId } : {},
|
|
10200
10312
|
syncedAt: new Date().toISOString()
|
|
10201
10313
|
};
|
|
10202
|
-
writeFileSync9(
|
|
10314
|
+
writeFileSync9(join17(dir, PULL_MARKER_FILE), `${JSON.stringify(marker, null, 2)}
|
|
10203
10315
|
`);
|
|
10204
10316
|
}
|
|
10205
10317
|
async function safeMeta(client, slug) {
|
|
@@ -10350,7 +10462,7 @@ import { dirname as dirname8, relative as relative3 } from "path";
|
|
|
10350
10462
|
// package.json
|
|
10351
10463
|
var package_default = {
|
|
10352
10464
|
name: "@hasna/skills",
|
|
10353
|
-
version: "0.1.
|
|
10465
|
+
version: "0.1.72",
|
|
10354
10466
|
description: "Skills library for AI coding agents",
|
|
10355
10467
|
type: "module",
|
|
10356
10468
|
bin: {
|
|
@@ -10442,6 +10554,7 @@ var package_default = {
|
|
|
10442
10554
|
"@aws-sdk/client-ecs": "^3.1079.0",
|
|
10443
10555
|
"@aws-sdk/client-s3": "^3.1079.0",
|
|
10444
10556
|
"@hasna/events": "0.1.16",
|
|
10557
|
+
"@hasna/paths": "0.1.0",
|
|
10445
10558
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
10446
10559
|
chalk: "^5.3.0",
|
|
10447
10560
|
commander: "^12.1.0",
|
|
@@ -11366,16 +11479,16 @@ function clone2(value) {
|
|
|
11366
11479
|
return JSON.parse(JSON.stringify(value));
|
|
11367
11480
|
}
|
|
11368
11481
|
// src/lib/feedback.ts
|
|
11369
|
-
import { existsSync as
|
|
11370
|
-
import { dirname as dirname9, join as
|
|
11482
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync11 } from "fs";
|
|
11483
|
+
import { dirname as dirname9, join as join18 } from "path";
|
|
11371
11484
|
import { Database } from "bun:sqlite";
|
|
11372
11485
|
function getFeedbackDbPath() {
|
|
11373
|
-
return
|
|
11486
|
+
return join18(getDataDir(), "skills.db");
|
|
11374
11487
|
}
|
|
11375
11488
|
function getFeedbackDb() {
|
|
11376
11489
|
const dbPath = getFeedbackDbPath();
|
|
11377
11490
|
const dir = dirname9(dbPath);
|
|
11378
|
-
if (!
|
|
11491
|
+
if (!existsSync17(dir))
|
|
11379
11492
|
mkdirSync11(dir, { recursive: true });
|
|
11380
11493
|
const db = new Database(dbPath);
|
|
11381
11494
|
db.exec("PRAGMA journal_mode = WAL");
|
|
@@ -11412,14 +11525,14 @@ function saveFeedback(input) {
|
|
|
11412
11525
|
// src/lib/native-storage.ts
|
|
11413
11526
|
import { createHash as createHash5, createHmac as createHmac2 } from "crypto";
|
|
11414
11527
|
import {
|
|
11415
|
-
existsSync as
|
|
11528
|
+
existsSync as existsSync18,
|
|
11416
11529
|
mkdirSync as mkdirSync12,
|
|
11417
11530
|
readFileSync as readFileSync15,
|
|
11418
11531
|
readdirSync as readdirSync9,
|
|
11419
11532
|
statSync as statSync9,
|
|
11420
11533
|
writeFileSync as writeFileSync11
|
|
11421
11534
|
} from "fs";
|
|
11422
|
-
import { dirname as dirname10, join as
|
|
11535
|
+
import { dirname as dirname10, join as join19, normalize as normalize3, relative as relative4, sep as sep2 } from "path";
|
|
11423
11536
|
var SKILLS_STORAGE_TABLES = [
|
|
11424
11537
|
"skills_sync_records",
|
|
11425
11538
|
"skills_sync_cursors"
|
|
@@ -11556,7 +11669,7 @@ function getSkillsNativeStorageStatus(options = {}) {
|
|
|
11556
11669
|
local: {
|
|
11557
11670
|
dataDir: getDataDir(),
|
|
11558
11671
|
projectStateDir: getProjectStateDir(targetDir),
|
|
11559
|
-
feedbackDbPath:
|
|
11672
|
+
feedbackDbPath: join19(getDataDir(), "skills.db")
|
|
11560
11673
|
},
|
|
11561
11674
|
remote: {
|
|
11562
11675
|
databaseConfigured: Boolean(config.databaseUrl),
|
|
@@ -11579,7 +11692,7 @@ function getStorageStatus(options = {}) {
|
|
|
11579
11692
|
function exportSkillsLocalSnapshot(targetDir = process.cwd(), options = {}) {
|
|
11580
11693
|
const projectStateDir = getProjectStateDir(targetDir);
|
|
11581
11694
|
const files = [];
|
|
11582
|
-
if (
|
|
11695
|
+
if (existsSync18(projectStateDir)) {
|
|
11583
11696
|
for (const filePath of walkFiles2(projectStateDir)) {
|
|
11584
11697
|
const bytes = readFileSync15(filePath);
|
|
11585
11698
|
const relativePath = toPosix(relative4(targetDir, filePath));
|
|
@@ -11606,7 +11719,7 @@ function importSkillsLocalSnapshot(snapshot, targetDir = process.cwd(), options
|
|
|
11606
11719
|
continue;
|
|
11607
11720
|
}
|
|
11608
11721
|
const absolutePath = resolveSnapshotPath(targetDir, file.path);
|
|
11609
|
-
if (
|
|
11722
|
+
if (existsSync18(absolutePath) && !options.overwrite) {
|
|
11610
11723
|
skipped += 1;
|
|
11611
11724
|
continue;
|
|
11612
11725
|
}
|
|
@@ -11913,7 +12026,7 @@ function parsePositiveInteger(value) {
|
|
|
11913
12026
|
function walkFiles2(dir) {
|
|
11914
12027
|
const files = [];
|
|
11915
12028
|
for (const entry of readdirSync9(dir)) {
|
|
11916
|
-
const full =
|
|
12029
|
+
const full = join19(dir, entry);
|
|
11917
12030
|
const stats = statSync9(full);
|
|
11918
12031
|
if (stats.isDirectory())
|
|
11919
12032
|
files.push(...walkFiles2(full));
|
|
@@ -11930,7 +12043,7 @@ function resolveSnapshotPath(targetDir, snapshotPath) {
|
|
|
11930
12043
|
if (!toPosix(normalizedPath).startsWith(".skills/")) {
|
|
11931
12044
|
throw new Error(`Snapshot path must stay inside .skills: ${snapshotPath}`);
|
|
11932
12045
|
}
|
|
11933
|
-
return
|
|
12046
|
+
return join19(targetDir, normalizedPath);
|
|
11934
12047
|
}
|
|
11935
12048
|
function normalizeS3Prefix(prefix) {
|
|
11936
12049
|
return (prefix ?? "").trim().replace(/^\/+|\/+$/g, "");
|
|
@@ -11990,14 +12103,642 @@ function toArrayBuffer(bytes) {
|
|
|
11990
12103
|
new Uint8Array(buffer).set(bytes);
|
|
11991
12104
|
return buffer;
|
|
11992
12105
|
}
|
|
12106
|
+
// src/lib/station-snapshot.ts
|
|
12107
|
+
import { createHash as createHash6 } from "crypto";
|
|
12108
|
+
import {
|
|
12109
|
+
copyFileSync as copyFileSync2,
|
|
12110
|
+
mkdirSync as mkdirSync13,
|
|
12111
|
+
readFileSync as readFileSync16,
|
|
12112
|
+
statSync as statSync11,
|
|
12113
|
+
writeFileSync as writeFileSync12
|
|
12114
|
+
} from "fs";
|
|
12115
|
+
import { dirname as dirname11, isAbsolute as isAbsolute3, relative as relative5, resolve as resolve2, sep as sep4 } from "path";
|
|
12116
|
+
|
|
12117
|
+
// src/lib/portable-snapshot-filter.ts
|
|
12118
|
+
import { readdirSync as readdirSync10, statSync as statSync10 } from "fs";
|
|
12119
|
+
import { homedir as homedir6 } from "os";
|
|
12120
|
+
import { join as join20, sep as sep3 } from "path";
|
|
12121
|
+
var SYNC_HOMES = [
|
|
12122
|
+
{ name: "skills", subClass: "skills", agent: null },
|
|
12123
|
+
{ name: "custom", subClass: "custom", agent: null },
|
|
12124
|
+
{ name: "claude", subClass: "agent-homes", agent: "claude" },
|
|
12125
|
+
{ name: "codewith", subClass: "agent-homes", agent: "codewith" },
|
|
12126
|
+
{ name: "codex", subClass: "agent-homes", agent: "codex" },
|
|
12127
|
+
{ name: "opencode", subClass: "agent-homes", agent: "opencode" },
|
|
12128
|
+
{ name: "cursor", subClass: "agent-homes", agent: "cursor" }
|
|
12129
|
+
];
|
|
12130
|
+
var EXCLUDE_DIR_NAMES = new Set([
|
|
12131
|
+
".git",
|
|
12132
|
+
"node_modules",
|
|
12133
|
+
"__pycache__",
|
|
12134
|
+
".cache",
|
|
12135
|
+
".pytest_cache",
|
|
12136
|
+
".mypy_cache",
|
|
12137
|
+
".ruff_cache"
|
|
12138
|
+
]);
|
|
12139
|
+
var EXCLUDE_DIR_PATTERNS = [
|
|
12140
|
+
/^\.merge-pr\.rollback-/
|
|
12141
|
+
];
|
|
12142
|
+
var EXCLUDE_FILE_NAMES = new Set([
|
|
12143
|
+
".DS_Store",
|
|
12144
|
+
"package-lock.json",
|
|
12145
|
+
"pnpm-lock.yaml",
|
|
12146
|
+
"yarn.lock",
|
|
12147
|
+
"Cargo.lock"
|
|
12148
|
+
]);
|
|
12149
|
+
var EXCLUDE_FILE_PATTERNS = [
|
|
12150
|
+
/^\._/,
|
|
12151
|
+
/\.bak$/,
|
|
12152
|
+
/\.orig$/,
|
|
12153
|
+
/\.rej$/,
|
|
12154
|
+
/~$/,
|
|
12155
|
+
/\.pyc$/,
|
|
12156
|
+
/\.pyo$/,
|
|
12157
|
+
/\.log$/,
|
|
12158
|
+
/\.db$/,
|
|
12159
|
+
/\.sqlite(\d)?$/,
|
|
12160
|
+
/^bun\.lock/,
|
|
12161
|
+
/\.env($|\.)/,
|
|
12162
|
+
/\.pem$/,
|
|
12163
|
+
/\.key$/,
|
|
12164
|
+
/\.p12$/,
|
|
12165
|
+
/\.pfx$/,
|
|
12166
|
+
/\.jks$/,
|
|
12167
|
+
/^id_rsa/,
|
|
12168
|
+
/^id_ed25519/,
|
|
12169
|
+
/^credentials/
|
|
12170
|
+
];
|
|
12171
|
+
var PORTABLE_TOP_LEVEL = new Set(["SKILL.md", "skill.json"]);
|
|
12172
|
+
var PORTABLE_SUBDIRS = new Set(["scripts", "assets", "references"]);
|
|
12173
|
+
var REFUSED_SCANNER_FLAGGED = new Set([
|
|
12174
|
+
"aws-cross-account-app-migration/SKILL.md",
|
|
12175
|
+
"aws-cross-account-app-migration/scripts/selftest.sh",
|
|
12176
|
+
"gateway-serve/SKILL.md",
|
|
12177
|
+
"infinity-drain/SKILL.md",
|
|
12178
|
+
"infinity-run/SKILL.md",
|
|
12179
|
+
"oss-saas-code-cleanup/SKILL.md",
|
|
12180
|
+
"repo-project-familiarization/scripts/repo_shape.py",
|
|
12181
|
+
"repo-project-familiarization/scripts/session_history.py",
|
|
12182
|
+
"scale-check/SKILL.md",
|
|
12183
|
+
"standard-align-repo/SKILL.md",
|
|
12184
|
+
"standard-build-iapp/SKILL.md",
|
|
12185
|
+
"standard-build-oss/SKILL.md",
|
|
12186
|
+
"skill-image/SKILL.md",
|
|
12187
|
+
"skill-scale-check/SKILL.md",
|
|
12188
|
+
"sqlite-to-rds-parity-migrate/scripts/parity-migrate.ts",
|
|
12189
|
+
"pdf-operations/scripts/pdf_ops.py"
|
|
12190
|
+
]);
|
|
12191
|
+
function isExcludedSkillFileName(fileName) {
|
|
12192
|
+
if (EXCLUDE_FILE_NAMES.has(fileName)) {
|
|
12193
|
+
return true;
|
|
12194
|
+
}
|
|
12195
|
+
return EXCLUDE_FILE_PATTERNS.some((pattern) => pattern.test(fileName));
|
|
12196
|
+
}
|
|
12197
|
+
function isPortableWithinSkill(relativeParts) {
|
|
12198
|
+
if (relativeParts.length < 2) {
|
|
12199
|
+
return false;
|
|
12200
|
+
}
|
|
12201
|
+
const [, second] = relativeParts;
|
|
12202
|
+
if (PORTABLE_TOP_LEVEL.has(second)) {
|
|
12203
|
+
return relativeParts.length === 2;
|
|
12204
|
+
}
|
|
12205
|
+
if (relativeParts.length < 3) {
|
|
12206
|
+
return false;
|
|
12207
|
+
}
|
|
12208
|
+
return PORTABLE_SUBDIRS.has(second);
|
|
12209
|
+
}
|
|
12210
|
+
function homePathFor(definition, homesRoot) {
|
|
12211
|
+
const home = homesRoot ?? homedir6();
|
|
12212
|
+
if (definition.subClass === "skills" || definition.subClass === "custom") {
|
|
12213
|
+
return join20(skillsDataRootForHome(home), definition.name);
|
|
12214
|
+
}
|
|
12215
|
+
if (definition.agent === "opencode") {
|
|
12216
|
+
return join20(home, ".config", "opencode", "skills");
|
|
12217
|
+
}
|
|
12218
|
+
return join20(home, `.${definition.agent}`, "skills");
|
|
12219
|
+
}
|
|
12220
|
+
function destinationFor(definition, stationId, relativePath) {
|
|
12221
|
+
const category = definition.subClass === "agent-homes" ? join20("agent-homes", definition.agent ?? "") : definition.name;
|
|
12222
|
+
return join20("resources", stationId, "skills", category, ...relativePath.split(sep3));
|
|
12223
|
+
}
|
|
12224
|
+
function walkEntries(absoluteRoot) {
|
|
12225
|
+
let entries;
|
|
12226
|
+
try {
|
|
12227
|
+
entries = readdirSync10(absoluteRoot, { withFileTypes: true });
|
|
12228
|
+
} catch {
|
|
12229
|
+
return [];
|
|
12230
|
+
}
|
|
12231
|
+
const output = [];
|
|
12232
|
+
for (const entry of entries) {
|
|
12233
|
+
const childFull = join20(absoluteRoot, entry.name);
|
|
12234
|
+
if (entry.isSymbolicLink()) {
|
|
12235
|
+
output.push({ kind: "symlink", relativePath: entry.name, fullPath: childFull });
|
|
12236
|
+
continue;
|
|
12237
|
+
}
|
|
12238
|
+
if (entry.isDirectory()) {
|
|
12239
|
+
if (EXCLUDE_DIR_NAMES.has(entry.name) || EXCLUDE_DIR_PATTERNS.some((pattern) => pattern.test(entry.name))) {
|
|
12240
|
+
continue;
|
|
12241
|
+
}
|
|
12242
|
+
const nested = walkEntries(childFull);
|
|
12243
|
+
for (const item of nested) {
|
|
12244
|
+
output.push({ ...item, relativePath: join20(entry.name, item.relativePath) });
|
|
12245
|
+
}
|
|
12246
|
+
continue;
|
|
12247
|
+
}
|
|
12248
|
+
if (entry.isFile()) {
|
|
12249
|
+
output.push({ kind: "file", relativePath: entry.name, fullPath: childFull });
|
|
12250
|
+
}
|
|
12251
|
+
}
|
|
12252
|
+
return output;
|
|
12253
|
+
}
|
|
12254
|
+
function isRegularFile(filePath) {
|
|
12255
|
+
try {
|
|
12256
|
+
return statSync10(filePath).isFile();
|
|
12257
|
+
} catch {
|
|
12258
|
+
return false;
|
|
12259
|
+
}
|
|
12260
|
+
}
|
|
12261
|
+
|
|
12262
|
+
// src/lib/station-snapshot.ts
|
|
12263
|
+
var STATION_SYNC_MANIFEST_SCHEMA = "hasna.fleet-resources.skills-sync-manifest/v1";
|
|
12264
|
+
var STATION_SNAPSHOT_PRODUCER = { name: "@hasna/skills", version: package_default.version };
|
|
12265
|
+
|
|
12266
|
+
class StationSnapshotError extends Error {
|
|
12267
|
+
code;
|
|
12268
|
+
detail;
|
|
12269
|
+
constructor(code, message, detail = []) {
|
|
12270
|
+
super(message);
|
|
12271
|
+
this.name = "StationSnapshotError";
|
|
12272
|
+
this.code = code;
|
|
12273
|
+
this.detail = detail;
|
|
12274
|
+
}
|
|
12275
|
+
}
|
|
12276
|
+
function validateStationId(stationId) {
|
|
12277
|
+
if (!/^[a-z0-9-]+$/.test(stationId)) {
|
|
12278
|
+
throw new StationSnapshotError("INVALID_STATION", `station id must be a slug, got: ${stationId}`);
|
|
12279
|
+
}
|
|
12280
|
+
}
|
|
12281
|
+
function sha256File(filePath) {
|
|
12282
|
+
return createHash6("sha256").update(readFileSync16(filePath)).digest("hex");
|
|
12283
|
+
}
|
|
12284
|
+
function scanHome(definition, homesRoot) {
|
|
12285
|
+
const homePath = homePathFor(definition, homesRoot);
|
|
12286
|
+
const entries = walkEntries(homePath);
|
|
12287
|
+
const portable = [];
|
|
12288
|
+
const skipped = [];
|
|
12289
|
+
for (const entry of entries) {
|
|
12290
|
+
const relativeParts = entry.relativePath.split(sep4);
|
|
12291
|
+
const fileName = relativeParts[relativeParts.length - 1];
|
|
12292
|
+
if (entry.kind === "symlink") {
|
|
12293
|
+
skipped.push({ relativePath: entry.relativePath, reason: "symlink" });
|
|
12294
|
+
continue;
|
|
12295
|
+
}
|
|
12296
|
+
if (!isPortableWithinSkill(relativeParts)) {
|
|
12297
|
+
skipped.push({ relativePath: entry.relativePath, reason: "not-portable" });
|
|
12298
|
+
continue;
|
|
12299
|
+
}
|
|
12300
|
+
if (isExcludedSkillFileName(fileName)) {
|
|
12301
|
+
skipped.push({ relativePath: entry.relativePath, reason: "excluded" });
|
|
12302
|
+
continue;
|
|
12303
|
+
}
|
|
12304
|
+
if (REFUSED_SCANNER_FLAGGED.has(entry.relativePath)) {
|
|
12305
|
+
skipped.push({ relativePath: entry.relativePath, reason: "refused-scanner-flagged" });
|
|
12306
|
+
continue;
|
|
12307
|
+
}
|
|
12308
|
+
if (!isRegularFile(entry.fullPath)) {
|
|
12309
|
+
skipped.push({ relativePath: entry.relativePath, reason: "not-regular-file" });
|
|
12310
|
+
continue;
|
|
12311
|
+
}
|
|
12312
|
+
const info = statSync11(entry.fullPath);
|
|
12313
|
+
portable.push({
|
|
12314
|
+
relativePath: entry.relativePath,
|
|
12315
|
+
fullPath: entry.fullPath,
|
|
12316
|
+
size: info.size,
|
|
12317
|
+
mtimeMs: info.mtimeMs,
|
|
12318
|
+
mtimeIso: info.mtime.toISOString()
|
|
12319
|
+
});
|
|
12320
|
+
}
|
|
12321
|
+
return { definition, homePath, portable, skipped };
|
|
12322
|
+
}
|
|
12323
|
+
function planStationSnapshot(options) {
|
|
12324
|
+
validateStationId(options.stationId);
|
|
12325
|
+
const scanned = SYNC_HOMES.map((definition) => scanHome(definition, options.homesRoot));
|
|
12326
|
+
const symlinks = scanned.reduce((sum, item) => sum + item.skipped.filter((entry) => entry.reason === "symlink").length, 0);
|
|
12327
|
+
if (symlinks > 0) {
|
|
12328
|
+
throw new StationSnapshotError("SYMLINKS_REFUSED", `${symlinks} symlink(s) inside skill homes; symlinks are refused (fail closed)`);
|
|
12329
|
+
}
|
|
12330
|
+
const plans = [];
|
|
12331
|
+
for (const item of scanned) {
|
|
12332
|
+
for (const file of item.portable) {
|
|
12333
|
+
plans.push({
|
|
12334
|
+
definition: item.definition,
|
|
12335
|
+
source: file,
|
|
12336
|
+
destination: destinationFor(item.definition, options.stationId, file.relativePath),
|
|
12337
|
+
digest: sha256File(file.fullPath)
|
|
12338
|
+
});
|
|
12339
|
+
}
|
|
12340
|
+
}
|
|
12341
|
+
const totalBytes = plans.reduce((sum, plan) => sum + plan.source.size, 0);
|
|
12342
|
+
return { scanned, plans, totalBytes };
|
|
12343
|
+
}
|
|
12344
|
+
function humanHomes(scanned) {
|
|
12345
|
+
return scanned.map((item) => ({
|
|
12346
|
+
name: item.definition.name,
|
|
12347
|
+
homePath: item.homePath,
|
|
12348
|
+
files: item.portable.length,
|
|
12349
|
+
skipped: item.skipped.length
|
|
12350
|
+
}));
|
|
12351
|
+
}
|
|
12352
|
+
function writeStationSnapshot(options) {
|
|
12353
|
+
const repoRoot = resolve2(options.repoRoot ?? process.cwd());
|
|
12354
|
+
const { scanned, plans, totalBytes } = planStationSnapshot(options);
|
|
12355
|
+
const manifestFiles = plans.map((plan) => ({
|
|
12356
|
+
relativePath: plan.source.relativePath,
|
|
12357
|
+
destination: plan.destination,
|
|
12358
|
+
subClass: plan.definition.subClass,
|
|
12359
|
+
agent: plan.definition.agent,
|
|
12360
|
+
sha256: plan.digest,
|
|
12361
|
+
sourceMtimeMs: plan.source.mtimeMs,
|
|
12362
|
+
sourceMtimeIso: plan.source.mtimeIso,
|
|
12363
|
+
size: plan.source.size
|
|
12364
|
+
}));
|
|
12365
|
+
const base = {
|
|
12366
|
+
stationId: options.stationId,
|
|
12367
|
+
mode: "dry-run",
|
|
12368
|
+
repoRoot,
|
|
12369
|
+
stats: { files: plans.length, bytes: totalBytes },
|
|
12370
|
+
homes: humanHomes(scanned),
|
|
12371
|
+
files: manifestFiles
|
|
12372
|
+
};
|
|
12373
|
+
if (options.dryRun !== false) {
|
|
12374
|
+
return base;
|
|
12375
|
+
}
|
|
12376
|
+
const conflicts = [];
|
|
12377
|
+
const untouched = [];
|
|
12378
|
+
for (const plan of plans) {
|
|
12379
|
+
const destination = resolve2(repoRoot, plan.destination);
|
|
12380
|
+
const destinationRelative = relative5(repoRoot, destination);
|
|
12381
|
+
if (destinationRelative.startsWith("..") || destinationRelative.startsWith(sep4) || isAbsolute3(destinationRelative)) {
|
|
12382
|
+
throw new StationSnapshotError("DESTINATION_ESCAPE", `destination escapes repo root: ${plan.destination}`);
|
|
12383
|
+
}
|
|
12384
|
+
let existingDigest = null;
|
|
12385
|
+
try {
|
|
12386
|
+
existingDigest = sha256File(destination);
|
|
12387
|
+
} catch {}
|
|
12388
|
+
if (existingDigest !== null) {
|
|
12389
|
+
if (existingDigest === plan.digest) {
|
|
12390
|
+
continue;
|
|
12391
|
+
}
|
|
12392
|
+
conflicts.push(`existing destination differs from staged source: ${plan.destination}`);
|
|
12393
|
+
continue;
|
|
12394
|
+
}
|
|
12395
|
+
untouched.push(plan);
|
|
12396
|
+
}
|
|
12397
|
+
if (conflicts.length > 0) {
|
|
12398
|
+
throw new StationSnapshotError("CONFLICT", `${conflicts.length} conflict(s); terminal non-acceptance, nothing written`, conflicts);
|
|
12399
|
+
}
|
|
12400
|
+
let written = 0;
|
|
12401
|
+
for (const plan of untouched) {
|
|
12402
|
+
const destination = resolve2(repoRoot, plan.destination);
|
|
12403
|
+
mkdirSync13(dirname11(destination), { recursive: true });
|
|
12404
|
+
copyFileSync2(plan.source.fullPath, destination);
|
|
12405
|
+
written += 1;
|
|
12406
|
+
}
|
|
12407
|
+
const unchanged = plans.length - untouched.length;
|
|
12408
|
+
const manifest = {
|
|
12409
|
+
schema: STATION_SYNC_MANIFEST_SCHEMA,
|
|
12410
|
+
stationId: options.stationId,
|
|
12411
|
+
syncedAt: new Date().toISOString(),
|
|
12412
|
+
producer: STATION_SNAPSHOT_PRODUCER,
|
|
12413
|
+
stats: {
|
|
12414
|
+
written,
|
|
12415
|
+
unchanged,
|
|
12416
|
+
files: plans.length,
|
|
12417
|
+
bytes: totalBytes
|
|
12418
|
+
},
|
|
12419
|
+
files: manifestFiles
|
|
12420
|
+
};
|
|
12421
|
+
const manifestPath = resolve2(repoRoot, "resources", options.stationId, "skills", "sync-manifest.json");
|
|
12422
|
+
mkdirSync13(dirname11(manifestPath), { recursive: true });
|
|
12423
|
+
writeFileSync12(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
12424
|
+
`);
|
|
12425
|
+
return {
|
|
12426
|
+
...base,
|
|
12427
|
+
mode: "populate",
|
|
12428
|
+
stats: { files: plans.length, bytes: totalBytes, written, unchanged },
|
|
12429
|
+
manifestPath
|
|
12430
|
+
};
|
|
12431
|
+
}
|
|
12432
|
+
// src/lib/station-hydrate.ts
|
|
12433
|
+
import { createHash as createHash7 } from "crypto";
|
|
12434
|
+
import {
|
|
12435
|
+
copyFileSync as copyFileSync3,
|
|
12436
|
+
mkdirSync as mkdirSync14,
|
|
12437
|
+
readdirSync as readdirSync11,
|
|
12438
|
+
readFileSync as readFileSync17,
|
|
12439
|
+
statSync as statSync12,
|
|
12440
|
+
writeFileSync as writeFileSync13
|
|
12441
|
+
} from "fs";
|
|
12442
|
+
import { dirname as dirname12, join as join21, resolve as resolve3, sep as sep5 } from "path";
|
|
12443
|
+
var STATION_HYDRATION_MANIFEST_SCHEMA = "hasna.fleet-resources.skills-hydration-manifest/v1";
|
|
12444
|
+
var STATION_HYDRATION_PRODUCER = { name: "@hasna/skills", version: package_default.version };
|
|
12445
|
+
var MANIFEST_HASH_KEY_SEP = String.fromCharCode(0);
|
|
12446
|
+
function fail(code, message, detail = []) {
|
|
12447
|
+
throw new StationSnapshotError(code, message, detail);
|
|
12448
|
+
}
|
|
12449
|
+
function snapshotRootFor(repoRoot, stationId) {
|
|
12450
|
+
return join21(repoRoot, "resources", stationId, "skills");
|
|
12451
|
+
}
|
|
12452
|
+
function readSnapshotManifest(repoRoot, stationId) {
|
|
12453
|
+
const snapshotRoot = snapshotRootFor(repoRoot, stationId);
|
|
12454
|
+
const manifestPath = join21(snapshotRoot, "sync-manifest.json");
|
|
12455
|
+
let manifest;
|
|
12456
|
+
try {
|
|
12457
|
+
manifest = JSON.parse(readFileSync17(manifestPath, "utf8"));
|
|
12458
|
+
} catch (error) {
|
|
12459
|
+
fail("MANIFEST_UNREADABLE", `cannot read snapshot manifest: ${manifestPath}: ${error.message}`);
|
|
12460
|
+
}
|
|
12461
|
+
const sourceSnapshotSha = sha256File(manifestPath);
|
|
12462
|
+
return { manifest, manifestPath, sourceSnapshotSha };
|
|
12463
|
+
}
|
|
12464
|
+
function planStationHydration(stationId, repoRoot) {
|
|
12465
|
+
validateStationId(stationId);
|
|
12466
|
+
const { manifest, sourceSnapshotSha } = readSnapshotManifest(repoRoot, stationId);
|
|
12467
|
+
const snapshotRoot = snapshotRootFor(repoRoot, stationId);
|
|
12468
|
+
const manifestHashes = new Map;
|
|
12469
|
+
for (const file of manifest.files ?? []) {
|
|
12470
|
+
const relativePath = file.relativePath;
|
|
12471
|
+
const agent = file.agent;
|
|
12472
|
+
if (agent && relativePath) {
|
|
12473
|
+
manifestHashes.set(`${agent}${MANIFEST_HASH_KEY_SEP}${relativePath}`, file.sha256);
|
|
12474
|
+
}
|
|
12475
|
+
}
|
|
12476
|
+
const candidates = [];
|
|
12477
|
+
const symlinks = [];
|
|
12478
|
+
const hashMismatches = [];
|
|
12479
|
+
const skippedByRule = [];
|
|
12480
|
+
for (const agent of SYNC_AGENTS) {
|
|
12481
|
+
const agentRoot = join21(snapshotRoot, "agent-homes", agent);
|
|
12482
|
+
let identEntries;
|
|
12483
|
+
try {
|
|
12484
|
+
identEntries = readdirSync11(agentRoot, { withFileTypes: true });
|
|
12485
|
+
} catch {
|
|
12486
|
+
continue;
|
|
12487
|
+
}
|
|
12488
|
+
for (const identEntry of identEntries) {
|
|
12489
|
+
if (!identEntry.isDirectory() || identEntry.name.startsWith(".")) {
|
|
12490
|
+
continue;
|
|
12491
|
+
}
|
|
12492
|
+
const identRoot = join21(agentRoot, identEntry.name);
|
|
12493
|
+
const entries = walkEntries(identRoot);
|
|
12494
|
+
for (const entry of entries) {
|
|
12495
|
+
const relativeParts = [identEntry.name, ...entry.relativePath.split(sep5)];
|
|
12496
|
+
if (entry.kind === "symlink") {
|
|
12497
|
+
symlinks.push({ ident: identEntry.name, agent, relativePath: entry.relativePath });
|
|
12498
|
+
continue;
|
|
12499
|
+
}
|
|
12500
|
+
if (!isPortableWithinSkill(relativeParts)) {
|
|
12501
|
+
skippedByRule.push({
|
|
12502
|
+
ident: identEntry.name,
|
|
12503
|
+
agent,
|
|
12504
|
+
relativePath: entry.relativePath,
|
|
12505
|
+
reason: "not-portable"
|
|
12506
|
+
});
|
|
12507
|
+
continue;
|
|
12508
|
+
}
|
|
12509
|
+
const fileName = relativeParts[relativeParts.length - 1];
|
|
12510
|
+
if (isExcludedSkillFileName(fileName)) {
|
|
12511
|
+
skippedByRule.push({
|
|
12512
|
+
ident: identEntry.name,
|
|
12513
|
+
agent,
|
|
12514
|
+
relativePath: entry.relativePath,
|
|
12515
|
+
reason: "excluded"
|
|
12516
|
+
});
|
|
12517
|
+
continue;
|
|
12518
|
+
}
|
|
12519
|
+
const withinIdent = relativeParts.slice(1).join(sep5);
|
|
12520
|
+
const homeRelative = relativeParts.join(sep5);
|
|
12521
|
+
if (REFUSED_SCANNER_FLAGGED.has(homeRelative)) {
|
|
12522
|
+
skippedByRule.push({
|
|
12523
|
+
ident: identEntry.name,
|
|
12524
|
+
agent,
|
|
12525
|
+
relativePath: entry.relativePath,
|
|
12526
|
+
reason: "refused-scanner-flagged"
|
|
12527
|
+
});
|
|
12528
|
+
continue;
|
|
12529
|
+
}
|
|
12530
|
+
if (!isRegularFile(entry.fullPath)) {
|
|
12531
|
+
skippedByRule.push({
|
|
12532
|
+
ident: identEntry.name,
|
|
12533
|
+
agent,
|
|
12534
|
+
relativePath: entry.relativePath,
|
|
12535
|
+
reason: "not-regular-file"
|
|
12536
|
+
});
|
|
12537
|
+
continue;
|
|
12538
|
+
}
|
|
12539
|
+
const info = statSync12(entry.fullPath);
|
|
12540
|
+
const manifestHash = manifestHashes.get(`${agent}${MANIFEST_HASH_KEY_SEP}${homeRelative}`) ?? null;
|
|
12541
|
+
let verified = false;
|
|
12542
|
+
if (manifestHash !== null) {
|
|
12543
|
+
verified = sha256File(entry.fullPath) === manifestHash;
|
|
12544
|
+
if (!verified) {
|
|
12545
|
+
hashMismatches.push({
|
|
12546
|
+
ident: identEntry.name,
|
|
12547
|
+
agent,
|
|
12548
|
+
relativePath: entry.relativePath
|
|
12549
|
+
});
|
|
12550
|
+
}
|
|
12551
|
+
}
|
|
12552
|
+
candidates.push({
|
|
12553
|
+
ident: identEntry.name,
|
|
12554
|
+
agent,
|
|
12555
|
+
withinIdent,
|
|
12556
|
+
fullPath: entry.fullPath,
|
|
12557
|
+
size: info.size,
|
|
12558
|
+
mtimeMs: info.mtimeMs,
|
|
12559
|
+
manifestHash,
|
|
12560
|
+
verified
|
|
12561
|
+
});
|
|
12562
|
+
}
|
|
12563
|
+
}
|
|
12564
|
+
}
|
|
12565
|
+
if (symlinks.length > 0) {
|
|
12566
|
+
fail("SYMLINKS_REFUSED", `${symlinks.length} symlink(s) inside the snapshot; symlinks are refused (fail closed)`);
|
|
12567
|
+
}
|
|
12568
|
+
if (hashMismatches.length > 0) {
|
|
12569
|
+
fail("MANIFEST_HASH_MISMATCH", `${hashMismatches.length} snapshot file(s) no longer match their sync-manifest sha256; ` + "stale or tampered content is refused (fail closed), nothing written \u2014 re-run sync to refresh the manifest", hashMismatches.map((mismatch) => `agent-homes/${mismatch.agent}/${mismatch.ident}/${mismatch.relativePath}`));
|
|
12570
|
+
}
|
|
12571
|
+
const byIdent = new Map;
|
|
12572
|
+
for (const candidate of candidates) {
|
|
12573
|
+
const group = byIdent.get(candidate.ident) ?? [];
|
|
12574
|
+
group.push(candidate);
|
|
12575
|
+
byIdent.set(candidate.ident, group);
|
|
12576
|
+
}
|
|
12577
|
+
const winners = [];
|
|
12578
|
+
for (const [ident, group] of byIdent) {
|
|
12579
|
+
const byFile = new Map;
|
|
12580
|
+
for (const candidate of group) {
|
|
12581
|
+
const copies = byFile.get(candidate.withinIdent) ?? [];
|
|
12582
|
+
copies.push(candidate);
|
|
12583
|
+
byFile.set(candidate.withinIdent, copies);
|
|
12584
|
+
}
|
|
12585
|
+
const files = [];
|
|
12586
|
+
for (const [withinIdent, copies] of byFile) {
|
|
12587
|
+
let eligible = copies;
|
|
12588
|
+
if (withinIdent === "SKILL.md") {
|
|
12589
|
+
const content = [];
|
|
12590
|
+
for (const copy of copies) {
|
|
12591
|
+
let isStub = false;
|
|
12592
|
+
try {
|
|
12593
|
+
isStub = isPointerSkillMd(readFileSync17(copy.fullPath, "utf8"));
|
|
12594
|
+
} catch {
|
|
12595
|
+
isStub = false;
|
|
12596
|
+
}
|
|
12597
|
+
if (!isStub)
|
|
12598
|
+
content.push(copy);
|
|
12599
|
+
}
|
|
12600
|
+
if (content.length > 0) {
|
|
12601
|
+
eligible = content;
|
|
12602
|
+
}
|
|
12603
|
+
}
|
|
12604
|
+
eligible.sort((left, right) => {
|
|
12605
|
+
const leftHash = left.verified;
|
|
12606
|
+
const rightHash = right.verified;
|
|
12607
|
+
if (leftHash !== rightHash) {
|
|
12608
|
+
return leftHash ? -1 : 1;
|
|
12609
|
+
}
|
|
12610
|
+
if (right.mtimeMs !== left.mtimeMs) {
|
|
12611
|
+
return right.mtimeMs - left.mtimeMs;
|
|
12612
|
+
}
|
|
12613
|
+
return SYNC_AGENTS.indexOf(left.agent) - SYNC_AGENTS.indexOf(right.agent);
|
|
12614
|
+
});
|
|
12615
|
+
const winner = eligible[0];
|
|
12616
|
+
files.push({
|
|
12617
|
+
withinIdent,
|
|
12618
|
+
winner,
|
|
12619
|
+
alternates: copies.filter((copy) => copy !== winner).map((copy) => copy.agent)
|
|
12620
|
+
});
|
|
12621
|
+
}
|
|
12622
|
+
files.sort((left, right) => left.withinIdent.localeCompare(right.withinIdent));
|
|
12623
|
+
winners.push({ ident, files });
|
|
12624
|
+
}
|
|
12625
|
+
winners.sort((left, right) => left.ident.localeCompare(right.ident));
|
|
12626
|
+
const totalFiles = winners.reduce((sum, skill) => sum + skill.files.length, 0);
|
|
12627
|
+
const totalBytes = winners.reduce((sum, skill) => sum + skill.files.reduce((inner, file) => inner + file.winner.size, 0), 0);
|
|
12628
|
+
return { manifest, sourceSnapshotSha, winners, skippedByRule, totalFiles, totalBytes };
|
|
12629
|
+
}
|
|
12630
|
+
function skillSha256(skill) {
|
|
12631
|
+
const skillMd = skill.files.find((file) => file.withinIdent === "SKILL.md");
|
|
12632
|
+
if (skillMd) {
|
|
12633
|
+
return sha256File(skillMd.winner.fullPath);
|
|
12634
|
+
}
|
|
12635
|
+
if (skill.files.length === 1) {
|
|
12636
|
+
return sha256File(skill.files[0].winner.fullPath);
|
|
12637
|
+
}
|
|
12638
|
+
const joined = skill.files.map((file) => sha256File(file.winner.fullPath));
|
|
12639
|
+
return createHash7("sha256").update(joined.sort().join(`
|
|
12640
|
+
`)).digest("hex");
|
|
12641
|
+
}
|
|
12642
|
+
function writeStationHydration(options) {
|
|
12643
|
+
const repoRoot = resolve3(options.repoRoot ?? process.cwd());
|
|
12644
|
+
const cacheRoot = resolve3(options.cacheRoot ?? resolveCorpusRoot());
|
|
12645
|
+
const plan = planStationHydration(options.stationId, repoRoot);
|
|
12646
|
+
const resultSkills = plan.winners.map((skill) => ({
|
|
12647
|
+
ident: skill.ident,
|
|
12648
|
+
files: skill.files.map((file) => ({
|
|
12649
|
+
relativePath: file.withinIdent,
|
|
12650
|
+
sourceAgent: file.winner.agent,
|
|
12651
|
+
sourceMtimeMs: file.winner.mtimeMs,
|
|
12652
|
+
size: file.winner.size
|
|
12653
|
+
})),
|
|
12654
|
+
sha256: skillSha256(skill)
|
|
12655
|
+
}));
|
|
12656
|
+
const base = {
|
|
12657
|
+
stationId: options.stationId,
|
|
12658
|
+
mode: "dry-run",
|
|
12659
|
+
cacheRoot,
|
|
12660
|
+
snapshotRoot: snapshotRootFor(repoRoot, options.stationId),
|
|
12661
|
+
sourceSnapshotSha: plan.sourceSnapshotSha,
|
|
12662
|
+
stats: {
|
|
12663
|
+
idents: plan.winners.length,
|
|
12664
|
+
files: plan.totalFiles,
|
|
12665
|
+
bytes: plan.totalBytes
|
|
12666
|
+
},
|
|
12667
|
+
winners: plan.winners,
|
|
12668
|
+
skills: resultSkills
|
|
12669
|
+
};
|
|
12670
|
+
if (options.dryRun !== false) {
|
|
12671
|
+
return base;
|
|
12672
|
+
}
|
|
12673
|
+
const conflicts = [];
|
|
12674
|
+
const toWrite = [];
|
|
12675
|
+
for (const skill of plan.winners) {
|
|
12676
|
+
for (const file of skill.files) {
|
|
12677
|
+
const destination = join21(cacheRoot, skill.ident, file.withinIdent);
|
|
12678
|
+
const digest = sha256File(file.winner.fullPath);
|
|
12679
|
+
let existingDigest = null;
|
|
12680
|
+
try {
|
|
12681
|
+
existingDigest = sha256File(destination);
|
|
12682
|
+
} catch {}
|
|
12683
|
+
if (existingDigest !== null) {
|
|
12684
|
+
if (existingDigest === digest) {
|
|
12685
|
+
continue;
|
|
12686
|
+
}
|
|
12687
|
+
conflicts.push(`existing destination differs from snapshot winner: ${destination}`);
|
|
12688
|
+
continue;
|
|
12689
|
+
}
|
|
12690
|
+
toWrite.push({ destination, fullPath: file.winner.fullPath });
|
|
12691
|
+
}
|
|
12692
|
+
}
|
|
12693
|
+
if (conflicts.length > 0) {
|
|
12694
|
+
fail("CONFLICT", `${conflicts.length} conflict(s); terminal non-acceptance, nothing written`, conflicts);
|
|
12695
|
+
}
|
|
12696
|
+
let written = 0;
|
|
12697
|
+
for (const entry of toWrite) {
|
|
12698
|
+
mkdirSync14(dirname12(entry.destination), { recursive: true });
|
|
12699
|
+
copyFileSync3(entry.fullPath, entry.destination);
|
|
12700
|
+
written += 1;
|
|
12701
|
+
}
|
|
12702
|
+
const unchanged = plan.totalFiles - written;
|
|
12703
|
+
const hydration = {
|
|
12704
|
+
schema: STATION_HYDRATION_MANIFEST_SCHEMA,
|
|
12705
|
+
stationId: options.stationId,
|
|
12706
|
+
hydratedAt: new Date().toISOString(),
|
|
12707
|
+
producer: STATION_HYDRATION_PRODUCER,
|
|
12708
|
+
sourceSnapshotSha: plan.sourceSnapshotSha,
|
|
12709
|
+
cacheRoot,
|
|
12710
|
+
stats: {
|
|
12711
|
+
idents: plan.winners.length,
|
|
12712
|
+
written,
|
|
12713
|
+
unchanged,
|
|
12714
|
+
files: plan.totalFiles,
|
|
12715
|
+
bytes: plan.totalBytes
|
|
12716
|
+
},
|
|
12717
|
+
skills: resultSkills
|
|
12718
|
+
};
|
|
12719
|
+
const hydrationManifestPath = join21(dirname12(cacheRoot), `hydration-${options.stationId}.json`);
|
|
12720
|
+
mkdirSync14(dirname12(hydrationManifestPath), { recursive: true });
|
|
12721
|
+
writeFileSync13(hydrationManifestPath, `${JSON.stringify(hydration, null, 2)}
|
|
12722
|
+
`);
|
|
12723
|
+
return {
|
|
12724
|
+
...base,
|
|
12725
|
+
mode: "apply",
|
|
12726
|
+
stats: { ...base.stats, written, unchanged },
|
|
12727
|
+
manifestPath: hydrationManifestPath
|
|
12728
|
+
};
|
|
12729
|
+
}
|
|
11993
12730
|
export {
|
|
12731
|
+
writeStationSnapshot,
|
|
12732
|
+
writeStationHydration,
|
|
11994
12733
|
writeRunLogs,
|
|
11995
12734
|
writeRegistrySyncArtifact,
|
|
11996
12735
|
writeManagedSkillDir,
|
|
11997
12736
|
writeManagedAgentSkill,
|
|
11998
12737
|
writeCorpusSkill,
|
|
12738
|
+
walkEntries,
|
|
11999
12739
|
verifyContentHash,
|
|
12000
12740
|
validateToolPrimitiveCoverage,
|
|
12741
|
+
validateStationId,
|
|
12001
12742
|
validateSkillsCliMcpParity,
|
|
12002
12743
|
validateSkillDirectory,
|
|
12003
12744
|
validateRegistryConsistency,
|
|
@@ -12016,6 +12757,7 @@ export {
|
|
|
12016
12757
|
skillsPostgresSyncSchemaSql,
|
|
12017
12758
|
skillExists,
|
|
12018
12759
|
signSkillsAwsV4Request,
|
|
12760
|
+
sha256File,
|
|
12019
12761
|
setSkillDisabled,
|
|
12020
12762
|
setScheduleEnabled,
|
|
12021
12763
|
searchSkills,
|
|
@@ -12043,6 +12785,8 @@ export {
|
|
|
12043
12785
|
portPortableSkillDirectory,
|
|
12044
12786
|
portPortableSkill,
|
|
12045
12787
|
pointerSkillMd,
|
|
12788
|
+
planStationSnapshot,
|
|
12789
|
+
planStationHydration,
|
|
12046
12790
|
planSkillsS3SnapshotUpload,
|
|
12047
12791
|
pinSkill,
|
|
12048
12792
|
pinProjectSkill,
|
|
@@ -12068,7 +12812,10 @@ export {
|
|
|
12068
12812
|
listPinnedSkills,
|
|
12069
12813
|
listMcpToolContracts,
|
|
12070
12814
|
isSyncAgent,
|
|
12815
|
+
isRegularFile,
|
|
12816
|
+
isPortableWithinSkill,
|
|
12071
12817
|
isGatewayBackedSkill,
|
|
12818
|
+
isExcludedSkillFileName,
|
|
12072
12819
|
isBasicSkillName,
|
|
12073
12820
|
installSkills,
|
|
12074
12821
|
installSkillSource,
|
|
@@ -12076,6 +12823,7 @@ export {
|
|
|
12076
12823
|
installSkillForAgent,
|
|
12077
12824
|
installSkill,
|
|
12078
12825
|
importSkillsLocalSnapshot,
|
|
12826
|
+
homePathFor,
|
|
12079
12827
|
getToolPrimitive,
|
|
12080
12828
|
getStorageStatus,
|
|
12081
12829
|
getStorageDatabaseUrl,
|
|
@@ -12125,6 +12873,7 @@ export {
|
|
|
12125
12873
|
ensureProjectConfig,
|
|
12126
12874
|
enableSkill,
|
|
12127
12875
|
disableSkill,
|
|
12876
|
+
destinationFor,
|
|
12128
12877
|
describeMcpToolContracts,
|
|
12129
12878
|
createSkillsSnapshotSyncRecord,
|
|
12130
12879
|
createSkillsS3ObjectStore,
|
|
@@ -12148,12 +12897,16 @@ export {
|
|
|
12148
12897
|
adaptSkillMdForAgent,
|
|
12149
12898
|
TOOL_PRIMITIVE_SCHEMA_VERSION,
|
|
12150
12899
|
TOOL_PRIMITIVES,
|
|
12900
|
+
StationSnapshotError,
|
|
12151
12901
|
SkillsS3ObjectStore,
|
|
12152
12902
|
SkillsPostgresSyncStore,
|
|
12153
12903
|
SYNC_MARKER_MANAGED_BY,
|
|
12154
12904
|
SYNC_MARKER_FILE,
|
|
12905
|
+
SYNC_HOMES,
|
|
12155
12906
|
SYNC_AGENTS,
|
|
12156
12907
|
STORAGE_TABLES,
|
|
12908
|
+
STATION_SYNC_MANIFEST_SCHEMA,
|
|
12909
|
+
STATION_HYDRATION_MANIFEST_SCHEMA,
|
|
12157
12910
|
SKILL_SYSTEM_DEPS_ALLOWLIST,
|
|
12158
12911
|
SKILL_SANDBOX_MODES,
|
|
12159
12912
|
SKILL_RUNTIMES,
|
|
@@ -12170,6 +12923,7 @@ export {
|
|
|
12170
12923
|
RemoteRouteUnsupportedError,
|
|
12171
12924
|
RemoteRequestError,
|
|
12172
12925
|
REMOTE_SKILL_RUN_CONTRACT_VERSION,
|
|
12926
|
+
REFUSED_SCANNER_FLAGGED,
|
|
12173
12927
|
PullSkillError,
|
|
12174
12928
|
PROJECT_CONFIG_FILE,
|
|
12175
12929
|
PORTABLE_SKILL_STANDARD,
|