hngr 0.3.0 → 0.4.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/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { a as kebabCase, i as normalizeCommandName, n as COMMANDS, o as COMMAND_ALIASES, r as findCommand, t as commandCatalog } from "./catalog-DbGLqQ8a.mjs";
2
- import { C as renderError, S as HngrError, c as redactUrlForDisplay, g as resolveConnection, i as buildRequestUrl, o as redactQuery, r as apiRequest, s as redactSensitive, u as DEFAULT_SERVER_URL, x as CancelledError } from "./http-2HWxLIOq.mjs";
3
- import { t as collectPages } from "./pagination-BbKLKSw3.mjs";
1
+ import { a as kebabCase, i as normalizeCommandName, n as COMMANDS, o as COMMAND_ALIASES, r as findCommand, t as commandCatalog } from "./catalog-Bv304SXJ.mjs";
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-DgHX7Fux.mjs";
3
+ import { t as collectPages } from "./pagination-BXyFxGYA.mjs";
4
4
  import { createRequire } from "node:module";
5
5
  import { notStrictEqual, strictEqual } from "assert";
6
6
  import { basename, dirname, extname, join, normalize, relative, resolve } from "path";
@@ -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.3.0";
5282
+ var version = "0.4.0";
5283
5283
  // src/generated/completion.ts
5284
5284
  function isCompletionShell(value) {
5285
5285
  return value === "bash" || value === "zsh" || value === "fish";
@@ -6499,6 +6499,32 @@ let u$1 = class u extends y {
6499
6499
  });
6500
6500
  }
6501
6501
  };
6502
+ let n$1 = class n extends y {
6503
+ options;
6504
+ cursor = 0;
6505
+ get _selectedValue() {
6506
+ return this.options[this.cursor];
6507
+ }
6508
+ changeValue() {
6509
+ const e = this._selectedValue;
6510
+ this.value = e === void 0 ? void 0 : e.value;
6511
+ }
6512
+ constructor(e) {
6513
+ super(e, false), this.options = e.options;
6514
+ const o = this.options.findIndex(({ value: s }) => s === e.initialValue), t = o === -1 ? 0 : o;
6515
+ this.cursor = this.options[t]?.disabled ? findCursor(t, 1, this.options) : t, this.changeValue(), this.on("cursor", (s) => {
6516
+ switch (s) {
6517
+ case "left":
6518
+ case "up":
6519
+ this.cursor = findCursor(this.cursor, -1, this.options);
6520
+ break;
6521
+ case "down":
6522
+ case "right": this.cursor = findCursor(this.cursor, 1, this.options);
6523
+ }
6524
+ this.changeValue();
6525
+ });
6526
+ }
6527
+ };
6502
6528
  var n = class extends y {
6503
6529
  get userInputWithCursor() {
6504
6530
  if (this.state === "submit") return this.userInput;
@@ -6534,6 +6560,8 @@ const S_STEP_ERROR = unicodeOr("▲", "x");
6534
6560
  const S_STEP_SUBMIT = unicodeOr("◇", "o");
6535
6561
  const S_BAR = unicodeOr("│", "|");
6536
6562
  const S_BAR_END = unicodeOr("└", "—");
6563
+ const S_RADIO_ACTIVE = unicodeOr("●", ">");
6564
+ const S_RADIO_INACTIVE = unicodeOr("○", " ");
6537
6565
  const S_CHECKBOX_ACTIVE = unicodeOr("◻", "[•]");
6538
6566
  const S_CHECKBOX_SELECTED = unicodeOr("◼", "[+]");
6539
6567
  const S_CHECKBOX_INACTIVE = unicodeOr("◻", "[ ]");
@@ -6720,7 +6748,64 @@ ${r ? styleText("cyan", S_BAR_END) : ""}
6720
6748
  }
6721
6749
  }
6722
6750
  }).prompt();
