hngr 0.3.0 → 0.5.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 +13 -3
- package/dist/{auth-CgBUQUQn.mjs → auth-ByGgd_Yn.mjs} +49 -11
- package/dist/{catalog-DbGLqQ8a.mjs → catalog-Bv304SXJ.mjs} +412 -20
- package/dist/{commands-BHXSZ04F.mjs → commands-CLqLwQHY.mjs} +1 -1
- package/dist/{http-2HWxLIOq.mjs → http-DgHX7Fux.mjs} +13 -7
- package/dist/index.mjs +136 -21
- package/dist/{mcp-b6bE3ZRt.mjs → mcp-B0sbQABp.mjs} +27 -50
- package/dist/{pagination-BbKLKSw3.mjs → pagination-BXyFxGYA.mjs} +1 -1
- package/dist/{publish-EUqgdp0G.mjs → scope-DVfDeQi2.mjs} +37 -3
- package/dist/{skills-XDbBCC5f.mjs → skills-ZFmFyZtw.mjs} +80 -70
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { _ as
|
|
1
|
+
import { C as HngrError, b as writeDirectoryAtomically, g as resolveConnection, r as apiRequest } from "./http-DgHX7Fux.mjs";
|
|
2
|
+
import { _ as resolveCapability, a as errorBody, c as runBatch, f as homeDir, g as parseCapabilityPath, h as SKILL_KIND, i as runPublish, l as stringValues, n as explicitName, o as failedResult, p as downloadContent, r as readPublishFlags, s as positionalPaths, t as resolveInstallScope, u as AGENTS, v as resolveCapabilityVersion } from "./scope-DVfDeQi2.mjs";
|
|
3
3
|
import { lstat, mkdir, readFile, readdir, rename, rm, symlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
@@ -92,13 +92,6 @@ function downloadSkillPackage(connection, capabilityId, version, signal) {
|
|
|
92
92
|
async function pathExists(path) {
|
|
93
93
|
return lstat(path).then(() => true, () => false);
|
|
94
94
|
}
|
|
95
|
-
function targetExistsError(path) {
|
|
96
|
-
return new HngrError({
|
|
97
|
-
type: "io",
|
|
98
|
-
title: `Target already exists: ${path}`,
|
|
99
|
-
detail: "Pass --force to replace it."
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
95
|
async function writeEntries(dest, entries) {
|
|
103
96
|
await mkdir(dest, { recursive: true });
|
|
104
97
|
for (const entry of entries) {
|
|
@@ -114,25 +107,22 @@ async function writeEntries(dest, entries) {
|
|
|
114
107
|
}
|
|
115
108
|
/**
|
|
116
109
|
* Writes a skill through "temp directory → validate → rename": the target is
|
|
117
|
-
* either absent or complete, never half-written.
|
|
118
|
-
* directory
|
|
110
|
+
* either absent or complete, never half-written. Re-installing replaces the
|
|
111
|
+
* previous directory, with the old one moved aside so a failed swap puts it
|
|
112
|
+
* back.
|
|
119
113
|
*/
|
|
120
|
-
async function installAtomically(dest, entries
|
|
121
|
-
await writeDirectoryAtomically(dest, (temp) => writeEntries(temp, entries)
|
|
122
|
-
force,
|
|
123
|
-
existsError: () => targetExistsError(dest)
|
|
124
|
-
});
|
|
114
|
+
async function installAtomically(dest, entries) {
|
|
115
|
+
await writeDirectoryAtomically(dest, (temp) => writeEntries(temp, entries));
|
|
125
116
|
}
|
|
126
117
|
/**
|
|
127
118
|
* Creates a relative symlink to the canonical directory, falling back to a
|
|
128
119
|
* copy when the platform refuses the link (Windows without Developer Mode).
|
|
129
120
|
* Windows prefers a junction, which does not need elevated privileges; its
|
|
130
121
|
* target must be absolute. A temp link plus rename replaces an existing link
|
|
131
|
-
* atomically.
|
|
122
|
+
* atomically; re-installing replaces an existing directory or link.
|
|
132
123
|
*/
|
|
133
|
-
async function linkAtomically(canonical, target, entries,
|
|
124
|
+
async function linkAtomically(canonical, target, entries, options = {}) {
|
|
134
125
|
const exists = await pathExists(target);
|
|
135
|
-
if (exists && !force) throw targetExistsError(target);
|
|
136
126
|
await mkdir(dirname(target), { recursive: true });
|
|
137
127
|
const temp = resolve(dirname(target), `.hngr-${basename(target)}-${randomUUID()}.link`);
|
|
138
128
|
const windows = (options.platform ?? process.platform) === "win32";
|
|
@@ -150,7 +140,7 @@ async function linkAtomically(canonical, target, entries, force, options = {}) {
|
|
|
150
140
|
return { mode: "symlink" };
|
|
151
141
|
} catch {
|
|
152
142
|
await rm(temp, { force: true }).catch(() => {});
|
|
153
|
-
await installAtomically(target, entries
|
|
143
|
+
await installAtomically(target, entries);
|
|
154
144
|
return {
|
|
155
145
|
mode: "copy",
|
|
156
146
|
warning: `warning: could not create a link at ${target}; copied instead`
|
|
@@ -158,8 +148,8 @@ async function linkAtomically(canonical, target, entries, force, options = {}) {
|
|
|
158
148
|
}
|
|
159
149
|
}
|
|
160
150
|
/** Copies one target directory atomically, without a canonical store. */
|
|
161
|
-
async function copyAtomically(target, entries
|
|
162
|
-
await installAtomically(target, entries
|
|
151
|
+
async function copyAtomically(target, entries) {
|
|
152
|
+
await installAtomically(target, entries);
|
|
163
153
|
}
|
|
164
154
|
// src/skills/resolve.ts
|
|
165
155
|
/** Parses `<name>` or `<namespace>/<name>`, with an optional `@YYYY.MM.DD.N`. */
|
|
@@ -562,17 +552,19 @@ function failureResult(parsed, err, version) {
|
|
|
562
552
|
canonicalPath: null
|
|
563
553
|
};
|
|
564
554
|
}
|
|
565
|
-
/**
|
|
555
|
+
/** Agent prompts need a TTY on both ends, a multi-select prompter, and human output mode. */
|
|
566
556
|
function canPrompt(ctx) {
|
|
567
|
-
return ctx.io.stdin.isTTY && ctx.io.stdout.isTTY && ctx.io.prompts?.multiselect !== void 0;
|
|
557
|
+
return !ctx.flags.json && ctx.io.stdin.isTTY && ctx.io.stdout.isTTY && ctx.io.prompts?.multiselect !== void 0;
|
|
568
558
|
}
|
|
569
559
|
function noAgentSelectionError(examplePath, global) {
|
|
570
560
|
return usage("No agent target selected", `Pass --agent <id> (repeatable) or --all. For example: hngr skills add ${examplePath} --agent claude-code${global ? " --global" : ""}`);
|
|
571
561
|
}
|
|
562
|
+
const ALL_AGENTS = "__all__";
|
|
572
563
|
/**
|
|
573
|
-
* One multi-select for
|
|
574
|
-
*
|
|
575
|
-
*
|
|
564
|
+
* One multi-select for the agents that keep their own skills directory.
|
|
565
|
+
* `.agents` is always installed, so universal agents never appear here.
|
|
566
|
+
* Detection only preselects; a non-TTY invocation never consults the
|
|
567
|
+
* filesystem and must pass --agent or --all.
|
|
576
568
|
*/
|
|
577
569
|
async function selectAgents(ctx, argv, examplePath, global) {
|
|
578
570
|
const requested = stringValues(argv.agent);
|
|
@@ -582,26 +574,28 @@ async function selectAgents(ctx, argv, examplePath, global) {
|
|
|
582
574
|
return AGENT_TARGETS.filter((agent) => requested.includes(agent.id));
|
|
583
575
|
}
|
|
584
576
|
if (argv.all === true) return [...AGENT_TARGETS];
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
577
|
+
const multiselect = ctx.io.prompts?.multiselect;
|
|
578
|
+
if (!canPrompt(ctx) || !multiselect) throw noAgentSelectionError(examplePath, global);
|
|
579
|
+
const cwd = process.cwd();
|
|
580
|
+
const linkAgents = AGENT_TARGETS.filter((agent) => agent.link);
|
|
581
|
+
const options = [{
|
|
582
|
+
value: ALL_AGENTS,
|
|
583
|
+
label: "All agents",
|
|
584
|
+
hint: "select every agent below"
|
|
585
|
+
}];
|
|
586
|
+
const initialValues = [];
|
|
587
|
+
for (const agent of linkAgents) {
|
|
588
|
+
const dir = targetSkillsDir(agent, cwd, ctx.env, global);
|
|
589
|
+
options.push({
|
|
590
|
+
value: agent.id,
|
|
591
|
+
label: agent.id,
|
|
592
|
+
hint: displayPath(dir, cwd)
|
|
593
|
+
});
|
|
594
|
+
if (await pathExists(dirname(dir))) initialValues.push(agent.id);
|
|
603
595
|
}
|
|
604
|
-
|
|
596
|
+
const selected = await multiselect("Install for another agent", options, initialValues, ".agents/skills is always installed; press Enter to skip");
|
|
597
|
+
const chosen = selected.includes(ALL_AGENTS) ? new Set(linkAgents.map((agent) => agent.id)) : new Set(selected);
|
|
598
|
+
return linkAgents.filter((agent) => chosen.has(agent.id));
|
|
605
599
|
}
|
|
606
600
|
function frontmatterName(manifest) {
|
|
607
601
|
const text = manifest.toString("utf8");
|
|
@@ -657,16 +651,12 @@ function planInstall(cwd, env, destName, agents, options) {
|
|
|
657
651
|
async function install(ctx, plan, entries) {
|
|
658
652
|
const cwd = process.cwd();
|
|
659
653
|
const display = (path) => displayPath(path, cwd);
|
|
660
|
-
|
|
661
|
-
if (!ctx.flags.force) {
|
|
662
|
-
for (const path of destinations) if (await pathExists(path)) throw targetExistsError(display(path));
|
|
663
|
-
}
|
|
664
|
-
if (plan.canonical) await installAtomically(plan.canonical, entries, ctx.flags.force);
|
|
654
|
+
if (plan.canonical) await installAtomically(plan.canonical, entries);
|
|
665
655
|
const installed = [];
|
|
666
656
|
let failure = null;
|
|
667
657
|
for (const target of plan.targets) try {
|
|
668
658
|
if (target.mode === "copy" || !plan.canonical) {
|
|
669
|
-
await copyAtomically(target.path, entries
|
|
659
|
+
await copyAtomically(target.path, entries);
|
|
670
660
|
installed.push({
|
|
671
661
|
agent: target.agent,
|
|
672
662
|
path: display(target.path),
|
|
@@ -675,7 +665,7 @@ async function install(ctx, plan, entries) {
|
|
|
675
665
|
});
|
|
676
666
|
continue;
|
|
677
667
|
}
|
|
678
|
-
const result = await linkAtomically(plan.canonical, target.path, entries
|
|
668
|
+
const result = await linkAtomically(plan.canonical, target.path, entries);
|
|
679
669
|
if (result.warning) ctx.summary(result.warning);
|
|
680
670
|
installed.push({
|
|
681
671
|
agent: target.agent,
|
|
@@ -697,6 +687,26 @@ async function install(ctx, plan, entries) {
|
|
|
697
687
|
failure
|
|
698
688
|
};
|
|
699
689
|
}
|
|
690
|
+
function withDot(path) {
|
|
691
|
+
return path.startsWith("/") ? path : `./${path}`;
|
|
692
|
+
}
|
|
693
|
+
/** The boxed result shown after an interactive install. */
|
|
694
|
+
function installSummary(outcome, canonicalPath) {
|
|
695
|
+
const lines = [];
|
|
696
|
+
const installed = outcome.targets.filter((target) => target.status === "installed");
|
|
697
|
+
if (canonicalPath) {
|
|
698
|
+
lines.push(`✓ ${withDot(canonicalPath)}`);
|
|
699
|
+
const universal = AGENT_TARGETS.filter((agent) => !agent.link).map((agent) => agent.id);
|
|
700
|
+
if (universal.length > 0) lines.push(` universal: ${universal.join(", ")}`);
|
|
701
|
+
} else for (const target of installed) lines.push(`✓ ${withDot(target.path)}`);
|
|
702
|
+
const symlinked = installed.filter((target) => target.mode === "symlink" && target.agent).map((target) => target.agent);
|
|
703
|
+
const copied = installed.filter((target) => target.mode === "copy" && target.agent).map((target) => target.agent);
|
|
704
|
+
if (symlinked.length > 0) lines.push(` symlinked: ${symlinked.join(", ")}`);
|
|
705
|
+
if (copied.length > 0) lines.push(` copied: ${copied.join(", ")}`);
|
|
706
|
+
const failed = outcome.targets.filter((target) => target.status === "failed");
|
|
707
|
+
if (failed.length > 0) lines.push(` failed: ${failed.map((target) => target.path).join(", ")}`);
|
|
708
|
+
return lines.join("\n");
|
|
709
|
+
}
|
|
700
710
|
async function addOne(ctx, connection, parsed, destName, agents, dir, global, copy, state) {
|
|
701
711
|
const capability = await resolveSkill(connection, parsed);
|
|
702
712
|
const version = resolveVersion(capability, parsed);
|
|
@@ -708,10 +718,9 @@ async function addOne(ctx, connection, parsed, destName, agents, dir, global, co
|
|
|
708
718
|
});
|
|
709
719
|
const canonicalPath = plan.canonical ? displayPath(plan.canonical, process.cwd()) : null;
|
|
710
720
|
if (ctx.flags.dryRun) {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
}
|
|
714
|
-
ctx.summary(`resolved ${parsed.namespace}/${parsed.name} to version ${version}`);
|
|
721
|
+
ctx.detail(`dry-run: would install ${parsed.namespace}/${parsed.name}@${version}`);
|
|
722
|
+
if (canonicalPath) ctx.detail(` ${canonicalPath}`);
|
|
723
|
+
for (const target of plan.targets) ctx.detail(` ${displayPath(target.path, process.cwd())} (${target.mode})`);
|
|
715
724
|
return { result: {
|
|
716
725
|
name: capability.name,
|
|
717
726
|
namespace: capability.namespace,
|
|
@@ -727,19 +736,18 @@ async function addOne(ctx, connection, parsed, destName, agents, dir, global, co
|
|
|
727
736
|
error: null
|
|
728
737
|
} };
|
|
729
738
|
}
|
|
730
|
-
const
|
|
731
|
-
const pkg = readSkillPackage(download.bytes);
|
|
732
|
-
ctx.summary(`resolved ${parsed.namespace}/${parsed.name} to version ${version}`);
|
|
739
|
+
const pkg = readSkillPackage((await downloadSkillPackage(connection, capability.id, version)).bytes);
|
|
733
740
|
const declaredName = frontmatterName(pkg.manifest);
|
|
734
741
|
if (declaredName !== void 0 && declaredName !== destName) ctx.summary(`warning: SKILL.md declares name ${JSON.stringify(declaredName)} but the directory is ${JSON.stringify(destName)}`);
|
|
735
742
|
const outcome = await install(ctx, plan, pkg.entries);
|
|
736
|
-
const
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
+
const verb = outcome.failure ? "Partially installed" : "Installed";
|
|
744
|
+
const note = ctx.io.prompts?.note;
|
|
745
|
+
if (!ctx.flags.json && !ctx.flags.quiet && ctx.io.stderr.isTTY && note) note(installSummary(outcome, canonicalPath), `${verb} ${capability.namespace}/${capability.name}@${version}`);
|
|
746
|
+
else {
|
|
747
|
+
ctx.progress(`${verb.toLowerCase()} ${capability.namespace}/${capability.name}@${version}`);
|
|
748
|
+
if (canonicalPath) ctx.progress(` ${canonicalPath}`);
|
|
749
|
+
for (const target of outcome.targets) ctx.progress(target.status === "failed" ? ` failed ${target.path}` : ` ${target.path} (${target.mode})`);
|
|
750
|
+
}
|
|
743
751
|
return { result: {
|
|
744
752
|
name: capability.name,
|
|
745
753
|
namespace: capability.namespace,
|
|
@@ -849,12 +857,13 @@ async function add(ctx, argv) {
|
|
|
849
857
|
const dir = stringFlag(argv, "dir");
|
|
850
858
|
const as = stringFlag(argv, "as");
|
|
851
859
|
if (as !== void 0 && (as.includes("/") || as.includes("\\") || as === "." || as === "..")) throw usage(`Invalid --as name ${JSON.stringify(as)}`, "Pass a single directory name.");
|
|
852
|
-
const
|
|
860
|
+
const requestedGlobal = argv.global === true;
|
|
853
861
|
const copy = argv.copy === true;
|
|
854
|
-
if (dir !== void 0 &&
|
|
862
|
+
if (dir !== void 0 && requestedGlobal) throw usage("--dir cannot be combined with --global");
|
|
855
863
|
if (dir !== void 0 && (stringValues(argv.agent).length > 0 || argv.all === true)) throw usage("--dir cannot be combined with --agent or --all");
|
|
856
864
|
if (as !== void 0) assertNotReserved(as);
|
|
857
865
|
for (const parsed of parsedPaths) assertNotReserved(as ?? parsed.name);
|
|
866
|
+
const global = dir === void 0 ? await resolveInstallScope(ctx, requestedGlobal) === "global" : false;
|
|
858
867
|
const agents = dir === void 0 ? await selectAgents(ctx, argv, rawPaths[0] ?? "", global) : [];
|
|
859
868
|
const connection = await resolveConnection(ctx.flags, ctx.env);
|
|
860
869
|
const { results, failed } = await runBatch({
|
|
@@ -865,7 +874,8 @@ async function add(ctx, argv) {
|
|
|
865
874
|
},
|
|
866
875
|
failure: failureResult
|
|
867
876
|
});
|
|
868
|
-
ctx.writePayload({ results });
|
|
877
|
+
if (ctx.flags.json || !ctx.io.stdout.isTTY) ctx.writePayload({ results });
|
|
878
|
+
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.");
|
|
869
879
|
return failed ? 1 : 0;
|
|
870
880
|
}
|
|
871
881
|
export { add, publish };
|