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.
@@ -1,4 +1,4 @@
1
- import { t as commandCatalog } from "./catalog-CmtREMFr.mjs";
1
+ import { t as commandCatalog } from "./catalog-CfpeEevk.mjs";
2
2
  // src/commands/commands.ts
3
3
  function run(ctx) {
4
4
  const catalog = commandCatalog();
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-CmtREMFr.mjs";
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.9.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-Bjui8-Nt.mjs")).run(ctx)
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-CqHGxo_M.mjs")).add(ctx, matchedArgv)
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-CqHGxo_M.mjs")).publish(ctx, matchedArgv)
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-C-oJ6J5B.mjs")).add(ctx, matchedArgv)
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-C-oJ6J5B.mjs")).publish(ctx, matchedArgv)
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 { S as parseCapabilityPath, T as resolveCapabilityVersion, _ 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, w as resolveCapability } from "./scope-8BZjgIXy.mjs";
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 { parseCapabilityTarget as C, parseCapabilityPath as S, resolveCapabilityVersion 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, resolveCapability as w, listCapabilityNamespace as x, SKILL_KIND as y };
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 parseCapabilityTarget, S as parseCapabilityPath, T as resolveCapabilityVersion, _ 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 resolveCapability, x as listCapabilityNamespace, y as SKILL_KIND } from "./scope-8BZjgIXy.mjs";
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
- export { add, publish };
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 };