6723
- `${styleText("dim", "↑/↓")}`, `${styleText("dim", "Enter:")}`;
6751
+ const SELECT_INSTRUCTIONS = [`${styleText("dim", "↑/↓")} to navigate`, `${styleText("dim", "Enter:")} confirm`];
6752
+ const c = (t, o) => t.includes(`
6753
+ `) ? t.split(`
6754
+ `).map((d) => o(d)).join(`
6755
+ `) : o(t);
6756
+ const select = (t) => {
6757
+ const o = (n, m) => {
6758
+ if (n === void 0) return "";
6759
+ const s = n.label ?? String(n.value);
6760
+ switch (m) {
6761
+ case "disabled": return `${styleText("gray", S_RADIO_INACTIVE)} ${c(s, (i) => styleText("gray", i))}${n.hint ? ` ${styleText("dim", `(${n.hint ?? "disabled"})`)}` : ""}`;
6762
+ case "selected": return `${c(s, (i) => styleText("dim", i))}`;
6763
+ case "active": return `${styleText("green", S_RADIO_ACTIVE)} ${s}${n.hint ? ` ${styleText("dim", `(${n.hint})`)}` : ""}`;
6764
+ case "cancelled": return `${c(s, (i) => styleText(["strikethrough", "dim"], i))}`;
6765
+ default: return `${styleText("dim", S_RADIO_INACTIVE)} ${c(s, (i) => styleText("dim", i))}`;
6766
+ }
6767
+ }, d = t.showInstructions ?? true;
6768
+ return new n$1({
6769
+ options: t.options,
6770
+ signal: t.signal,
6771
+ input: t.input,
6772
+ output: t.output,
6773
+ initialValue: t.initialValue,
6774
+ render() {
6775
+ const n = t.withGuide ?? settings.withGuide, m = `${symbol(this.state)} `, s = `${symbolBar(this.state)} `, i = wrapTextWithPrefix(t.output, t.message, s, m), u = `${n ? `${styleText("gray", S_BAR)}
6776
+ ` : ""}${i}
6777
+ `;
6778
+ switch (this.state) {
6779
+ case "submit": {
6780
+ const r = n ? `${styleText("gray", S_BAR)} ` : "";
6781
+ return `${u}${wrapTextWithPrefix(t.output, o(this.options[this.cursor], "selected"), r)}`;
6782
+ }
6783
+ case "cancel": {
6784
+ const r = n ? `${styleText("gray", S_BAR)} ` : "";
6785
+ return `${u}${wrapTextWithPrefix(t.output, o(this.options[this.cursor], "cancelled"), r)}${n ? `
6786
+ ${styleText("gray", S_BAR)}` : ""}`;
6787
+ }
6788
+ default: {
6789
+ const r = n ? `${styleText("cyan", S_BAR)} ` : "", a = u.split(`
6790
+ `).length, p = d ? formatInstructionFooter(SELECT_INSTRUCTIONS, n) : n ? [styleText("cyan", S_BAR_END)] : [], f = p.join(`
6791
+ `), b = p.length + 1;
6792
+ return `${u}${r}${limitOptions({
6793
+ output: t.output,
6794
+ cursor: this.cursor,
6795
+ options: this.options,
6796
+ maxItems: t.maxItems,
6797
+ columnPadding: r.length,
6798
+ rowPadding: a + b,
6799
+ style: (g, x) => o(g, g.disabled ? "disabled" : x ? "active" : "inactive")
6800
+ }).join(`
6801
+ ${r}`)}
6802
+ ${f}
6803
+ `;
6804
+ }
6805
+ }
6806
+ }
6807
+ }).prompt();
6808
+ };
6724
6809
  `${styleText("gray", S_BAR)}`;
6725
6810
  const text = (t) => new n({
6726
6811
  validate: t.validate,
@@ -6804,6 +6889,15 @@ function clackPrompter(streams = {}) {
6804
6889
  }
6805
6890
  }));
6806
6891
  },
