hngr 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/dist/{catalog-CmtREMFr.mjs → catalog-CfpeEevk.mjs} +630 -8
- package/dist/{commands-Bjui8-Nt.mjs → commands-UsF7HECR.mjs} +1 -1
- package/dist/index.mjs +138 -7
- package/dist/{mcp-C-oJ6J5B.mjs → mcp-BKyfjqcx.mjs} +1 -1
- package/dist/{scope-8BZjgIXy.mjs → scope-DRg-ddFn.mjs} +16 -1
- package/dist/{skills-CqHGxo_M.mjs → skills-gJqYAVg2.mjs} +260 -5
- package/dist/sync-D25awtDG.mjs +278 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as kebabCase, i as normalizeCommandName, n as COMMANDS, o as COMMAND_ALIASES, r as findCommand, t as commandCatalog } from "./catalog-
|
|
1
|
+
import { a as kebabCase, i as normalizeCommandName, n as COMMANDS, o as COMMAND_ALIASES, r as findCommand, t as commandCatalog } from "./catalog-CfpeEevk.mjs";
|
|
2
2
|
import { C as HngrError, S as CancelledError, c as redactUrlForDisplay, g as resolveConnection, i as buildRequestUrl, o as redactQuery, r as apiRequest, s as redactSensitive, u as DEFAULT_SERVER_URL, w as renderError } from "./http-CLG2jrIE.mjs";
|
|
3
3
|
import { t as collectPages } from "./pagination-DtoXmj2F.mjs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
@@ -5279,7 +5279,7 @@ function isYargsInstance(y) {
|
|
|
5279
5279
|
// ../../node_modules/.pnpm/yargs@18.2.0/node_modules/yargs/index.mjs
|
|
5280
5280
|
const Yargs = YargsFactory(esm_default);
|
|
5281
5281
|
// package.json
|
|
5282
|
-
var version = "0.
|
|
5282
|
+
var version = "0.10.0";
|
|
5283
5283
|
// src/generated/completion.ts
|
|
5284
5284
|
function isCompletionShell(value) {
|
|
5285
5285
|
return value === "bash" || value === "zsh" || value === "fish";
|
|
@@ -7228,7 +7228,7 @@ function buildParser(onMatched) {
|
|
|
7228
7228
|
parser.command("commands", "Print the command catalog", () => {}, (argv) => {
|
|
7229
7229
|
onMatched({
|
|
7230
7230
|
argv,
|
|
7231
|
-
run: async (ctx) => (await import("./commands-
|
|
7231
|
+
run: async (ctx) => (await import("./commands-UsF7HECR.mjs")).run(ctx)
|
|
7232
7232
|
});
|
|
7233
7233
|
});
|
|
7234
7234
|
parser.command("auth", "Manage credentials", (auth) => {
|
|
@@ -7280,6 +7280,114 @@ function buildParser(onMatched) {
|
|
|
7280
7280
|
registerGeneratedAuth(auth, onMatched);
|
|
7281
7281
|
return auth.demandCommand(1);
|
|
7282
7282
|
});
|
|
7283
|
+
parser.command("sync", "Manage seek sync tasks", (sync) => {
|
|
7284
|
+
sync.command("list", "List sync tasks", (list) => list.option("include-deleted", {
|
|
7285
|
+
type: "boolean",
|
|
7286
|
+
default: false,
|
|
7287
|
+
describe: "Include soft-deleted tasks and their retained history"
|
|
7288
|
+
}).option("limit", {
|
|
7289
|
+
type: "number",
|
|
7290
|
+
describe: "Maximum tasks per page"
|
|
7291
|
+
}), (argv) => {
|
|
7292
|
+
onMatched({
|
|
7293
|
+
argv,
|
|
7294
|
+
run: async (ctx, matchedArgv) => (await import("./sync-D25awtDG.mjs")).list(ctx, matchedArgv)
|
|
7295
|
+
});
|
|
7296
|
+
});
|
|
7297
|
+
sync.command("create", "Create a sync task (disabled unless --enable)", (create) => create.option("namespace", {
|
|
7298
|
+
type: "string",
|
|
7299
|
+
demandOption: true,
|
|
7300
|
+
describe: "Target Namespace name"
|
|
7301
|
+
}).option("interval", {
|
|
7302
|
+
type: "string",
|
|
7303
|
+
describe: "5m, 15m, 1h, 6h, 24h or minutes (default: 1h)"
|
|
7304
|
+
}).option("visibility", {
|
|
7305
|
+
type: "string",
|
|
7306
|
+
describe: "public | internal | private (default: internal)"
|
|
7307
|
+
}).option("enable", {
|
|
7308
|
+
type: "boolean",
|
|
7309
|
+
default: false,
|
|
7310
|
+
describe: "Enable the task immediately; a dry-run first is recommended"
|
|
7311
|
+
}), (argv) => {
|
|
7312
|
+
onMatched({
|
|
7313
|
+
argv,
|
|
7314
|
+
run: async (ctx, matchedArgv) => (await import("./sync-D25awtDG.mjs")).create(ctx, matchedArgv)
|
|
7315
|
+
});
|
|
7316
|
+
});
|
|
7317
|
+
sync.command("show <task-id>", "Show one sync task", () => {}, (argv) => {
|
|
7318
|
+
onMatched({
|
|
7319
|
+
argv,
|
|
7320
|
+
run: async (ctx, matchedArgv) => (await import("./sync-D25awtDG.mjs")).show(ctx, matchedArgv)
|
|
7321
|
+
});
|
|
7322
|
+
});
|
|
7323
|
+
sync.command("update <task-id>", "Change a task's enabled state, interval or default visibility", (update) => update.option("enable", {
|
|
7324
|
+
type: "boolean",
|
|
7325
|
+
describe: "Enable the task"
|
|
7326
|
+
}).option("disable", {
|
|
7327
|
+
type: "boolean",
|
|
7328
|
+
describe: "Disable the task"
|
|
7329
|
+
}).option("interval", {
|
|
7330
|
+
type: "string",
|
|
7331
|
+
describe: "5m, 15m, 1h, 6h, 24h or minutes"
|
|
7332
|
+
}).option("visibility", {
|
|
7333
|
+
type: "string",
|
|
7334
|
+
describe: "public | internal | private"
|
|
7335
|
+
}).conflicts("enable", "disable"), (argv) => {
|
|
7336
|
+
onMatched({
|
|
7337
|
+
argv,
|
|
7338
|
+
run: async (ctx, matchedArgv) => (await import("./sync-D25awtDG.mjs")).update(ctx, matchedArgv)
|
|
7339
|
+
});
|
|
7340
|
+
});
|
|
7341
|
+
sync.command("delete <task-id>", "Soft-delete a task; Capabilities are kept", () => {}, (argv) => {
|
|
7342
|
+
onMatched({
|
|
7343
|
+
argv,
|
|
7344
|
+
run: async (ctx, matchedArgv) => (await import("./sync-D25awtDG.mjs")).remove(ctx, matchedArgv)
|
|
7345
|
+
});
|
|
7346
|
+
});
|
|
7347
|
+
sync.command("run <task-id>", "Start a manual run; --dry-run plans without writing", (run) => run.option("wait", {
|
|
7348
|
+
type: "boolean",
|
|
7349
|
+
default: false,
|
|
7350
|
+
describe: "Wait for completion and print the per-Skill results"
|
|
7351
|
+
}), (argv) => {
|
|
7352
|
+
onMatched({
|
|
7353
|
+
argv,
|
|
7354
|
+
run: async (ctx, matchedArgv) => (await import("./sync-D25awtDG.mjs")).run(ctx, matchedArgv)
|
|
7355
|
+
});
|
|
7356
|
+
});
|
|
7357
|
+
sync.command("runs", "Inspect a task's runs", (runs) => {
|
|
7358
|
+
runs.command("list <task-id>", "List runs, newest first", (list) => list.option("limit", {
|
|
7359
|
+
type: "number",
|
|
7360
|
+
describe: "Maximum runs per page"
|
|
7361
|
+
}), (argv) => {
|
|
7362
|
+
onMatched({
|
|
7363
|
+
argv,
|
|
7364
|
+
run: async (ctx, matchedArgv) => (await import("./sync-D25awtDG.mjs")).runs(ctx, matchedArgv)
|
|
7365
|
+
});
|
|
7366
|
+
});
|
|
7367
|
+
runs.command("show <task-id> <run-id>", "Show one run and its per-Skill results", () => {}, (argv) => {
|
|
7368
|
+
onMatched({
|
|
7369
|
+
argv,
|
|
7370
|
+
run: async (ctx, matchedArgv) => (await import("./sync-D25awtDG.mjs")).runShow(ctx, matchedArgv)
|
|
7371
|
+
});
|
|
7372
|
+
});
|
|
7373
|
+
return runs.demandCommand(1);
|
|
7374
|
+
});
|
|
7375
|
+
sync.command("items <task-id>", "Show the task's current per-Skill state", (items) => items.option("state", {
|
|
7376
|
+
type: "string",
|
|
7377
|
+
array: true,
|
|
7378
|
+
nargs: 1,
|
|
7379
|
+
describe: "Filter by state (repeatable): active, conflict, missing, orphaned, deleted_locally, error"
|
|
7380
|
+
}).option("limit", {
|
|
7381
|
+
type: "number",
|
|
7382
|
+
describe: "Maximum items per page (default 50)"
|
|
7383
|
+
}), (argv) => {
|
|
7384
|
+
onMatched({
|
|
7385
|
+
argv,
|
|
7386
|
+
run: async (ctx, matchedArgv) => (await import("./sync-D25awtDG.mjs")).items(ctx, matchedArgv)
|
|
7387
|
+
});
|
|
7388
|
+
});
|
|
7389
|
+
return sync.demandCommand(1);
|
|
7390
|
+
});
|
|
7283
7391
|
registerGeneratedCommands(parser, onMatched);
|
|
7284
7392
|
parser.command("skills", "Install skills into coding agent targets", (skills) => {
|
|
7285
7393
|
skills.command("add <paths..>", "Install skills from the catalog into agent targets", (add) => add.option("agent", {
|
|
@@ -7312,7 +7420,30 @@ function buildParser(onMatched) {
|
|
|
7312
7420
|
}), (argv) => {
|
|
7313
7421
|
onMatched({
|
|
7314
7422
|
argv,
|
|
7315
|
-
run: async (ctx, matchedArgv) => (await import("./skills-
|
|
7423
|
+
run: async (ctx, matchedArgv) => (await import("./skills-gJqYAVg2.mjs")).add(ctx, matchedArgv)
|
|
7424
|
+
});
|
|
7425
|
+
});
|
|
7426
|
+
skills.command("restore [names..]", "Reinstall skills recorded in the lock file", (restore) => restore.option("global", {
|
|
7427
|
+
type: "boolean",
|
|
7428
|
+
default: false,
|
|
7429
|
+
describe: "Read the user-level lock file instead of the project one"
|
|
7430
|
+
}).option("agent", {
|
|
7431
|
+
type: "string",
|
|
7432
|
+
array: true,
|
|
7433
|
+
nargs: 1,
|
|
7434
|
+
describe: "Agent target id (repeatable), e.g. claude-code"
|
|
7435
|
+
}).option("all", {
|
|
7436
|
+
type: "boolean",
|
|
7437
|
+
default: false,
|
|
7438
|
+
describe: "Install to every known agent target"
|
|
7439
|
+
}).option("copy", {
|
|
7440
|
+
type: "boolean",
|
|
7441
|
+
default: false,
|
|
7442
|
+
describe: "Copy into each target instead of canonical plus symlink"
|
|
7443
|
+
}), (argv) => {
|
|
7444
|
+
onMatched({
|
|
7445
|
+
argv,
|
|
7446
|
+
run: async (ctx, matchedArgv) => (await import("./skills-gJqYAVg2.mjs")).restore(ctx, matchedArgv)
|
|
7316
7447
|
});
|
|
7317
7448
|
});
|
|
7318
7449
|
skills.command("publish <paths..>", "Publish a local skill directory as a capability or a new version", (publish) => publish.option("namespace", {
|
|
@@ -7340,7 +7471,7 @@ function buildParser(onMatched) {
|
|
|
7340
7471
|
}), (argv) => {
|
|
7341
7472
|
onMatched({
|
|
7342
7473
|
argv,
|
|
7343
|
-
run: async (ctx, matchedArgv) => (await import("./skills-
|
|
7474
|
+
run: async (ctx, matchedArgv) => (await import("./skills-gJqYAVg2.mjs")).publish(ctx, matchedArgv)
|
|
7344
7475
|
});
|
|
7345
7476
|
});
|
|
7346
7477
|
return skills.demandCommand(1);
|
|
@@ -7362,7 +7493,7 @@ function buildParser(onMatched) {
|
|
|
7362
7493
|
}), (argv) => {
|
|
7363
7494
|
onMatched({
|
|
7364
7495
|
argv,
|
|
7365
|
-
run: async (ctx, matchedArgv) => (await import("./mcp-
|
|
7496
|
+
run: async (ctx, matchedArgv) => (await import("./mcp-BKyfjqcx.mjs")).add(ctx, matchedArgv)
|
|
7366
7497
|
});
|
|
7367
7498
|
});
|
|
7368
7499
|
mcp.command("publish <paths..>", "Publish an MCP snapshot as a capability or a new version", (publish) => publish.option("namespace", {
|
|
@@ -7390,7 +7521,7 @@ function buildParser(onMatched) {
|
|
|
7390
7521
|
}), (argv) => {
|
|
7391
7522
|
onMatched({
|
|
7392
7523
|
argv,
|
|
7393
|
-
run: async (ctx, matchedArgv) => (await import("./mcp-
|
|
7524
|
+
run: async (ctx, matchedArgv) => (await import("./mcp-BKyfjqcx.mjs")).publish(ctx, matchedArgv)
|
|
7394
7525
|
});
|
|
7395
7526
|
});
|
|
7396
7527
|
return mcp.demandCommand(1);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { C as HngrError, g as resolveConnection, s as redactSensitive, t as apiFetch, x as writeFileAtomically } from "./http-CLG2jrIE.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { C as parseCapabilityPath, E as resolveCapabilityVersion, T as resolveCapability, _ as downloadContent, a as errorBody, c as runBatch, h as findAgentDefinition, i as runPublish, l as stringValues, m as AGENTS, n as explicitName, o as failedResult, p as pathExists, r as readPublishFlags, s as positionalPaths, t as resolveInstallScope, v as MCP_KIND } from "./scope-DRg-ddFn.mjs";
|
|
3
3
|
import { constants } from "node:fs";
|
|
4
4
|
import { access, readFile, stat } from "node:fs/promises";
|
|
5
5
|
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
@@ -156,6 +156,21 @@ async function resolveCapability(connection, parsed, kind, signal) {
|
|
|
156
156
|
}
|
|
157
157
|
return capability;
|
|
158
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Reads one Capability by id. A lock entry records only the Content URL, so
|
|
161
|
+
* restoring it needs the Capability's namespace and name; a 404 means the
|
|
162
|
+
* Capability is gone (or not readable), which callers report as a skip.
|
|
163
|
+
*/
|
|
164
|
+
async function lookupCapabilityById(connection, id, signal) {
|
|
165
|
+
try {
|
|
166
|
+
const body = await apiGet(connection, `${CAPABILITIES_PATH}/${encodeURIComponent(id)}`, void 0, { signal });
|
|
167
|
+
if (body === null || typeof body !== "object") return null;
|
|
168
|
+
return body;
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (err instanceof HngrError && err.body.status === 404) return null;
|
|
171
|
+
throw err;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
159
174
|
/** The pinned version, or the capability's Current Version. */
|
|
160
175
|
function resolveCapabilityVersion(capability, parsed, kind) {
|
|
161
176
|
if (parsed.version) return parsed.version;
|
|
@@ -681,4 +696,4 @@ async function resolveInstallScope(ctx, global) {
|
|
|
681
696
|
if (!select || ctx.flags.json || !ctx.io.stdin.isTTY || !ctx.io.stdout.isTTY) return "project";
|
|
682
697
|
return await select("Install scope", SCOPE_OPTIONS, "project") === "global" ? "global" : "project";
|
|
683
698
|
}
|
|
684
|
-
export {
|
|
699
|
+
export { parseCapabilityPath as C, resolveCapabilityVersion as E, lookupCapabilityById as S, resolveCapability as T, downloadContent as _, errorBody as a, findCapability as b, runBatch as c, installAtomically as d, linkAtomically as f, homeDir as g, findAgentDefinition as h, runPublish as i, stringValues as l, AGENTS as m, explicitName as n, failedResult as o, pathExists as p, readPublishFlags as r, positionalPaths as s, resolveInstallScope as t, copyAtomically as u, MCP_KIND as v, parseCapabilityTarget as w, listCapabilityNamespace as x, SKILL_KIND as y };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { C as HngrError, g as resolveConnection, r as apiRequest } from "./http-CLG2jrIE.mjs";
|
|
2
|
-
import { C as
|
|
1
|
+
import { C as HngrError, g as resolveConnection, r as apiRequest, x as writeFileAtomically } from "./http-CLG2jrIE.mjs";
|
|
2
|
+
import { C as parseCapabilityPath, E as resolveCapabilityVersion, S as lookupCapabilityById, T as resolveCapability, _ as downloadContent, a as errorBody, b as findCapability, c as runBatch, d as installAtomically, f as linkAtomically, g as homeDir, i as runPublish, l as stringValues, m as AGENTS, n as explicitName, o as failedResult, p as pathExists, r as readPublishFlags, s as positionalPaths, t as resolveInstallScope, u as copyAtomically, w as parseCapabilityTarget, x as listCapabilityNamespace, y as SKILL_KIND } from "./scope-DRg-ddFn.mjs";
|
|
3
3
|
import { lstat, readFile, readdir } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
-
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
6
6
|
import { deflateRawSync, inflateRawSync } from "node:zlib";
|
|
7
7
|
// src/http-multipart.ts
|
|
8
8
|
function isFile(part) {
|
|
@@ -88,6 +88,192 @@ function isReservedDirName(name) {
|
|
|
88
88
|
function downloadSkillPackage(connection, capabilityId, version, signal) {
|
|
89
89
|
return downloadContent(connection, capabilityId, version, "application/zip", signal);
|
|
90
90
|
}
|
|
91
|
+
function projectLockPath(cwd) {
|
|
92
|
+
return join(cwd, "skills-lock.json");
|
|
93
|
+
}
|
|
94
|
+
/** `$XDG_STATE_HOME/skills/.skill-lock.json`, or `~/.agents/.skill-lock.json`. */
|
|
95
|
+
function globalLockPath(env) {
|
|
96
|
+
const state = env.XDG_STATE_HOME;
|
|
97
|
+
return state && state.length > 0 ? join(state, "skills", ".skill-lock.json") : join(homeDir(env), ".agents", ".skill-lock.json");
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Reads a lock file. An absent file is not an error. A malformed file, or one
|
|
101
|
+
* whose schema version this CLI does not write, is reported instead of being
|
|
102
|
+
* replaced: another tool owns that file, and clobbering it would lose entries.
|
|
103
|
+
*/
|
|
104
|
+
async function readLockDocument(path, version) {
|
|
105
|
+
let raw;
|
|
106
|
+
try {
|
|
107
|
+
raw = await readFile(path, "utf8");
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (isNotFound(err)) return { status: "absent" };
|
|
110
|
+
return {
|
|
111
|
+
status: "unusable",
|
|
112
|
+
reason: errorText(err)
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
let parsed;
|
|
116
|
+
try {
|
|
117
|
+
parsed = JSON.parse(raw);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
return {
|
|
120
|
+
status: "unusable",
|
|
121
|
+
reason: `it is not valid JSON: ${errorText(err)}`
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {
|
|
125
|
+
status: "unusable",
|
|
126
|
+
reason: "its top level is not an object"
|
|
127
|
+
};
|
|
128
|
+
const document = parsed;
|
|
129
|
+
if (document.version !== version) return {
|
|
130
|
+
status: "unusable",
|
|
131
|
+
reason: `its version is ${JSON.stringify(document.version)}, but this command writes version ${version}`
|
|
132
|
+
};
|
|
133
|
+
if (document.skills === null || typeof document.skills !== "object" || Array.isArray(document.skills)) return {
|
|
134
|
+
status: "unusable",
|
|
135
|
+
reason: "its skills field is not an object"
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
status: "ok",
|
|
139
|
+
document
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Writes a lock file the way `npx skills` does: skill keys sorted, two-space
|
|
144
|
+
* JSON and a trailing newline. Top-level fields and skill entries are
|
|
145
|
+
* preserved; only the entry this install owns is replaced.
|
|
146
|
+
*/
|
|
147
|
+
async function writeLockDocument(path, document) {
|
|
148
|
+
const skills = {};
|
|
149
|
+
for (const name of Object.keys(document.skills).sort()) skills[name] = document.skills[name];
|
|
150
|
+
await writeFileAtomically(path, `${JSON.stringify({
|
|
151
|
+
...document,
|
|
152
|
+
skills
|
|
153
|
+
}, null, 2)}\n`);
|
|
154
|
+
}
|
|
155
|
+
/** Records a project install in `<cwd>/skills-lock.json`. */
|
|
156
|
+
async function recordProjectInstall(cwd, install) {
|
|
157
|
+
const path = projectLockPath(cwd);
|
|
158
|
+
const read = await readLockDocument(path, 2);
|
|
159
|
+
if (read.status === "unusable") return {
|
|
160
|
+
status: "skipped",
|
|
161
|
+
reason: `left ${path} alone: ${read.reason}`
|
|
162
|
+
};
|
|
163
|
+
const document = read.status === "ok" ? read.document : {
|
|
164
|
+
version: 2,
|
|
165
|
+
skills: {}
|
|
166
|
+
};
|
|
167
|
+
const existing = document.skills[install.name];
|
|
168
|
+
document.skills[install.name] = {
|
|
169
|
+
...existing,
|
|
170
|
+
...downloadFields(install),
|
|
171
|
+
computedHash: install.computedHash,
|
|
172
|
+
installMode: install.copy ? "copy" : void 0
|
|
173
|
+
};
|
|
174
|
+
await writeLockDocument(path, document);
|
|
175
|
+
return { status: "written" };
|
|
176
|
+
}
|
|
177
|
+
/** Records a global install in the user-level lock file. */
|
|
178
|
+
async function recordGlobalInstall(env, install, now) {
|
|
179
|
+
const path = globalLockPath(env);
|
|
180
|
+
const read = await readLockDocument(path, 3);
|
|
181
|
+
if (read.status === "unusable") return {
|
|
182
|
+
status: "skipped",
|
|
183
|
+
reason: `left ${path} alone: ${read.reason}`
|
|
184
|
+
};
|
|
185
|
+
const document = read.status === "ok" ? read.document : {
|
|
186
|
+
version: 3,
|
|
187
|
+
skills: {}
|
|
188
|
+
};
|
|
189
|
+
const existing = document.skills[install.name];
|
|
190
|
+
const timestamp = now.toISOString();
|
|
191
|
+
document.skills[install.name] = {
|
|
192
|
+
...existing,
|
|
193
|
+
...downloadFields(install),
|
|
194
|
+
skillFolderHash: install.computedHash,
|
|
195
|
+
installedAt: typeof existing?.installedAt === "string" ? existing.installedAt : timestamp,
|
|
196
|
+
updatedAt: timestamp,
|
|
197
|
+
installMode: install.copy ? "copy" : void 0
|
|
198
|
+
};
|
|
199
|
+
await writeLockDocument(path, document);
|
|
200
|
+
return { status: "written" };
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* The fields `npx skills` writes for a direct download source. `source` and
|
|
204
|
+
* `sourceUrl` both carry the Content URL: it is the exact ZIP the install
|
|
205
|
+
* consumed, so `npx skills update` can re-fetch and compare it.
|
|
206
|
+
*/
|
|
207
|
+
function downloadFields(install) {
|
|
208
|
+
return {
|
|
209
|
+
source: install.contentUrl,
|
|
210
|
+
sourceUrl: install.contentUrl,
|
|
211
|
+
sourceType: "download",
|
|
212
|
+
skillPath: install.skillPath
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/** The Hangar Content URL for one Capability Version. */
|
|
216
|
+
function hangarContentUrl(server, capabilityId, version) {
|
|
217
|
+
return `${server}/api/v1/capabilities/${encodeURIComponent(capabilityId)}/versions/${encodeURIComponent(version)}/content`;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Reads a Hangar Content URL recorded in a lock entry. Only URLs under the
|
|
221
|
+
* configured server match: an entry another installation wrote belongs to
|
|
222
|
+
* that installation, not this one.
|
|
223
|
+
*/
|
|
224
|
+
function parseHangarContentUrl(sourceUrl, server) {
|
|
225
|
+
if (!sourceUrl) return null;
|
|
226
|
+
const prefix = `${server}/api/v1/capabilities/`;
|
|
227
|
+
if (!sourceUrl.startsWith(prefix)) return null;
|
|
228
|
+
const match = /^([^/]+)\/versions\/([^/]+)\/content$/.exec(sourceUrl.slice(prefix.length));
|
|
229
|
+
if (!match) return null;
|
|
230
|
+
return {
|
|
231
|
+
capabilityId: decodeURIComponent(match[1]),
|
|
232
|
+
version: decodeURIComponent(match[2])
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The portable content hash `npx skills` records as `computedHash` (v2) and
|
|
237
|
+
* `skillFolderHash` (v3): files sorted by the UTF-8 bytes of their paths, each
|
|
238
|
+
* framed as `path NUL length NUL content`, with CRLF normalized to LF in text
|
|
239
|
+
* files and build metadata excluded. It must match byte for byte, or
|
|
240
|
+
* `npx skills list` reports the install as modified.
|
|
241
|
+
*/
|
|
242
|
+
function portableSkillHash(entries) {
|
|
243
|
+
const files = entries.filter((entry) => !isExcludedPath(entry.path)).map((entry) => ({
|
|
244
|
+
path: Buffer.from(entry.path.split("\\").join("/"), "utf8"),
|
|
245
|
+
content: normalizeLineEndings(entry.data)
|
|
246
|
+
})).sort((left, right) => Buffer.compare(left.path, right.path));
|
|
247
|
+
const hash = createHash("sha256");
|
|
248
|
+
for (const file of files) {
|
|
249
|
+
hash.update(file.path);
|
|
250
|
+
hash.update("\0");
|
|
251
|
+
hash.update(String(file.content.length));
|
|
252
|
+
hash.update("\0");
|
|
253
|
+
hash.update(file.content);
|
|
254
|
+
}
|
|
255
|
+
return hash.digest("hex");
|
|
256
|
+
}
|
|
257
|
+
/** npx skills' exclusions, applied to the same relative paths. */
|
|
258
|
+
function isExcludedPath(path) {
|
|
259
|
+
const parts = path.split(/[\\/]/).filter(Boolean);
|
|
260
|
+
return parts.some((part, index) => isExcluded(part, index < parts.length - 1));
|
|
261
|
+
}
|
|
262
|
+
function isExcluded(name, isDirectory) {
|
|
263
|
+
if (name === "metadata.json") return true;
|
|
264
|
+
return isDirectory && (name === ".git" || name === "__pycache__" || name === "__pypackages__");
|
|
265
|
+
}
|
|
266
|
+
/** Text files get CRLF replaced by LF; a NUL byte marks a binary file. */
|
|
267
|
+
function normalizeLineEndings(content) {
|
|
268
|
+
if (content.includes(0) || !content.includes("\r\n")) return content;
|
|
269
|
+
return Buffer.from(content.toString("latin1").replaceAll("\r\n", "\n"), "latin1");
|
|
270
|
+
}
|
|
271
|
+
function isNotFound(err) {
|
|
272
|
+
return err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
273
|
+
}
|
|
274
|
+
function errorText(err) {
|
|
275
|
+
return err instanceof Error ? err.message : String(err);
|
|
276
|
+
}
|
|
91
277
|
// src/skills/resolve.ts
|
|
92
278
|
/** Parses `<name>` or `<namespace>/<name>`, with an optional `@YYYY.MM.DD.N`. */
|
|
93
279
|
function parseSkillPath(raw) {
|
|
@@ -265,7 +451,8 @@ function readSkillPackage(buffer) {
|
|
|
265
451
|
if (!manifest) throw invalid$1("the package has no SKILL.md");
|
|
266
452
|
return {
|
|
267
453
|
entries,
|
|
268
|
-
manifest
|
|
454
|
+
manifest,
|
|
455
|
+
skillPath: `${root}SKILL.md`
|
|
269
456
|
};
|
|
270
457
|
}
|
|
271
458
|
// src/skills/zip-write.ts
|
|
@@ -877,6 +1064,27 @@ function installSummary(outcome, canonicalPath) {
|
|
|
877
1064
|
if (failed.length > 0) lines.push(` failed: ${failed.map((target) => target.path).join(", ")}`);
|
|
878
1065
|
return lines.join("\n");
|
|
879
1066
|
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Records the install in the lock file `npx skills` reads, so either tool can
|
|
1069
|
+
* list, update and remove it. A lock file another schema version owns is left
|
|
1070
|
+
* alone, and a write failure only warns: the skill is already on disk.
|
|
1071
|
+
*/
|
|
1072
|
+
async function recordLockEntry(ctx, connection, name, capabilityId, version, pkg, global, copy) {
|
|
1073
|
+
const install = {
|
|
1074
|
+
name,
|
|
1075
|
+
contentUrl: hangarContentUrl(connection.server, capabilityId, version),
|
|
1076
|
+
skillPath: pkg.skillPath,
|
|
1077
|
+
computedHash: portableSkillHash(pkg.entries),
|
|
1078
|
+
copy
|
|
1079
|
+
};
|
|
1080
|
+
try {
|
|
1081
|
+
const outcome = global ? await recordGlobalInstall(ctx.env, install, /* @__PURE__ */ new Date()) : await recordProjectInstall(process.cwd(), install);
|
|
1082
|
+
if (outcome.status === "skipped") ctx.summary(`warning: ${outcome.reason}`);
|
|
1083
|
+
} catch (err) {
|
|
1084
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1085
|
+
ctx.summary(`warning: could not update the skills lock file: ${detail}`);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
880
1088
|
async function addOne(ctx, connection, parsed, destName, agents, dir, global, copy, state, resolved) {
|
|
881
1089
|
const capability = resolved ?? await resolveSkill(connection, parsed);
|
|
882
1090
|
const version = resolveVersion(capability, parsed);
|
|
@@ -910,6 +1118,7 @@ async function addOne(ctx, connection, parsed, destName, agents, dir, global, co
|
|
|
910
1118
|
const declaredName = frontmatterName(pkg.manifest);
|
|
911
1119
|
if (declaredName !== void 0 && declaredName !== destName) ctx.summary(`warning: SKILL.md declares name ${JSON.stringify(declaredName)} but the directory is ${JSON.stringify(destName)}`);
|
|
912
1120
|
const outcome = await install(ctx, plan, pkg.entries);
|
|
1121
|
+
if (dir === void 0 && outcome.targets.some((target) => target.status === "installed")) await recordLockEntry(ctx, connection, destName, capability.id, version, pkg, global, copy);
|
|
913
1122
|
const verb = outcome.failure ? "Partially installed" : "Installed";
|
|
914
1123
|
const note = ctx.io.prompts?.note;
|
|
915
1124
|
if (!ctx.flags.json && !ctx.flags.quiet && ctx.io.stderr.isTTY && note) note(installSummary(outcome, canonicalPath), `${verb} ${capability.namespace}/${capability.name}@${version}`);
|
|
@@ -1109,4 +1318,50 @@ async function add(ctx, argv) {
|
|
|
1109
1318
|
if (!ctx.flags.json && !ctx.flags.dryRun && !ctx.flags.quiet && !failed && ctx.io.stderr.isTTY) ctx.io.prompts?.outro?.("Done! Review skills before use; they run with full agent permissions.");
|
|
1110
1319
|
return failed ? 1 : 0;
|
|
1111
1320
|
}
|
|
1112
|
-
|
|
1321
|
+
/**
|
|
1322
|
+
* Reinstalls the skills a lock file records. Only entries this CLI wrote —
|
|
1323
|
+
* a Hangar Content URL under the configured server — are restored; entries
|
|
1324
|
+
* another tool owns are reported and left untouched. The normal `add` path
|
|
1325
|
+
* performs the install, so agent selection, `--copy` and scope behave alike.
|
|
1326
|
+
*/
|
|
1327
|
+
async function restore(ctx, argv) {
|
|
1328
|
+
const global = argv.global === true;
|
|
1329
|
+
const connection = await resolveConnection(ctx.flags, ctx.env);
|
|
1330
|
+
const path = global ? globalLockPath(ctx.env) : projectLockPath(process.cwd());
|
|
1331
|
+
const shown = displayPath(path, process.cwd());
|
|
1332
|
+
const read = await readLockDocument(path, global ? 3 : 2);
|
|
1333
|
+
if (read.status === "absent") {
|
|
1334
|
+
ctx.detail(`No lock file at ${shown}.`);
|
|
1335
|
+
return 0;
|
|
1336
|
+
}
|
|
1337
|
+
if (read.status === "unusable") throw usage("Cannot read the lock file", `${shown}: ${read.reason}`);
|
|
1338
|
+
const names = positionalPaths(argv.names);
|
|
1339
|
+
const wanted = names.length > 0 ? new Set(names) : null;
|
|
1340
|
+
const refs = [];
|
|
1341
|
+
const skipped = [];
|
|
1342
|
+
for (const name of Object.keys(read.document.skills).sort()) {
|
|
1343
|
+
if (wanted !== null && !wanted.has(name)) continue;
|
|
1344
|
+
const entry = read.document.skills[name];
|
|
1345
|
+
const reference = parseHangarContentUrl(entry.sourceUrl ?? entry.source, connection.server);
|
|
1346
|
+
if (reference === null) {
|
|
1347
|
+
skipped.push(name);
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
const capability = await lookupCapabilityById(connection, reference.capabilityId);
|
|
1351
|
+
if (capability === null || capability.kind !== "skill" || capability.name !== name) {
|
|
1352
|
+
skipped.push(name);
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1355
|
+
refs.push(`${capability.namespace}/${capability.name}@${reference.version}`);
|
|
1356
|
+
}
|
|
1357
|
+
if (skipped.length > 0) ctx.summary(`warning: skipped ${skipped.join(", ")}: not installed from ${connection.server}`);
|
|
1358
|
+
if (refs.length === 0) {
|
|
1359
|
+
ctx.detail(`Nothing to restore from ${shown}.`);
|
|
1360
|
+
return 0;
|
|
1361
|
+
}
|
|
1362
|
+
return add(ctx, {
|
|
1363
|
+
...argv,
|
|
1364
|
+
paths: refs
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
export { add, publish, restore };
|