6892
+ async select(message, options, initialValue) {
6893
+ return unwrapPrompt(await select({
6894
+ message,
6895
+ input,
6896
+ output,
6897
+ options,
6898
+ initialValue
6899
+ }));
6900
+ },
6807
6901
  async multiselect(message, options, initialValues = []) {
6808
6902
  return unwrapPrompt(await multiselect({
6809
6903
  message,
@@ -6846,7 +6940,7 @@ const GLOBAL_OPTIONS = {
6846
6940
  force: {
6847
6941
  type: "boolean",
6848
6942
  default: false,
6849
- describe: "Overwrite existing targets; the only way to skip destructive confirmations",
6943
+ describe: "Skip destructive confirmations",
6850
6944
  global: true
6851
6945
  },
6852
6946
  "dry-run": {
@@ -6911,7 +7005,7 @@ function buildParser(onMatched) {
6911
7005
  parser.command("commands", "Print the command catalog", () => {}, (argv) => {
6912
7006
  onMatched({
6913
7007
  argv,
6914
- run: async (ctx) => (await import("./commands-BHXSZ04F.mjs")).run(ctx)
7008
+ run: async (ctx) => (await import("./commands-CLqLwQHY.mjs")).run(ctx)
6915
7009
  });
6916
7010
  });
6917
7011
  parser.command("auth", "Manage credentials", (auth) => {
@@ -6924,35 +7018,40 @@ function buildParser(onMatched) {
6924
7018
  }).conflicts("password", "token"), (argv) => {
6925
7019
  onMatched({
6926
7020
  argv,
6927
- run: async (ctx, matchedArgv) => (await import("./auth-CgBUQUQn.mjs")).login(ctx, matchedArgv)
7021
+ run: async (ctx, matchedArgv) => (await import("./auth-ByGgd_Yn.mjs")).login(ctx, matchedArgv)
6928
7022
  });
6929
7023
  });
6930
7024
  auth.command("logout", "Revoke the stored token and delete the local config", () => {}, (argv) => {
6931
7025
  onMatched({
6932
7026
  argv,
6933
- run: async (ctx) => (await import("./auth-CgBUQUQn.mjs")).logout(ctx)
7027
+ run: async (ctx) => (await import("./auth-ByGgd_Yn.mjs")).logout(ctx)
6934
7028
  });
6935
7029
  });
6936
- auth.command("status", "Show credential source, server, principal and token prefix", (status) => status.option("offline", {
7030
+ auth.command("status", "Show credential source, server, principal, token prefix and audience", (status) => status.option("offline", {
6937
7031
  type: "boolean",
6938
7032
  default: false,
6939
7033
  describe: "Skip the live GET /me check"
6940
7034
  }), (argv) => {
6941
7035
  onMatched({
6942
7036
  argv,
6943
- run: async (ctx, matchedArgv) => (await import("./auth-CgBUQUQn.mjs")).status(ctx, matchedArgv)
7037
+ run: async (ctx, matchedArgv) => (await import("./auth-ByGgd_Yn.mjs")).status(ctx, matchedArgv)
6944
7038
  });
6945
7039
  });
6946
- auth.command("token", "Print the access token to stdout and nothing else", () => {}, (argv) => {
7040
+ auth.command("token", "Print the access token to stdout and nothing else", (token) => token.option("audience", {
7041
+ type: "string",
7042
+ choices: ["management", "mcp"],
7043
+ default: "management",
7044
+ describe: "management prints the stored token; mcp mints a new MCP token that can read and write the catalog content you have access to, without changing the stored credential"
7045
+ }), (argv) => {
6947
7046
  onMatched({
6948
7047
  argv,
6949
- run: async (ctx) => (await import("./auth-CgBUQUQn.mjs")).token(ctx)
7048
+ run: async (ctx, matchedArgv) => (await import("./auth-ByGgd_Yn.mjs")).token(ctx, matchedArgv)
6950
7049
  });
6951
7050
  });
6952
7051
  auth.command("whoami", "Print the GET /me payload", () => {}, (argv) => {
6953
7052
  onMatched({
6954
7053
  argv,
6955
- run: async (ctx) => (await import("./auth-CgBUQUQn.mjs")).whoami(ctx)
7054
+ run: async (ctx) => (await import("./auth-ByGgd_Yn.mjs")).whoami(ctx)
6956
7055
  });
6957
7056
  });
6958
7057
  registerGeneratedAuth(auth, onMatched);
@@ -6986,7 +7085,7 @@ function buildParser(onMatched) {
6986
7085
  }), (argv) => {
6987
7086
  onMatched({
6988
7087
  argv,
6989
- run: async (ctx, matchedArgv) => (await import("./skills-XDbBCC5f.mjs")).add(ctx, matchedArgv)
7088
+ run: async (ctx, matchedArgv) => (await import("./skills-CxMeJ5PF.mjs")).add(ctx, matchedArgv)
6990
7089
  });
6991
7090
  });
6992
7091
  skills.command("publish <paths..>", "Publish a local skill directory as a capability or a new version", (publish) => publish.option("namespace", {
@@ -7014,7 +7113,7 @@ function buildParser(onMatched) {
7014
7113
  }), (argv) => {
7015
7114
  onMatched({
7016
7115
  argv,
7017
- run: async (ctx, matchedArgv) => (await import("./skills-XDbBCC5f.mjs")).publish(ctx, matchedArgv)
7116
+ run: async (ctx, matchedArgv) => (await import("./skills-CxMeJ5PF.mjs")).publish(ctx, matchedArgv)
7018
7117
  });
7019
7118
  });
7020
7119
  return skills.demandCommand(1);
@@ -7036,7 +7135,7 @@ function buildParser(onMatched) {
7036
7135
  }), (argv) => {
7037
7136
  onMatched({
7038
7137
  argv,
7039
- run: async (ctx, matchedArgv) => (await import("./mcp-b6bE3ZRt.mjs")).add(ctx, matchedArgv)
7138
+ run: async (ctx, matchedArgv) => (await import("./mcp-DWF_ELDU.mjs")).add(ctx, matchedArgv)
7040
7139
  });
7041
7140
  });
7042
7141
  mcp.command("publish <paths..>", "Publish an MCP snapshot as a capability or a new version", (publish) => publish.option("namespace", {
@@ -7064,7 +7163,7 @@ function buildParser(onMatched) {
7064
7163
  }), (argv) => {
7065
7164
  onMatched({
7066
7165
  argv,
7067
- run: async (ctx, matchedArgv) => (await import("./mcp-b6bE3ZRt.mjs")).publish(ctx, matchedArgv)
7166
+ run: async (ctx, matchedArgv) => (await import("./mcp-DWF_ELDU.mjs")).publish(ctx, matchedArgv)
7068
7167
  });
7069
7168
  });
7070
7169
  return mcp.demandCommand(1);
@@ -1,5 +1,5 @@
1
- import { S as HngrError, b as writeFileAtomically, g as resolveConnection, s as redactSensitive, t as apiFetch } from "./http-2HWxLIOq.mjs";
2
- import { _ as resolveCapabilityVersion, a as failedResult, c as stringValues, f as downloadContent, g as resolveCapability, h as parseCapabilityPath, i as errorBody, l as AGENTS, n as readPublishFlags, o as positionalPaths, p as MCP_KIND, r as runPublish, s as runBatch, t as explicitName, u as findAgentDefinition } from "./publish-EUqgdp0G.mjs";
1
+ import { C as HngrError, g as resolveConnection, s as redactSensitive, t as apiFetch, x as writeFileAtomically } from "./http-DgHX7Fux.mjs";
2
+ import { _ as resolveCapability, a as errorBody, c as runBatch, d as findAgentDefinition, g as parseCapabilityPath, i as runPublish, l as stringValues, m as MCP_KIND, 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-Ch6wZCgE.mjs";
3
3
  import { constants } from "node:fs";
4
4
  import { access, readFile, stat } from "node:fs/promises";
5
5
  import { basename, delimiter, isAbsolute, join, relative, resolve, sep } from "node:path";
@@ -98,31 +98,24 @@ async function userServerState(executable, name, env) {
98
98
  /**
99
99
  * Installs one user-scope server through Claude Code itself.
100
100
  * `~/.claude.json` is never edited directly: Claude Code owns that file and may
101
- * hold live session state. A conflicting user-scope server fails without
102
- * `force`; with `force` it is removed through the CLI before the new entry is
103
- * added, so every write still goes through Claude Code.
101
+ * hold live session state. Re-installing replaces an existing user-scope
102
+ * server through the CLI before the new entry is added, so every write still
103
+ * goes through Claude Code.
104
104
  */
105
- async function addJsonWithClaude(env, name, config, force) {
105
+ async function addJsonWithClaude(env, name, config) {
106
106
  const executable = await findExecutable(EXECUTABLE, env);
107
107
  if (!executable) throw new HngrError({
108
108
  type: "config",
109
109
  title: "Claude Code CLI not found",
110
110
  detail: "`claude` must be on PATH to install user-level MCP servers; install Claude Code or use project-level (no --global) installation."
111
111
  });
112
- if (await userServerState(executable, name, env) === "present") {
113
- if (!force) throw new HngrError({
114
- type: "io",
115
- title: `MCP server ${JSON.stringify(name)} already exists in Claude Code's user config`,
116
- detail: "Pass --force to replace it."
117
- });
118
- await runChecked(executable, [
119
- "mcp",
120
- "remove",
121
- "--scope",
122
- "user",
123
- name
124
- ], env, name);
125
- }
112
+ if (await userServerState(executable, name, env) === "present") await runChecked(executable, [
113
+ "mcp",
114
+ "remove",
115
+ "--scope",
116
+ "user",
117
+ name
118
+ ], env, name);
126
119
  await runChecked(executable, [
127
120
  "mcp",
128
121
  "add-json",
@@ -202,8 +195,8 @@ function readTargetDocument(raw, label) {
202
195
  }
203
196
  /**
204
197
  * Merges the incoming servers into the document's `mcpServers`. Existing
205
- * servers and other top-level keys survive. A name collision fails unless
206
- * `force`, which replaces only the colliding entries.
198
+ * servers and other top-level keys survive; a name collision is replaced, so
199
+ * re-installing a capability is idempotent.
207
200
  */
208
201
  function mergeSnapshot(document, incoming, options) {
209
202
  const existing = document.mcpServers;
@@ -211,21 +204,13 @@ function mergeSnapshot(document, incoming, options) {
211
204
  if (existing === null || typeof existing !== "object" || Array.isArray(existing)) throw ioError(`"mcpServers" in ${options.label} is not an object`, "Fix the target file before installing MCP servers.");
212
205
  }
213
206
  const servers = { ...existing ?? {} };
214
- const conflicts = [];
215
207
  const added = [];
216
208
  const replaced = [];
217
- for (const [name, entry] of Object.entries(incoming)) if (Object.hasOwn(servers, name)) {
218
- if (!options.force) {
219
- conflicts.push(name);
220
- continue;
221
- }
209
+ for (const [name, entry] of Object.entries(incoming)) {
210
+ if (Object.hasOwn(servers, name)) replaced.push(name);
211
+ else added.push(name);
222
212
  servers[name] = entry;
223
- replaced.push(name);
224
- } else {
225
- servers[name] = entry;
226
- added.push(name);
227
213
  }
228
- if (conflicts.length > 0) throw ioError(`MCP ${conflicts.length === 1 ? "server" : "servers"} already configured in ${options.label}: ${conflicts.join(", ")}`, "Pass --force to replace only the conflicting entries.");
229
214
  return {
230
215
  document: {
231
216
  ...document,
@@ -381,11 +366,8 @@ async function readExisting(path, label) {
381
366
  * redacted. The file is never written by dry-run, so this only affects the
382
367
  * preview.
383
368
  */
384
- function previewJsonTarget(plan, existing, servers, force) {
385
- const merged = mergeSnapshot(readTargetDocument(existing, plan.path), servers, {
386
- force,
387
- label: plan.path
388
- });
369
+ function previewJsonTarget(plan, existing, servers) {
370
+ const merged = mergeSnapshot(readTargetDocument(existing, plan.path), servers, { label: plan.path });
389
371
  return serializeTargetDocument(redactSensitive(merged.document));
390
372
  }
391
373
  /** Reports one target's failure without aborting the remaining targets. */
@@ -393,16 +375,13 @@ function reportTargetFailure(ctx, plan, err) {
393
375
  const body = errorBody(err);
394
376
  ctx.summary(`error: ${plan.agent} ${plan.path}: ${body.title}`);
395
377
  }
396
- async function installJsonTarget(plan, servers, force) {
378
+ async function installJsonTarget(plan, servers) {
397
379
  const filePath = plan.filePath;
398
380
  if (!filePath) throw new HngrError({
399
381
  type: "io",
400
382
  title: `No file path configured for ${plan.agent}`
401
383
  });
402
- const merged = mergeSnapshot(readTargetDocument(await readExisting(filePath, plan.path), plan.path), servers, {
403
- force,
404
- label: plan.path
405
- });
384
+ const merged = mergeSnapshot(readTargetDocument(await readExisting(filePath, plan.path), plan.path), servers, { label: plan.path });
406
385
  await writeFileAtomically(filePath, serializeTargetDocument(merged.document), { label: plan.path });
407
386
  }
408
387
  async function addOne(ctx, connection, parsed, targets, state) {
@@ -419,7 +398,7 @@ async function addOne(ctx, connection, parsed, targets, state) {
419
398
  let failure = null;
420
399
  for (const plan of targets) try {
421
400
  if (plan.kind === "json") {
422
- const preview = previewJsonTarget(plan, plan.filePath ? await readExisting(plan.filePath, plan.path) : void 0, snapshot.servers, ctx.flags.force);
401
+ const preview = previewJsonTarget(plan, plan.filePath ? await readExisting(plan.filePath, plan.path) : void 0, snapshot.servers);
423
402
  ctx.detail(`dry-run: would write ${plan.path}\n${preview}`);
424
403
  } else for (const [name, entry] of Object.entries(snapshot.servers)) ctx.detail(`dry-run: claude mcp add-json --scope user ${name} ${JSON.stringify(redactSensitive(entry))}`);
425
404
  dryTargets.push({
@@ -449,8 +428,8 @@ async function addOne(ctx, connection, parsed, targets, state) {
449
428
  const installedTargets = [];
450
429
  let failure = null;
451
430
  for (const plan of targets) try {
452
- if (plan.kind === "json") await installJsonTarget(plan, snapshot.servers, ctx.flags.force);
453
- else for (const [name, entry] of Object.entries(snapshot.servers)) await addJsonWithClaude(ctx.env, name, entry, ctx.flags.force);
431
+ if (plan.kind === "json") await installJsonTarget(plan, snapshot.servers);
432
+ else for (const [name, entry] of Object.entries(snapshot.servers)) await addJsonWithClaude(ctx.env, name, entry);
454
433
  installedTargets.push({
455
434
  agent: plan.agent,
456
435
  path: plan.path,
@@ -587,7 +566,7 @@ async function add(ctx, argv) {
587
566
  const rawPaths = positionalPaths(argv.paths);
588
567
  if (rawPaths.length === 0) throw usage("No capability path given", "Pass one or more `<name>` or `<namespace>/<name>` arguments.");
589
568
  const parsedPaths = rawPaths.map(parseMcpPath);
590
- const scope = argv.global === true ? "global" : "project";
569
+ const scope = await resolveInstallScope(ctx, argv.global === true);
591
570
  const targets = selectMcpTargets(stringValues(argv.agent), argv.all === true, scope, process.cwd(), ctx.env);
592
571
  const connection = await resolveConnection(ctx.flags, ctx.env);
593
572
  let approvalHintShown = false;
@@ -1,4 +1,4 @@
1
- import { S as HngrError } from "./http-2HWxLIOq.mjs";
1
+ import { C as HngrError } from "./http-DgHX7Fux.mjs";
2
2
  // src/pagination.ts
3
3
  function asCursorPage(body, path) {
4
4
  if (body === null || typeof body !== "object" || !Array.isArray(body.items) || !("next_cursor" in body)) throw new HngrError({
@@ -1,5 +1,5 @@
1
- import { S as HngrError, _ as scrubCredentials, a as toHngrError, g as resolveConnection, h as redactUrl, i as buildRequestUrl, l as apiErrorFromResponse, n as apiGet } from "./http-2HWxLIOq.mjs";
2
- import { t as collectPages } from "./pagination-BbKLKSw3.mjs";
1
+ import { C as HngrError, _ as scrubCredentials, a as toHngrError, g as resolveConnection, h as redactUrl, i as buildRequestUrl, l as apiErrorFromResponse, n as apiGet } from "./http-DgHX7Fux.mjs";
2
+ import { t as collectPages } from "./pagination-BXyFxGYA.mjs";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { join, resolve } from "node:path";
@@ -539,4 +539,25 @@ async function runPublish(options) {
539
539
  ctx.writePayload({ results });
540
540
  return results.some((result) => result.status === "failed") ? 1 : 0;
541
541
  }
542
- export { resolveCapabilityVersion as _, failedResult as a, stringValues as c, homeDir as d, downloadContent as f, resolveCapability as g, parseCapabilityPath as h, errorBody as i, AGENTS as l, SKILL_KIND as m, readPublishFlags as n, positionalPaths as o, MCP_KIND as p, runPublish as r, runBatch as s, explicitName as t, findAgentDefinition as u };
542
+ // src/commands/scope.ts
543
+ const SCOPE_OPTIONS = [{
544
+ value: "project",
545
+ label: "Project",
546
+ hint: "install into the current directory"
547
+ }, {
548
+ value: "global",
549
+ label: "Global",
550
+ hint: "install for the current user"
551
+ }];
552
+ /**
553
+ * Resolves project vs global install scope. `--global` wins without asking.
554
+ * Without an interactive prompter the current-directory default is kept, so
555
+ * scripts, pipes and `--json` runs never block on a prompt.
556
+ */
557
+ async function resolveInstallScope(ctx, global) {
558
+ if (global) return "global";
559
+ const select = ctx.io.prompts?.select;
560
+ if (!select || !ctx.io.stdin.isTTY || !ctx.io.stdout.isTTY) return "project";
561
+ return await select("Install scope", SCOPE_OPTIONS, "project") === "global" ? "global" : "project";
562
+ }
563
+ export { resolveCapability as _, errorBody as a, runBatch as c, findAgentDefinition as d, homeDir as f, parseCapabilityPath as g, SKILL_KIND as h, runPublish as i, stringValues as l, MCP_KIND as m, explicitName as n, failedResult as o, downloadContent as p, readPublishFlags as r, positionalPaths as s, resolveInstallScope as t, AGENTS as u, resolveCapabilityVersion as v };
@@ -1,5 +1,5 @@
1
- import { S as HngrError, g as resolveConnection, r as apiRequest, y as writeDirectoryAtomically } from "./http-2HWxLIOq.mjs";
2
- import { _ as resolveCapabilityVersion, a as failedResult, c as stringValues, d as homeDir, f as downloadContent, g as resolveCapability, h as parseCapabilityPath, i as errorBody, l as AGENTS, m as SKILL_KIND, n as readPublishFlags, o as positionalPaths, r as runPublish, s as runBatch, t as explicitName } from "./publish-EUqgdp0G.mjs";
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-Ch6wZCgE.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. `--force` moves the old
118
- * directory aside before the rename so a failed swap puts it back.
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, force) {
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, force, options = {}) {
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, false);
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, force) {
162
- await installAtomically(target, entries, force);
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`. */
@@ -657,16 +647,12 @@ function planInstall(cwd, env, destName, agents, options) {
657
647
  async function install(ctx, plan, entries) {
658
648
  const cwd = process.cwd();
659
649
  const display = (path) => displayPath(path, cwd);
660
- const destinations = [plan.canonical, ...plan.targets.map((target) => target.path)].filter((path) => path !== null);
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);
650
+ if (plan.canonical) await installAtomically(plan.canonical, entries);
665
651
  const installed = [];
666
652
  let failure = null;
667
653
  for (const target of plan.targets) try {
668
654
  if (target.mode === "copy" || !plan.canonical) {
669
- await copyAtomically(target.path, entries, ctx.flags.force);
655
+ await copyAtomically(target.path, entries);
670
656
  installed.push({
671
657
  agent: target.agent,
672
658
  path: display(target.path),
@@ -675,7 +661,7 @@ async function install(ctx, plan, entries) {
675
661
  });
676
662
  continue;
677
663
  }
678
- const result = await linkAtomically(plan.canonical, target.path, entries, ctx.flags.force);
664
+ const result = await linkAtomically(plan.canonical, target.path, entries);
679
665
  if (result.warning) ctx.summary(result.warning);
680
666
  installed.push({
681
667
  agent: target.agent,
@@ -708,9 +694,6 @@ async function addOne(ctx, connection, parsed, destName, agents, dir, global, co
708
694
  });
709
695
  const canonicalPath = plan.canonical ? displayPath(plan.canonical, process.cwd()) : null;
710
696
  if (ctx.flags.dryRun) {
711
- if (!ctx.flags.force) {
712
- for (const path of [plan.canonical, ...plan.targets.map((target) => target.path)]) if (path && await pathExists(path)) throw targetExistsError(displayPath(path, process.cwd()));
713
- }
714
697
  ctx.summary(`resolved ${parsed.namespace}/${parsed.name} to version ${version}`);
715
698
  return { result: {
716
699
  name: capability.name,
@@ -849,12 +832,13 @@ async function add(ctx, argv) {
849
832
  const dir = stringFlag(argv, "dir");
850
833
  const as = stringFlag(argv, "as");
851
834
  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 global = argv.global === true;
835
+ const requestedGlobal = argv.global === true;
853
836
  const copy = argv.copy === true;
854
- if (dir !== void 0 && global) throw usage("--dir cannot be combined with --global");
837
+ if (dir !== void 0 && requestedGlobal) throw usage("--dir cannot be combined with --global");
855
838
  if (dir !== void 0 && (stringValues(argv.agent).length > 0 || argv.all === true)) throw usage("--dir cannot be combined with --agent or --all");
856
839
  if (as !== void 0) assertNotReserved(as);
857
840
  for (const parsed of parsedPaths) assertNotReserved(as ?? parsed.name);
841
+ const global = dir === void 0 ? await resolveInstallScope(ctx, requestedGlobal) === "global" : false;
858
842
  const agents = dir === void 0 ? await selectAgents(ctx, argv, rawPaths[0] ?? "", global) : [];
859
843
  const connection = await resolveConnection(ctx.flags, ctx.env);
860
844
  const { results, failed } = await runBatch({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hngr",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "The Hangar CLI: install skills and MCP servers into coding agents, and drive the Management API.",
5
5
  "keywords": [
6
6
  "hangar",