cloudhop 1.2.1 → 1.3.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.
Files changed (3) hide show
  1. package/README.md +22 -4
  2. package/dist/cli.js +1486 -747
  3. package/package.json +4 -4
package/dist/cli.js CHANGED
@@ -26,9 +26,9 @@ var __export = (target, all) => {
26
26
  };
27
27
  var __copyProps = (to, from, except, desc) => {
28
28
  if (from && typeof from === "object" || typeof from === "function") {
29
- for (let key of __getOwnPropNames(from))
30
- if (!__hasOwnProp.call(to, key) && key !== except)
31
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
29
+ for (let key2 of __getOwnPropNames(from))
30
+ if (!__hasOwnProp.call(to, key2) && key2 !== except)
31
+ __defProp(to, key2, { get: () => from[key2], enumerable: !(desc = __getOwnPropDesc(from, key2)) || desc.enumerable });
32
32
  }
33
33
  return to;
34
34
  };
@@ -118,11 +118,11 @@ var require_visit = __commonJS({
118
118
  visit.BREAK = BREAK;
119
119
  visit.SKIP = SKIP;
120
120
  visit.REMOVE = REMOVE;
121
- function visit_(key, node, visitor, path2) {
122
- const ctrl = callVisitor(key, node, visitor, path2);
121
+ function visit_(key2, node, visitor, path2) {
122
+ const ctrl = callVisitor(key2, node, visitor, path2);
123
123
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
124
- replaceNode(key, path2, ctrl);
125
- return visit_(key, ctrl, visitor, path2);
124
+ replaceNode(key2, path2, ctrl);
125
+ return visit_(key2, ctrl, visitor, path2);
126
126
  }
127
127
  if (typeof ctrl !== "symbol") {
128
128
  if (identity.isCollection(node)) {
@@ -166,11 +166,11 @@ var require_visit = __commonJS({
166
166
  visitAsync.BREAK = BREAK;
167
167
  visitAsync.SKIP = SKIP;
168
168
  visitAsync.REMOVE = REMOVE;
169
- async function visitAsync_(key, node, visitor, path2) {
170
- const ctrl = await callVisitor(key, node, visitor, path2);
169
+ async function visitAsync_(key2, node, visitor, path2) {
170
+ const ctrl = await callVisitor(key2, node, visitor, path2);
171
171
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
172
- replaceNode(key, path2, ctrl);
173
- return visitAsync_(key, ctrl, visitor, path2);
172
+ replaceNode(key2, path2, ctrl);
173
+ return visitAsync_(key2, ctrl, visitor, path2);
174
174
  }
175
175
  if (typeof ctrl !== "symbol") {
176
176
  if (identity.isCollection(node)) {
@@ -220,27 +220,27 @@ var require_visit = __commonJS({
220
220
  }
221
221
  return visitor;
222
222
  }
223
- function callVisitor(key, node, visitor, path2) {
223
+ function callVisitor(key2, node, visitor, path2) {
224
224
  if (typeof visitor === "function")
225
- return visitor(key, node, path2);
225
+ return visitor(key2, node, path2);
226
226
  if (identity.isMap(node))
227
- return visitor.Map?.(key, node, path2);
227
+ return visitor.Map?.(key2, node, path2);
228
228
  if (identity.isSeq(node))
229
- return visitor.Seq?.(key, node, path2);
229
+ return visitor.Seq?.(key2, node, path2);
230
230
  if (identity.isPair(node))
231
- return visitor.Pair?.(key, node, path2);
231
+ return visitor.Pair?.(key2, node, path2);
232
232
  if (identity.isScalar(node))
233
- return visitor.Scalar?.(key, node, path2);
233
+ return visitor.Scalar?.(key2, node, path2);
234
234
  if (identity.isAlias(node))
235
- return visitor.Alias?.(key, node, path2);
235
+ return visitor.Alias?.(key2, node, path2);
236
236
  return void 0;
237
237
  }
238
- function replaceNode(key, path2, node) {
238
+ function replaceNode(key2, path2, node) {
239
239
  const parent = path2[path2.length - 1];
240
240
  if (identity.isCollection(parent)) {
241
- parent.items[key] = node;
241
+ parent.items[key2] = node;
242
242
  } else if (identity.isPair(parent)) {
243
- if (key === "key")
243
+ if (key2 === "key")
244
244
  parent.key = node;
245
245
  else
246
246
  parent.value = node;
@@ -501,7 +501,7 @@ var require_anchors = __commonJS({
501
501
  var require_applyReviver = __commonJS({
502
502
  "../../node_modules/.pnpm/yaml@2.9.1/node_modules/yaml/dist/doc/applyReviver.js"(exports) {
503
503
  "use strict";
504
- function applyReviver(reviver, obj, key, val) {
504
+ function applyReviver(reviver, obj, key2, val) {
505
505
  if (val && typeof val === "object") {
506
506
  if (Array.isArray(val)) {
507
507
  for (let i2 = 0, len = val.length; i2 < len; ++i2) {
@@ -541,7 +541,7 @@ var require_applyReviver = __commonJS({
541
541
  }
542
542
  }
543
543
  }
544
- return reviver.call(obj, key, val);
544
+ return reviver.call(obj, key2, val);
545
545
  }
546
546
  exports.applyReviver = applyReviver;
547
547
  }
@@ -904,14 +904,14 @@ var require_Collection = __commonJS({
904
904
  if (isEmptyPath(path2))
905
905
  this.add(value);
906
906
  else {
907
- const [key, ...rest] = path2;
908
- const node = this.get(key, true);
907
+ const [key2, ...rest] = path2;
908
+ const node = this.get(key2, true);
909
909
  if (identity.isCollection(node))
910
910
  node.addIn(rest, value);
911
911
  else if (node === void 0 && this.schema)
912
- this.set(key, collectionFromPath(this.schema, rest, value));
912
+ this.set(key2, collectionFromPath(this.schema, rest, value));
913
913
  else
914
- throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
914
+ throw new Error(`Expected YAML collection at ${key2}. Remaining path: ${rest}`);
915
915
  }
916
916
  }
917
917
  /**
@@ -919,14 +919,14 @@ var require_Collection = __commonJS({
919
919
  * @returns `true` if the item was found and removed.
920
920
  */
921
921
  deleteIn(path2) {
922
- const [key, ...rest] = path2;
922
+ const [key2, ...rest] = path2;
923
923
  if (rest.length === 0)
924
- return this.delete(key);
925
- const node = this.get(key, true);
924
+ return this.delete(key2);
925
+ const node = this.get(key2, true);
926
926
  if (identity.isCollection(node))
927
927
  return node.deleteIn(rest);
928
928
  else
929
- throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
929
+ throw new Error(`Expected YAML collection at ${key2}. Remaining path: ${rest}`);
930
930
  }
931
931
  /**
932
932
  * Returns item at `key`, or `undefined` if not found. By default unwraps
@@ -934,8 +934,8 @@ var require_Collection = __commonJS({
934
934
  * `true` (collections are always returned intact).
935
935
  */
936
936
  getIn(path2, keepScalar) {
937
- const [key, ...rest] = path2;
938
- const node = this.get(key, true);
937
+ const [key2, ...rest] = path2;
938
+ const node = this.get(key2, true);
939
939
  if (rest.length === 0)
940
940
  return !keepScalar && identity.isScalar(node) ? node.value : node;
941
941
  else
@@ -953,10 +953,10 @@ var require_Collection = __commonJS({
953
953
  * Checks if the collection includes a value with the key `key`.
954
954
  */
955
955
  hasIn(path2) {
956
- const [key, ...rest] = path2;
956
+ const [key2, ...rest] = path2;
957
957
  if (rest.length === 0)
958
- return this.has(key);
959
- const node = this.get(key, true);
958
+ return this.has(key2);
959
+ const node = this.get(key2, true);
960
960
  return identity.isCollection(node) ? node.hasIn(rest) : false;
961
961
  }
962
962
  /**
@@ -964,17 +964,17 @@ var require_Collection = __commonJS({
964
964
  * boolean to add/remove the item from the set.
965
965
  */
966
966
  setIn(path2, value) {
967
- const [key, ...rest] = path2;
967
+ const [key2, ...rest] = path2;
968
968
  if (rest.length === 0) {
969
- this.set(key, value);
969
+ this.set(key2, value);
970
970
  } else {
971
- const node = this.get(key, true);
971
+ const node = this.get(key2, true);
972
972
  if (identity.isCollection(node))
973
973
  node.setIn(rest, value);
974
974
  else if (node === void 0 && this.schema)
975
- this.set(key, collectionFromPath(this.schema, rest, value));
975
+ this.set(key2, collectionFromPath(this.schema, rest, value));
976
976
  else
977
- throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
977
+ throw new Error(`Expected YAML collection at ${key2}. Remaining path: ${rest}`);
978
978
  }
979
979
  }
980
980
  };
@@ -1552,19 +1552,19 @@ var require_stringifyPair = __commonJS({
1552
1552
  var Scalar = require_Scalar();
1553
1553
  var stringify3 = require_stringify();
1554
1554
  var stringifyComment = require_stringifyComment();
1555
- function stringifyPair({ key, value }, ctx, onComment, onChompKeep) {
1555
+ function stringifyPair({ key: key2, value }, ctx, onComment, onChompKeep) {
1556
1556
  const { allNullValues, doc, indent, indentStep, options: { commentString, indentSeq, simpleKeys } } = ctx;
1557
- let keyComment = identity.isNode(key) && key.comment || null;
1557
+ let keyComment = identity.isNode(key2) && key2.comment || null;
1558
1558
  if (simpleKeys) {
1559
1559
  if (keyComment) {
1560
1560
  throw new Error("With simple keys, key nodes cannot have comments");
1561
1561
  }
1562
- if (identity.isCollection(key) || !identity.isNode(key) && typeof key === "object") {
1562
+ if (identity.isCollection(key2) || !identity.isNode(key2) && typeof key2 === "object") {
1563
1563
  const msg = "With simple keys, collection cannot be used as a key value";
1564
1564
  throw new Error(msg);
1565
1565
  }
1566
1566
  }
1567
- let explicitKey = !simpleKeys && (!key || keyComment && value == null && !ctx.inFlow || identity.isCollection(key) || (identity.isScalar(key) ? key.type === Scalar.Scalar.BLOCK_FOLDED || key.type === Scalar.Scalar.BLOCK_LITERAL : typeof key === "object"));
1567
+ let explicitKey = !simpleKeys && (!key2 || keyComment && value == null && !ctx.inFlow || identity.isCollection(key2) || (identity.isScalar(key2) ? key2.type === Scalar.Scalar.BLOCK_FOLDED || key2.type === Scalar.Scalar.BLOCK_LITERAL : typeof key2 === "object"));
1568
1568
  ctx = Object.assign({}, ctx, {
1569
1569
  allNullValues: false,
1570
1570
  implicitKey: !explicitKey && (simpleKeys || !allNullValues),
@@ -1572,7 +1572,7 @@ var require_stringifyPair = __commonJS({
1572
1572
  });
1573
1573
  let keyCommentDone = false;
1574
1574
  let chompKeep = false;
1575
- let str = stringify3.stringify(key, ctx, () => keyCommentDone = true, () => chompKeep = true);
1575
+ let str = stringify3.stringify(key2, ctx, () => keyCommentDone = true, () => chompKeep = true);
1576
1576
  if (!explicitKey && !ctx.inFlow && str.length > 1024) {
1577
1577
  if (simpleKeys)
1578
1578
  throw new Error("With simple keys, single line scalar must not span more than 1024 characters");
@@ -1716,7 +1716,7 @@ var require_merge = __commonJS({
1716
1716
  }),
1717
1717
  stringify: () => MERGE_KEY
1718
1718
  };
1719
- var isMergeKey = (ctx, key) => (merge.identify(key) || identity.isScalar(key) && (!key.type || key.type === Scalar.Scalar.PLAIN) && merge.identify(key.value)) && ctx?.doc.schema.tags.some((tag) => tag.tag === merge.tag && tag.default);
1719
+ var isMergeKey = (ctx, key2) => (merge.identify(key2) || identity.isScalar(key2) && (!key2.type || key2.type === Scalar.Scalar.PLAIN) && merge.identify(key2.value)) && ctx?.doc.schema.tags.some((tag) => tag.tag === merge.tag && tag.default);
1720
1720
  function addMergeToJSMap(ctx, map, value) {
1721
1721
  const source = resolveAliasValue(ctx, value);
1722
1722
  if (identity.isSeq(source))
@@ -1733,14 +1733,14 @@ var require_merge = __commonJS({
1733
1733
  if (!identity.isMap(source))
1734
1734
  throw new Error("Merge sources must be maps or map aliases");
1735
1735
  const srcMap = source.toJSON(null, ctx, Map);
1736
- for (const [key, value2] of srcMap) {
1736
+ for (const [key2, value2] of srcMap) {
1737
1737
  if (map instanceof Map) {
1738
- if (!map.has(key))
1739
- map.set(key, value2);
1738
+ if (!map.has(key2))
1739
+ map.set(key2, value2);
1740
1740
  } else if (map instanceof Set) {
1741
- map.add(key);
1742
- } else if (!Object.prototype.hasOwnProperty.call(map, key)) {
1743
- Object.defineProperty(map, key, {
1741
+ map.add(key2);
1742
+ } else if (!Object.prototype.hasOwnProperty.call(map, key2)) {
1743
+ Object.defineProperty(map, key2, {
1744
1744
  value: value2,
1745
1745
  writable: true,
1746
1746
  enumerable: true,
@@ -1768,19 +1768,19 @@ var require_addPairToJSMap = __commonJS({
1768
1768
  var stringify3 = require_stringify();
1769
1769
  var identity = require_identity();
1770
1770
  var toJS = require_toJS();
1771
- function addPairToJSMap(ctx, map, { key, value }) {
1772
- if (identity.isNode(key) && key.addToJSMap)
1773
- key.addToJSMap(ctx, map, value);
1774
- else if (merge.isMergeKey(ctx, key))
1771
+ function addPairToJSMap(ctx, map, { key: key2, value }) {
1772
+ if (identity.isNode(key2) && key2.addToJSMap)
1773
+ key2.addToJSMap(ctx, map, value);
1774
+ else if (merge.isMergeKey(ctx, key2))
1775
1775
  merge.addMergeToJSMap(ctx, map, value);
1776
1776
  else {
1777
- const jsKey = toJS.toJS(key, "", ctx);
1777
+ const jsKey = toJS.toJS(key2, "", ctx);
1778
1778
  if (map instanceof Map) {
1779
1779
  map.set(jsKey, toJS.toJS(value, jsKey, ctx));
1780
1780
  } else if (map instanceof Set) {
1781
1781
  map.add(jsKey);
1782
1782
  } else {
1783
- const stringKey = stringifyKey(key, jsKey, ctx);
1783
+ const stringKey = stringifyKey(key2, jsKey, ctx);
1784
1784
  const jsValue = toJS.toJS(value, stringKey, ctx);
1785
1785
  if (stringKey in map)
1786
1786
  Object.defineProperty(map, stringKey, {
@@ -1795,19 +1795,19 @@ var require_addPairToJSMap = __commonJS({
1795
1795
  }
1796
1796
  return map;
1797
1797
  }
1798
- function stringifyKey(key, jsKey, ctx) {
1798
+ function stringifyKey(key2, jsKey, ctx) {
1799
1799
  if (jsKey === null)
1800
1800
  return "";
1801
1801
  if (typeof jsKey !== "object")
1802
1802
  return String(jsKey);
1803
- if (identity.isNode(key) && ctx?.doc) {
1803
+ if (identity.isNode(key2) && ctx?.doc) {
1804
1804
  const strCtx = stringify3.createStringifyContext(ctx.doc, {});
1805
1805
  strCtx.anchors = /* @__PURE__ */ new Set();
1806
1806
  for (const node of ctx.anchors.keys())
1807
1807
  strCtx.anchors.add(node.anchor);
1808
1808
  strCtx.inFlow = true;
1809
1809
  strCtx.inStringifyKey = true;
1810
- const strKey = key.toString(strCtx);
1810
+ const strKey = key2.toString(strCtx);
1811
1811
  if (!ctx.mapKeyWarned) {
1812
1812
  let jsonStr = JSON.stringify(strKey);
1813
1813
  if (jsonStr.length > 40)
@@ -1831,24 +1831,24 @@ var require_Pair = __commonJS({
1831
1831
  var stringifyPair = require_stringifyPair();
1832
1832
  var addPairToJSMap = require_addPairToJSMap();
1833
1833
  var identity = require_identity();
1834
- function createPair(key, value, ctx) {
1835
- const k = createNode.createNode(key, void 0, ctx);
1834
+ function createPair(key2, value, ctx) {
1835
+ const k = createNode.createNode(key2, void 0, ctx);
1836
1836
  const v = createNode.createNode(value, void 0, ctx);
1837
1837
  return new Pair(k, v);
1838
1838
  }
1839
1839
  var Pair = class _Pair {
1840
- constructor(key, value = null) {
1840
+ constructor(key2, value = null) {
1841
1841
  Object.defineProperty(this, identity.NODE_TYPE, { value: identity.PAIR });
1842
- this.key = key;
1842
+ this.key = key2;
1843
1843
  this.value = value;
1844
1844
  }
1845
1845
  clone(schema) {
1846
- let { key, value } = this;
1847
- if (identity.isNode(key))
1848
- key = key.clone(schema);
1846
+ let { key: key2, value } = this;
1847
+ if (identity.isNode(key2))
1848
+ key2 = key2.clone(schema);
1849
1849
  if (identity.isNode(value))
1850
1850
  value = value.clone(schema);
1851
- return new _Pair(key, value);
1851
+ return new _Pair(key2, value);
1852
1852
  }
1853
1853
  toJSON(_2, ctx) {
1854
1854
  const pair = ctx?.mapAsMap ? /* @__PURE__ */ new Map() : {};
@@ -2024,11 +2024,11 @@ var require_YAMLMap = __commonJS({
2024
2024
  var identity = require_identity();
2025
2025
  var Pair = require_Pair();
2026
2026
  var Scalar = require_Scalar();
2027
- function findPair(items, key) {
2028
- const k = identity.isScalar(key) ? key.value : key;
2027
+ function findPair(items, key2) {
2028
+ const k = identity.isScalar(key2) ? key2.value : key2;
2029
2029
  for (const it of items) {
2030
2030
  if (identity.isPair(it)) {
2031
- if (it.key === key || it.key === k)
2031
+ if (it.key === key2 || it.key === k)
2032
2032
  return it;
2033
2033
  if (identity.isScalar(it.key) && it.key.value === k)
2034
2034
  return it;
@@ -2051,20 +2051,20 @@ var require_YAMLMap = __commonJS({
2051
2051
  static from(schema, obj, ctx) {
2052
2052
  const { keepUndefined, replacer } = ctx;
2053
2053
  const map = new this(schema);
2054
- const add = (key, value) => {
2054
+ const add = (key2, value) => {
2055
2055
  if (typeof replacer === "function")
2056
- value = replacer.call(obj, key, value);
2057
- else if (Array.isArray(replacer) && !replacer.includes(key))
2056
+ value = replacer.call(obj, key2, value);
2057
+ else if (Array.isArray(replacer) && !replacer.includes(key2))
2058
2058
  return;
2059
2059
  if (value !== void 0 || keepUndefined)
2060
- map.items.push(Pair.createPair(key, value, ctx));
2060
+ map.items.push(Pair.createPair(key2, value, ctx));
2061
2061
  };
2062
2062
  if (obj instanceof Map) {
2063
- for (const [key, value] of obj)
2064
- add(key, value);
2063
+ for (const [key2, value] of obj)
2064
+ add(key2, value);
2065
2065
  } else if (obj && typeof obj === "object") {
2066
- for (const key of Object.keys(obj))
2067
- add(key, obj[key]);
2066
+ for (const key2 of Object.keys(obj))
2067
+ add(key2, obj[key2]);
2068
2068
  }
2069
2069
  if (typeof schema.sortMapEntries === "function") {
2070
2070
  map.items.sort(schema.sortMapEntries);
@@ -2104,23 +2104,23 @@ var require_YAMLMap = __commonJS({
2104
2104
  this.items.push(_pair);
2105
2105
  }
2106
2106
  }
2107
- delete(key) {
2108
- const it = findPair(this.items, key);
2107
+ delete(key2) {
2108
+ const it = findPair(this.items, key2);
2109
2109
  if (!it)
2110
2110
  return false;
2111
2111
  const del = this.items.splice(this.items.indexOf(it), 1);
2112
2112
  return del.length > 0;
2113
2113
  }
2114
- get(key, keepScalar) {
2115
- const it = findPair(this.items, key);
2114
+ get(key2, keepScalar) {
2115
+ const it = findPair(this.items, key2);
2116
2116
  const node = it?.value;
2117
2117
  return (!keepScalar && identity.isScalar(node) ? node.value : node) ?? void 0;
2118
2118
  }
2119
- has(key) {
2120
- return !!findPair(this.items, key);
2119
+ has(key2) {
2120
+ return !!findPair(this.items, key2);
2121
2121
  }
2122
- set(key, value) {
2123
- this.add(new Pair.Pair(key, value), true);
2122
+ set(key2, value) {
2123
+ this.add(new Pair.Pair(key2, value), true);
2124
2124
  }
2125
2125
  /**
2126
2126
  * @param ctx - Conversion context, originally set in Document#toJS()
@@ -2209,15 +2209,15 @@ var require_YAMLSeq = __commonJS({
2209
2209
  *
2210
2210
  * @returns `true` if the item was found and removed.
2211
2211
  */
2212
- delete(key) {
2213
- const idx = asItemIndex(key);
2212
+ delete(key2) {
2213
+ const idx = asItemIndex(key2);
2214
2214
  if (typeof idx !== "number")
2215
2215
  return false;
2216
2216
  const del = this.items.splice(idx, 1);
2217
2217
  return del.length > 0;
2218
2218
  }
2219
- get(key, keepScalar) {
2220
- const idx = asItemIndex(key);
2219
+ get(key2, keepScalar) {
2220
+ const idx = asItemIndex(key2);
2221
2221
  if (typeof idx !== "number")
2222
2222
  return void 0;
2223
2223
  const it = this.items[idx];
@@ -2229,8 +2229,8 @@ var require_YAMLSeq = __commonJS({
2229
2229
  * `key` must contain a representation of an integer for this to succeed.
2230
2230
  * It may be wrapped in a `Scalar`.
2231
2231
  */
2232
- has(key) {
2233
- const idx = asItemIndex(key);
2232
+ has(key2) {
2233
+ const idx = asItemIndex(key2);
2234
2234
  return typeof idx === "number" && idx < this.items.length;
2235
2235
  }
2236
2236
  /**
@@ -2240,10 +2240,10 @@ var require_YAMLSeq = __commonJS({
2240
2240
  * If `key` does not contain a representation of an integer, this will throw.
2241
2241
  * It may be wrapped in a `Scalar`.
2242
2242
  */
2243
- set(key, value) {
2244
- const idx = asItemIndex(key);
2243
+ set(key2, value) {
2244
+ const idx = asItemIndex(key2);
2245
2245
  if (typeof idx !== "number")
2246
- throw new Error(`Expected a valid index, not ${key}.`);
2246
+ throw new Error(`Expected a valid index, not ${key2}.`);
2247
2247
  const prev = this.items[idx];
2248
2248
  if (identity.isScalar(prev) && Scalar.isScalarValue(value))
2249
2249
  prev.value = value;
@@ -2277,8 +2277,8 @@ var require_YAMLSeq = __commonJS({
2277
2277
  let i2 = 0;
2278
2278
  for (let it of obj) {
2279
2279
  if (typeof replacer === "function") {
2280
- const key = obj instanceof Set ? it : String(i2++);
2281
- it = replacer.call(obj, key, it);
2280
+ const key2 = obj instanceof Set ? it : String(i2++);
2281
+ it = replacer.call(obj, key2, it);
2282
2282
  }
2283
2283
  seq.items.push(createNode.createNode(it, void 0, ctx));
2284
2284
  }
@@ -2286,8 +2286,8 @@ var require_YAMLSeq = __commonJS({
2286
2286
  return seq;
2287
2287
  }
2288
2288
  };
2289
- function asItemIndex(key) {
2290
- let idx = identity.isScalar(key) ? key.value : key;
2289
+ function asItemIndex(key2) {
2290
+ let idx = identity.isScalar(key2) ? key2.value : key2;
2291
2291
  if (idx && typeof idx === "string")
2292
2292
  idx = Number(idx);
2293
2293
  return typeof idx === "number" && Number.isInteger(idx) && idx >= 0 ? idx : null;
@@ -2701,25 +2701,25 @@ ${cn.comment}` : item.comment;
2701
2701
  for (let it of iterable) {
2702
2702
  if (typeof replacer === "function")
2703
2703
  it = replacer.call(iterable, String(i2++), it);
2704
- let key, value;
2704
+ let key2, value;
2705
2705
  if (Array.isArray(it)) {
2706
2706
  if (it.length === 2) {
2707
- key = it[0];
2707
+ key2 = it[0];
2708
2708
  value = it[1];
2709
2709
  } else
2710
2710
  throw new TypeError(`Expected [key, value] tuple: ${it}`);
2711
2711
  } else if (it && it instanceof Object) {
2712
2712
  const keys = Object.keys(it);
2713
2713
  if (keys.length === 1) {
2714
- key = keys[0];
2715
- value = it[key];
2714
+ key2 = keys[0];
2715
+ value = it[key2];
2716
2716
  } else {
2717
2717
  throw new TypeError(`Expected tuple with one key, not ${keys.length} keys`);
2718
2718
  }
2719
2719
  } else {
2720
- key = it;
2720
+ key2 = it;
2721
2721
  }
2722
- pairs2.items.push(Pair.createPair(key, value, ctx));
2722
+ pairs2.items.push(Pair.createPair(key2, value, ctx));
2723
2723
  }
2724
2724
  return pairs2;
2725
2725
  }
@@ -2766,16 +2766,16 @@ var require_omap = __commonJS({
2766
2766
  if (ctx?.onCreate)
2767
2767
  ctx.onCreate(map);
2768
2768
  for (const pair of this.items) {
2769
- let key, value;
2769
+ let key2, value;
2770
2770
  if (identity.isPair(pair)) {
2771
- key = toJS.toJS(pair.key, "", ctx);
2772
- value = toJS.toJS(pair.value, key, ctx);
2771
+ key2 = toJS.toJS(pair.key, "", ctx);
2772
+ value = toJS.toJS(pair.value, key2, ctx);
2773
2773
  } else {
2774
- key = toJS.toJS(pair, "", ctx);
2774
+ key2 = toJS.toJS(pair, "", ctx);
2775
2775
  }
2776
- if (map.has(key))
2776
+ if (map.has(key2))
2777
2777
  throw new Error("Ordered maps must not include duplicate keys");
2778
- map.set(key, value);
2778
+ map.set(key2, value);
2779
2779
  }
2780
2780
  return map;
2781
2781
  }
@@ -2796,12 +2796,12 @@ var require_omap = __commonJS({
2796
2796
  resolve(seq, onError) {
2797
2797
  const pairs$1 = pairs.resolvePairs(seq, onError);
2798
2798
  const seenKeys = [];
2799
- for (const { key } of pairs$1.items) {
2800
- if (identity.isScalar(key)) {
2801
- if (seenKeys.includes(key.value)) {
2802
- onError(`Ordered maps must not include duplicate keys: ${key.value}`);
2799
+ for (const { key: key2 } of pairs$1.items) {
2800
+ if (identity.isScalar(key2)) {
2801
+ if (seenKeys.includes(key2.value)) {
2802
+ onError(`Ordered maps must not include duplicate keys: ${key2.value}`);
2803
2803
  } else {
2804
- seenKeys.push(key.value);
2804
+ seenKeys.push(key2.value);
2805
2805
  }
2806
2806
  }
2807
2807
  }
@@ -2986,14 +2986,14 @@ var require_set = __commonJS({
2986
2986
  super(schema);
2987
2987
  this.tag = _YAMLSet.tag;
2988
2988
  }
2989
- add(key) {
2989
+ add(key2) {
2990
2990
  let pair;
2991
- if (identity.isPair(key))
2992
- pair = key;
2993
- else if (key && typeof key === "object" && "key" in key && "value" in key && key.value === null)
2994
- pair = new Pair.Pair(key.key, null);
2991
+ if (identity.isPair(key2))
2992
+ pair = key2;
2993
+ else if (key2 && typeof key2 === "object" && "key" in key2 && "value" in key2 && key2.value === null)
2994
+ pair = new Pair.Pair(key2.key, null);
2995
2995
  else
2996
- pair = new Pair.Pair(key, null);
2996
+ pair = new Pair.Pair(key2, null);
2997
2997
  const prev = YAMLMap.findPair(this.items, pair.key);
2998
2998
  if (!prev)
2999
2999
  this.items.push(pair);
@@ -3002,18 +3002,18 @@ var require_set = __commonJS({
3002
3002
  * If `keepPair` is `true`, returns the Pair matching `key`.
3003
3003
  * Otherwise, returns the value of that Pair's key.
3004
3004
  */
3005
- get(key, keepPair) {
3006
- const pair = YAMLMap.findPair(this.items, key);
3005
+ get(key2, keepPair) {
3006
+ const pair = YAMLMap.findPair(this.items, key2);
3007
3007
  return !keepPair && identity.isPair(pair) ? identity.isScalar(pair.key) ? pair.key.value : pair.key : pair;
3008
3008
  }
3009
- set(key, value) {
3009
+ set(key2, value) {
3010
3010
  if (typeof value !== "boolean")
3011
3011
  throw new Error(`Expected boolean value for set(key, value) in a YAML set, not ${typeof value}`);
3012
- const prev = YAMLMap.findPair(this.items, key);
3012
+ const prev = YAMLMap.findPair(this.items, key2);
3013
3013
  if (prev && !value) {
3014
3014
  this.items.splice(this.items.indexOf(prev), 1);
3015
3015
  } else if (!prev && value) {
3016
- this.items.push(new Pair.Pair(key));
3016
+ this.items.push(new Pair.Pair(key2));
3017
3017
  }
3018
3018
  }
3019
3019
  toJSON(_2, ctx) {
@@ -3260,7 +3260,7 @@ var require_tags = __commonJS({
3260
3260
  if (Array.isArray(customTags))
3261
3261
  tags = [];
3262
3262
  else {
3263
- const keys = Array.from(schemas.keys()).filter((key) => key !== "yaml11").map((key) => JSON.stringify(key)).join(", ");
3263
+ const keys = Array.from(schemas.keys()).filter((key2) => key2 !== "yaml11").map((key2) => JSON.stringify(key2)).join(", ");
3264
3264
  throw new Error(`Unknown schema "${schemaName}"; use one of ${keys} or define customTags array`);
3265
3265
  }
3266
3266
  }
@@ -3276,7 +3276,7 @@ var require_tags = __commonJS({
3276
3276
  const tagObj = typeof tag === "string" ? tagsByName[tag] : tag;
3277
3277
  if (!tagObj) {
3278
3278
  const tagName = JSON.stringify(tag);
3279
- const keys = Object.keys(tagsByName).map((key) => JSON.stringify(key)).join(", ");
3279
+ const keys = Object.keys(tagsByName).map((key2) => JSON.stringify(key2)).join(", ");
3280
3280
  throw new Error(`Unknown custom tag ${tagName}; use one of ${keys}`);
3281
3281
  }
3282
3282
  if (!tags2.includes(tagObj))
@@ -3540,8 +3540,8 @@ var require_Document = __commonJS({
3540
3540
  * Convert a key and a value into a `Pair` using the current schema,
3541
3541
  * recursively wrapping all values as `Scalar` or `Collection` nodes.
3542
3542
  */
3543
- createPair(key, value, options = {}) {
3544
- const k = this.createNode(key, null, options);
3543
+ createPair(key2, value, options = {}) {
3544
+ const k = this.createNode(key2, null, options);
3545
3545
  const v = this.createNode(value, null, options);
3546
3546
  return new Pair.Pair(k, v);
3547
3547
  }
@@ -3549,8 +3549,8 @@ var require_Document = __commonJS({
3549
3549
  * Removes a value from the document.
3550
3550
  * @returns `true` if the item was found and removed.
3551
3551
  */
3552
- delete(key) {
3553
- return assertCollection(this.contents) ? this.contents.delete(key) : false;
3552
+ delete(key2) {
3553
+ return assertCollection(this.contents) ? this.contents.delete(key2) : false;
3554
3554
  }
3555
3555
  /**
3556
3556
  * Removes a value from the document.
@@ -3570,8 +3570,8 @@ var require_Document = __commonJS({
3570
3570
  * scalar values from their surrounding node; to disable set `keepScalar` to
3571
3571
  * `true` (collections are always returned intact).
3572
3572
  */
3573
- get(key, keepScalar) {
3574
- return identity.isCollection(this.contents) ? this.contents.get(key, keepScalar) : void 0;
3573
+ get(key2, keepScalar) {
3574
+ return identity.isCollection(this.contents) ? this.contents.get(key2, keepScalar) : void 0;
3575
3575
  }
3576
3576
  /**
3577
3577
  * Returns item at `path`, or `undefined` if not found. By default unwraps
@@ -3586,8 +3586,8 @@ var require_Document = __commonJS({
3586
3586
  /**
3587
3587
  * Checks if the document includes a value with the key `key`.
3588
3588
  */
3589
- has(key) {
3590
- return identity.isCollection(this.contents) ? this.contents.has(key) : false;
3589
+ has(key2) {
3590
+ return identity.isCollection(this.contents) ? this.contents.has(key2) : false;
3591
3591
  }
3592
3592
  /**
3593
3593
  * Checks if the document includes a value at `path`.
@@ -3601,11 +3601,11 @@ var require_Document = __commonJS({
3601
3601
  * Sets a value in this document. For `!!set`, `value` needs to be a
3602
3602
  * boolean to add/remove the item from the set.
3603
3603
  */
3604
- set(key, value) {
3604
+ set(key2, value) {
3605
3605
  if (this.contents == null) {
3606
- this.contents = Collection.collectionFromPath(this.schema, [key], value);
3606
+ this.contents = Collection.collectionFromPath(this.schema, [key2], value);
3607
3607
  } else if (assertCollection(this.contents)) {
3608
- this.contents.set(key, value);
3608
+ this.contents.set(key2, value);
3609
3609
  }
3610
3610
  }
3611
3611
  /**
@@ -3913,24 +3913,24 @@ var require_resolve_props = __commonJS({
3913
3913
  var require_util_contains_newline = __commonJS({
3914
3914
  "../../node_modules/.pnpm/yaml@2.9.1/node_modules/yaml/dist/compose/util-contains-newline.js"(exports) {
3915
3915
  "use strict";
3916
- function containsNewline(key) {
3917
- if (!key)
3916
+ function containsNewline(key2) {
3917
+ if (!key2)
3918
3918
  return null;
3919
- switch (key.type) {
3919
+ switch (key2.type) {
3920
3920
  case "alias":
3921
3921
  case "scalar":
3922
3922
  case "double-quoted-scalar":
3923
3923
  case "single-quoted-scalar":
3924
- if (key.source.includes("\n"))
3924
+ if (key2.source.includes("\n"))
3925
3925
  return true;
3926
- if (key.end) {
3927
- for (const st of key.end)
3926
+ if (key2.end) {
3927
+ for (const st of key2.end)
3928
3928
  if (st.type === "newline")
3929
3929
  return true;
3930
3930
  }
3931
3931
  return false;
3932
3932
  case "flow-collection":
3933
- for (const it of key.items) {
3933
+ for (const it of key2.items) {
3934
3934
  for (const st of it.start)
3935
3935
  if (st.type === "newline")
3936
3936
  return true;
@@ -4004,10 +4004,10 @@ var require_resolve_block_map = __commonJS({
4004
4004
  let offset = bm.offset;
4005
4005
  let commentEnd = null;
4006
4006
  for (const collItem of bm.items) {
4007
- const { start, key, sep, value } = collItem;
4007
+ const { start, key: key2, sep, value } = collItem;
4008
4008
  const keyProps = resolveProps.resolveProps(start, {
4009
4009
  indicator: "explicit-key-ind",
4010
- next: key ?? sep?.[0],
4010
+ next: key2 ?? sep?.[0],
4011
4011
  offset,
4012
4012
  onError,
4013
4013
  parentIndent: bm.indent,
@@ -4015,10 +4015,10 @@ var require_resolve_block_map = __commonJS({
4015
4015
  });
4016
4016
  const implicitKey = !keyProps.found;
4017
4017
  if (implicitKey) {
4018
- if (key) {
4019
- if (key.type === "block-seq")
4018
+ if (key2) {
4019
+ if (key2.type === "block-seq")
4020
4020
  onError(offset, "BLOCK_AS_IMPLICIT_KEY", "A block sequence may not be used as an implicit map key");
4021
- else if ("indent" in key && key.indent !== bm.indent)
4021
+ else if ("indent" in key2 && key2.indent !== bm.indent)
4022
4022
  onError(offset, "BAD_INDENT", startColMsg);
4023
4023
  }
4024
4024
  if (!keyProps.anchor && !keyProps.tag && !sep) {
@@ -4031,17 +4031,17 @@ var require_resolve_block_map = __commonJS({
4031
4031
  }
4032
4032
  continue;
4033
4033
  }
4034
- if (keyProps.newlineAfterProp || utilContainsNewline.containsNewline(key)) {
4035
- onError(key ?? start[start.length - 1], "MULTILINE_IMPLICIT_KEY", "Implicit keys need to be on a single line");
4034
+ if (keyProps.newlineAfterProp || utilContainsNewline.containsNewline(key2)) {
4035
+ onError(key2 ?? start[start.length - 1], "MULTILINE_IMPLICIT_KEY", "Implicit keys need to be on a single line");
4036
4036
  }
4037
4037
  } else if (keyProps.found?.indent !== bm.indent) {
4038
4038
  onError(offset, "BAD_INDENT", startColMsg);
4039
4039
  }
4040
4040
  ctx.atKey = true;
4041
4041
  const keyStart = keyProps.end;
4042
- const keyNode = key ? composeNode(ctx, key, keyProps, onError) : composeEmptyNode(ctx, keyStart, start, null, keyProps, onError);
4042
+ const keyNode = key2 ? composeNode(ctx, key2, keyProps, onError) : composeEmptyNode(ctx, keyStart, start, null, keyProps, onError);
4043
4043
  if (ctx.schema.compat)
4044
- utilFlowIndentCheck.flowIndentCheck(bm.indent, key, onError);
4044
+ utilFlowIndentCheck.flowIndentCheck(bm.indent, key2, onError);
4045
4045
  ctx.atKey = false;
4046
4046
  if (utilMapIncludes.mapIncludes(ctx, map.items, keyNode))
4047
4047
  onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
@@ -4051,7 +4051,7 @@ var require_resolve_block_map = __commonJS({
4051
4051
  offset: keyNode.range[2],
4052
4052
  onError,
4053
4053
  parentIndent: bm.indent,
4054
- startOnNewline: !key || key.type === "block-scalar"
4054
+ startOnNewline: !key2 || key2.type === "block-scalar"
4055
4055
  });
4056
4056
  offset = valueProps.end;
4057
4057
  if (valueProps.found) {
@@ -4215,11 +4215,11 @@ var require_resolve_flow_collection = __commonJS({
4215
4215
  let offset = fc.offset + fc.start.source.length;
4216
4216
  for (let i2 = 0; i2 < fc.items.length; ++i2) {
4217
4217
  const collItem = fc.items[i2];
4218
- const { start, key, sep, value } = collItem;
4218
+ const { start, key: key2, sep, value } = collItem;
4219
4219
  const props = resolveProps.resolveProps(start, {
4220
4220
  flow: fcName,
4221
4221
  indicator: "explicit-key-ind",
4222
- next: key ?? sep?.[0],
4222
+ next: key2 ?? sep?.[0],
4223
4223
  offset,
4224
4224
  onError,
4225
4225
  parentIndent: fc.indent,
@@ -4240,9 +4240,9 @@ var require_resolve_flow_collection = __commonJS({
4240
4240
  offset = props.end;
4241
4241
  continue;
4242
4242
  }
4243
- if (!isMap && ctx.options.strict && utilContainsNewline.containsNewline(key))
4243
+ if (!isMap && ctx.options.strict && utilContainsNewline.containsNewline(key2))
4244
4244
  onError(
4245
- key,
4245
+ key2,
4246
4246
  // checked by containsNewline()
4247
4247
  "MULTILINE_IMPLICIT_KEY",
4248
4248
  "Implicit keys of flow sequence pairs need to be on a single line"
@@ -4289,8 +4289,8 @@ var require_resolve_flow_collection = __commonJS({
4289
4289
  } else {
4290
4290
  ctx.atKey = true;
4291
4291
  const keyStart = props.end;
4292
- const keyNode = key ? composeNode(ctx, key, props, onError) : composeEmptyNode(ctx, keyStart, start, null, props, onError);
4293
- if (isBlock(key))
4292
+ const keyNode = key2 ? composeNode(ctx, key2, props, onError) : composeEmptyNode(ctx, keyStart, start, null, props, onError);
4293
+ if (isBlock(key2))
4294
4294
  onError(keyNode.range, "BLOCK_IN_FLOW", blockMsg);
4295
4295
  ctx.atKey = false;
4296
4296
  const valueProps = resolveProps.resolveProps(sep ?? [], {
@@ -5440,9 +5440,9 @@ var require_cst_scalar = __commonJS({
5440
5440
  ];
5441
5441
  if (!addEndtoBlockProps(props, "end" in token ? token.end : void 0))
5442
5442
  props.push({ type: "newline", offset: -1, indent, source: "\n" });
5443
- for (const key of Object.keys(token))
5444
- if (key !== "type" && key !== "offset")
5445
- delete token[key];
5443
+ for (const key2 of Object.keys(token))
5444
+ if (key2 !== "type" && key2 !== "offset")
5445
+ delete token[key2];
5446
5446
  Object.assign(token, { type: "block-scalar", indent, props, source: body });
5447
5447
  }
5448
5448
  }
@@ -5490,9 +5490,9 @@ var require_cst_scalar = __commonJS({
5490
5490
  default: {
5491
5491
  const indent = "indent" in token ? token.indent : -1;
5492
5492
  const end = "end" in token && Array.isArray(token.end) ? token.end.filter((st) => st.type === "space" || st.type === "comment" || st.type === "newline") : [];
5493
- for (const key of Object.keys(token))
5494
- if (key !== "type" && key !== "offset")
5495
- delete token[key];
5493
+ for (const key2 of Object.keys(token))
5494
+ if (key2 !== "type" && key2 !== "offset")
5495
+ delete token[key2];
5496
5496
  Object.assign(token, { type, indent, source, end });
5497
5497
  }
5498
5498
  }
@@ -5547,12 +5547,12 @@ var require_cst_stringify = __commonJS({
5547
5547
  }
5548
5548
  }
5549
5549
  }
5550
- function stringifyItem({ start, key, sep, value }) {
5550
+ function stringifyItem({ start, key: key2, sep, value }) {
5551
5551
  let res = "";
5552
5552
  for (const st of start)
5553
5553
  res += st.source;
5554
- if (key)
5555
- res += stringifyToken(key);
5554
+ if (key2)
5555
+ res += stringifyToken(key2);
5556
5556
  if (sep)
5557
5557
  for (const st of sep)
5558
5558
  res += st.source;
@@ -6884,7 +6884,7 @@ var require_parser = __commonJS({
6884
6884
  });
6885
6885
  } else if (isFlowToken(it.key) && !includesToken(it.sep, "newline")) {
6886
6886
  const start2 = getFirstKeyStartProps(it.start);
6887
- const key = it.key;
6887
+ const key2 = it.key;
6888
6888
  const sep = it.sep;
6889
6889
  sep.push(this.sourceToken);
6890
6890
  delete it.key;
@@ -6893,7 +6893,7 @@ var require_parser = __commonJS({
6893
6893
  type: "block-map",
6894
6894
  offset: this.offset,
6895
6895
  indent: this.indent,
6896
- items: [{ start: start2, key, sep }]
6896
+ items: [{ start: start2, key: key2, sep }]
6897
6897
  });
6898
6898
  } else if (start.length > 0) {
6899
6899
  it.sep = it.sep.concat(start, this.sourceToken);
@@ -7430,162 +7430,11 @@ var require_src = __commonJS({
7430
7430
  // src/cli.ts
7431
7431
  import { basename } from "path";
7432
7432
 
7433
- // ../adapters-node/src/node-browser-opener.ts
7434
- import { spawn } from "child_process";
7435
- var NodeBrowserOpener = class {
7436
- async open(url) {
7437
- const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", '""', url.replace(/&/g, "^&")]] : ["xdg-open", [url]];
7438
- await new Promise((resolve) => {
7439
- const child = spawn(command, args, { stdio: "ignore", detached: true });
7440
- child.on("error", () => resolve());
7441
- child.on("spawn", () => {
7442
- child.unref();
7443
- resolve();
7444
- });
7445
- });
7446
- }
7447
- };
7448
-
7449
- // ../adapters-node/src/node-process-runner.ts
7450
- import { spawn as spawn2 } from "child_process";
7451
- var STDERR_TAIL_BYTES = 8 * 1024;
7452
- function buildEnv(overrides) {
7453
- const env = { ...process.env };
7454
- for (const [key, value] of Object.entries(overrides ?? {})) {
7455
- if (value === void 0) delete env[key];
7456
- else env[key] = value;
7457
- }
7458
- return env;
7459
- }
7460
- var useShell = process.platform === "win32";
7461
- var NodeProcessRunner = class {
7462
- run(command, args, options = {}) {
7463
- return new Promise((resolve, reject) => {
7464
- const child = spawn2(command, args, {
7465
- env: buildEnv(options.env),
7466
- stdio: ["inherit", "inherit", options.captureStderr ? "pipe" : "inherit"],
7467
- shell: useShell
7468
- });
7469
- let tail = "";
7470
- child.stderr?.on("data", (chunk) => {
7471
- process.stderr.write(chunk);
7472
- tail = (tail + chunk.toString()).slice(-STDERR_TAIL_BYTES);
7473
- });
7474
- const ignore = () => {
7475
- };
7476
- process.on("SIGINT", ignore);
7477
- process.on("SIGTERM", ignore);
7478
- const cleanup = () => {
7479
- process.off("SIGINT", ignore);
7480
- process.off("SIGTERM", ignore);
7481
- };
7482
- child.on("error", (error) => {
7483
- cleanup();
7484
- reject(error);
7485
- });
7486
- child.on("close", (code, signal) => {
7487
- cleanup();
7488
- resolve({ exitCode: code ?? (signal ? 128 + 2 : 1), stderrTail: tail });
7489
- });
7490
- });
7491
- }
7492
- capture(command, args, options = {}) {
7493
- return new Promise((resolve) => {
7494
- const child = spawn2(command, args, { env: buildEnv(options.env), shell: useShell });
7495
- child.stdin.end();
7496
- let stdout2 = "";
7497
- let stderr = "";
7498
- child.stdout.on("data", (c3) => {
7499
- stdout2 += c3.toString();
7500
- });
7501
- child.stderr.on("data", (c3) => {
7502
- stderr += c3.toString();
7503
- });
7504
- child.on("error", (error) => resolve({ exitCode: 127, stdout: stdout2, stderr: error.message }));
7505
- child.on("close", (code) => resolve({ exitCode: code ?? 1, stdout: stdout2, stderr }));
7506
- });
7507
- }
7508
- async probe(command) {
7509
- const result = await this.capture(command, ["--version"]);
7510
- if (result.exitCode !== 0) return { installed: false };
7511
- const output = `${result.stdout} ${result.stderr}`;
7512
- const version = output.match(/(\d+\.\d+\.\d+)/)?.[1];
7513
- return { installed: true, ...version ? { version } : {} };
7514
- }
7515
- };
7516
-
7517
- // ../adapters-node/src/system-tool-installer.ts
7518
- var TOOLS = {
7519
- aws: {
7520
- manualUrl: "https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html",
7521
- recipes: {
7522
- darwin: [{ via: "Homebrew", command: "brew", args: ["install", "awscli"] }],
7523
- win32: [
7524
- { via: "winget", command: "winget", args: ["install", "--exact", "--id", "Amazon.AWSCLI"] }
7525
- ]
7526
- }
7527
- },
7528
- az: {
7529
- manualUrl: "https://learn.microsoft.com/cli/azure/install-azure-cli",
7530
- recipes: {
7531
- darwin: [{ via: "Homebrew", command: "brew", args: ["install", "azure-cli"] }],
7532
- win32: [
7533
- {
7534
- via: "winget",
7535
- command: "winget",
7536
- args: ["install", "--exact", "--id", "Microsoft.AzureCLI"]
7537
- }
7538
- ]
7539
- }
7540
- },
7541
- gcloud: {
7542
- manualUrl: "https://cloud.google.com/sdk/docs/install",
7543
- recipes: {
7544
- darwin: [
7545
- { via: "Homebrew", command: "brew", args: ["install", "--cask", "google-cloud-sdk"] }
7546
- ],
7547
- win32: [
7548
- {
7549
- via: "winget",
7550
- command: "winget",
7551
- args: ["install", "--exact", "--id", "Google.CloudSDK"]
7552
- }
7553
- ]
7554
- }
7555
- }
7556
- };
7557
- var SystemToolInstaller = class {
7558
- constructor(runner, platform = process.platform) {
7559
- this.runner = runner;
7560
- this.platform = platform;
7561
- }
7562
- runner;
7563
- platform;
7564
- available = /* @__PURE__ */ new Map();
7565
- recipeFor(command) {
7566
- const tool = TOOLS[command];
7567
- if (!tool) return void 0;
7568
- const recipe = tool.recipes[this.platform]?.[0];
7569
- return recipe ? { ...recipe, manualUrl: tool.manualUrl } : { via: "official installer", command: "", args: [], manualUrl: tool.manualUrl };
7570
- }
7571
- async install(recipe) {
7572
- if (!recipe.command) return false;
7573
- if (!await this.hasCommand(recipe.command)) return false;
7574
- const { exitCode } = await this.runner.run(recipe.command, recipe.args);
7575
- return exitCode === 0;
7576
- }
7577
- async hasCommand(command) {
7578
- if (!this.available.has(command)) {
7579
- this.available.set(command, (await this.runner.probe(command)).installed);
7580
- }
7581
- return this.available.get(command) ?? false;
7582
- }
7583
- };
7584
-
7585
- // ../adapters-node/src/yaml-config-store.ts
7586
- import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
7587
- import { homedir } from "os";
7588
- import { join } from "path";
7433
+ // ../adapters-node/src/folder-files.ts
7434
+ import { createHash } from "crypto";
7435
+ import { readFileSync, realpathSync, statSync } from "fs";
7436
+ import { mkdir, readFile, writeFile } from "fs/promises";
7437
+ import { dirname, join, resolve } from "path";
7589
7438
 
7590
7439
  // ../core/src/domain/errors.ts
7591
7440
  var CloudhopError = class extends Error {
@@ -7602,6 +7451,8 @@ var CloudhopError = class extends Error {
7602
7451
  // ../core/src/domain/naming.ts
7603
7452
  var RESERVED_NAMES = /* @__PURE__ */ new Set([
7604
7453
  "add",
7454
+ "allow",
7455
+ "deny",
7605
7456
  "agent",
7606
7457
  "alias",
7607
7458
  "login",
@@ -7647,8 +7498,15 @@ function assertValidName(kind, name) {
7647
7498
  }
7648
7499
  }
7649
7500
  function suggestAliasName(...parts) {
7650
- const slug = parts.join("-").toLowerCase().replace(/access$/i, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
7651
- return slug || "alias";
7501
+ const slug = parts.join("-").toLowerCase().replace(/access$/i, "").replace(/[^a-z0-9]+/g, "-");
7502
+ return trimDashes(slug).slice(0, 63) || "alias";
7503
+ }
7504
+ function trimDashes(text4) {
7505
+ let start = 0;
7506
+ let end = text4.length;
7507
+ while (start < end && text4[start] === "-") start++;
7508
+ while (end > start && text4[end - 1] === "-") end--;
7509
+ return text4.slice(start, end);
7652
7510
  }
7653
7511
  var slugTokens = (text4) => text4.toLowerCase().replace(/access$/i, "").split(/[^a-z0-9]+/).filter(Boolean);
7654
7512
  var ROLE_WORDS = {
@@ -8078,6 +7936,29 @@ ${result.stderr}`)) {
8078
7936
  await this.ensureSession(resolved.source.id);
8079
7937
  return provider.processEnv(resolved, mode);
8080
7938
  }
7939
+ /**
7940
+ * What `cloudhop use` puts in a shell: the alias' CLI environment (profile
7941
+ * and region, never credentials) and every variable of that provider to
7942
+ * clear first. With `login: false` it never signs in, so entering a folder
7943
+ * never opens a browser: the first command does it if needed.
7944
+ */
7945
+ async shellEnv(name, { login }) {
7946
+ const resolved = await this.usableAlias(name, {});
7947
+ const provider = this.provider(resolved.source.provider);
7948
+ if (login) await this.ensureSession(resolved.source.id);
7949
+ return {
7950
+ provider: provider.id,
7951
+ clear: provider.shellVars,
7952
+ env: await provider.processEnv(resolved, "cli"),
7953
+ region: resolved.region,
7954
+ label: resolved.target?.label,
7955
+ prod: resolved.alias.prod === true
7956
+ };
7957
+ }
7958
+ /** The variables `cloudhop use --off <provider>` removes from a shell. */
7959
+ shellVars(provider) {
7960
+ return this.provider(provider).shellVars;
7961
+ }
8081
7962
  // ── Health ───────────────────────────────────────────────────────────────
8082
7963
  async doctor() {
8083
7964
  const config = await this.deps.config.load();
@@ -8193,19 +8074,268 @@ ${result.stderr}`)) {
8193
8074
  "Run `cloudhop ls` to see your aliases."
8194
8075
  );
8195
8076
  }
8196
- return alias;
8077
+ return alias;
8078
+ }
8079
+ };
8080
+
8081
+ // ../core/src/domain/model.ts
8082
+ var PROVIDER_IDS = ["aws", "azure", "gcp"];
8083
+ var emptyConfig = () => ({ providers: [], sources: [], aliases: [] });
8084
+
8085
+ // ../core/src/domain/folder.ts
8086
+ var FOLDER_FILE = ".cloudhoprc";
8087
+ var bad = (why) => new CloudhopError(
8088
+ "INVALID_NAME",
8089
+ `Not a valid ${FOLDER_FILE}: ${why}.`,
8090
+ "Write one line per cloud, like `aws: my-alias`."
8091
+ );
8092
+ function parseFolderFile(text4) {
8093
+ const result = {};
8094
+ for (const raw of text4.split(/\r?\n/)) {
8095
+ const line = raw.replace(/#.*/, "").trim();
8096
+ if (!line) continue;
8097
+ const match = /^([a-z]+)\s*[:=]\s*(\S+)$/.exec(line);
8098
+ const key2 = match ? match[1] : "any";
8099
+ const alias = match ? match[2] : line;
8100
+ if (key2 !== "any" && !PROVIDER_IDS.includes(key2)) throw bad(`unknown cloud "${key2}"`);
8101
+ if (result[key2]) throw bad(`"${key2}" appears twice`);
8102
+ assertValidName("alias", alias);
8103
+ result[key2] = alias;
8104
+ }
8105
+ if (Object.keys(result).length === 0) throw bad("it names no alias");
8106
+ return result;
8107
+ }
8108
+ function formatFolderFile(aliases) {
8109
+ const lines = Object.entries(aliases).map(
8110
+ ([key2, alias]) => key2 === "any" ? alias : `${key2}: ${alias}`
8111
+ );
8112
+ return `# cloudhop: aliases for this folder (https://dayepesb.github.io/cloudhop/guides/shell/)
8113
+ ${lines.join("\n")}
8114
+ `;
8115
+ }
8116
+
8117
+ // ../adapters-node/src/folder-files.ts
8118
+ function findFolderFile(start) {
8119
+ let dir = resolve(start);
8120
+ for (; ; ) {
8121
+ const file = join(dir, FOLDER_FILE);
8122
+ try {
8123
+ if (statSync(file).isFile()) return file;
8124
+ } catch {
8125
+ }
8126
+ const parent = dirname(dir);
8127
+ if (parent === dir) return void 0;
8128
+ dir = parent;
8129
+ }
8130
+ }
8131
+ var key = (path2) => {
8132
+ try {
8133
+ return realpathSync.native(path2);
8134
+ } catch {
8135
+ return resolve(path2);
8136
+ }
8137
+ };
8138
+ var digest = (content) => createHash("sha256").update(content).digest("hex");
8139
+ var TrustedFolders = class {
8140
+ file;
8141
+ constructor(configDir) {
8142
+ this.file = join(configDir, "trusted-folders.json");
8143
+ }
8144
+ isTrusted(path2, content) {
8145
+ return this.load().files[key(path2)] === digest(content);
8146
+ }
8147
+ async trust(path2, content) {
8148
+ const data = this.load();
8149
+ data.files[key(path2)] = digest(content);
8150
+ await this.save(data);
8151
+ }
8152
+ /** Returns false when the file was not trusted. */
8153
+ async forget(path2) {
8154
+ const data = this.load();
8155
+ const file = key(path2);
8156
+ if (!(file in data.files)) return false;
8157
+ delete data.files[file];
8158
+ await this.save(data);
8159
+ return true;
8160
+ }
8161
+ load() {
8162
+ try {
8163
+ const data = JSON.parse(readFileSync(this.file, "utf8"));
8164
+ return data?.version === 1 && data.files ? data : { version: 1, files: {} };
8165
+ } catch {
8166
+ return { version: 1, files: {} };
8167
+ }
8168
+ }
8169
+ async save(data) {
8170
+ await mkdir(dirname(this.file), { recursive: true });
8171
+ await writeFile(this.file, `${JSON.stringify(data, null, 2)}
8172
+ `, { mode: 384 });
8173
+ }
8174
+ };
8175
+ async function readFolderFile(path2) {
8176
+ return readFile(path2, "utf8").catch(() => void 0);
8177
+ }
8178
+
8179
+ // ../adapters-node/src/node-browser-opener.ts
8180
+ import { spawn } from "child_process";
8181
+ var NodeBrowserOpener = class {
8182
+ async open(url) {
8183
+ const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", '""', url.replace(/&/g, "^&")]] : ["xdg-open", [url]];
8184
+ await new Promise((resolve3) => {
8185
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
8186
+ child.on("error", () => resolve3());
8187
+ child.on("spawn", () => {
8188
+ child.unref();
8189
+ resolve3();
8190
+ });
8191
+ });
8192
+ }
8193
+ };
8194
+
8195
+ // ../adapters-node/src/node-process-runner.ts
8196
+ import { spawn as spawn2 } from "child_process";
8197
+ var STDERR_TAIL_BYTES = 8 * 1024;
8198
+ function buildEnv(overrides) {
8199
+ const env = { ...process.env };
8200
+ for (const [key2, value] of Object.entries(overrides ?? {})) {
8201
+ if (value === void 0) delete env[key2];
8202
+ else env[key2] = value;
8203
+ }
8204
+ return env;
8205
+ }
8206
+ var useShell = process.platform === "win32";
8207
+ var NodeProcessRunner = class {
8208
+ run(command, args, options = {}) {
8209
+ return new Promise((resolve3, reject) => {
8210
+ const child = spawn2(command, args, {
8211
+ env: buildEnv(options.env),
8212
+ stdio: ["inherit", "inherit", options.captureStderr ? "pipe" : "inherit"],
8213
+ shell: useShell
8214
+ });
8215
+ let tail = "";
8216
+ child.stderr?.on("data", (chunk) => {
8217
+ process.stderr.write(chunk);
8218
+ tail = (tail + chunk.toString()).slice(-STDERR_TAIL_BYTES);
8219
+ });
8220
+ const ignore = () => {
8221
+ };
8222
+ process.on("SIGINT", ignore);
8223
+ process.on("SIGTERM", ignore);
8224
+ const cleanup = () => {
8225
+ process.off("SIGINT", ignore);
8226
+ process.off("SIGTERM", ignore);
8227
+ };
8228
+ child.on("error", (error) => {
8229
+ cleanup();
8230
+ reject(error);
8231
+ });
8232
+ child.on("close", (code, signal) => {
8233
+ cleanup();
8234
+ resolve3({ exitCode: code ?? (signal ? 128 + 2 : 1), stderrTail: tail });
8235
+ });
8236
+ });
8237
+ }
8238
+ capture(command, args, options = {}) {
8239
+ return new Promise((resolve3) => {
8240
+ const child = spawn2(command, args, { env: buildEnv(options.env), shell: useShell });
8241
+ child.stdin.end();
8242
+ let stdout2 = "";
8243
+ let stderr = "";
8244
+ child.stdout.on("data", (c3) => {
8245
+ stdout2 += c3.toString();
8246
+ });
8247
+ child.stderr.on("data", (c3) => {
8248
+ stderr += c3.toString();
8249
+ });
8250
+ child.on("error", (error) => resolve3({ exitCode: 127, stdout: stdout2, stderr: error.message }));
8251
+ child.on("close", (code) => resolve3({ exitCode: code ?? 1, stdout: stdout2, stderr }));
8252
+ });
8253
+ }
8254
+ async probe(command) {
8255
+ const result = await this.capture(command, ["--version"]);
8256
+ if (result.exitCode !== 0) return { installed: false };
8257
+ const output = `${result.stdout} ${result.stderr}`;
8258
+ const version = output.match(/(\d+\.\d+\.\d+)/)?.[1];
8259
+ return { installed: true, ...version ? { version } : {} };
8260
+ }
8261
+ };
8262
+
8263
+ // ../adapters-node/src/system-tool-installer.ts
8264
+ var TOOLS = {
8265
+ aws: {
8266
+ manualUrl: "https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html",
8267
+ recipes: {
8268
+ darwin: [{ via: "Homebrew", command: "brew", args: ["install", "awscli"] }],
8269
+ win32: [
8270
+ { via: "winget", command: "winget", args: ["install", "--exact", "--id", "Amazon.AWSCLI"] }
8271
+ ]
8272
+ }
8273
+ },
8274
+ az: {
8275
+ manualUrl: "https://learn.microsoft.com/cli/azure/install-azure-cli",
8276
+ recipes: {
8277
+ darwin: [{ via: "Homebrew", command: "brew", args: ["install", "azure-cli"] }],
8278
+ win32: [
8279
+ {
8280
+ via: "winget",
8281
+ command: "winget",
8282
+ args: ["install", "--exact", "--id", "Microsoft.AzureCLI"]
8283
+ }
8284
+ ]
8285
+ }
8286
+ },
8287
+ gcloud: {
8288
+ manualUrl: "https://cloud.google.com/sdk/docs/install",
8289
+ recipes: {
8290
+ darwin: [
8291
+ { via: "Homebrew", command: "brew", args: ["install", "--cask", "google-cloud-sdk"] }
8292
+ ],
8293
+ win32: [
8294
+ {
8295
+ via: "winget",
8296
+ command: "winget",
8297
+ args: ["install", "--exact", "--id", "Google.CloudSDK"]
8298
+ }
8299
+ ]
8300
+ }
8301
+ }
8302
+ };
8303
+ var SystemToolInstaller = class {
8304
+ constructor(runner, platform = process.platform) {
8305
+ this.runner = runner;
8306
+ this.platform = platform;
8307
+ }
8308
+ runner;
8309
+ platform;
8310
+ available = /* @__PURE__ */ new Map();
8311
+ recipeFor(command) {
8312
+ const tool = TOOLS[command];
8313
+ if (!tool) return void 0;
8314
+ const recipe = tool.recipes[this.platform]?.[0];
8315
+ return recipe ? { ...recipe, manualUrl: tool.manualUrl } : { via: "official installer", command: "", args: [], manualUrl: tool.manualUrl };
8316
+ }
8317
+ async install(recipe) {
8318
+ if (!recipe.command) return false;
8319
+ if (!await this.hasCommand(recipe.command)) return false;
8320
+ const { exitCode } = await this.runner.run(recipe.command, recipe.args);
8321
+ return exitCode === 0;
8322
+ }
8323
+ async hasCommand(command) {
8324
+ if (!this.available.has(command)) {
8325
+ this.available.set(command, (await this.runner.probe(command)).installed);
8326
+ }
8327
+ return this.available.get(command) ?? false;
8197
8328
  }
8198
8329
  };
8199
8330
 
8200
- // ../core/src/domain/model.ts
8201
- var PROVIDER_IDS = ["aws", "azure", "gcp"];
8202
- var emptyConfig = () => ({ providers: [], sources: [], aliases: [] });
8203
-
8204
8331
  // ../adapters-node/src/yaml-config-store.ts
8332
+ import { mkdir as mkdir2, readFile as readFile2, rename, rm, writeFile as writeFile2 } from "fs/promises";
8333
+ import { homedir } from "os";
8334
+ import { join as join2 } from "path";
8205
8335
  var import_yaml = __toESM(require_dist(), 1);
8206
8336
  function defaultConfigDir(env = process.env) {
8207
8337
  if (env.CLOUDHOP_HOME) return env.CLOUDHOP_HOME;
8208
- return join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "cloudhop");
8338
+ return join2(env.XDG_CONFIG_HOME || join2(homedir(), ".config"), "cloudhop");
8209
8339
  }
8210
8340
  var HEADER = "# cloudhop configuration. Contains no secrets.\n# Docs: https://github.com/dayepesb/cloudhop\n";
8211
8341
  var YamlConfigStore = class {
@@ -8214,10 +8344,10 @@ var YamlConfigStore = class {
8214
8344
  }
8215
8345
  dir;
8216
8346
  get configPath() {
8217
- return join(this.dir, "config.yaml");
8347
+ return join2(this.dir, "config.yaml");
8218
8348
  }
8219
8349
  get targetsPath() {
8220
- return join(this.dir, "targets.json");
8350
+ return join2(this.dir, "targets.json");
8221
8351
  }
8222
8352
  async load() {
8223
8353
  const raw = await this.read(this.configPath);
@@ -8265,7 +8395,7 @@ var YamlConfigStore = class {
8265
8395
  }
8266
8396
  async read(path2) {
8267
8397
  try {
8268
- return await readFile(path2, "utf8");
8398
+ return await readFile2(path2, "utf8");
8269
8399
  } catch (error) {
8270
8400
  if (error.code === "ENOENT") return void 0;
8271
8401
  throw error;
@@ -8273,9 +8403,9 @@ var YamlConfigStore = class {
8273
8403
  }
8274
8404
  /** Writes through a temp file so a crash never leaves a half-written config. */
8275
8405
  async write(path2, content) {
8276
- await mkdir(this.dir, { recursive: true, mode: 448 });
8406
+ await mkdir2(this.dir, { recursive: true, mode: 448 });
8277
8407
  const tmp = `${path2}.${process.pid}.tmp`;
8278
- await writeFile(tmp, content, { mode: 384 });
8408
+ await writeFile2(tmp, content, { mode: 384 });
8279
8409
  await rename(tmp, path2);
8280
8410
  }
8281
8411
  };
@@ -8288,17 +8418,17 @@ var MAX_OUTPUT = 1e5;
8288
8418
  var cap = (text4) => text4.length > MAX_OUTPUT ? `${text4.slice(0, MAX_OUTPUT)}
8289
8419
  \u2026 [truncated: ${text4.length - MAX_OUTPUT} more characters. Narrow the command with --query or filters.]` : text4;
8290
8420
  var WINDOWS_SHELL_CHARS = /[&|<>^%!"\r\n]/;
8291
- var text = (args, key) => {
8292
- const value = args[key];
8421
+ var text = (args, key2) => {
8422
+ const value = args[key2];
8293
8423
  if (typeof value !== "string" || !value.trim()) {
8294
- throw new CloudhopError("INVALID_NAME", `"${key}" is required.`);
8424
+ throw new CloudhopError("INVALID_NAME", `"${key2}" is required.`);
8295
8425
  }
8296
8426
  return value.trim();
8297
8427
  };
8298
- var stringArray = (args, key) => {
8299
- const value = args[key];
8428
+ var stringArray = (args, key2) => {
8429
+ const value = args[key2];
8300
8430
  if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) {
8301
- throw new CloudhopError("INVALID_NAME", `"${key}" must be an array of strings.`);
8431
+ throw new CloudhopError("INVALID_NAME", `"${key2}" must be an array of strings.`);
8302
8432
  }
8303
8433
  return value;
8304
8434
  };
@@ -8308,15 +8438,15 @@ var aliasInput = {
8308
8438
  description: "The cloudhop alias to use, as listed by list_aliases. Ask the user which one."
8309
8439
  }
8310
8440
  };
8311
- function createTools(app) {
8441
+ function createTools(app, context) {
8312
8442
  return [
8313
8443
  {
8314
8444
  name: "list_aliases",
8315
8445
  title: "List cloudhop aliases",
8316
- description: "Lists every cloudhop source (SSO portal or personal account) with its session state, and every alias: name, account/role label, region and whether a sign-in is needed. Call this first, then ask the user which alias to work with. Never contains secrets.",
8446
+ description: "Lists every cloudhop source (SSO portal or personal account) with its session state, and every alias: name, account/role label, region and whether a sign-in is needed. Also hints: `folder` (the aliases this project pins in its .cloudhoprc, and whether the user trusted it) and `inUse` (aliases the user switched their terminal to). Call this first, then ask the user which alias to work with, offering the hinted one first. Never contains secrets.",
8317
8447
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
8318
8448
  annotations: { readOnlyHint: true, openWorldHint: false },
8319
- run: () => app.overview()
8449
+ run: async () => ({ ...await app.overview(), ...await context?.() })
8320
8450
  },
8321
8451
  {
8322
8452
  name: "alias_status",
@@ -8476,7 +8606,7 @@ function createTools(app) {
8476
8606
 
8477
8607
  // ../mcp/src/server.ts
8478
8608
  var MCP_INSTRUCTIONS = `cloudhop gives each cloud account + role + region a short alias and signs the user in only when a session expired.
8479
- Before the first cloud operation in a conversation: call list_aliases, show the user the aliases (name, account/role, region) and ask which one to use. Never guess the alias.
8609
+ Before the first cloud operation in a conversation: call list_aliases, show the user the aliases (name, account/role, region) and ask which one to use. Never guess the alias. If list_aliases has folder.aliases (the project's .cloudhoprc) or inUse, offer that alias first, but still ask.
8480
8610
  Use run_cloud_cli for cloud CLI commands, never --profile. Read-only commands may run once the alias is chosen; before anything that creates, changes or deletes resources, show the alias, account, region and exact command, and wait for the user's explicit OK. Aliases with prod: true are production: run_cloud_cli refuses changes there until the user approves and you pass confirm_production: true.
8481
8611
  To show the user something in the web console, call open_console (it opens their browser, already signed in).
8482
8612
  If a tool returns LOGIN_REQUIRED or alias_status says browser-sign-in: tell the user their browser will open, then call login (it waits up to 5 minutes for them to approve). Never print or ask for credentials.`;
@@ -8485,9 +8615,10 @@ function serveMcp({
8485
8615
  app,
8486
8616
  version,
8487
8617
  input = process.stdin,
8488
- output = process.stdout
8618
+ output = process.stdout,
8619
+ context
8489
8620
  }) {
8490
- const tools = createTools(app);
8621
+ const tools = createTools(app, context);
8491
8622
  const byName = new Map(tools.map((t2) => [t2.name, t2]));
8492
8623
  const send2 = (message) => output.write(`${JSON.stringify(message)}
8493
8624
  `);
@@ -8553,7 +8684,7 @@ function serveMcp({
8553
8684
  if (!isNotification) fail(id, -32601, `Method not found: ${method}`);
8554
8685
  }
8555
8686
  };
8556
- return new Promise((resolve) => {
8687
+ return new Promise((resolve3) => {
8557
8688
  const lines = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
8558
8689
  const pending = /* @__PURE__ */ new Set();
8559
8690
  lines.on("line", (line) => {
@@ -8573,7 +8704,7 @@ function serveMcp({
8573
8704
  task.finally(() => pending.delete(task));
8574
8705
  });
8575
8706
  lines.on("close", () => {
8576
- Promise.allSettled([...pending]).then(() => resolve());
8707
+ Promise.allSettled([...pending]).then(() => resolve3());
8577
8708
  });
8578
8709
  });
8579
8710
  }
@@ -8588,7 +8719,9 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
8588
8719
  "noMcp",
8589
8720
  "force",
8590
8721
  "prod",
8591
- "off"
8722
+ "off",
8723
+ "save",
8724
+ "leave"
8592
8725
  ]);
8593
8726
  var camel = (name) => name.replace(/-([a-z])/g, (_2, c3) => c3.toUpperCase());
8594
8727
  function parseArgs(argv) {
@@ -10009,7 +10142,7 @@ import { styleText as styleText2, stripVTControlCharacters } from "util";
10009
10142
  import process$1 from "process";
10010
10143
  var import_sisteransi2 = __toESM(require_src(), 1);
10011
10144
  import { existsSync, lstatSync, readdirSync } from "fs";
10012
- import { dirname, join as join2 } from "path";
10145
+ import { dirname as dirname2, join as join3 } from "path";
10013
10146
  function isUnicodeSupported() {
10014
10147
  if (process$1.platform !== "win32") {
10015
10148
  return process$1.env.TERM !== "linux";
@@ -10947,10 +11080,10 @@ var path = (e) => {
10947
11080
  return [];
10948
11081
  try {
10949
11082
  let i2;
10950
- existsSync(t2) ? lstatSync(t2).isDirectory() && (!e.directory || t2.endsWith("/")) ? i2 = t2 : i2 = dirname(t2) : i2 = dirname(t2);
11083
+ existsSync(t2) ? lstatSync(t2).isDirectory() && (!e.directory || t2.endsWith("/")) ? i2 = t2 : i2 = dirname2(t2) : i2 = dirname2(t2);
10951
11084
  const c3 = t2.length > 1 && t2.endsWith("/") ? t2.slice(0, -1) : t2;
10952
11085
  return readdirSync(i2).map((r2) => {
10953
- const n3 = join2(i2, r2), m3 = lstatSync(n3);
11086
+ const n3 = join3(i2, r2), m3 = lstatSync(n3);
10954
11087
  return {
10955
11088
  name: r2,
10956
11089
  path: n3,
@@ -11707,16 +11840,16 @@ import { homedir as homedir2 } from "os";
11707
11840
  import { isAbsolute, relative } from "path";
11708
11841
 
11709
11842
  // ../assistants/src/installer.ts
11710
- import { createHash } from "crypto";
11843
+ import { createHash as createHash2 } from "crypto";
11711
11844
  import { existsSync as existsSync2 } from "fs";
11712
- import { mkdir as mkdir2, readFile as readFile2, rename as rename2, rm as rm2, rmdir, writeFile as writeFile2 } from "fs/promises";
11713
- import { dirname as dirname2, join as join4 } from "path";
11845
+ import { mkdir as mkdir3, readFile as readFile3, rename as rename2, rm as rm2, rmdir, writeFile as writeFile3 } from "fs/promises";
11846
+ import { dirname as dirname3, join as join5 } from "path";
11714
11847
 
11715
11848
  // ../assistants/src/assets/skill.md
11716
- var skill_default = "---\nname: cloudhop\ndescription: Run AWS CLI commands and other cloud tools as a specific account, role and region through cloudhop aliases (the `hop` and `cloudhop` commands). Use whenever the user asks to do anything in their AWS accounts, inspect or change cloud resources, run aws/terraform/cdk against an account, list their cloud accounts or aliases, or mentions hop or cloudhop.\n---\n\n# Working with cloud accounts through cloudhop\n\nThe user manages their cloud access with **cloudhop**. Each **alias** (for\nexample `acme-dev`) points to one account, one role and one region. cloudhop\nsigns the user in through their browser only when a session has expired. There\nare no access keys: never ask for, create, print or store credentials.\n\n## 1. Pick the alias with the user, every time\n\nBefore the first cloud operation in a conversation:\n\n1. List the aliases. Use the `list_aliases` tool if the cloudhop MCP server is\n connected; otherwise run `cloudhop ls --json`.\n2. Show the user the options (alias, account / role, region) and **ask which\n one to use**. Don't guess, even if one name looks right, and don't pick\n one just because there is only one: confirm it.\n3. Use that alias for the rest of the conversation. Ask again if the user\n moves to another environment, account or task.\n\n## 2. Check the session before running anything\n\nRun `cloudhop status <alias> --json` (or the `alias_status` tool) and read\n`next`:\n\n| `next` | Meaning | What to do |\n| ----------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `ready` | Session valid | Go ahead |\n| `silent-renewal` | Expired but renewable without the user | Go ahead; cloudhop renews it |\n| `browser-sign-in` | The user must sign in | Tell the user their browser will open and they need to approve, then run `cloudhop login <source>` (or the `login` tool) and wait up to 5 minutes |\n| `unknown` | Managed by the cloud CLI (personal accounts) | Go ahead; if it fails, run `cloudhop login <source>` |\n\nDon't start several cloud commands in parallel while a sign-in is pending.\n\n## 3. Run commands through the alias\n\n- **AWS CLI**: `hop <alias> <aws arguments>`, e.g. `hop acme-dev s3 ls`.\n It is exactly `aws <arguments>` with the alias' account, role and region.\n Never add `--profile`. Use `--region <r>` only to override the region once.\n With the MCP server, use `run_cloud_cli` with `args` (no program name).\n- **Who am I**: `hop <alias>` with no arguments (or the `whoami` tool).\n- **The same command in several aliases** (after the user chose them):\n `cloudhop each a,b,c --yes -- <aws arguments>`; add `--json` to parse the\n results.\n- **The AWS web console**: `cloudhop console <alias> [service]` (or the\n `open_console` tool) opens it in the user's browser, already signed in.\n Never try to print or fetch its URL.\n- **Always name the alias.** Never run the interactive forms meant for a\n person at a terminal: `hop` or `hop -` without an alias, `cloudhop console`\n without an alias, `cloudhop use` (it only changes an interactive shell),\n `cloudhop setup`, or `cloudhop add`/`alias` without flags. Nobody can answer\n their prompts in your terminal.\n- **Other tools** (Terraform, CDK, SDK scripts):\n `cloudhop exec <alias> -- <command>`, e.g.\n `cloudhop exec acme-dev -- terraform plan`.\n- Prefer `--output json` and `--query` to keep output small.\n- Don't export `AWS_PROFILE`, `AWS_ACCESS_KEY_ID` or similar variables, don't\n run commands that print credentials (`env`, `printenv`, `export-credentials`)\n and never edit `~/.aws/config`, `~/.aws/credentials` or cloudhop's own files.\n\n## 4. Confirm before changing anything\n\n- Read-only commands (`list`, `describe`, `get`, `s3 ls`, `logs tail`) can run\n once the user has chosen the alias.\n- Before any command that **creates, changes, deletes or costs money**, show\n the alias, account, region and the exact command, and wait for the user's\n explicit \"yes\". One confirmation covers one command, not a series.\n- **Production aliases** have `\"prod\": true` in `cloudhop ls --json` (and\n `PROD` in `cloudhop ls`). Confirm every change there and suggest a dry run\n (`--dryrun`, `terraform plan`) first. cloudhop enforces it: without a\n terminal, `hop`/`cloudhop exec` refuse changes on production with\n `PROD_CONFIRMATION_REQUIRED`. After the user explicitly approves the exact\n command, rerun it with `CLOUDHOP_CONFIRM_PROD=1` in that command's\n environment (with MCP: `confirm_production: true`). Never set it on your own.\n- Treat aliases or accounts that merely look like production (`prd`, `live`)\n the same way, even if they are not marked.\n- Don't change cloudhop itself (`cloudhop add`, `rm`, `mv`, `region`, `prod`,\n `team import`, `agent`, `login` for another source) unless the user asked\n for it. Never unmark a production alias to get around a confirmation.\n\n## 5. When something goes wrong\n\n| You see | Do this |\n| --------------------------------------------------- | ------------------------------------------------------------------------------ |\n| \"Opening your browser to log in\" | Tell the user to approve in the browser; the command continues by itself |\n| The command was cut off while waiting for a sign-in | Run `cloudhop login <source>` with a timeout of at least 5 minutes, then retry |\n| `LOGIN_REQUIRED` | Same as `browser-sign-in` above |\n| No browser on this machine (remote, container) | Run `cloudhop login <source> --device-code` and show the user the URL and code |\n| `Alias \"x\" not found` | Run `cloudhop ls` and ask the user again |\n| AWS CLI missing or too old | Suggest `cloudhop doctor`; don't install anything without asking |\n| `AccessDenied` | The role lacks permission: tell the user; don't try other aliases on your own |\n\nMore: https://dayepesb.github.io/cloudhop/guides/hop-like-aws/\n";
11849
+ var skill_default = "---\nname: cloudhop\ndescription: Run AWS CLI commands and other cloud tools as a specific account, role and region through cloudhop aliases (the `hop` and `cloudhop` commands). Use whenever the user asks to do anything in their AWS accounts, inspect or change cloud resources, run aws/terraform/cdk against an account, list their cloud accounts or aliases, or mentions hop or cloudhop.\n---\n\n# Working with cloud accounts through cloudhop\n\nThe user manages their cloud access with **cloudhop**. Each **alias** (for\nexample `acme-dev`) points to one account, one role and one region. cloudhop\nsigns the user in through their browser only when a session has expired. There\nare no access keys: never ask for, create, print or store credentials.\n\n## How the user works (to guide them)\n\nWhen the user asks how to set up or use cloudhop themselves, describe this\npath; these are commands for **them** to run in their own terminal:\n\n1. **Once**: `npm install --global cloudhop`, then `cloudhop add` (signs in\n to an SSO portal or personal account and names each account/role as an\n alias), then `cloudhop shell` (adds one line to their shell's startup\n file and, with Starship, the alias segment to `starship.toml`; they open\n a new terminal afterwards). `cloudhop setup` does all of it in one\n guided run. Works in zsh, bash (Git Bash too), fish and PowerShell;\n in Windows `cmd.exe`, `cloudhop use` opens a new `cmd` with the alias in\n its prompt.\n2. **Every day**: `cloudhop use <alias>` switches that terminal to an\n account; after that they type plain `aws ...`, Terraform\n or SDK commands. `cloudhop use <other>` switches accounts and\n `cloudhop use --off` goes back. Expired sessions sign in by themselves,\n and on production aliases `aws` asks before changes. Their prompt shows\n the alias in use, in red for production, and `cloudhop status` tells\n them which alias their terminal uses.\n3. **Per project**: `cloudhop use <alias> --save` writes a `.cloudhoprc` in\n the folder, so `cd` into it switches the terminal; teammates run\n `cloudhop allow` once after cloning (and again after the file changes);\n `cloudhop deny` withdraws it. A file does nothing until it is trusted.\n4. **One-offs**: `hop <alias> <aws args>` runs one command in another\n account without switching; `cloudhop each` runs it in many;\n `cloudhop console <alias> [service]` opens the web console signed in.\n\nWhy `cloudhop shell` is needed: a program can't change the environment of\nthe terminal that started it, only the shell can.\nWithout it, `cloudhop use` opens a new shell instead; `hop` always works.\n\nSwitches, for the user: `CLOUDHOP_WRAP=0` keeps `aws` as the plain AWS\nCLI, `CLOUDHOP_AUTO_USE=0` ignores `.cloudhoprc` files, `CLOUDHOP_PROMPT=0`\nhides the prompt label. `cloudhop shell starship` prints the Starship\nsegment for people who place it by hand.\n\nWhen their terminal doesn't do what they expect:\n\n| They say | Suggest |\n| --- | --- |\n| \"Which account am I in?\" | `cloudhop status` (first line) |\n| `cloudhop use` opened a new shell / says it needs the integration | `cloudhop shell`, then a new terminal |\n| No label in the prompt | Run `cloudhop shell` again (it adds what's missing), then a new terminal |\n| `cd` into the project doesn't switch | `cloudhop allow` in that folder; check the alias name exists (`cloudhop ls`) |\n| `aws` runs in the wrong account | `cloudhop status`, then `cloudhop use <alias>` or `cloudhop use --off` |\n\nThat is the user's way. **You** work differently, below: you always name the\nalias with `hop <alias>`, because your shell doesn't have their integration.\n\n## 1. Pick the alias with the user, every time\n\nBefore the first cloud operation in a conversation:\n\n1. List the aliases. Use the `list_aliases` tool if the cloudhop MCP server is\n connected; otherwise run `cloudhop ls --json`.\n2. Show the user the options (alias, account / role, region) and **ask which\n one to use**. Don't guess, even if one name looks right, and don't pick\n one just because there is only one: confirm it.\n - The listing may carry hints: `folder.aliases` is what this project pins\n in its `.cloudhoprc` (`folder.trusted` says whether the user approved\n it), and `inUse` is what the user switched their terminal to with\n `cloudhop use` (`cloudhop status --json` has `inUse` too). Offer that\n alias first (\"This project uses\n `license-qa`, use it?\"), but still wait for the answer.\n3. Use that alias for the rest of the conversation. Ask again if the user\n moves to another environment, account or task.\n\n## 2. Check the session before running anything\n\nRun `cloudhop status <alias> --json` (or the `alias_status` tool) and read\n`next`:\n\n| `next` | Meaning | What to do |\n| ----------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `ready` | Session valid | Go ahead |\n| `silent-renewal` | Expired but renewable without the user | Go ahead; cloudhop renews it |\n| `browser-sign-in` | The user must sign in | Tell the user their browser will open and they need to approve, then run `cloudhop login <source>` (or the `login` tool) and wait up to 5 minutes |\n| `unknown` | Managed by the cloud CLI (personal accounts) | Go ahead; if it fails, run `cloudhop login <source>` |\n\nDon't start several cloud commands in parallel while a sign-in is pending.\n\n## 3. Run commands through the alias\n\n- **AWS CLI**: `hop <alias> <aws arguments>`, e.g. `hop acme-dev s3 ls`.\n It is exactly `aws <arguments>` with the alias' account, role and region.\n Never add `--profile`. Use `--region <r>` only to override the region once.\n With the MCP server, use `run_cloud_cli` with `args` (no program name).\n- **Who am I**: `hop <alias>` with no arguments (or the `whoami` tool).\n- **The same command in several aliases** (after the user chose them):\n `cloudhop each a,b,c --yes -- <aws arguments>`; add `--json` to parse the\n results.\n- **The AWS web console**: `cloudhop console <alias> [service]` (or the\n `open_console` tool) opens it in the user's browser, already signed in.\n Never try to print or fetch its URL.\n- **Always name the alias**, even when the project has a `.cloudhoprc` or\n the user's terminal uses an alias: your shell doesn't have the user's\n cloudhop integration, so a plain `aws` may run as anything. Use\n `hop <alias> ...`, never bare `aws`.\n- Never run the interactive forms meant for a person at a terminal: `hop` or\n `hop -` without an alias, `cloudhop console` without an alias,\n `cloudhop use` (it changes or opens an interactive shell), `cloudhop\n allow`/`deny` (trusting a folder is the user's decision), `cloudhop setup`,\n or `cloudhop add`/`alias` without flags. Nobody can answer their prompts in\n your terminal.\n- **Other tools** (Terraform, CDK, SDK scripts):\n `cloudhop exec <alias> -- <command>`, e.g.\n `cloudhop exec acme-dev -- terraform plan`.\n- Prefer `--output json` and `--query` to keep output small.\n- Don't export `AWS_PROFILE`, `AWS_ACCESS_KEY_ID` or similar variables, don't\n run commands that print credentials (`env`, `printenv`, `export-credentials`)\n and never edit `~/.aws/config`, `~/.aws/credentials` or cloudhop's own files.\n\n## 4. Confirm before changing anything\n\n- Read-only commands (`list`, `describe`, `get`, `s3 ls`, `logs tail`) can run\n once the user has chosen the alias.\n- Before any command that **creates, changes, deletes or costs money**, show\n the alias, account, region and the exact command, and wait for the user's\n explicit \"yes\". One confirmation covers one command, not a series.\n- **Production aliases** have `\"prod\": true` in `cloudhop ls --json` (and\n `PROD` in `cloudhop ls`). Confirm every change there and suggest a dry run\n (`--dryrun`, `terraform plan`) first. cloudhop enforces it: without a\n terminal, `hop`/`cloudhop exec` refuse changes on production with\n `PROD_CONFIRMATION_REQUIRED`. After the user explicitly approves the exact\n command, rerun it with `CLOUDHOP_CONFIRM_PROD=1` in that command's\n environment (with MCP: `confirm_production: true`). Never set it on your own.\n- Treat aliases or accounts that merely look like production (`prd`, `live`)\n the same way, even if they are not marked.\n- Don't change cloudhop itself (`cloudhop add`, `rm`, `mv`, `region`, `prod`,\n `team import`, `agent`, `login` for another source) or write a\n `.cloudhoprc` unless the user asked for it. Never unmark a production\n alias to get around a confirmation.\n\n## 5. When something goes wrong\n\n| You see | Do this |\n| --------------------------------------------------- | ------------------------------------------------------------------------------ |\n| \"Opening your browser to log in\" | Tell the user to approve in the browser; the command continues by itself |\n| The command was cut off while waiting for a sign-in | Run `cloudhop login <source>` with a timeout of at least 5 minutes, then retry |\n| `LOGIN_REQUIRED` | Same as `browser-sign-in` above |\n| No browser on this machine (remote, container) | Run `cloudhop login <source> --device-code` and show the user the URL and code |\n| `Alias \"x\" not found` | Run `cloudhop ls` and ask the user again |\n| AWS CLI missing or too old | Suggest `cloudhop doctor`; don't install anything without asking |\n| `AccessDenied` | The role lacks permission: tell the user; don't try other aliases on your own |\n\nMore: https://dayepesb.github.io/cloudhop/guides/hop-like-aws/\n";
11717
11850
 
11718
11851
  // ../assistants/src/targets.ts
11719
- import { join as join3 } from "path";
11852
+ import { join as join4 } from "path";
11720
11853
  var ASSISTANT_IDS = [
11721
11854
  "claude",
11722
11855
  "kiro",
@@ -11731,9 +11864,9 @@ var ASSISTANTS = {
11731
11864
  claude: {
11732
11865
  id: "claude",
11733
11866
  name: "Claude Code",
11734
- markers: ({ home }) => [join3(home, ".claude")],
11735
- skillDir: (scope, { home, project }) => join3(scope === "user" ? home : project, ".claude", "skills", SKILL_NAME),
11736
- mcp: (scope, { project }) => scope === "project" ? { kind: "json", file: join3(project, ".mcp.json"), key: "mcpServers", withType: false } : {
11867
+ markers: ({ home }) => [join4(home, ".claude")],
11868
+ skillDir: (scope, { home, project }) => join4(scope === "user" ? home : project, ".claude", "skills", SKILL_NAME),
11869
+ mcp: (scope, { project }) => scope === "project" ? { kind: "json", file: join4(project, ".mcp.json"), key: "mcpServers", withType: false } : {
11737
11870
  kind: "cli",
11738
11871
  command: "claude",
11739
11872
  add: ["mcp", "add", "--scope", "user", "cloudhop", "--", MCP_COMMAND, ...MCP_ARGS],
@@ -11744,11 +11877,11 @@ var ASSISTANTS = {
11744
11877
  kiro: {
11745
11878
  id: "kiro",
11746
11879
  name: "Kiro",
11747
- markers: ({ home }) => [join3(home, ".kiro")],
11748
- skillDir: (scope, { home, project }) => join3(scope === "user" ? home : project, ".kiro", "skills", SKILL_NAME),
11880
+ markers: ({ home }) => [join4(home, ".kiro")],
11881
+ skillDir: (scope, { home, project }) => join4(scope === "user" ? home : project, ".kiro", "skills", SKILL_NAME),
11749
11882
  mcp: (scope, { home, project }) => ({
11750
11883
  kind: "json",
11751
- file: join3(scope === "user" ? home : project, ".kiro", "settings", "mcp.json"),
11884
+ file: join4(scope === "user" ? home : project, ".kiro", "settings", "mcp.json"),
11752
11885
  key: "mcpServers",
11753
11886
  withType: false
11754
11887
  })
@@ -11756,11 +11889,11 @@ var ASSISTANTS = {
11756
11889
  copilot: {
11757
11890
  id: "copilot",
11758
11891
  name: "GitHub Copilot (VS Code)",
11759
- markers: ({ home }) => [join3(home, ".copilot"), join3(home, ".vscode")],
11760
- skillDir: (scope, { home, project }) => scope === "user" ? join3(home, ".copilot", "skills", SKILL_NAME) : join3(project, ".github", "skills", SKILL_NAME),
11892
+ markers: ({ home }) => [join4(home, ".copilot"), join4(home, ".vscode")],
11893
+ skillDir: (scope, { home, project }) => scope === "user" ? join4(home, ".copilot", "skills", SKILL_NAME) : join4(project, ".github", "skills", SKILL_NAME),
11761
11894
  mcp: (scope, { project }) => scope === "project" ? {
11762
11895
  kind: "json",
11763
- file: join3(project, ".vscode", "mcp.json"),
11896
+ file: join4(project, ".vscode", "mcp.json"),
11764
11897
  key: "servers",
11765
11898
  withType: true
11766
11899
  } : {
@@ -11782,11 +11915,11 @@ var ASSISTANTS = {
11782
11915
  cursor: {
11783
11916
  id: "cursor",
11784
11917
  name: "Cursor",
11785
- markers: ({ home }) => [join3(home, ".cursor")],
11786
- skillDir: (scope, { home, project }) => join3(scope === "user" ? home : project, ".cursor", "skills", SKILL_NAME),
11918
+ markers: ({ home }) => [join4(home, ".cursor")],
11919
+ skillDir: (scope, { home, project }) => join4(scope === "user" ? home : project, ".cursor", "skills", SKILL_NAME),
11787
11920
  mcp: (scope, { home, project }) => ({
11788
11921
  kind: "json",
11789
- file: join3(scope === "user" ? home : project, ".cursor", "mcp.json"),
11922
+ file: join4(scope === "user" ? home : project, ".cursor", "mcp.json"),
11790
11923
  key: "mcpServers",
11791
11924
  withType: true
11792
11925
  })
@@ -11794,29 +11927,29 @@ var ASSISTANTS = {
11794
11927
  codex: {
11795
11928
  id: "codex",
11796
11929
  name: "Codex",
11797
- markers: ({ home }) => [join3(home, ".codex"), join3(home, ".agents")],
11798
- skillDir: (scope, { home, project }) => join3(scope === "user" ? home : project, ".agents", "skills", SKILL_NAME),
11930
+ markers: ({ home }) => [join4(home, ".codex"), join4(home, ".agents")],
11931
+ skillDir: (scope, { home, project }) => join4(scope === "user" ? home : project, ".agents", "skills", SKILL_NAME),
11799
11932
  mcp: (scope, { home, project }) => ({
11800
11933
  kind: "toml",
11801
- file: join3(scope === "user" ? home : project, ".codex", "config.toml")
11934
+ file: join4(scope === "user" ? home : project, ".codex", "config.toml")
11802
11935
  })
11803
11936
  }
11804
11937
  };
11805
11938
 
11806
11939
  // ../assistants/src/installer.ts
11807
- var sha = (text4) => createHash("sha256").update(text4).digest("hex");
11940
+ var sha = (text4) => createHash2("sha256").update(text4).digest("hex");
11808
11941
  async function writeAtomic(file, content) {
11809
- await mkdir2(dirname2(file), { recursive: true });
11942
+ await mkdir3(dirname3(file), { recursive: true });
11810
11943
  const tmp = `${file}.cloudhop-${process.pid}.tmp`;
11811
- await writeFile2(tmp, content);
11944
+ await writeFile3(tmp, content);
11812
11945
  await rename2(tmp, file);
11813
11946
  }
11814
11947
  async function removeFileAndEmptyDir(file) {
11815
11948
  await rm2(file, { force: true });
11816
- await rmdir(dirname2(file)).catch(() => {
11949
+ await rmdir(dirname3(file)).catch(() => {
11817
11950
  });
11818
11951
  }
11819
- var readText = (file) => readFile2(file, "utf8").catch(() => void 0);
11952
+ var readText = (file) => readFile3(file, "utf8").catch(() => void 0);
11820
11953
  var AssistantsInstaller = class {
11821
11954
  constructor(options) {
11822
11955
  this.options = options;
@@ -11840,7 +11973,7 @@ var AssistantsInstaller = class {
11840
11973
  return Promise.all(
11841
11974
  ASSISTANT_IDS.map(async (id) => {
11842
11975
  const assistant = ASSISTANTS[id];
11843
- const skillPath = join4(assistant.skillDir(scope, this.options.places), "SKILL.md");
11976
+ const skillPath = join5(assistant.skillDir(scope, this.options.places), "SKILL.md");
11844
11977
  const entry = manifest.find((e) => e.skillFile === skillPath);
11845
11978
  return {
11846
11979
  assistant: id,
@@ -11859,7 +11992,7 @@ var AssistantsInstaller = class {
11859
11992
  const manifest = await this.readManifest();
11860
11993
  for (const id of ids) {
11861
11994
  const assistant = ASSISTANTS[id];
11862
- const skillPath = join4(assistant.skillDir(scope, this.options.places), "SKILL.md");
11995
+ const skillPath = join5(assistant.skillDir(scope, this.options.places), "SKILL.md");
11863
11996
  const content = this.skillContent;
11864
11997
  await writeAtomic(skillPath, content);
11865
11998
  const kept = manifest.filter((e) => e.skillFile !== skillPath);
@@ -11884,9 +12017,9 @@ var AssistantsInstaller = class {
11884
12017
  for (const id of ids) {
11885
12018
  const assistant = ASSISTANTS[id];
11886
12019
  const dir = assistant.skillDir(scope, this.options.places);
11887
- const skillPath = join4(dir, "SKILL.md");
12020
+ const skillPath = join5(dir, "SKILL.md");
11888
12021
  await rm2(dir, { recursive: true, force: true });
11889
- await rmdir(dirname2(dir)).then(() => rmdir(dirname2(dirname2(dir)))).catch(() => {
12022
+ await rmdir(dirname3(dir)).then(() => rmdir(dirname3(dirname3(dir)))).catch(() => {
11890
12023
  });
11891
12024
  manifest = manifest.filter((e) => e.skillFile !== skillPath);
11892
12025
  const outcome = await this.removeMcp(assistant.mcp(scope, this.options.places));
@@ -12040,7 +12173,7 @@ args = [${MCP_ARGS.map((a2) => `"${a2}"`).join(", ")}]
12040
12173
  }
12041
12174
  // ───────────── manifest ─────────────
12042
12175
  get manifestPath() {
12043
- return join4(this.options.configDir, "assistants.json");
12176
+ return join5(this.options.configDir, "assistants.json");
12044
12177
  }
12045
12178
  async readManifest() {
12046
12179
  const text4 = await readText(this.manifestPath);
@@ -12271,9 +12404,16 @@ async function consoleCommand(app, args) {
12271
12404
  }
12272
12405
 
12273
12406
  // src/commands/each.ts
12407
+ var MAX_PATTERN = 200;
12274
12408
  function matcher(selector) {
12275
12409
  const regex = /^\/(.+)\/([a-z]*)$/.exec(selector);
12276
12410
  if (regex) {
12411
+ if (selector.length > MAX_PATTERN) {
12412
+ throw new CloudhopError(
12413
+ "INVALID_NAME",
12414
+ `That regular expression is too long (max ${MAX_PATTERN}).`
12415
+ );
12416
+ }
12277
12417
  try {
12278
12418
  const re = new RegExp(regex[1], regex[2]);
12279
12419
  return (name) => re.test(name);
@@ -12371,56 +12511,831 @@ ${listing}
12371
12511
  ))
12372
12512
  return 130;
12373
12513
  }
12374
- if (risky.length > 0 && process.env.CLOUDHOP_CONFIRM_PROD !== "1") {
12375
- if (!tty) {
12514
+ if (risky.length > 0 && process.env.CLOUDHOP_CONFIRM_PROD !== "1") {
12515
+ if (!tty) {
12516
+ throw new CloudhopError(
12517
+ "PROD_CONFIRMATION_REQUIRED",
12518
+ `This may change resources in production: ${risky.join(", ")}.`,
12519
+ "Run it in a terminal to confirm, or set CLOUDHOP_CONFIRM_PROD=1 once a person has approved it."
12520
+ );
12521
+ }
12522
+ dist_exports.log.warn(
12523
+ `${paint("red", bold("PRODUCTION"))} ${risky.join(", ")}: this may change resources.`
12524
+ );
12525
+ if (!answer(await dist_exports.confirm({ message: "Also run it in production?", initialValue: false })))
12526
+ return 130;
12527
+ }
12528
+ for (const source of new Set(selected.map((a2) => a2.source))) {
12529
+ await app.ensureSession(source, { interactive: tty });
12530
+ }
12531
+ const parallel = Math.max(1, Math.min(16, Number(flags.parallel) || 4));
12532
+ const outcomes = await mapLimit(selected, parallel, async (a2) => {
12533
+ try {
12534
+ const r2 = await app.captureAsAlias(a2.name, cliArgs);
12535
+ const outcome = { alias: a2.name, exitCode: r2.exitCode, stdout: r2.stdout, stderr: r2.stderr };
12536
+ if (!flags.json) printOutcome(outcome, width);
12537
+ return outcome;
12538
+ } catch (error) {
12539
+ const outcome = { alias: a2.name, exitCode: 1, stdout: "", stderr: error.message };
12540
+ if (!flags.json) printOutcome(outcome, width);
12541
+ return outcome;
12542
+ }
12543
+ });
12544
+ const failed = outcomes.filter((o2) => o2.exitCode !== 0);
12545
+ if (flags.json) {
12546
+ console.log(JSON.stringify({ version: 1, command: cliArgs, results: outcomes }, null, 2));
12547
+ } else {
12548
+ const summary = `${outcomes.length - failed.length} ok${failed.length ? `, ${failed.length} failed: ${failed.map((f2) => f2.alias).join(", ")}` : ""}`;
12549
+ process.stderr.write(`${failed.length ? paint("red", summary) : paint("green", summary)}
12550
+ `);
12551
+ }
12552
+ return failed.length ? 1 : 0;
12553
+ }
12554
+ function printOutcome({ alias, exitCode, stdout: stdout2, stderr }, width) {
12555
+ const tag = paint(exitCode === 0 ? "sky" : "red", `[${alias}]`.padEnd(width + 2));
12556
+ for (const line of stdout2.replace(/\n$/, "").split("\n"))
12557
+ if (line) process.stdout.write(`${tag} ${line}
12558
+ `);
12559
+ for (const line of stderr.replace(/\n$/, "").split("\n"))
12560
+ if (line) process.stderr.write(`${tag} ${paint("dim", line)}
12561
+ `);
12562
+ if (exitCode !== 0) process.stderr.write(`${tag} ${paint("red", `exit ${exitCode}`)}
12563
+ `);
12564
+ }
12565
+
12566
+ // src/commands/use.ts
12567
+ import { spawnSync } from "child_process";
12568
+ import { writeFile as writeFile5 } from "fs/promises";
12569
+ import { homedir as homedir4 } from "os";
12570
+ import { dirname as dirname5, join as join7, resolve as resolve2 } from "path";
12571
+
12572
+ // src/commands/shell.ts
12573
+ import { readFileSync as readFileSync2 } from "fs";
12574
+ import { appendFile, mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
12575
+ import { homedir as homedir3 } from "os";
12576
+ import { dirname as dirname4, join as join6 } from "path";
12577
+
12578
+ // src/commands/starship.ts
12579
+ var BEGIN = "# >>> cloudhop >>> (added by `cloudhop shell`; remove this block and the ${env_var.cloudhop_*} segments in `format` to undo)";
12580
+ var END = "# <<< cloudhop <<<";
12581
+ var SKY = "#3bb8f5";
12582
+ var ON_SKY = "#0b1124";
12583
+ var RED = "#be123c";
12584
+ var STARSHIP_SEGMENTS = PROVIDER_IDS.map(
12585
+ (p3) => `\${env_var.cloudhop_${p3}}\${env_var.cloudhop_${p3}_prod}`
12586
+ ).join("");
12587
+ function starshipModules(powerline) {
12588
+ const segment = (provider, prod) => {
12589
+ if (!powerline) {
12590
+ return prod ? `[${provider} $env_value PROD ](bold #fb7185)` : `[${provider} $env_value ](bold ${SKY})`;
12591
+ }
12592
+ const { glyph, color } = powerline;
12593
+ const bg = prod ? RED : SKY;
12594
+ const fg = prod ? "#ffffff" : ON_SKY;
12595
+ return `[${glyph}](fg:${color} bg:${bg})[ ${provider} $env_value${prod ? " PROD" : ""} ](bold fg:${fg} bg:${bg})[${glyph}](fg:${bg} bg:${color})`;
12596
+ };
12597
+ return PROVIDER_IDS.map((p3) => {
12598
+ const variable = `CLOUDHOP_${p3.toUpperCase()}`;
12599
+ return `[env_var.cloudhop_${p3}]
12600
+ variable = '${variable}_SEGMENT'
12601
+ format = '${segment(p3, false)}'
12602
+
12603
+ [env_var.cloudhop_${p3}_prod]
12604
+ variable = '${variable}_PROD_SEGMENT'
12605
+ format = '${segment(p3, true)}'`;
12606
+ }).join("\n\n");
12607
+ }
12608
+ var hasCloudhopStarship = (text4) => text4.includes("[env_var.cloudhop_aws]");
12609
+ var CLOSING_GROUP = /\[\s*([-])\s*\]\(\s*fg:\s*(#[0-9a-fA-F]{3,8}|[a-z_]+)\s*\)/g;
12610
+ function addToStarshipConfig(text4) {
12611
+ if (hasCloudhopStarship(text4)) return text4;
12612
+ let result = text4;
12613
+ let powerline;
12614
+ const format = findTopLevelFormat(result);
12615
+ if (format && !/\$all\b|\$\{?env_var\}?(?![._])/.test(format.content)) {
12616
+ const { content, multiline: multiline2 } = format;
12617
+ const continuation = multiline2 ? "\\\n" : "";
12618
+ const closing = [...content.matchAll(CLOSING_GROUP)].at(-1);
12619
+ const lineBreak = [content.lastIndexOf("$line_break"), content.lastIndexOf("$character")].find(
12620
+ (i2) => i2 >= 0
12621
+ );
12622
+ let updated;
12623
+ if (closing?.index !== void 0 && (lineBreak === void 0 || closing.index < lineBreak)) {
12624
+ powerline = { glyph: closing[1], color: closing[2] };
12625
+ updated = `${content.slice(0, closing.index)}${STARSHIP_SEGMENTS}${continuation}${content.slice(closing.index)}`;
12626
+ } else if (lineBreak !== void 0) {
12627
+ updated = `${content.slice(0, lineBreak)}${STARSHIP_SEGMENTS}${continuation}${content.slice(lineBreak)}`;
12628
+ } else {
12629
+ const body = content.trimEnd();
12630
+ const tail = content.slice(body.length);
12631
+ const joiner = multiline2 && body.endsWith("\\") ? "\n" : "";
12632
+ updated = `${body}${joiner}${STARSHIP_SEGMENTS}${multiline2 ? "\\" : ""}${tail}`;
12633
+ }
12634
+ result = result.slice(0, format.start) + updated + result.slice(format.end);
12635
+ }
12636
+ const separator = result.length === 0 || result.endsWith("\n") ? "" : "\n";
12637
+ return `${result}${separator}
12638
+ ${BEGIN}
12639
+ ${starshipModules(powerline)}
12640
+ ${END}
12641
+ `;
12642
+ }
12643
+ function findTopLevelFormat(text4) {
12644
+ const firstTable = text4.search(/^[ \t]*\[/m);
12645
+ const head = firstTable === -1 ? text4 : text4.slice(0, firstTable);
12646
+ const match = /^format\s*=\s*("""|'''|"|')/m.exec(head);
12647
+ if (!match) return void 0;
12648
+ const quote2 = match[1];
12649
+ const start = match.index + match[0].length;
12650
+ const end = text4.indexOf(quote2, start);
12651
+ if (end === -1) return void 0;
12652
+ return { start, end, content: text4.slice(start, end), multiline: quote2.length === 3 };
12653
+ }
12654
+
12655
+ // src/commands/shell.ts
12656
+ var SHELLS = ["zsh", "bash", "fish", "pwsh"];
12657
+ var ALIAS_COMMANDS = "use status console region mv prod rm exec";
12658
+ var SOURCE_COMMANDS = "alias login sync ls";
12659
+ var aliasVar = (provider) => `CLOUDHOP_${provider.toUpperCase()}_ALIAS`;
12660
+ var prodVar = (provider) => `CLOUDHOP_${provider.toUpperCase()}_PROD`;
12661
+ function posixScript(shell, clis) {
12662
+ const wrappers = clis.map(
12663
+ ({ command, provider }) => `${command}() {
12664
+ case " $* " in *" --profile"*) command ${command} "$@"; return $?;; esac
12665
+ if [ -n "$${aliasVar(provider)}" ] && [ "$CLOUDHOP_WRAP" != 0 ]; then
12666
+ command cloudhop "$${aliasVar(provider)}" "$@"
12667
+ else
12668
+ command ${command} "$@"
12669
+ fi
12670
+ }`
12671
+ ).join("\n");
12672
+ const prompt = PROVIDER_IDS.map(
12673
+ (p3) => ` [ -n "$${aliasVar(p3)}" ] && out="$out\${out:+ }${p3}:$${aliasVar(p3)}\${${prodVar(p3)}:+ PROD}"`
12674
+ ).join("\n");
12675
+ const hook = shell === "zsh" ? `autoload -Uz add-zsh-hook && add-zsh-hook chpwd _cloudhop_cd` : `case ";$PROMPT_COMMAND;" in *";_cloudhop_cd;"*) ;; *) PROMPT_COMMAND="_cloudhop_cd;_cloudhop_show_prompt\${PROMPT_COMMAND:+;$PROMPT_COMMAND}";; esac`;
12676
+ const promptHook = shell === "zsh" ? `_cloudhop_show_prompt() {
12677
+ [ "$CLOUDHOP_PROMPT" = 0 ] && return 0
12678
+ case "$PROMPT$RPROMPT" in *starship*|*oh-my-posh*|*_cloudhop_rprompt*) return 0 ;; esac
12679
+ setopt prompt_subst
12680
+ RPROMPT='$(_cloudhop_rprompt)'"\${RPROMPT:+ $RPROMPT}"
12681
+ }
12682
+ # Sky for the alias, rose for production, like the CLI.
12683
+ _cloudhop_rprompt() {
12684
+ local p; p="$(cloudhop_prompt)"; [ -n "$p" ] || return 0
12685
+ case "$p" in *PROD*) printf '%%F{204}%s%%f' "$p" ;; *) printf '%%F{75}%s%%f' "$p" ;; esac
12686
+ }
12687
+ add-zsh-hook precmd _cloudhop_show_prompt` : `# \\001 and \\002 tell readline the colors take no room, so long lines still wrap right.
12688
+ _cloudhop_ps1() {
12689
+ local p c=75; p="$(cloudhop_prompt)"; [ -n "$p" ] || return 0
12690
+ case "$p" in *PROD*) c=204 ;; esac
12691
+ printf '\\001\\033[38;5;%sm\\002[%s]\\001\\033[0m\\002 ' "$c" "$p"
12692
+ }
12693
+ _cloudhop_show_prompt() {
12694
+ [ "$CLOUDHOP_PROMPT" = 0 ] && return 0
12695
+ case "$PS1" in *starship*|*oh-my-posh*|*_cloudhop_ps1*) return 0 ;; esac
12696
+ PS1='$(_cloudhop_ps1)'"$PS1"
12697
+ }`;
12698
+ const completion = shell === "zsh" ? `_cloudhop_words() { command cloudhop __complete "$1" 2>/dev/null }
12699
+ _hop() { (( CURRENT == 2 )) && { local -a a; a=(\${(f)"$(_cloudhop_words aliases)"}); compadd -a a; } }
12700
+ _cloudhop() {
12701
+ local -a w
12702
+ if (( CURRENT == 2 )); then w=(\${(f)"$(_cloudhop_words commands)"}); compadd -a w; return; fi
12703
+ (( CURRENT == 3 )) || return
12704
+ case " ${ALIAS_COMMANDS} " in *" $words[2] "*) w=(\${(f)"$(_cloudhop_words aliases)"});; esac
12705
+ case " ${SOURCE_COMMANDS} " in *" $words[2] "*) w=(\${(f)"$(_cloudhop_words sources)"});; esac
12706
+ compadd -a w
12707
+ }
12708
+ (( $+functions[compdef] )) && { compdef _hop hop; compdef _cloudhop cloudhop; }` : `_cloudhop_complete() {
12709
+ local cur=\${COMP_WORDS[COMP_CWORD]} cmd=\${COMP_WORDS[0]##*/} kind=""
12710
+ if [ "$cmd" = hop ]; then [ "$COMP_CWORD" -eq 1 ] && kind=aliases
12711
+ elif [ "$COMP_CWORD" -eq 1 ]; then kind=commands
12712
+ elif [ "$COMP_CWORD" -eq 2 ]; then
12713
+ case " ${ALIAS_COMMANDS} " in *" \${COMP_WORDS[1]} "*) kind=aliases;; esac
12714
+ case " ${SOURCE_COMMANDS} " in *" \${COMP_WORDS[1]} "*) kind=sources;; esac
12715
+ fi
12716
+ [ -n "$kind" ] && COMPREPLY=($(compgen -W "$(command cloudhop __complete $kind 2>/dev/null)" -- "$cur"))
12717
+ }
12718
+ complete -o default -F _cloudhop_complete hop cloudhop`;
12719
+ return `# cloudhop shell integration (${shell}). Load it${shell === "zsh" ? " after compinit" : ""}: ${LOAD_LINE[shell]}
12720
+ # - \`cloudhop use <alias>\` switches this shell; \`cloudhop use --off\` goes back.
12721
+ # - A folder with a trusted .cloudhoprc switches on cd (off: CLOUDHOP_AUTO_USE=0).
12722
+ # - With an alias in use, \`${clis.map((c3) => c3.command).join("`, `") || "aws"}\` signs in when needed and asks before changing production (off: CLOUDHOP_WRAP=0).
12723
+ # - Your prompt shows the alias in use (off: CLOUDHOP_PROMPT=0).
12724
+ cloudhop() {
12725
+ case "$1" in
12726
+ use)
12727
+ local out; out="$(command cloudhop use --emit ${shell} "\${@:2}")" || return $?
12728
+ eval "$out"; _cloudhop_rc_seen=\${CLOUDHOP_RC:-$_cloudhop_rc_seen} ;;
12729
+ allow|deny) command cloudhop "$@" || return $?; _cloudhop_rc_seen=-; _cloudhop_cd ;;
12730
+ *) command cloudhop "$@" ;;
12731
+ esac
12732
+ }
12733
+ _cloudhop_cd() {
12734
+ [ "$CLOUDHOP_AUTO_USE" = 0 ] && return
12735
+ [ "$PWD" = "$_cloudhop_pwd" ] && [ "$_cloudhop_rc_seen" != - ] && return
12736
+ _cloudhop_pwd=$PWD
12737
+ local dir=$PWD rc=
12738
+ while :; do
12739
+ if [ -f "$dir/.cloudhoprc" ]; then rc="$dir/.cloudhoprc"; break; fi
12740
+ { [ -z "$dir" ] || [ "$dir" = / ]; } && break
12741
+ dir="\${dir%/*}"
12742
+ done
12743
+ [ "$rc" = "$_cloudhop_rc_seen" ] && return
12744
+ _cloudhop_rc_seen=$rc
12745
+ local out
12746
+ if [ -n "$rc" ]; then out="$(command cloudhop use --emit ${shell} --from "$rc")"
12747
+ elif [ -n "$CLOUDHOP_RC" ]; then out="$(command cloudhop use --emit ${shell} --leave)"
12748
+ else return 0; fi
12749
+ eval "$out"
12750
+ }
12751
+ ${hook}
12752
+ _cloudhop_cd
12753
+ ${wrappers}
12754
+ ${completion}
12755
+ # The aliases in use, e.g. "aws:acme-dev". Shown in your prompt by itself
12756
+ # (off: CLOUDHOP_PROMPT=0); Starship gets its own segments from \`cloudhop shell\`.
12757
+ cloudhop_prompt() {
12758
+ local out=
12759
+ ${prompt}
12760
+ [ -n "$out" ] && printf '%s' "$out"
12761
+ }
12762
+ ${promptHook}
12763
+ `;
12764
+ }
12765
+ function fishScript(clis) {
12766
+ const wrappers = clis.map(
12767
+ ({ command, provider }) => `function ${command} --wraps ${command}
12768
+ if test -n "$${aliasVar(provider)}" -a "$CLOUDHOP_WRAP" != 0; and not string match -q -- '--profile*' $argv
12769
+ command cloudhop $${aliasVar(provider)} $argv
12770
+ else
12771
+ command ${command} $argv
12772
+ end
12773
+ end`
12774
+ ).join("\n");
12775
+ const prompt = PROVIDER_IDS.map(
12776
+ (p3) => ` test -n "$${aliasVar(p3)}"; and set out $out "${p3}:$${aliasVar(p3)}"(test -n "$${prodVar(p3)}"; and echo ' PROD')`
12777
+ ).join("\n");
12778
+ return `# cloudhop shell integration (fish): ${LOAD_LINE.fish}
12779
+ # - \`cloudhop use <alias>\` switches this shell; \`cloudhop use --off\` goes back.
12780
+ # - A folder with a trusted .cloudhoprc switches on cd (off: set -gx CLOUDHOP_AUTO_USE 0).
12781
+ # - With an alias in use, the cloud CLI signs in when needed and asks before changing production (off: set -gx CLOUDHOP_WRAP 0).
12782
+ function cloudhop
12783
+ switch "$argv[1]"
12784
+ case use
12785
+ set -l out (command cloudhop use --emit fish $argv[2..-1]); or return $status
12786
+ printf '%s\\n' $out | source
12787
+ set -q CLOUDHOP_RC; and set -g _cloudhop_rc_seen $CLOUDHOP_RC
12788
+ case allow deny
12789
+ command cloudhop $argv; or return $status
12790
+ set -g _cloudhop_rc_seen -
12791
+ _cloudhop_cd
12792
+ case '*'
12793
+ command cloudhop $argv
12794
+ end
12795
+ end
12796
+ function _cloudhop_cd --on-variable PWD
12797
+ test "$CLOUDHOP_AUTO_USE" = 0; and return
12798
+ set -l dir $PWD
12799
+ set -l rc ''
12800
+ while true
12801
+ if test -f "$dir/.cloudhoprc"; set rc "$dir/.cloudhoprc"; break; end
12802
+ if test -z "$dir" -o "$dir" = /; break; end
12803
+ set dir (string replace -r '/[^/]*$' '' -- $dir)
12804
+ end
12805
+ test "$rc" = "$_cloudhop_rc_seen"; and return
12806
+ set -g _cloudhop_rc_seen $rc
12807
+ if test -n "$rc"
12808
+ command cloudhop use --emit fish --from $rc | source
12809
+ else if test -n "$CLOUDHOP_RC"
12810
+ command cloudhop use --emit fish --leave | source
12811
+ end
12812
+ end
12813
+ _cloudhop_cd
12814
+ ${wrappers}
12815
+ complete -c hop -f -n 'test (count (commandline -opc)) -eq 1' -a '(command cloudhop __complete aliases 2>/dev/null)'
12816
+ complete -c cloudhop -f -n 'test (count (commandline -opc)) -eq 1' -a '(command cloudhop __complete commands 2>/dev/null)'
12817
+ complete -c cloudhop -f -n '__fish_seen_subcommand_from ${ALIAS_COMMANDS}; and test (count (commandline -opc)) -eq 2' -a '(command cloudhop __complete aliases 2>/dev/null)'
12818
+ complete -c cloudhop -f -n '__fish_seen_subcommand_from ${SOURCE_COMMANDS}; and test (count (commandline -opc)) -eq 2' -a '(command cloudhop __complete sources 2>/dev/null)'
12819
+ # The aliases in use, e.g. "aws:acme-dev", at the right of the prompt unless
12820
+ # something else (like Starship) draws it (off: set -gx CLOUDHOP_PROMPT 0).
12821
+ function cloudhop_prompt
12822
+ set -l out
12823
+ ${prompt}
12824
+ test -n "$out"; and printf '%s' (string join ' ' $out)
12825
+ end
12826
+ if not functions -q fish_right_prompt
12827
+ function fish_right_prompt
12828
+ test "$CLOUDHOP_PROMPT" = 0; and return
12829
+ set -l label (cloudhop_prompt)
12830
+ test -n "$label"; or return
12831
+ if string match -q '*PROD*' -- $label; set_color fb7185; else; set_color 3bb8f5; end
12832
+ printf '%s' $label; set_color normal
12833
+ end
12834
+ end
12835
+ `;
12836
+ }
12837
+ function pwshScript(clis) {
12838
+ const wrappers = clis.map(
12839
+ ({
12840
+ command,
12841
+ provider
12842
+ }) => `$script:CloudhopCli_${provider} = (Get-Command ${command} -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1).Source
12843
+ function ${command} {
12844
+ if ($env:${aliasVar(provider)} -and $env:CLOUDHOP_WRAP -ne '0' -and -not ($args -like '--profile*')) { & $script:CloudhopExe $env:${aliasVar(provider)} @args }
12845
+ elseif ($script:CloudhopCli_${provider}) { & $script:CloudhopCli_${provider} @args }
12846
+ else { Write-Error '${command}: command not found' }
12847
+ }`
12848
+ ).join("\n");
12849
+ const prompt = PROVIDER_IDS.map(
12850
+ (p3) => ` if ($env:${aliasVar(p3)}) { $out += "${p3}:$env:${aliasVar(p3)}$(if ($env:${prodVar(p3)}) { ' PROD' })" }`
12851
+ ).join("\n");
12852
+ return `# cloudhop shell integration (PowerShell): ${LOAD_LINE.pwsh}
12853
+ # - \`cloudhop use <alias>\` switches this shell; \`cloudhop use --off\` goes back.
12854
+ # - A folder with a trusted .cloudhoprc switches on cd (off: $env:CLOUDHOP_AUTO_USE = '0').
12855
+ # - With an alias in use, the cloud CLI signs in when needed and asks before changing production (off: $env:CLOUDHOP_WRAP = '0').
12856
+ $script:CloudhopExe = (Get-Command cloudhop -CommandType Application, ExternalScript | Select-Object -First 1).Source
12857
+ function cloudhop {
12858
+ if ($args.Count -gt 0 -and $args[0] -eq 'use') {
12859
+ $out = & $script:CloudhopExe use --emit pwsh @($args | Select-Object -Skip 1)
12860
+ if ($LASTEXITCODE -eq 0) { $out | Out-String | Invoke-Expression; if ($env:CLOUDHOP_RC) { $script:CloudhopRcSeen = $env:CLOUDHOP_RC } }
12861
+ } elseif ($args.Count -gt 0 -and $args[0] -in 'allow', 'deny') {
12862
+ & $script:CloudhopExe @args
12863
+ if ($LASTEXITCODE -eq 0) { $script:CloudhopRcSeen = '-'; $script:CloudhopPwd = ''; _CloudhopCd }
12864
+ } else { & $script:CloudhopExe @args }
12865
+ }
12866
+ function _CloudhopCd {
12867
+ if ($env:CLOUDHOP_AUTO_USE -eq '0' -or $PWD.ProviderPath -eq $script:CloudhopPwd) { return }
12868
+ $script:CloudhopPwd = $PWD.ProviderPath
12869
+ $dir = $PWD.ProviderPath; $rc = ''
12870
+ while ($dir) {
12871
+ $file = Join-Path $dir '.cloudhoprc'
12872
+ if (Test-Path -LiteralPath $file -PathType Leaf) { $rc = $file; break }
12873
+ $dir = Split-Path -Parent $dir
12874
+ }
12875
+ if ($rc -eq $script:CloudhopRcSeen) { return }
12876
+ $script:CloudhopRcSeen = $rc
12877
+ $out = if ($rc) { & $script:CloudhopExe use --emit pwsh --from $rc } elseif ($env:CLOUDHOP_RC) { & $script:CloudhopExe use --emit pwsh --leave }
12878
+ if ($out) { $out | Out-String | Invoke-Expression }
12879
+ }
12880
+ $script:CloudhopPrompt = $function:prompt
12881
+ function prompt {
12882
+ _CloudhopCd
12883
+ $out = & $script:CloudhopPrompt
12884
+ # The alias in use before your prompt, unless Starship draws it (off: $env:CLOUDHOP_PROMPT = '0').
12885
+ if ($env:CLOUDHOP_PROMPT -ne '0' -and "$script:CloudhopPrompt" -notmatch 'starship|oh-my-posh') {
12886
+ $label = cloudhop_prompt
12887
+ if ($label) {
12888
+ if (-not $Host.UI.SupportsVirtualTerminal) { return "[$label] $out" }
12889
+ $color = if ($label -match 'PROD') { 204 } else { 75 }
12890
+ return "$([char]27)[38;5;$($color)m[$label]$([char]27)[0m $out"
12891
+ }
12892
+ }
12893
+ $out
12894
+ }
12895
+ ${wrappers}
12896
+ Register-ArgumentCompleter -Native -CommandName hop, cloudhop -ScriptBlock {
12897
+ param($word, $ast)
12898
+ $n = $ast.CommandElements.Count - [int]($word -eq '')
12899
+ $kind = if ($ast.CommandElements[0].Value -match 'hop$' -and $ast.CommandElements[0].Value -notmatch 'cloudhop') { if ($n -le 1) { 'aliases' } }
12900
+ elseif ($n -le 1) { 'commands' }
12901
+ elseif ($n -eq 2 -and ' ${ALIAS_COMMANDS} ' -match " $($ast.CommandElements[1].Value) ") { 'aliases' }
12902
+ elseif ($n -eq 2 -and ' ${SOURCE_COMMANDS} ' -match " $($ast.CommandElements[1].Value) ") { 'sources' }
12903
+ if ($kind) { & $script:CloudhopExe __complete $kind | Where-Object { $_ -like "$word*" } }
12904
+ }
12905
+ function cloudhop_prompt {
12906
+ $out = @()
12907
+ ${prompt}
12908
+ $out -join ' '
12909
+ }
12910
+ `;
12911
+ }
12912
+ function shellScript(shell, clis) {
12913
+ if (shell === "fish") return fishScript(clis);
12914
+ if (shell === "pwsh") return pwshScript(clis);
12915
+ return posixScript(shell, clis);
12916
+ }
12917
+ function starshipSnippet() {
12918
+ return `# cloudhop: the alias each cloud uses in this shell (\`cloudhop use\`, .cloudhoprc).
12919
+ # \`cloudhop shell\` adds this for you. By hand: append it to starship.toml and, if
12920
+ # your config has a top-level \`format\`, add this where you want the segments:
12921
+ # ${STARSHIP_SEGMENTS}
12922
+
12923
+ ${starshipModules()}
12924
+ `;
12925
+ }
12926
+ var quote = (shell, value) => shell === "pwsh" ? `'${value.replace(/'/g, "''")}'` : `'${value.replace(/'/g, "'\\''")}'`;
12927
+ function shellExports(shell, env) {
12928
+ return Object.entries(env).map(([key2, value]) => {
12929
+ if (value === void 0) {
12930
+ if (shell === "fish") return `set -e ${key2}`;
12931
+ if (shell === "pwsh") return `Remove-Item Env:${key2} -ErrorAction SilentlyContinue`;
12932
+ return `unset ${key2}`;
12933
+ }
12934
+ if (shell === "fish") return `set -gx ${key2} ${quote(shell, value)}`;
12935
+ if (shell === "pwsh") return `$env:${key2} = ${quote(shell, value)}`;
12936
+ return `export ${key2}=${quote(shell, value)}`;
12937
+ }).join("\n");
12938
+ }
12939
+ function detectShell() {
12940
+ if (process.platform === "win32") return "pwsh";
12941
+ const name = (process.env.SHELL ?? "").split("/").pop();
12942
+ return SHELLS.includes(name) ? name : "bash";
12943
+ }
12944
+ function parseShell(value) {
12945
+ if (!value) return detectShell();
12946
+ const shell = value === "powershell" ? "pwsh" : value;
12947
+ if (!SHELLS.includes(shell)) {
12948
+ throw new CloudhopError(
12949
+ "INVALID_NAME",
12950
+ `Unknown shell "${value}".`,
12951
+ `Use one of: ${SHELLS.join(", ")}.`
12952
+ );
12953
+ }
12954
+ return shell;
12955
+ }
12956
+ var RC_FILES = {
12957
+ zsh: (home) => join6(home, ".zshrc"),
12958
+ bash: (home) => join6(home, ".bashrc"),
12959
+ fish: (home) => join6(home, ".config", "fish", "config.fish"),
12960
+ pwsh: (home) => process.platform === "win32" ? join6(home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1") : join6(home, ".config", "powershell", "Microsoft.PowerShell_profile.ps1")
12961
+ };
12962
+ var LOAD_LINE = {
12963
+ zsh: 'eval "$(cloudhop shell zsh)"',
12964
+ bash: 'eval "$(cloudhop shell bash)"',
12965
+ fish: "cloudhop shell fish | source",
12966
+ pwsh: "cloudhop shell pwsh | Out-String | Invoke-Expression"
12967
+ };
12968
+ function shellIntegrationInstalled() {
12969
+ const shell = detectShell();
12970
+ const read = (file) => {
12971
+ try {
12972
+ return readFileSync2(file, "utf8");
12973
+ } catch {
12974
+ return void 0;
12975
+ }
12976
+ };
12977
+ const rc = read(RC_FILES[shell](homedir3())) ?? "";
12978
+ if (!rc.includes(LOAD_LINE[shell])) return false;
12979
+ const starship = read(starshipFile());
12980
+ if (starship === void 0) return !rc.includes("starship init");
12981
+ return hasCloudhopStarship(starship);
12982
+ }
12983
+ var starshipFile = () => process.env.STARSHIP_CONFIG || join6(homedir3(), ".config", "starship.toml");
12984
+ async function shellCommand(args, clis) {
12985
+ const { positional, flags } = parseArgs(args);
12986
+ if (positional[0] === "starship") {
12987
+ process.stdout.write(starshipSnippet());
12988
+ return;
12989
+ }
12990
+ const setUp = positional[0] === "install" || !positional[0] && process.stdout.isTTY;
12991
+ if (!setUp) {
12992
+ process.stdout.write(shellScript(parseShell(positional[0]), clis));
12993
+ return;
12994
+ }
12995
+ const shell = parseShell(positional[1]);
12996
+ const file = RC_FILES[shell](homedir3());
12997
+ const line = LOAD_LINE[shell];
12998
+ const current = await readIfExists(file) ?? "";
12999
+ const starship = await starshipConfig(current);
13000
+ const needsLine = !current.includes(line);
13001
+ const needsStarship = starship !== void 0 && !hasCloudhopStarship(starship.text);
13002
+ const rcName = file.replace(homedir3(), "~");
13003
+ if (!needsLine && !needsStarship) {
13004
+ console.log(`Already set up in ${rcName}${starship ? " and your Starship prompt" : ""}.`);
13005
+ return;
13006
+ }
13007
+ const changes = [
13008
+ needsLine && `add one line to ${rcName}: \`cloudhop use\`, .cloudhoprc folders, the alias in your prompt`,
13009
+ needsStarship && `show the alias in your Starship prompt (${starship.file.replace(homedir3(), "~")}, keeping a backup)`
13010
+ ].filter(Boolean);
13011
+ if (!flags.yes) {
13012
+ if (!interactive())
13013
+ throw new CloudhopError(
13014
+ "NOT_INTERACTIVE",
13015
+ `Rerun with --yes to ${changes.join(" and ")}.${needsLine ? `
13016
+ Or add this line to ${file} yourself:
13017
+ ${line}` : ""}`
13018
+ );
13019
+ console.log(`cloudhop will:
13020
+ ${changes.map((c3) => ` - ${c3}`).join("\n")}`);
13021
+ const ok = answer(await dist_exports.confirm({ message: "Set up your terminal?", initialValue: true }));
13022
+ if (!ok) return;
13023
+ }
13024
+ if (needsLine) {
13025
+ await mkdir4(dirname4(file), { recursive: true });
13026
+ const note2 = shell === "zsh" ? " # after compinit" : "";
13027
+ await appendFile(
13028
+ file,
13029
+ `${current && !current.endsWith("\n") ? "\n" : ""}
13030
+ # cloudhop: \`cloudhop use\`, .cloudhoprc folders, tab completion, prompt${note2}
13031
+ ${line}
13032
+ `
13033
+ );
13034
+ console.log(`${paint("green", "\u2714")} Added to ${rcName}.`);
13035
+ }
13036
+ if (needsStarship) {
13037
+ await mkdir4(dirname4(starship.file), { recursive: true });
13038
+ if (starship.exists) await writeFile4(`${starship.file}.cloudhop-backup`, starship.text);
13039
+ await writeFile4(starship.file, addToStarshipConfig(starship.text));
13040
+ console.log(
13041
+ `${paint("green", "\u2714")} Your Starship prompt shows the alias in use (backup: ${starship.file.replace(homedir3(), "~")}.cloudhop-backup).`
13042
+ );
13043
+ }
13044
+ console.log(
13045
+ `Open a new terminal (or run ${paint("sky", shell === "pwsh" ? ". $PROFILE" : `exec ${shell}`)}), then:`
13046
+ );
13047
+ console.log(` ${paint("sky", "cloudhop use <alias>")} switch this terminal to an account`);
13048
+ console.log(` ${paint("sky", "aws s3 ls")} plain aws commands, as that alias`);
13049
+ }
13050
+ async function starshipConfig(rc) {
13051
+ const file = starshipFile();
13052
+ const text4 = await readIfExists(file);
13053
+ if (text4 === void 0 && !rc.includes("starship init") && !process.env.STARSHIP_SHELL) {
13054
+ return void 0;
13055
+ }
13056
+ return { file, text: text4 ?? "", exists: text4 !== void 0 };
13057
+ }
13058
+ async function readIfExists(file) {
13059
+ try {
13060
+ return await readFile4(file, "utf8");
13061
+ } catch (error) {
13062
+ if (error.code === "ENOENT") return void 0;
13063
+ throw error;
13064
+ }
13065
+ }
13066
+ async function completeCommand(app, kind) {
13067
+ if (kind === "commands") {
13068
+ console.log(
13069
+ [...RESERVED_NAMES, "each", "shell", "use"].filter((c3, i2, a2) => a2.indexOf(c3) === i2).sort().join("\n")
13070
+ );
13071
+ return;
13072
+ }
13073
+ const config = await app.config();
13074
+ const words = kind === "sources" ? config.sources.map((s) => s.id) : config.aliases.map((a2) => a2.name);
13075
+ if (words.length) console.log(words.join("\n"));
13076
+ }
13077
+
13078
+ // src/commands/use.ts
13079
+ var activeVars = (provider) => {
13080
+ const prefix = `CLOUDHOP_${provider.toUpperCase()}`;
13081
+ return {
13082
+ alias: `${prefix}_ALIAS`,
13083
+ prod: `${prefix}_PROD`,
13084
+ prompt: `${prefix}_PROMPT`,
13085
+ // One or the other, so prompt themes can style production apart (Starship's env_var).
13086
+ segment: `${prefix}_SEGMENT`,
13087
+ prodSegment: `${prefix}_PROD_SEGMENT`
13088
+ };
13089
+ };
13090
+ function activeAliases(env = process.env) {
13091
+ return Object.fromEntries(
13092
+ PROVIDER_IDS.filter((p3) => env[activeVars(p3).alias]).map((p3) => [p3, env[activeVars(p3).alias]])
13093
+ );
13094
+ }
13095
+ async function shellContext(trusted, cwd = process.cwd()) {
13096
+ const file = findFolderFile(cwd);
13097
+ const content = file ? await readFolderFile(file) : void 0;
13098
+ let folder;
13099
+ if (file && content !== void 0) {
13100
+ try {
13101
+ folder = {
13102
+ file,
13103
+ aliases: parseFolderFile(content),
13104
+ trusted: trusted.isTrusted(file, content)
13105
+ };
13106
+ } catch {
13107
+ }
13108
+ }
13109
+ return { inUse: activeAliases(), ...folder ? { folder } : {} };
13110
+ }
13111
+ var tilde2 = (path2) => path2.replace(homedir4(), "~");
13112
+ var folderProviders = () => (process.env.CLOUDHOP_RC_PROVIDERS ?? "").split(",").filter((p3) => PROVIDER_IDS.includes(p3));
13113
+ async function activate(app, alias, login) {
13114
+ const shellEnv = await app.shellEnv(alias, { login });
13115
+ const vars = activeVars(shellEnv.provider);
13116
+ const label = shellEnv.region ? `${alias} (${shellEnv.region})` : alias;
13117
+ const env = Object.fromEntries(shellEnv.clear.map((name) => [name, void 0]));
13118
+ Object.assign(env, shellEnv.env, {
13119
+ [vars.alias]: alias,
13120
+ [vars.prod]: shellEnv.prod ? "1" : void 0,
13121
+ [vars.prompt]: label,
13122
+ [vars.segment]: shellEnv.prod ? void 0 : label,
13123
+ [vars.prodSegment]: shellEnv.prod ? label : void 0,
13124
+ // The cloud-neutral pair from cloudhop 1.2, kept for prompts already using it.
13125
+ CLOUDHOP_ALIAS: alias,
13126
+ CLOUDHOP_PROD: shellEnv.prod ? "1" : void 0
13127
+ });
13128
+ const where = [shellEnv.label, shellEnv.region ?? "no region"].filter(Boolean).join(", ");
13129
+ return { provider: shellEnv.provider, env, summary: `${alias} (${where})`, prod: shellEnv.prod };
13130
+ }
13131
+ function deactivate(app, provider) {
13132
+ const vars = activeVars(provider);
13133
+ const env = Object.fromEntries(app.shellVars(provider).map((n3) => [n3, void 0]));
13134
+ for (const name of Object.values(vars)) env[name] = void 0;
13135
+ if (process.env.CLOUDHOP_ALIAS && process.env.CLOUDHOP_ALIAS === process.env[vars.alias]) {
13136
+ Object.assign(env, { CLOUDHOP_ALIAS: void 0, CLOUDHOP_PROD: void 0 });
13137
+ }
13138
+ return env;
13139
+ }
13140
+ function warnProduction(activation) {
13141
+ if (!activation.prod) return;
13142
+ const alias = activation.summary.split(" ")[0];
13143
+ notify(
13144
+ paint(
13145
+ "red",
13146
+ `${alias} is production: with the shell integration, \`aws\` asks before changes. Other tools (terraform, scripts) don't.`
13147
+ )
13148
+ );
13149
+ }
13150
+ async function useCommand(app, trusted, args) {
13151
+ const { positional, flags } = parseArgs(args);
13152
+ const shell = flags.emit ? parseShell(flags.emit) : void 0;
13153
+ if (flags.from !== void 0 || flags.leave) {
13154
+ if (!shell) throw new CloudhopError("INVALID_NAME", "--from and --leave need --emit <shell>.");
13155
+ const env2 = await folderEnv(app, trusted, flags.leave ? void 0 : flags.from);
13156
+ process.stdout.write(`${shellExports(shell, env2)}
13157
+ `);
13158
+ return 0;
13159
+ }
13160
+ if (flags.off) {
13161
+ if (!shell) throw noIntegration("Nothing to turn off: this shell has no cloudhop integration.");
13162
+ const which = positional[0];
13163
+ if (which && !PROVIDER_IDS.includes(which)) {
12376
13164
  throw new CloudhopError(
12377
- "PROD_CONFIRMATION_REQUIRED",
12378
- `This may change resources in production: ${risky.join(", ")}.`,
12379
- "Run it in a terminal to confirm, or set CLOUDHOP_CONFIRM_PROD=1 once a person has approved it."
13165
+ "INVALID_NAME",
13166
+ `Unknown cloud "${which}".`,
13167
+ `Use one of: ${PROVIDER_IDS.join(", ")}, or nothing to turn them all off.`
12380
13168
  );
12381
13169
  }
12382
- dist_exports.log.warn(
12383
- `${paint("red", bold("PRODUCTION"))} ${risky.join(", ")}: this may change resources.`
13170
+ const providers = which ? [which] : app.availableProviders().map((provider) => provider.id);
13171
+ const env2 = {};
13172
+ for (const provider of providers) Object.assign(env2, deactivate(app, provider));
13173
+ const remaining = folderProviders().filter((p3) => !providers.includes(p3));
13174
+ Object.assign(
13175
+ env2,
13176
+ folderMarks(remaining.length ? process.env.CLOUDHOP_RC : void 0, remaining)
12384
13177
  );
12385
- if (!answer(await dist_exports.confirm({ message: "Also run it in production?", initialValue: false })))
12386
- return 130;
13178
+ process.stdout.write(`${shellExports(shell, env2)}
13179
+ `);
13180
+ notify(which ? `No ${which} alias in this shell anymore.` : "No alias in this shell anymore.");
13181
+ return 0;
12387
13182
  }
12388
- for (const source of new Set(selected.map((a2) => a2.source))) {
12389
- await app.ensureSession(source, { interactive: tty });
13183
+ const alias = positional[0] ?? await pickAlias(app, "Use which alias in this shell?");
13184
+ const activation = await activate(app, alias, true);
13185
+ let folder;
13186
+ if (flags.save) folder = await saveFolderFile(trusted, activation.provider, alias);
13187
+ if (!shell) {
13188
+ if (!interactive()) {
13189
+ throw noIntegration("`cloudhop use` needs a terminal or the shell integration.");
13190
+ }
13191
+ return subshell(alias, activation);
12390
13192
  }
12391
- const parallel = Math.max(1, Math.min(16, Number(flags.parallel) || 4));
12392
- const outcomes = await mapLimit(selected, parallel, async (a2) => {
13193
+ const fromFolder = folderProviders().filter((p3) => p3 !== activation.provider);
13194
+ const env = { ...activation.env };
13195
+ if (folder) Object.assign(env, folderMarks(folder, [...fromFolder, activation.provider]));
13196
+ else
13197
+ Object.assign(
13198
+ env,
13199
+ folderMarks(fromFolder.length ? process.env.CLOUDHOP_RC : void 0, fromFolder)
13200
+ );
13201
+ process.stdout.write(`${shellExports(shell, env)}
13202
+ `);
13203
+ notify(`This shell now uses ${activation.summary}.`);
13204
+ if (folder) notify(`Saved in ${tilde2(folder)}: this folder switches to ${alias} on cd.`);
13205
+ warnProduction(activation);
13206
+ return 0;
13207
+ }
13208
+ var folderMarks = (file, providers) => ({
13209
+ CLOUDHOP_RC: providers.length && file ? file : void 0,
13210
+ CLOUDHOP_RC_PROVIDERS: providers.length && file ? providers.join(",") : void 0
13211
+ });
13212
+ var noIntegration = (message) => new CloudhopError(
13213
+ "NOT_INTERACTIVE",
13214
+ message,
13215
+ `Set it up once with \`cloudhop shell install\` (or add ${LOAD_LINE[detectShell()]} to your shell's startup file), then open a new terminal.`
13216
+ );
13217
+ async function folderEnv(app, trusted, file) {
13218
+ const previous = folderProviders();
13219
+ const activations = [];
13220
+ const path2 = file ? resolve2(file) : void 0;
13221
+ const content = path2 ? await readFolderFile(path2) : void 0;
13222
+ if (path2 && content !== void 0) {
13223
+ let aliases;
12393
13224
  try {
12394
- const r2 = await app.captureAsAlias(a2.name, cliArgs);
12395
- const outcome = { alias: a2.name, exitCode: r2.exitCode, stdout: r2.stdout, stderr: r2.stderr };
12396
- if (!flags.json) printOutcome(outcome, width);
12397
- return outcome;
13225
+ aliases = parseFolderFile(content);
12398
13226
  } catch (error) {
12399
- const outcome = { alias: a2.name, exitCode: 1, stdout: "", stderr: error.message };
12400
- if (!flags.json) printOutcome(outcome, width);
12401
- return outcome;
13227
+ notify(`${tilde2(path2)}: ${error.message}`);
13228
+ }
13229
+ if (aliases && !trusted.isTrusted(path2, content)) {
13230
+ notify(
13231
+ `${tilde2(path2)} wants this folder to use ${Object.values(aliases).join(", ")}. Run ${bold("cloudhop allow")} if you trust it.`
13232
+ );
13233
+ } else if (aliases) {
13234
+ for (const alias of Object.values(aliases)) {
13235
+ try {
13236
+ activations.push(await activate(app, alias, false));
13237
+ } catch (error) {
13238
+ notify(`${tilde2(path2)}: ${error.message}`);
13239
+ }
13240
+ }
12402
13241
  }
12403
- });
12404
- const failed = outcomes.filter((o2) => o2.exitCode !== 0);
12405
- if (flags.json) {
12406
- console.log(JSON.stringify({ version: 1, command: cliArgs, results: outcomes }, null, 2));
12407
- } else {
12408
- const summary = `${outcomes.length - failed.length} ok${failed.length ? `, ${failed.length} failed: ${failed.map((f2) => f2.alias).join(", ")}` : ""}`;
12409
- process.stderr.write(`${failed.length ? paint("red", summary) : paint("green", summary)}
12410
- `);
12411
13242
  }
12412
- return failed.length ? 1 : 0;
13243
+ const applied = activations.map((a2) => a2.provider);
13244
+ const env = {};
13245
+ const left = previous.filter((p3) => !applied.includes(p3));
13246
+ for (const provider of left) Object.assign(env, deactivate(app, provider));
13247
+ for (const activation of activations) Object.assign(env, activation.env);
13248
+ Object.assign(env, folderMarks(path2, applied));
13249
+ if (activations.length) {
13250
+ notify(`Using ${activations.map((a2) => a2.summary).join(" and ")}, from ${FOLDER_FILE}.`);
13251
+ for (const activation of activations) warnProduction(activation);
13252
+ } else if (left.length) {
13253
+ const names = left.map((p3) => process.env[activeVars(p3).alias]).filter(Boolean);
13254
+ notify(`Left the folder of ${names.join(", ")}: no alias in this shell.`);
13255
+ }
13256
+ return env;
12413
13257
  }
12414
- function printOutcome({ alias, exitCode, stdout: stdout2, stderr }, width) {
12415
- const tag = paint(exitCode === 0 ? "sky" : "red", `[${alias}]`.padEnd(width + 2));
12416
- for (const line of stdout2.replace(/\n$/, "").split("\n"))
12417
- if (line) process.stdout.write(`${tag} ${line}
12418
- `);
12419
- for (const line of stderr.replace(/\n$/, "").split("\n"))
12420
- if (line) process.stderr.write(`${tag} ${paint("dim", line)}
12421
- `);
12422
- if (exitCode !== 0) process.stderr.write(`${tag} ${paint("red", `exit ${exitCode}`)}
12423
- `);
13258
+ async function saveFolderFile(trusted, provider, alias) {
13259
+ const file = join7(process.cwd(), FOLDER_FILE);
13260
+ const content = await readFolderFile(file);
13261
+ let aliases = {};
13262
+ if (content !== void 0) {
13263
+ aliases = parseFolderFile(content);
13264
+ delete aliases.any;
13265
+ }
13266
+ aliases[provider] = alias;
13267
+ const text4 = formatFolderFile(aliases);
13268
+ await writeFile5(file, text4);
13269
+ await trusted.trust(file, text4);
13270
+ return file;
13271
+ }
13272
+ function subshell(alias, activation) {
13273
+ const env = { ...process.env, CLOUDHOP_SUBSHELL: alias };
13274
+ for (const [name, value] of Object.entries(activation.env)) {
13275
+ if (value === void 0) delete env[name];
13276
+ else env[name] = value;
13277
+ }
13278
+ const shell = process.platform === "win32" ? process.env.ComSpec ?? "cmd.exe" : process.env.SHELL ?? "/bin/sh";
13279
+ if (/cmd(\.exe)?$/i.test(shell)) {
13280
+ const label = `${activation.provider}:${alias}${activation.prod ? " PROD" : ""}`;
13281
+ env.PROMPT = `[${label}] ${process.env.PROMPT ?? "$P$G"}`;
13282
+ }
13283
+ if (process.env.CLOUDHOP_SUBSHELL) {
13284
+ notify(
13285
+ `You are already in a cloudhop shell (${process.env.CLOUDHOP_SUBSHELL}): this one goes inside it.`
13286
+ );
13287
+ }
13288
+ notify(`New shell using ${activation.summary}. Type ${bold("exit")} to go back.`);
13289
+ notify(
13290
+ paint(
13291
+ "dim",
13292
+ "Tip: `cloudhop shell install` makes `cloudhop use` switch this same terminal instead."
13293
+ )
13294
+ );
13295
+ warnProduction(activation);
13296
+ const result = spawnSync(shell, [], { stdio: "inherit", env });
13297
+ notify(`Back from ${alias}.`);
13298
+ return result.status ?? 0;
13299
+ }
13300
+ async function allowCommand(app, trusted, args, allow) {
13301
+ const { positional } = parseArgs(args);
13302
+ const given = positional[0];
13303
+ const file = given ? given.endsWith(FOLDER_FILE) ? resolve2(given) : join7(resolve2(given), FOLDER_FILE) : findFolderFile(process.cwd());
13304
+ if (!file) {
13305
+ throw new CloudhopError(
13306
+ "INVALID_NAME",
13307
+ `No ${FOLDER_FILE} in this folder or above it.`,
13308
+ "Create one with `cloudhop use <alias> --save`."
13309
+ );
13310
+ }
13311
+ if (!allow) {
13312
+ const forgotten = await trusted.forget(file);
13313
+ console.log(
13314
+ forgotten ? `${paint("green", "\u2714")} ${tilde2(file)} is no longer trusted: its folder won't switch aliases.` : `${tilde2(file)} was not trusted.`
13315
+ );
13316
+ return;
13317
+ }
13318
+ const content = await readFolderFile(file);
13319
+ if (content === void 0) throw new CloudhopError("INVALID_NAME", `Can't read ${file}.`);
13320
+ const aliases = parseFolderFile(content);
13321
+ const lines = [];
13322
+ for (const alias of Object.values(aliases)) {
13323
+ try {
13324
+ const status = await app.aliasStatus(alias);
13325
+ lines.push(
13326
+ ` ${bold(alias)} ${status.label ?? status.target} ${status.region ?? ""}${status.prod ? ` ${paint("red", "PROD")}` : ""}`
13327
+ );
13328
+ } catch {
13329
+ lines.push(
13330
+ ` ${bold(alias)} ${paint("yellow", "you don't have this alias: it will be skipped")}`
13331
+ );
13332
+ }
13333
+ }
13334
+ await trusted.trust(file, content);
13335
+ console.log(
13336
+ `${paint("green", "\u2714")} Trusted ${tilde2(file)}. In ${tilde2(dirname5(file))} this shell uses:`
13337
+ );
13338
+ console.log(lines.join("\n"));
12424
13339
  }
12425
13340
 
12426
13341
  // src/commands/manage.ts
@@ -12468,11 +13383,11 @@ var SESSION_LABEL = {
12468
13383
  };
12469
13384
  var JSON_VERSION = 1;
12470
13385
  var printJson = (value) => console.log(JSON.stringify({ version: JSON_VERSION, ...value }, null, 2));
12471
- async function listCommand(app, args = []) {
13386
+ async function listCommand(app, args = [], context) {
12472
13387
  const { positional, flags } = parseArgs(args);
12473
13388
  const sourceId = positional[0];
12474
13389
  if (flags.json) {
12475
- const overview = await app.overview();
13390
+ const overview = { ...await app.overview(), ...await context?.() };
12476
13391
  if (sourceId && !overview.sources.some((s) => s.id === sourceId)) {
12477
13392
  throw new CloudhopError("SOURCE_NOT_FOUND", `Source "${sourceId}" not found.`);
12478
13393
  }
@@ -12651,7 +13566,16 @@ async function statusCommand(app, args) {
12651
13566
  return;
12652
13567
  }
12653
13568
  const overview = await app.overview();
12654
- if (flags.json) return printJson({ sources: overview.sources });
13569
+ const inUse = activeAliases();
13570
+ if (flags.json) return printJson({ inUse, sources: overview.sources });
13571
+ const using = Object.entries(inUse).map(([cloud, alias]) => {
13572
+ const status = overview.aliases.find((a2) => a2.name === alias);
13573
+ const region = status?.region ? ` (${status.region})` : "";
13574
+ return `${cloud} ${bold(paint(status?.prod ? "red" : "sky", alias))}${region}${status?.prod ? ` ${paint("red", "PROD")}` : ""}`;
13575
+ });
13576
+ console.log(
13577
+ using.length ? `This terminal uses ${using.join(", ")}.` : paint("dim", "This terminal uses no alias (`cloudhop use <alias>` to pick one).")
13578
+ );
12655
13579
  if (overview.sources.length === 0) {
12656
13580
  console.log("Nothing here yet. Run `cloudhop setup` to get started.");
12657
13581
  return;
@@ -12793,6 +13717,23 @@ async function setupCommand(app, assistants) {
12793
13717
  const addNow = answer(await dist_exports.confirm({ message: "Add an account or SSO portal now?" }));
12794
13718
  if (addNow) await addCommand(app, { positional: [], flags: {} });
12795
13719
  else dist_exports.log.info("Run `cloudhop add` whenever you are ready.");
13720
+ if (!shellIntegrationInstalled()) {
13721
+ dist_exports.note(
13722
+ [
13723
+ "`hop <alias>`, `each` and `console` work as they are. To also switch a terminal",
13724
+ "to an alias (`cloudhop use`, then plain `aws`), switch on `cd` into a folder with",
13725
+ "a .cloudhoprc, see the alias in your prompt (Starship too) and complete aliases",
13726
+ "with <Tab>, your shell loads one line from its startup file. A program can only",
13727
+ "change your terminal this way."
13728
+ ].join("\n"),
13729
+ "Terminal"
13730
+ );
13731
+ const install = answer(
13732
+ await dist_exports.confirm({ message: "Set up your terminal for cloudhop?", initialValue: true })
13733
+ );
13734
+ if (install) await shellCommand(["install", "--yes"], []);
13735
+ else dist_exports.log.info("Later: `cloudhop shell install`. `cloudhop use` still works, in a new shell.");
13736
+ }
12796
13737
  const detected = assistants.detect();
12797
13738
  if (detected.length > 0) {
12798
13739
  const names = detected.map((id) => ASSISTANTS[id].name).join(", ");
@@ -12830,268 +13771,37 @@ async function doctorCommand(app) {
12830
13771
  );
12831
13772
  }
12832
13773
 
12833
- // src/commands/shell.ts
12834
- import { existsSync as existsSync3 } from "fs";
12835
- import { appendFile, mkdir as mkdir3, readFile as readFile3 } from "fs/promises";
12836
- import { homedir as homedir3 } from "os";
12837
- import { dirname as dirname3, join as join5 } from "path";
12838
- var SHELLS = ["zsh", "bash", "fish", "pwsh"];
12839
- var ALIAS_COMMANDS = "use status console region mv prod rm exec";
12840
- var SOURCE_COMMANDS = "alias login sync ls";
12841
- var SCRIPTS = {
12842
- zsh: `# cloudhop shell integration (zsh). Load it after compinit: eval "$(cloudhop shell zsh)"
12843
- cloudhop() {
12844
- if [ "$1" = "use" ]; then
12845
- local out; out="$(command cloudhop use --emit zsh "\${@:2}")" || return $?
12846
- eval "$out"
12847
- else
12848
- command cloudhop "$@"
12849
- fi
12850
- }
12851
- _cloudhop_words() { command cloudhop __complete "$1" 2>/dev/null }
12852
- _hop() { (( CURRENT == 2 )) && { local -a a; a=(\${(f)"$(_cloudhop_words aliases)"}); compadd -a a; } }
12853
- _cloudhop() {
12854
- local -a w
12855
- if (( CURRENT == 2 )); then w=(\${(f)"$(_cloudhop_words commands)"}); compadd -a w; return; fi
12856
- (( CURRENT == 3 )) || return
12857
- case " ${ALIAS_COMMANDS} " in *" $words[2] "*) w=(\${(f)"$(_cloudhop_words aliases)"});; esac
12858
- case " ${SOURCE_COMMANDS} " in *" $words[2] "*) w=(\${(f)"$(_cloudhop_words sources)"});; esac
12859
- compadd -a w
12860
- }
12861
- (( $+functions[compdef] )) && { compdef _hop hop; compdef _cloudhop cloudhop; }
12862
- # Add $(cloudhop_prompt) to your prompt to see the alias set with \`cloudhop use\`.
12863
- cloudhop_prompt() { [ -n "$CLOUDHOP_ALIAS" ] && printf '\u2601 %s%s' "$CLOUDHOP_ALIAS" "\${CLOUDHOP_PROD:+ PROD}"; }
12864
- `,
12865
- bash: `# cloudhop shell integration (bash): eval "$(cloudhop shell bash)"
12866
- cloudhop() {
12867
- if [ "$1" = "use" ]; then
12868
- local out; out="$(command cloudhop use --emit bash "\${@:2}")" || return $?
12869
- eval "$out"
12870
- else
12871
- command cloudhop "$@"
12872
- fi
12873
- }
12874
- _cloudhop_complete() {
12875
- local cur=\${COMP_WORDS[COMP_CWORD]} cmd=\${COMP_WORDS[0]##*/} kind=""
12876
- if [ "$cmd" = hop ]; then [ "$COMP_CWORD" -eq 1 ] && kind=aliases
12877
- elif [ "$COMP_CWORD" -eq 1 ]; then kind=commands
12878
- elif [ "$COMP_CWORD" -eq 2 ]; then
12879
- case " ${ALIAS_COMMANDS} " in *" \${COMP_WORDS[1]} "*) kind=aliases;; esac
12880
- case " ${SOURCE_COMMANDS} " in *" \${COMP_WORDS[1]} "*) kind=sources;; esac
12881
- fi
12882
- [ -n "$kind" ] && COMPREPLY=($(compgen -W "$(command cloudhop __complete $kind 2>/dev/null)" -- "$cur"))
12883
- }
12884
- complete -o default -F _cloudhop_complete hop cloudhop
12885
- # Add $(cloudhop_prompt) to PS1 to see the alias set with \`cloudhop use\`.
12886
- cloudhop_prompt() { [ -n "$CLOUDHOP_ALIAS" ] && printf '\u2601 %s%s' "$CLOUDHOP_ALIAS" "\${CLOUDHOP_PROD:+ PROD}"; }
12887
- `,
12888
- fish: `# cloudhop shell integration (fish): cloudhop shell fish | source
12889
- function cloudhop
12890
- if test "$argv[1]" = use
12891
- set -l out (command cloudhop use --emit fish $argv[2..-1]); or return $status
12892
- printf '%s\\n' $out | source
12893
- else
12894
- command cloudhop $argv
12895
- end
12896
- end
12897
- complete -c hop -f -n 'test (count (commandline -opc)) -eq 1' -a '(command cloudhop __complete aliases 2>/dev/null)'
12898
- complete -c cloudhop -f -n 'test (count (commandline -opc)) -eq 1' -a '(command cloudhop __complete commands 2>/dev/null)'
12899
- complete -c cloudhop -f -n '__fish_seen_subcommand_from ${ALIAS_COMMANDS}; and test (count (commandline -opc)) -eq 2' -a '(command cloudhop __complete aliases 2>/dev/null)'
12900
- complete -c cloudhop -f -n '__fish_seen_subcommand_from ${SOURCE_COMMANDS}; and test (count (commandline -opc)) -eq 2' -a '(command cloudhop __complete sources 2>/dev/null)'
12901
- # Call cloudhop_prompt from fish_prompt to see the alias set with \`cloudhop use\`.
12902
- function cloudhop_prompt
12903
- test -n "$CLOUDHOP_ALIAS"; and printf '\u2601 %s%s' $CLOUDHOP_ALIAS (test -n "$CLOUDHOP_PROD"; and echo ' PROD')
12904
- end
12905
- `,
12906
- pwsh: `# cloudhop shell integration (PowerShell): cloudhop shell pwsh | Out-String | Invoke-Expression
12907
- $script:CloudhopExe = (Get-Command cloudhop -CommandType Application, ExternalScript | Select-Object -First 1).Source
12908
- function cloudhop {
12909
- if ($args.Count -gt 0 -and $args[0] -eq 'use') {
12910
- $out = & $script:CloudhopExe use --emit pwsh @($args | Select-Object -Skip 1)
12911
- if ($LASTEXITCODE -eq 0) { $out | Out-String | Invoke-Expression }
12912
- } else { & $script:CloudhopExe @args }
12913
- }
12914
- Register-ArgumentCompleter -Native -CommandName hop, cloudhop -ScriptBlock {
12915
- param($word, $ast)
12916
- $n = $ast.CommandElements.Count - [int]($word -eq '')
12917
- $kind = if ($ast.CommandElements[0].Value -match 'hop$' -and $ast.CommandElements[0].Value -notmatch 'cloudhop') { if ($n -le 1) { 'aliases' } }
12918
- elseif ($n -le 1) { 'commands' }
12919
- elseif ($n -eq 2 -and ' ${ALIAS_COMMANDS} ' -match " $($ast.CommandElements[1].Value) ") { 'aliases' }
12920
- elseif ($n -eq 2 -and ' ${SOURCE_COMMANDS} ' -match " $($ast.CommandElements[1].Value) ") { 'sources' }
12921
- if ($kind) { & $script:CloudhopExe __complete $kind | Where-Object { $_ -like "$word*" } }
12922
- }
12923
- function cloudhop_prompt { if ($env:CLOUDHOP_ALIAS) { "\u2601 $env:CLOUDHOP_ALIAS$(if ($env:CLOUDHOP_PROD) { ' PROD' })" } }
12924
- `
12925
- };
12926
- var quote = (shell, value) => shell === "pwsh" ? `'${value.replace(/'/g, "''")}'` : `'${value.replace(/'/g, "'\\''")}'`;
12927
- function shellExports(shell, env) {
12928
- return Object.entries(env).map(([key, value]) => {
12929
- if (value === void 0) {
12930
- if (shell === "fish") return `set -e ${key}`;
12931
- if (shell === "pwsh") return `Remove-Item Env:${key} -ErrorAction SilentlyContinue`;
12932
- return `unset ${key}`;
12933
- }
12934
- if (shell === "fish") return `set -gx ${key} ${quote(shell, value)}`;
12935
- if (shell === "pwsh") return `$env:${key} = ${quote(shell, value)}`;
12936
- return `export ${key}=${quote(shell, value)}`;
12937
- }).join("\n");
12938
- }
12939
- function detectShell() {
12940
- if (process.platform === "win32") return "pwsh";
12941
- const name = (process.env.SHELL ?? "").split("/").pop();
12942
- return SHELLS.includes(name) ? name : "bash";
12943
- }
12944
- function parseShell(value) {
12945
- if (!value) return detectShell();
12946
- const shell = value === "powershell" ? "pwsh" : value;
12947
- if (!SHELLS.includes(shell)) {
12948
- throw new CloudhopError(
12949
- "INVALID_NAME",
12950
- `Unknown shell "${value}".`,
12951
- `Use one of: ${SHELLS.join(", ")}.`
12952
- );
12953
- }
12954
- return shell;
12955
- }
12956
- var RC_FILES = {
12957
- zsh: (home) => join5(home, ".zshrc"),
12958
- bash: (home) => join5(home, ".bashrc"),
12959
- fish: (home) => join5(home, ".config", "fish", "config.fish"),
12960
- pwsh: (home) => process.platform === "win32" ? join5(home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1") : join5(home, ".config", "powershell", "Microsoft.PowerShell_profile.ps1")
12961
- };
12962
- var LOAD_LINE = {
12963
- zsh: 'eval "$(cloudhop shell zsh)"',
12964
- bash: 'eval "$(cloudhop shell bash)"',
12965
- fish: "cloudhop shell fish | source",
12966
- pwsh: "cloudhop shell pwsh | Out-String | Invoke-Expression"
12967
- };
12968
- async function shellCommand(args) {
12969
- const { positional, flags } = parseArgs(args);
12970
- if (positional[0] !== "install") {
12971
- process.stdout.write(SCRIPTS[parseShell(positional[0])]);
12972
- return;
12973
- }
12974
- const shell = parseShell(positional[1]);
12975
- const file = RC_FILES[shell](homedir3());
12976
- const line = LOAD_LINE[shell];
12977
- const current = existsSync3(file) ? await readFile3(file, "utf8") : "";
12978
- if (current.includes(line)) {
12979
- console.log(`Already set up in ${file.replace(homedir3(), "~")}.`);
12980
- return;
12981
- }
12982
- if (!flags.yes) {
12983
- if (!interactive())
12984
- throw new CloudhopError(
12985
- "NOT_INTERACTIVE",
12986
- `Add this line to ${file} yourself, or rerun with --yes:
12987
- ${line}`
12988
- );
12989
- const ok = answer(
12990
- await dist_exports.confirm({
12991
- message: `Add cloudhop to ${file.replace(homedir3(), "~")}?`,
12992
- initialValue: true
12993
- })
12994
- );
12995
- if (!ok) return;
12996
- }
12997
- await mkdir3(dirname3(file), { recursive: true });
12998
- const note2 = shell === "zsh" ? " # after compinit" : "";
12999
- await appendFile(
13000
- file,
13001
- `${current && !current.endsWith("\n") ? "\n" : ""}
13002
- # cloudhop: \`cloudhop use\`, tab completion and cloudhop_prompt${note2}
13003
- ${line}
13004
- `
13005
- );
13006
- console.log(
13007
- `${paint("green", "\u2714")} Added to ${file.replace(homedir3(), "~")}. Open a new terminal to use it.`
13008
- );
13009
- }
13010
- async function useCommand(app, args) {
13011
- const { positional, flags } = parseArgs(args);
13012
- const shell = flags.emit ? parseShell(flags.emit) : void 0;
13013
- if (!shell) {
13014
- throw new CloudhopError(
13015
- "NOT_INTERACTIVE",
13016
- "`cloudhop use` needs the shell integration to change your current shell.",
13017
- `Set it up once with \`cloudhop shell install\` (or add ${LOAD_LINE[detectShell()]} to your shell's startup file), then open a new terminal.`
13018
- );
13019
- }
13020
- const clear = {
13021
- AWS_ACCESS_KEY_ID: void 0,
13022
- AWS_SECRET_ACCESS_KEY: void 0,
13023
- AWS_SESSION_TOKEN: void 0
13024
- };
13025
- if (flags.off) {
13026
- process.stdout.write(
13027
- `${shellExports(shell, { ...clear, AWS_PROFILE: void 0, AWS_REGION: void 0, AWS_DEFAULT_REGION: void 0, CLOUDHOP_ALIAS: void 0, CLOUDHOP_PROD: void 0 })}
13028
- `
13029
- );
13030
- notify("No alias in this shell anymore.");
13031
- return;
13032
- }
13033
- const alias = positional[0] ?? await pickAlias(app, "Use which alias in this shell?");
13034
- const env = await app.envForAlias(alias, "cli");
13035
- const status = await app.aliasStatus(alias);
13036
- process.stdout.write(
13037
- `${shellExports(shell, { ...clear, ...env, CLOUDHOP_ALIAS: alias, CLOUDHOP_PROD: status.prod ? "1" : void 0 })}
13038
- `
13039
- );
13040
- notify(
13041
- `This shell now uses ${alias} (${status.label ?? status.target}, ${status.region ?? "no region"}).`
13042
- );
13043
- if (status.prod) {
13044
- notify(
13045
- paint(
13046
- "red",
13047
- `${alias} is production. Plain \`aws\` commands are not protected here: use \`hop ${alias}\` to be asked before changes.`
13048
- )
13049
- );
13050
- }
13051
- }
13052
- async function completeCommand(app, kind) {
13053
- if (kind === "commands") {
13054
- console.log(
13055
- [...RESERVED_NAMES, "each", "shell", "use"].filter((c3, i2, a2) => a2.indexOf(c3) === i2).sort().join("\n")
13056
- );
13057
- return;
13058
- }
13059
- const config = await app.config();
13060
- const words = kind === "sources" ? config.sources.map((s) => s.id) : config.aliases.map((a2) => a2.name);
13061
- if (words.length) console.log(words.join("\n"));
13062
- }
13063
-
13064
13774
  // src/commands/team.ts
13065
- import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
13775
+ import { readFile as readFile5, writeFile as writeFile6 } from "fs/promises";
13066
13776
  var import_yaml2 = __toESM(require_dist(), 1);
13067
13777
  var MAX_BYTES = 256 * 1024;
13068
13778
  var HEADER2 = `# cloudhop team setup: import it with \`cloudhop team import <this file or its https URL>\`.
13069
13779
  # No secrets: portal URLs, regions, account ids and role names only.
13070
13780
  `;
13071
13781
  function validate(app, raw) {
13072
- const bad = (why) => new CloudhopError("INVALID_NAME", `Not a cloudhop team file: ${why}.`);
13073
- if (!raw || typeof raw !== "object") throw bad("it is empty");
13782
+ const bad2 = (why) => new CloudhopError("INVALID_NAME", `Not a cloudhop team file: ${why}.`);
13783
+ if (!raw || typeof raw !== "object") throw bad2("it is empty");
13074
13784
  const file = raw;
13075
- if (file.version !== 1) throw bad('unknown "version" (expected 1)');
13785
+ if (file.version !== 1) throw bad2('unknown "version" (expected 1)');
13076
13786
  const sources = file.sources ?? {};
13077
13787
  const aliases = file.aliases ?? {};
13078
13788
  for (const [id, source] of Object.entries(sources)) {
13079
13789
  assertValidName("source", id);
13080
13790
  if (!PROVIDER_IDS.includes(source?.provider))
13081
- throw bad(`source "${id}" has an unknown provider`);
13791
+ throw bad2(`source "${id}" has an unknown provider`);
13082
13792
  const kind = app.provider(source.provider).sourceKinds.find((k) => k.kind === source.kind);
13083
- if (!kind) throw bad(`source "${id}" has an unknown kind`);
13793
+ if (!kind) throw bad2(`source "${id}" has an unknown kind`);
13084
13794
  for (const field of kind.fields) {
13085
13795
  const value = source.settings?.[field.key];
13086
- if (typeof value !== "string" || !value) throw bad(`source "${id}" is missing ${field.key}`);
13796
+ if (typeof value !== "string" || !value) throw bad2(`source "${id}" is missing ${field.key}`);
13087
13797
  const problem = field.validate?.(value);
13088
- if (problem) throw bad(`source "${id}": ${problem}`);
13798
+ if (problem) throw bad2(`source "${id}": ${problem}`);
13089
13799
  }
13090
13800
  }
13091
13801
  for (const [name, alias] of Object.entries(aliases)) {
13092
13802
  assertValidName("alias", name);
13093
13803
  if (!alias || typeof alias.target !== "string" || !sources[alias.source]) {
13094
- throw bad(`alias "${name}" must name a source of this file and a target`);
13804
+ throw bad2(`alias "${name}" must name a source of this file and a target`);
13095
13805
  }
13096
13806
  }
13097
13807
  return { version: 1, sources, aliases };
@@ -13107,7 +13817,7 @@ async function load(location) {
13107
13817
  throw new CloudhopError("INVALID_NAME", "That file is too big for a team setup.");
13108
13818
  return text4;
13109
13819
  }
13110
- return readFile4(location, "utf8");
13820
+ return readFile5(location, "utf8");
13111
13821
  }
13112
13822
  async function teamCommand(app, args) {
13113
13823
  const { positional, flags } = parseArgs(args);
@@ -13128,7 +13838,7 @@ async function teamCommand(app, args) {
13128
13838
  };
13129
13839
  const text4 = HEADER2 + (0, import_yaml2.stringify)(file);
13130
13840
  if (flags.out) {
13131
- await writeFile3(flags.out, text4);
13841
+ await writeFile6(flags.out, text4);
13132
13842
  console.error(
13133
13843
  `${paint("green", "\u2714")} Wrote ${sources.length} source(s) and ${Object.keys(file.aliases).length} alias(es) to ${flags.out}.`
13134
13844
  );
@@ -13233,8 +13943,8 @@ async function teamCommand(app, args) {
13233
13943
  }
13234
13944
 
13235
13945
  // ../ui/src/api.ts
13236
- var text3 = (body, key) => {
13237
- const value = body[key];
13946
+ var text3 = (body, key2) => {
13947
+ const value = body[key2];
13238
13948
  return typeof value === "string" ? value.trim() : "";
13239
13949
  };
13240
13950
  var badRequest = (message) => new CloudhopError("INVALID_NAME", message);
@@ -13307,8 +14017,8 @@ function createApi(app, assistants) {
13307
14017
  label: kind.label,
13308
14018
  hint: kind.hint,
13309
14019
  nameLabel: kind.nameLabel,
13310
- fields: kind.fields.map(({ key, label, placeholder, defaultValue }) => ({
13311
- key,
14020
+ fields: kind.fields.map(({ key: key2, label, placeholder, defaultValue }) => ({
14021
+ key: key2,
13312
14022
  label,
13313
14023
  placeholder,
13314
14024
  defaultValue
@@ -13446,8 +14156,8 @@ function createApi(app, assistants) {
13446
14156
  return { removed: name };
13447
14157
  }
13448
14158
  };
13449
- const table = Object.entries(routes).map(([key, handler]) => {
13450
- const [method, pattern] = key.split(" ");
14159
+ const table = Object.entries(routes).map(([key2, handler]) => {
14160
+ const [method, pattern] = key2.split(" ");
13451
14161
  const regex = new RegExp(`^${pattern.replace(/:[a-z]+/g, "([^/]+)")}$`);
13452
14162
  return { method, regex, handler };
13453
14163
  });
@@ -13476,7 +14186,7 @@ import { randomBytes, timingSafeEqual } from "crypto";
13476
14186
  import { createServer } from "http";
13477
14187
 
13478
14188
  // ../ui/src/assets/app.js
13479
- var app_default = "// cloudhop local UI. Plain DOM, no build step: served as-is by `cloudhop ui`.\n\nconst TOKEN_KEY = 'cloudhop-token';\nconst SESSION_TEXT = {\n valid: 'Session valid',\n refreshable: 'Renews on next use',\n expired: 'Expired: sign in on next use',\n unknown: 'Managed by the cloud CLI',\n};\n\n// The token arrives in the URL fragment once; keep it for this tab and clean the address bar.\nconst fromHash = new URLSearchParams(location.hash.slice(1)).get('token');\nif (fromHash) {\n try {\n sessionStorage.setItem(TOKEN_KEY, fromHash);\n } catch {}\n history.replaceState(null, '', location.pathname);\n}\nlet token = fromHash;\ntry {\n token = token ?? sessionStorage.getItem(TOKEN_KEY);\n} catch {}\n\n// ---------- pages ----------\n\nconst PAGES = {\n aliases: 'Aliases',\n sources: 'Sources',\n assistants: 'AI assistants',\n tools: 'Tools',\n};\nlet route = 'aliases';\nconst aliasFilter = { text: '', source: '', prodOnly: false };\n\n/** `#/aliases?source=acme` \u2192 route \"aliases\", filtered by that source. */\nfunction readRoute() {\n const match = /^#\\/([a-z]+)(?:\\?(.*))?$/.exec(location.hash);\n route = match && PAGES[match[1]] ? match[1] : 'aliases';\n const query = new URLSearchParams(match?.[2] ?? '');\n if (query.has('source')) aliasFilter.source = query.get('source');\n}\n\nfunction go(page, query) {\n location.hash = `#/${page}${query ? `?${new URLSearchParams(query)}` : ''}`;\n}\n\n// A fresh link opened in this same tab carries a new token: reload to pick it up.\n// Any other fragment is a page change.\naddEventListener('hashchange', () => {\n if (new URLSearchParams(location.hash.slice(1)).has('token')) {\n location.reload();\n return;\n }\n readRoute();\n if (state) render();\n});\n\nconst $main = document.getElementById('main');\nconst $dialog = document.getElementById('dialog');\nconst $toasts = document.getElementById('toasts');\nconst $addSource = document.getElementById('add-source');\nconst $nav = document.getElementById('nav');\nlet state;\n\n/** Tiny element builder: h('button', { class: 'primary', onclick }, 'Save'). */\nfunction h(tag, attrs = {}, ...children) {\n const el = document.createElement(tag);\n for (const [key, value] of Object.entries(attrs)) {\n if (value === undefined || value === null || value === false) continue;\n if (key.startsWith('on')) el.addEventListener(key.slice(2), value);\n else if (key === 'class') el.className = value;\n else if (value === true) el.setAttribute(key, '');\n else el.setAttribute(key, value);\n }\n for (const child of children.flat()) {\n if (child === undefined || child === null || child === false) continue;\n el.append(child instanceof Node ? child : document.createTextNode(String(child)));\n }\n return el;\n}\n\nclass ApiError extends Error {\n constructor(message, hint) {\n super(message);\n this.hint = hint;\n }\n}\n\n// Calls that may sign in wait for the person in the browser; everything else should answer quickly.\nconst MAY_SIGN_IN = /^\\/api\\/(sources(\\/[^/]+\\/(login|sync|targets))?|aliases\\/[^/]+\\/console)$/;\nconst QUICK_TIMEOUT_MS = 20_000;\n\nasync function api(method, path, body) {\n const waitsForPerson = MAY_SIGN_IN.test(path);\n let response;\n try {\n response = await fetch(path, {\n method,\n headers: {\n 'x-cloudhop-token': token ?? '',\n ...(body ? { 'content-type': 'application/json' } : {}),\n },\n body: body ? JSON.stringify(body) : undefined,\n signal: waitsForPerson ? undefined : AbortSignal.timeout(QUICK_TIMEOUT_MS),\n });\n } catch (error) {\n if (error.name === 'TimeoutError') {\n throw new ApiError(\n 'cloudhop ui is not responding.',\n 'Is it paused in your terminal (Ctrl-Z)? Run `fg` there, or stop it and run `cloudhop ui` again.',\n );\n }\n throw new ApiError(\n 'cloudhop ui is not running.',\n 'Run `cloudhop ui` again and open the new link it prints.',\n );\n }\n const data = await response.json().catch(() => ({}));\n if (!response.ok)\n throw new ApiError(data.error ?? `Request failed (${response.status}).`, data.hint);\n return data;\n}\n\nfunction toast(message, { error = false, hint } = {}) {\n const el = h(\n 'div',\n { class: `toast${error ? ' error' : ''}` },\n message,\n hint && h('small', {}, hint),\n );\n $toasts.append(el);\n setTimeout(() => el.remove(), error ? 9000 : 4000);\n}\n\nconst fail = (error) => toast(error.message, { error: true, hint: error.hint });\n\n/** Disables a button while its action runs. */\nasync function busy(button, action) {\n button.disabled = true;\n try {\n return await action();\n } finally {\n button.disabled = false;\n }\n}\n\nasync function refresh() {\n try {\n state = await api('GET', '/api/state');\n render();\n checkDoctor();\n } catch (error) {\n $main.replaceChildren(\n h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'Cannot reach cloudhop'),\n h('p', {}, error.message, error.hint ? ` ${error.hint}` : ''),\n ),\n );\n }\n}\n\nasync function checkDoctor() {\n try {\n const entries = await api('GET', '/api/doctor');\n const problems = entries.filter((e) => !e.installed || e.outdated);\n document.getElementById('doctor')?.remove();\n if (problems.length === 0) return;\n $main.prepend(\n h(\n 'div',\n { class: 'notice', id: 'doctor' },\n ...problems.map((e) =>\n h(\n 'div',\n {},\n e.installed\n ? `${e.cli} ${e.version} is older than ${e.minVersion}. `\n : `${e.cli} is not installed, so aliases cannot run commands yet. `,\n 'Run ',\n h('code', {}, 'cloudhop doctor'),\n ' in a terminal to install it.',\n ),\n ),\n ),\n );\n } catch {}\n}\n\n// ---------- main view ----------\n\nfunction render() {\n const enabled = state.enabledProviders;\n $addSource.hidden = enabled.length === 0;\n $nav.hidden = enabled.length === 0;\n if (enabled.length === 0) return renderChooseClouds();\n for (const link of $nav.querySelectorAll('a')) {\n if (link.dataset.page === route) link.setAttribute('aria-current', 'page');\n else link.removeAttribute('aria-current');\n }\n document.title = `${PAGES[route]} \xB7 cloudhop`;\n const pages = {\n aliases: aliasesPage,\n sources: sourcesPage,\n assistants: assistantsPage,\n tools: toolsPage,\n };\n $main.replaceChildren(...pages[route]());\n if (route === 'assistants') loadAssistants();\n if (route === 'tools') loadTools();\n}\n\nfunction pageHead(title, subtitle, ...actions) {\n return h(\n 'div',\n { class: 'page-head' },\n h('div', {}, h('h1', {}, title), subtitle ? h('p', {}, subtitle) : null),\n h('div', { class: 'actions' }, actions),\n );\n}\n\nfunction firstSourceHero() {\n return h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'Add your first account'),\n h(\n 'p',\n {},\n 'Add a company SSO portal or a personal account. cloudhop signs you in, finds every account and role you can use, and lets you give each one a short alias.',\n ),\n h('button', { class: 'primary', onclick: () => openAddSource() }, '+ Add source'),\n );\n}\n\n// ---------- Aliases page ----------\n\nfunction aliasesPage() {\n if (state.sources.length === 0) return [firstSourceHero()];\n if (aliasFilter.source && !state.sources.some((s) => s.id === aliasFilter.source))\n aliasFilter.source = '';\n\n const addAliases = () => {\n const source =\n state.sources.find((s) => s.id === aliasFilter.source) ??\n (state.sources.length === 1 ? state.sources[0] : undefined);\n if (source) openAliasPicker(source);\n else openSourceChooser();\n };\n const head = pageHead(\n 'Aliases',\n 'Each alias is one account, role and region. Run it with hop <alias>.',\n h('button', { onclick: addAliases }, '+ Aliases'),\n );\n if (state.aliases.length === 0) {\n return [\n head,\n h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'No aliases yet'),\n h('p', {}, 'Pick the accounts and roles you use and give each one a short name.'),\n h('button', { class: 'primary', onclick: addAliases }, 'Pick accounts'),\n ),\n ];\n }\n\n const rows = h('tbody', { id: 'alias-rows' });\n const count = h('span', { class: 'count' });\n const fill = () => {\n const words = aliasFilter.text.toLowerCase().split(/\\s+/).filter(Boolean);\n const visible = state.aliases.filter((a) => {\n if (aliasFilter.source && a.source !== aliasFilter.source) return false;\n if (aliasFilter.prodOnly && !a.prod) return false;\n const text = `${a.name} ${a.label ?? a.target} ${a.region ?? ''} ${a.source}`.toLowerCase();\n return words.every((w) => text.includes(w));\n });\n rows.replaceChildren(\n ...(visible.length\n ? visible.map(aliasRow)\n : [\n h(\n 'tr',\n {},\n h('td', { colspan: '5', class: 'empty' }, 'No alias matches these filters.'),\n ),\n ]),\n );\n count.textContent = `${visible.length} of ${state.aliases.length}`;\n };\n\n const search = h('input', {\n type: 'search',\n placeholder: 'Filter: name, account, role or region',\n value: aliasFilter.text,\n 'aria-label': 'Filter aliases',\n oninput: (e) => {\n aliasFilter.text = e.currentTarget.value;\n fill();\n },\n });\n const sourceSelect = h(\n 'select',\n {\n 'aria-label': 'Source',\n onchange: (e) => {\n aliasFilter.source = e.currentTarget.value;\n fill();\n },\n },\n h('option', { value: '' }, 'All sources'),\n state.sources.map((s) =>\n h('option', { value: s.id, selected: s.id === aliasFilter.source }, s.id),\n ),\n );\n const prodOnly = h(\n 'label',\n { class: 'prod-toggle' },\n h('input', {\n type: 'checkbox',\n checked: aliasFilter.prodOnly,\n onchange: (e) => {\n aliasFilter.prodOnly = e.currentTarget.checked;\n fill();\n },\n }),\n 'Production only',\n );\n\n const table = h(\n 'table',\n { class: 'aliases' },\n h(\n 'thead',\n {},\n h(\n 'tr',\n {},\n h('th', {}, 'Alias'),\n h('th', {}, 'Source'),\n h('th', {}, 'Region'),\n h('th', {}, 'Account / role'),\n h('th'),\n ),\n ),\n rows,\n );\n fill();\n return [\n head,\n h(\n 'div',\n { class: 'toolbar' },\n search,\n sourceSelect,\n prodOnly,\n h('div', { class: 'spacer' }),\n count,\n ),\n h('section', { class: 'card' }, table),\n ];\n}\n\n/** When several sources exist, ask which one to pick accounts from. */\nfunction openSourceChooser() {\n showDialog(\n 'Add aliases from',\n h(\n 'div',\n { class: 'choices' },\n state.sources.map((source) =>\n h(\n 'button',\n { class: 'choice', onclick: () => openAliasPicker(source) },\n h(\n 'span',\n {},\n h('strong', {}, source.id),\n h('small', {}, source.settings.startUrl ?? source.kindLabel),\n ),\n ),\n ),\n ),\n );\n}\n\n// ---------- Sources page ----------\n\nfunction sourcesPage() {\n const head = pageHead('Sources', 'The SSO portals and personal accounts you sign in to.');\n if (state.sources.length === 0) return [head, firstSourceHero()];\n return [head, ...state.sources.map(sourceCard)];\n}\n\n// ---------- AI assistants page ----------\n\nfunction assistantsPage() {\n return [assistantsCard()];\n}\n\n// ---------- Tools page ----------\n\nfunction snippet(command) {\n return h(\n 'div',\n { class: 'snippet' },\n h('code', {}, command),\n h(\n 'button',\n {\n class: 'link',\n title: 'Copy',\n 'aria-label': `Copy ${command}`,\n onclick: () =>\n navigator.clipboard\n ?.writeText(command)\n .then(() => toast('Copied.'))\n .catch(() =>\n toast('Copy it by hand: the browser blocked the clipboard.', { error: true }),\n ),\n },\n 'Copy',\n ),\n );\n}\n\nfunction toolCard(title, text, ...commands) {\n return h(\n 'section',\n { class: 'card' },\n h('div', { class: 'card-head' }, h('h2', {}, title)),\n h(\n 'div',\n { class: 'card-body padded' },\n h('p', { class: 'card-help' }, text),\n commands.map(snippet),\n ),\n );\n}\n\nfunction toolsPage() {\n const sample = state.aliases[0]?.name ?? 'acme-dev';\n return [\n pageHead('Tools', 'More ways to use cloudhop from your terminal.'),\n h(\n 'section',\n { class: 'card', id: 'tools-cli' },\n h('div', { class: 'card-head' }, h('h2', {}, 'Cloud CLIs')),\n h('div', { class: 'card-body padded' }, h('p', { class: 'card-help' }, 'Checking\u2026')),\n ),\n toolCard(\n 'Shell integration',\n 'Pick aliases from a list, tab-complete them, and use one in your shell so plain aws and Terraform work without hop.',\n 'cloudhop shell install',\n `cloudhop use ${sample}`,\n 'hop',\n ),\n toolCard(\n 'Run in many accounts',\n 'Run one command in many aliases at once: names, lists, wildcards or /regex/. It asks before running.',\n `cloudhop each '${sample.split('-')[0]}-*' -- s3 ls`,\n ),\n toolCard(\n 'Share with your team',\n 'Export your portals and aliases (no secrets) and let teammates import them in one command.',\n 'cloudhop team export --out team.yaml',\n 'cloudhop team import team.yaml',\n ),\n ];\n}\n\nasync function loadTools() {\n const card = document.querySelector('#tools-cli .card-body');\n if (!card) return;\n try {\n const entries = await api('GET', '/api/doctor');\n card.replaceChildren(\n ...entries.map((e) =>\n h(\n 'div',\n { class: 'tool-row' },\n h('span', { class: `badge ${e.installed && !e.outdated ? 'valid' : 'expired'}` }, e.cli),\n h(\n 'span',\n {},\n !e.installed\n ? 'Not installed.'\n : e.outdated\n ? `${e.version} is older than ${e.minVersion}.`\n : `Version ${e.version}.`,\n ),\n e.installed && !e.outdated ? null : snippet('cloudhop doctor'),\n ),\n ),\n );\n } catch (error) {\n card.replaceChildren(h('p', { class: 'card-help' }, error.message));\n }\n}\n\n// ---------- AI assistants ----------\n\nlet assistantScope = 'user';\ntry {\n assistantScope =\n localStorage.getItem('cloudhop-assistant-scope') === 'project' ? 'project' : 'user';\n} catch {}\nlet assistantsData;\n\nconst SKILL_TEXT = {\n current: ['valid', 'Up to date'],\n outdated: ['refreshable', 'Outdated'],\n modified: ['refreshable', 'Edited by you'],\n missing: ['', 'Not installed'],\n};\nconst MCP_TEXT = { registered: 'On', missing: 'Off', unknown: 'Check in the app' };\n\nfunction assistantsCard() {\n return h('section', { class: 'card', id: 'assistants', hidden: true });\n}\n\nasync function loadAssistants() {\n try {\n assistantsData = await api('GET', '/api/assistants');\n renderAssistants();\n } catch {\n document.getElementById('assistants')?.remove();\n }\n}\n\nfunction setAssistantScope(scope) {\n assistantScope = scope;\n try {\n localStorage.setItem('cloudhop-assistant-scope', scope);\n } catch {}\n renderAssistants();\n}\n\nfunction assistantAction(label, run, { danger = false, confirmText } = {}) {\n return h(\n 'button',\n {\n class: danger ? 'danger' : undefined,\n onclick: (e) => {\n if (confirmText && !confirm(confirmText)) return;\n busy(e.currentTarget, async () => {\n try {\n await run();\n } catch (error) {\n fail(error);\n }\n await loadAssistants();\n });\n },\n },\n label,\n );\n}\n\nfunction reportInstall(results) {\n for (const r of results) {\n if (r.mcp === 'manual') {\n toast(`${r.name}: skill installed. Add the MCP server by hand.`, { hint: r.mcpManual });\n } else {\n toast(`${r.name} now knows cloudhop. Start a new chat so it picks it up.`);\n }\n }\n}\n\nfunction renderAssistants() {\n const el = document.getElementById('assistants');\n if (!el || !assistantsData) return;\n const rows = assistantScope === 'user' ? assistantsData.user : assistantsData.projectScope;\n const scope = assistantScope;\n const install = (tools) =>\n api('POST', '/api/assistants', { tools, scope, mcp: true }).then((r) =>\n reportInstall(r.results),\n );\n const missingDetected = rows\n .filter((r) => r.detected && r.skill === 'missing')\n .map((r) => r.assistant);\n\n const seg = h(\n 'div',\n { class: 'seg', role: 'group', 'aria-label': 'Where' },\n h(\n 'button',\n { 'aria-pressed': String(scope === 'user'), onclick: () => setAssistantScope('user') },\n 'For me',\n ),\n h(\n 'button',\n { 'aria-pressed': String(scope === 'project'), onclick: () => setAssistantScope('project') },\n 'This project',\n ),\n );\n\n const head = h(\n 'div',\n { class: 'card-head' },\n h('h2', {}, 'AI assistants'),\n h(\n 'span',\n { class: 'meta' },\n scope === 'user'\n ? 'Installed in your home folder, for every project'\n : assistantsData.project,\n ),\n h('div', { class: 'spacer' }),\n h(\n 'div',\n { class: 'actions' },\n seg,\n missingDetected.length > 1\n ? assistantAction('Install for all detected', () => install(missingDetected))\n : null,\n ),\n );\n\n const table = h(\n 'table',\n { class: 'assistants' },\n h(\n 'thead',\n {},\n h(\n 'tr',\n {},\n h('th', {}, 'Assistant'),\n h('th', {}, 'Skill'),\n h('th', {}, 'MCP server'),\n h('th'),\n ),\n ),\n h(\n 'tbody',\n {},\n rows.map((r) => {\n const [badgeClass, skillText] = SKILL_TEXT[r.skill];\n const actions = [];\n if (r.skill === 'missing')\n actions.push(assistantAction('Install', () => install([r.assistant])));\n if (r.skill === 'outdated') {\n actions.push(\n assistantAction('Update', () =>\n api('POST', '/api/assistants/update', {}).then(() =>\n toast(`${r.name}: skill updated.`),\n ),\n ),\n );\n }\n if (r.skill === 'modified') {\n actions.push(\n assistantAction(\n 'Replace',\n () =>\n api('POST', '/api/assistants/update', { force: true }).then(() =>\n toast('Edited skills replaced.'),\n ),\n {\n confirmText: 'Replace your edited copies with the current cloudhop skill?',\n },\n ),\n );\n }\n if (r.skill !== 'missing' || r.mcp === 'registered') {\n actions.push(\n assistantAction(\n 'Remove',\n () => api('POST', '/api/assistants/remove', { tools: [r.assistant], scope }),\n {\n danger: true,\n confirmText: `Remove the cloudhop skill and MCP server from ${r.name}?`,\n },\n ),\n );\n }\n return h(\n 'tr',\n {},\n h(\n 'td',\n { class: 'alias' },\n r.name,\n r.detected ? null : h('small', { class: 'muted-inline' }, ' not detected'),\n ),\n h('td', {}, h('span', { class: `badge ${badgeClass}`, title: r.skillPath }, skillText)),\n h('td', {}, r.skill === 'missing' && r.mcp !== 'registered' ? '' : MCP_TEXT[r.mcp]),\n h(\n 'td',\n { style: 'text-align:right' },\n h('div', { class: 'actions', style: 'justify-content:flex-end' }, actions),\n ),\n );\n }),\n ),\n );\n\n el.replaceChildren(\n head,\n h(\n 'div',\n { class: 'card-body' },\n h(\n 'p',\n { class: 'card-help' },\n 'Teach your AI assistant to use cloudhop: it asks you which alias to work with, runs commands with hop, and confirms before changing anything. Includes the cloudhop MCP server.',\n ),\n table,\n ),\n );\n el.hidden = false;\n}\n\nfunction renderChooseClouds() {\n const boxes = state.providers.map((p) =>\n h(\n 'label',\n { class: 'choice' },\n h('input', { type: 'checkbox', value: p.id, checked: true }),\n h('span', {}, p.name),\n ),\n );\n const save = h(\n 'button',\n {\n class: 'primary',\n onclick: (e) =>\n busy(e.currentTarget, async () => {\n const ids = boxes\n .map((b) => b.querySelector('input'))\n .filter((i) => i.checked)\n .map((i) => i.value);\n await api('PUT', '/api/providers', { enabled: ids }).catch(fail);\n await refresh();\n }),\n },\n 'Continue',\n );\n $main.replaceChildren(\n h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'Which clouds do you use?'),\n h('p', {}, 'Azure and Google Cloud are coming soon.'),\n h(\n 'div',\n { class: 'choices', style: 'max-width:320px;margin:0 auto 16px;text-align:left' },\n boxes,\n ),\n save,\n ),\n );\n}\n\nfunction sourceCard(source) {\n const aliases = state.aliases.filter((a) => a.source === source.id);\n const where = source.settings.startUrl ?? source.kindLabel;\n const head = h(\n 'div',\n { class: 'card-head' },\n h('h2', {}, source.id),\n h('span', { class: `badge ${source.session}` }, SESSION_TEXT[source.session] ?? source.session),\n h('span', { class: 'meta' }, `${source.providerName} \xB7 ${where}`),\n h('div', { class: 'spacer' }),\n h(\n 'div',\n { class: 'actions' },\n h('button', { onclick: () => openAliasPicker(source) }, '+ Aliases'),\n h(\n 'button',\n {\n title: 'Sign in again and refresh its accounts',\n onclick: (e) =>\n busy(e.currentTarget, async () => {\n toast('Opening the browser to sign in\u2026');\n const report = await api(\n 'POST',\n `/api/sources/${encodeURIComponent(source.id)}/login`,\n ).catch(fail);\n if (report)\n toast(`${source.id}: signed in, ${report.targets.length} account/role(s).`);\n await refresh();\n }),\n },\n 'Sign in',\n ),\n h(\n 'button',\n {\n title: 'Refresh the accounts and roles you have access to',\n onclick: (e) =>\n busy(e.currentTarget, async () => {\n const report = await api(\n 'POST',\n `/api/sources/${encodeURIComponent(source.id)}/sync`,\n ).catch(fail);\n if (report) toast(syncMessage(source.id, report));\n await refresh();\n }),\n },\n 'Sync',\n ),\n h(\n 'button',\n {\n class: 'danger',\n onclick: (e) => {\n const count = aliases.length;\n const message = `Remove \"${source.id}\"${count ? ` and its ${count} alias(es)` : ''}?`;\n if (!confirm(message)) return;\n busy(e.currentTarget, async () => {\n await api('DELETE', `/api/sources/${encodeURIComponent(source.id)}`).catch(fail);\n await refresh();\n });\n },\n },\n 'Remove',\n ),\n ),\n );\n\n const prodCount = aliases.filter((a) => a.prod).length;\n const body =\n aliases.length === 0\n ? h(\n 'div',\n { class: 'empty' },\n 'No aliases yet. ',\n h(\n 'button',\n { class: 'primary', onclick: () => openAliasPicker(source) },\n 'Pick accounts',\n ),\n )\n : h(\n 'div',\n { class: 'source-summary' },\n h(\n 'span',\n {},\n `${aliases.length} alias(es)`,\n prodCount ? h('span', { class: 'prod-tag' }, `${prodCount} PROD`) : null,\n ),\n h(\n 'button',\n { class: 'link', onclick: () => go('aliases', { source: source.id }) },\n 'View aliases \u2192',\n ),\n );\n return h('section', { class: 'card' }, head, h('div', { class: 'card-body' }, body));\n}\n\nfunction syncMessage(id, report) {\n const parts = [`${report.targets.length} available`];\n if (report.added) parts.push(`${report.added} new`);\n if (report.removed) parts.push(`${report.removed} removed`);\n const orphaned = report.orphanedAliases.length\n ? ` No longer available: ${report.orphanedAliases.join(', ')}.`\n : '';\n return `${id}: ${parts.join(', ')}.${orphaned}`;\n}\n\nfunction aliasRow(alias) {\n const input = h('input', {\n value: alias.region ?? '',\n 'aria-label': `Region for ${alias.name}`,\n class: 'mono',\n spellcheck: 'false',\n });\n const save = h('button', { hidden: true }, 'Save');\n const commit = () =>\n busy(save, async () => {\n const region = input.value.trim();\n const done = await api('PATCH', `/api/aliases/${encodeURIComponent(alias.name)}`, {\n region,\n }).catch(fail);\n if (done) toast(`${alias.name} now runs in ${region}.`);\n await refresh();\n });\n input.addEventListener('input', () => {\n save.hidden = input.value.trim() === (alias.region ?? '');\n });\n input.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' && !save.hidden) commit();\n if (e.key === 'Escape') {\n input.value = alias.region ?? '';\n save.hidden = true;\n }\n });\n save.addEventListener('click', commit);\n\n const patch = (body, message) =>\n api('PATCH', `/api/aliases/${encodeURIComponent(alias.name)}`, body)\n .then(() => message && toast(message))\n .catch(fail)\n .then(refresh);\n const iconButton = (label, title, onclick, extra = {}) =>\n h('button', { class: 'link', title, 'aria-label': title, onclick, ...extra }, label);\n\n const actions = h(\n 'div',\n { class: 'row-actions' },\n h(\n 'button',\n {\n title: `Open the AWS console as ${alias.name}, already signed in`,\n disabled: alias.missing,\n onclick: (e) =>\n busy(e.currentTarget, async () => {\n const result = await api(\n 'POST',\n `/api/aliases/${encodeURIComponent(alias.name)}/console`,\n {},\n ).catch(fail);\n if (result) {\n toast(\n result.via === 'signed-in'\n ? `Opened the console for ${alias.name}.`\n : result.via === 'portal'\n ? `Opened ${alias.name} through the SSO portal.`\n : `Opened the console sign-in page for ${alias.name}.`,\n );\n }\n }),\n },\n 'Console',\n ),\n h(\n 'label',\n { class: 'prod-toggle', title: 'Production: hop and AI assistants ask before any change' },\n h('input', {\n type: 'checkbox',\n checked: alias.prod,\n 'aria-label': `${alias.name} is production`,\n onchange: (e) =>\n patch(\n { prod: e.currentTarget.checked },\n e.currentTarget.checked\n ? `${alias.name} is now production.`\n : `${alias.name} is no longer production.`,\n ),\n }),\n 'Prod',\n ),\n iconButton('\u270E', `Rename ${alias.name}`, () => {\n const to = prompt(`New name for \"${alias.name}\"`, alias.name)?.trim();\n if (to && to !== alias.name) patch({ name: to }, `${alias.name} is now ${to}.`);\n }),\n iconButton(\n '\u2715',\n `Remove ${alias.name}`,\n (e) => {\n if (!confirm(`Remove alias \"${alias.name}\"?`)) return;\n busy(e.currentTarget, async () => {\n await api('DELETE', `/api/aliases/${encodeURIComponent(alias.name)}`).catch(fail);\n await refresh();\n });\n },\n { class: 'link danger-hover' },\n ),\n );\n\n return h(\n 'tr',\n { class: alias.prod ? 'prod' : undefined },\n h(\n 'td',\n { class: 'alias', title: `hop ${alias.name} <command>` },\n alias.name,\n alias.prod ? h('span', { class: 'prod-tag' }, 'PROD') : null,\n ),\n h('td', { class: 'muted' }, alias.source),\n h('td', {}, h('div', { class: 'region' }, input, save)),\n h(\n 'td',\n {},\n alias.missing\n ? h('span', { class: 'missing' }, `${alias.target} (no longer available)`)\n : alias.label,\n ),\n h('td', { style: 'text-align:right' }, actions),\n );\n}\n\n// ---------- dialogs ----------\n\nfunction showDialog(title, body, footer) {\n $dialog.replaceChildren(\n h(\n 'div',\n { class: 'dlg-head' },\n h('h2', {}, title),\n h('button', { class: 'link', 'aria-label': 'Close', onclick: () => $dialog.close() }, '\u2715'),\n ),\n h('div', { class: 'dlg-body' }, body),\n footer ? h('div', { class: 'dlg-foot' }, footer) : null,\n );\n if (!$dialog.open) $dialog.showModal();\n}\n\nfunction suggestSourceId(settings, kind) {\n const host = /^https:\\/\\/([^./]+)\\./.exec(settings.startUrl ?? '')?.[1];\n return slug(host ?? (kind === 'console-login' ? 'personal' : 'main'));\n}\n\nconst slug = (text) =>\n text\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 63) || 'main';\n\n/** `previous` refills the form after a failed sign-in: { provider, kind, id, settings, error }. */\nfunction openAddSource(previous) {\n const providers = state.providers.filter((p) => state.enabledProviders.includes(p.id));\n let provider = providers.find((p) => p.id === previous?.provider) ?? providers[0];\n let kind = provider.kinds.find((k) => k.kind === previous?.kind) ?? provider.kinds[0];\n\n const form = h('form', { onsubmit: (e) => e.preventDefault() });\n const nameInput = h('input', {\n id: 'f-name',\n required: true,\n class: 'mono',\n spellcheck: 'false',\n });\n let nameTouched = Boolean(previous);\n nameInput.addEventListener('input', () => {\n nameTouched = true;\n });\n\n const fieldInputs = {};\n const fieldsBox = h('div');\n const nameLabel = h('label', { for: 'f-name' });\n\n function updateSuggestion() {\n if (nameTouched) return;\n const settings = Object.fromEntries(\n Object.entries(fieldInputs).map(([k, i]) => [k, i.value.trim()]),\n );\n const taken = new Set(state.sources.map((s) => s.id));\n let id = suggestSourceId(settings, kind.kind);\n for (let i = 2; taken.has(id); i++) id = `${suggestSourceId(settings, kind.kind)}-${i}`;\n nameInput.value = id;\n }\n\n function renderFields() {\n for (const key of Object.keys(fieldInputs)) delete fieldInputs[key];\n fieldsBox.replaceChildren(\n ...kind.fields.map((f) => {\n const input = h('input', {\n id: `f-${f.key}`,\n placeholder: f.placeholder ?? f.defaultValue ?? '',\n value: previous?.settings?.[f.key] ?? f.defaultValue ?? '',\n required: true,\n class: 'mono',\n spellcheck: 'false',\n oninput: updateSuggestion,\n });\n fieldInputs[f.key] = input;\n return h('div', { class: 'field' }, h('label', { for: input.id }, f.label), input);\n }),\n );\n nameLabel.textContent = kind.nameLabel ?? 'Short name for this source';\n updateSuggestion();\n }\n\n const providerSelect =\n providers.length > 1\n ? h(\n 'div',\n { class: 'field' },\n h('label', { for: 'f-provider' }, 'Cloud'),\n h(\n 'select',\n {\n id: 'f-provider',\n onchange: (e) => {\n provider = providers.find((p) => p.id === e.target.value);\n kind = provider.kinds[0];\n renderKinds();\n },\n },\n providers.map((p) => h('option', { value: p.id, selected: p === provider }, p.name)),\n ),\n )\n : null;\n\n const kindsBox = h('div', { class: 'choices' });\n function renderKinds() {\n kindsBox.replaceChildren(\n ...provider.kinds.map((k) =>\n h(\n 'label',\n { class: 'choice' },\n h('input', {\n type: 'radio',\n name: 'kind',\n value: k.kind,\n checked: k === kind,\n onchange: () => {\n kind = k;\n renderFields();\n },\n }),\n h('span', {}, h('strong', {}, k.label), k.hint ? h('small', {}, k.hint) : null),\n ),\n ),\n );\n renderFields();\n }\n\n form.append(\n previous?.error\n ? h(\n 'div',\n { class: 'notice error' },\n h('strong', {}, 'Could not sign in, so nothing was saved. '),\n previous.error.message,\n previous.error.hint ? h('div', { class: 'help' }, previous.error.hint) : null,\n )\n : '',\n providerSelect ?? '',\n h('div', { class: 'field' }, h('label', {}, 'What do you want to add?')),\n kindsBox,\n fieldsBox,\n h(\n 'div',\n { class: 'field' },\n nameLabel,\n nameInput,\n h(\n 'span',\n { class: 'help' },\n 'Lowercase letters, numbers and dashes. You will see it in ',\n h('code', {}, 'cloudhop ls'),\n '.',\n ),\n ),\n );\n renderKinds();\n if (previous) nameInput.value = previous.id;\n\n const submit = h('button', { class: 'primary', type: 'submit' }, 'Add and sign in');\n form.addEventListener('submit', async () => {\n if (!form.reportValidity()) return;\n const body = {\n provider: provider.id,\n kind: kind.kind,\n id: nameInput.value.trim(),\n settings: Object.fromEntries(\n Object.entries(fieldInputs).map(([k, i]) => [k, i.value.trim()]),\n ),\n };\n showDialog(\n 'Sign in',\n h(\n 'div',\n { class: 'waiting' },\n h('div', { class: 'spinner' }),\n h('strong', {}, 'Finish signing in in the browser tab that just opened.'),\n h('p', { class: 'help' }, 'This page continues on its own once you approve the request.'),\n ),\n );\n try {\n const report = await api('POST', '/api/sources', body);\n await refresh();\n const source = state.sources.find((s) => s.id === body.id);\n toast(`Signed in to ${body.id}. ${report.targets.length} account/role(s) found.`);\n openAliasPicker(source, report.targets);\n } catch (error) {\n // Nothing was saved: reopen the form with what was typed so it can be fixed.\n await refresh();\n openAddSource({ ...body, error });\n }\n });\n\n showDialog('Add source', form, [\n h('button', { type: 'button', onclick: () => $dialog.close() }, 'Cancel'),\n submit,\n ]);\n submit.setAttribute('form', 'add-form');\n form.id = 'add-form';\n}\n\nasync function openAliasPicker(source, preloaded) {\n let targets = preloaded;\n if (!targets) {\n showDialog(\n `Aliases for ${source.id}`,\n h('div', { class: 'waiting' }, h('div', { class: 'spinner' }), 'Loading accounts\u2026'),\n );\n try {\n targets = (await api('GET', `/api/sources/${encodeURIComponent(source.id)}/targets`)).targets;\n } catch (error) {\n $dialog.close();\n return fail(error);\n }\n }\n\n const used = new Map(\n state.aliases.filter((a) => a.source === source.id).map((a) => [a.target, a.name]),\n );\n const taken = new Set(state.aliases.map((a) => a.name));\n const defaultRegion = source.settings.region ?? '';\n const filter = h('input', {\n type: 'search',\n placeholder: `Filter ${targets.length} account/role(s)\u2026`,\n style: 'width:100%;margin-bottom:12px',\n });\n\n const rows = targets.map((t) => {\n const existing = used.get(t.id);\n let name = t.suggestedAlias;\n for (let i = 2; taken.has(name); i++) name = `${t.suggestedAlias}-${i}`;\n const check = h('input', {\n type: 'checkbox',\n disabled: Boolean(existing),\n 'aria-label': t.label,\n });\n const aliasInput = h('input', {\n value: name,\n class: 'mono',\n spellcheck: 'false',\n 'aria-label': `Alias for ${t.label}`,\n placeholder: 'alias',\n });\n const regionInput = h('input', {\n value: defaultRegion,\n class: 'mono',\n spellcheck: 'false',\n 'aria-label': `Region for ${t.label}`,\n placeholder: 'region',\n });\n // Only the checkbox and its text sit in the label, so clicking the inputs never toggles it.\n const el = h(\n 'div',\n { class: 'target' },\n h(\n 'label',\n { class: 'pick' },\n check,\n h(\n 'span',\n {},\n t.label,\n existing ? h('small', { class: 'already' }, `Already: ${existing}`) : null,\n ),\n ),\n h('div', { class: 'inputs' }, aliasInput, regionInput),\n );\n return { target: t, el, check, aliasInput, regionInput };\n });\n\n filter.addEventListener('input', () => {\n const q = filter.value.trim().toLowerCase();\n for (const row of rows) row.el.hidden = q !== '' && !row.target.label.toLowerCase().includes(q);\n });\n\n const save = h('button', { class: 'primary' }, 'Create aliases');\n save.addEventListener('click', () =>\n busy(save, async () => {\n const aliases = rows\n .filter((r) => r.check.checked)\n .map((r) => ({\n source: source.id,\n target: r.target.id,\n name: r.aliasInput.value.trim(),\n region: r.regionInput.value.trim(),\n }));\n if (aliases.length === 0) return toast('Pick at least one account/role.', { error: true });\n try {\n const result = await api('POST', '/api/aliases', { aliases });\n $dialog.close();\n toast(`Created ${result.created.join(', ')}. Try: hop ${result.created[0]}`);\n await refresh();\n } catch (error) {\n fail(error);\n }\n }),\n );\n\n const body =\n targets.length === 0\n ? h('p', { class: 'empty' }, 'This source has no accounts or roles assigned to you.')\n : h(\n 'div',\n {},\n filter,\n h(\n 'div',\n { class: 'targets' },\n rows.map((r) => r.el),\n ),\n );\n showDialog(`Aliases for ${source.id}`, body, [\n h('span', { class: 'note' }, 'Check an account/role, then name its alias and region.'),\n h('button', { onclick: () => $dialog.close() }, 'Cancel'),\n targets.length ? save : null,\n ]);\n}\n\n$addSource.addEventListener('click', () => openAddSource());\n\nif (!token) {\n $main.replaceChildren(\n h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'Open the link from your terminal'),\n h('p', {}, 'Run cloudhop ui and open the link it prints. It includes a one-time access key.'),\n ),\n );\n} else {\n readRoute();\n refresh();\n}\n";
14189
+ var app_default = "// cloudhop local UI. Plain DOM, no build step: served as-is by `cloudhop ui`.\n\nconst TOKEN_KEY = 'cloudhop-token';\nconst SESSION_TEXT = {\n valid: 'Session valid',\n refreshable: 'Renews on next use',\n expired: 'Expired: sign in on next use',\n unknown: 'Managed by the cloud CLI',\n};\n\n// The token arrives in the URL fragment once; keep it for this tab and clean the address bar.\nconst fromHash = new URLSearchParams(location.hash.slice(1)).get('token');\nif (fromHash) {\n try {\n sessionStorage.setItem(TOKEN_KEY, fromHash);\n } catch {}\n history.replaceState(null, '', location.pathname);\n}\nlet token = fromHash;\ntry {\n token = token ?? sessionStorage.getItem(TOKEN_KEY);\n} catch {}\n\n// ---------- pages ----------\n\nconst PAGES = {\n aliases: 'Aliases',\n sources: 'Sources',\n assistants: 'AI assistants',\n tools: 'Tools',\n};\nlet route = 'aliases';\nconst aliasFilter = { text: '', source: '', prodOnly: false };\n\n/** `#/aliases?source=acme` \u2192 route \"aliases\", filtered by that source. */\nfunction readRoute() {\n const match = /^#\\/([a-z]+)(?:\\?(.*))?$/.exec(location.hash);\n // Own keys only: \"#/constructor\" must not reach Object.prototype.\n route = match && Object.hasOwn(PAGES, match[1]) ? match[1] : 'aliases';\n const query = new URLSearchParams(match?.[2] ?? '');\n if (query.has('source')) aliasFilter.source = query.get('source');\n}\n\nfunction go(page, query) {\n location.hash = `#/${page}${query ? `?${new URLSearchParams(query)}` : ''}`;\n}\n\n// A fresh link opened in this same tab carries a new token: reload to pick it up.\n// Any other fragment is a page change.\naddEventListener('hashchange', () => {\n if (new URLSearchParams(location.hash.slice(1)).has('token')) {\n location.reload();\n return;\n }\n readRoute();\n if (state) render();\n});\n\nconst $main = document.getElementById('main');\nconst $dialog = document.getElementById('dialog');\nconst $toasts = document.getElementById('toasts');\nconst $addSource = document.getElementById('add-source');\nconst $nav = document.getElementById('nav');\nlet state;\n\n/** Tiny element builder: h('button', { class: 'primary', onclick }, 'Save'). */\nfunction h(tag, attrs = {}, ...children) {\n const el = document.createElement(tag);\n for (const [key, value] of Object.entries(attrs)) {\n if (value === undefined || value === null || value === false) continue;\n if (key.startsWith('on')) el.addEventListener(key.slice(2), value);\n else if (key === 'class') el.className = value;\n else if (value === true) el.setAttribute(key, '');\n else el.setAttribute(key, value);\n }\n for (const child of children.flat()) {\n if (child === undefined || child === null || child === false) continue;\n el.append(child instanceof Node ? child : document.createTextNode(String(child)));\n }\n return el;\n}\n\nclass ApiError extends Error {\n constructor(message, hint) {\n super(message);\n this.hint = hint;\n }\n}\n\n// Calls that may sign in wait for the person in the browser; everything else should answer quickly.\nconst MAY_SIGN_IN = /^\\/api\\/(sources(\\/[^/]+\\/(login|sync|targets))?|aliases\\/[^/]+\\/console)$/;\nconst QUICK_TIMEOUT_MS = 20_000;\n\nasync function api(method, path, body) {\n const waitsForPerson = MAY_SIGN_IN.test(path);\n let response;\n try {\n response = await fetch(path, {\n method,\n headers: {\n 'x-cloudhop-token': token ?? '',\n ...(body ? { 'content-type': 'application/json' } : {}),\n },\n body: body ? JSON.stringify(body) : undefined,\n signal: waitsForPerson ? undefined : AbortSignal.timeout(QUICK_TIMEOUT_MS),\n });\n } catch (error) {\n if (error.name === 'TimeoutError') {\n throw new ApiError(\n 'cloudhop ui is not responding.',\n 'Is it paused in your terminal (Ctrl-Z)? Run `fg` there, or stop it and run `cloudhop ui` again.',\n );\n }\n throw new ApiError(\n 'cloudhop ui is not running.',\n 'Run `cloudhop ui` again and open the new link it prints.',\n );\n }\n const data = await response.json().catch(() => ({}));\n if (!response.ok)\n throw new ApiError(data.error ?? `Request failed (${response.status}).`, data.hint);\n return data;\n}\n\nfunction toast(message, { error = false, hint } = {}) {\n const el = h(\n 'div',\n { class: `toast${error ? ' error' : ''}` },\n message,\n hint && h('small', {}, hint),\n );\n $toasts.append(el);\n setTimeout(() => el.remove(), error ? 9000 : 4000);\n}\n\nconst fail = (error) => toast(error.message, { error: true, hint: error.hint });\n\n/** Disables a button while its action runs. */\nasync function busy(button, action) {\n button.disabled = true;\n try {\n return await action();\n } finally {\n button.disabled = false;\n }\n}\n\nasync function refresh() {\n try {\n state = await api('GET', '/api/state');\n render();\n checkDoctor();\n } catch (error) {\n $main.replaceChildren(\n h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'Cannot reach cloudhop'),\n h('p', {}, error.message, error.hint ? ` ${error.hint}` : ''),\n ),\n );\n }\n}\n\nasync function checkDoctor() {\n try {\n const entries = await api('GET', '/api/doctor');\n const problems = entries.filter((e) => !e.installed || e.outdated);\n document.getElementById('doctor')?.remove();\n if (problems.length === 0) return;\n $main.prepend(\n h(\n 'div',\n { class: 'notice', id: 'doctor' },\n ...problems.map((e) =>\n h(\n 'div',\n {},\n e.installed\n ? `${e.cli} ${e.version} is older than ${e.minVersion}. `\n : `${e.cli} is not installed, so aliases cannot run commands yet. `,\n 'Run ',\n h('code', {}, 'cloudhop doctor'),\n ' in a terminal to install it.',\n ),\n ),\n ),\n );\n } catch {}\n}\n\n// ---------- main view ----------\n\nfunction render() {\n const enabled = state.enabledProviders;\n $addSource.hidden = enabled.length === 0;\n $nav.hidden = enabled.length === 0;\n if (enabled.length === 0) return renderChooseClouds();\n for (const link of $nav.querySelectorAll('a')) {\n if (link.dataset.page === route) link.setAttribute('aria-current', 'page');\n else link.removeAttribute('aria-current');\n }\n document.title = `${PAGES[route]} \xB7 cloudhop`;\n const pages = new Map([\n ['aliases', aliasesPage],\n ['sources', sourcesPage],\n ['assistants', assistantsPage],\n ['tools', toolsPage],\n ]);\n const page = pages.get(route) ?? aliasesPage;\n $main.replaceChildren(...page());\n if (route === 'assistants') loadAssistants();\n if (route === 'tools') loadTools();\n}\n\nfunction pageHead(title, subtitle, ...actions) {\n return h(\n 'div',\n { class: 'page-head' },\n h('div', {}, h('h1', {}, title), subtitle ? h('p', {}, subtitle) : null),\n h('div', { class: 'actions' }, actions),\n );\n}\n\nfunction firstSourceHero() {\n return h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'Add your first account'),\n h(\n 'p',\n {},\n 'Add a company SSO portal or a personal account. cloudhop signs you in, finds every account and role you can use, and lets you give each one a short alias.',\n ),\n h('button', { class: 'primary', onclick: () => openAddSource() }, '+ Add source'),\n );\n}\n\n// ---------- Aliases page ----------\n\nfunction aliasesPage() {\n if (state.sources.length === 0) return [firstSourceHero()];\n if (aliasFilter.source && !state.sources.some((s) => s.id === aliasFilter.source))\n aliasFilter.source = '';\n\n const addAliases = () => {\n const source =\n state.sources.find((s) => s.id === aliasFilter.source) ??\n (state.sources.length === 1 ? state.sources[0] : undefined);\n if (source) openAliasPicker(source);\n else openSourceChooser();\n };\n const head = pageHead(\n 'Aliases',\n 'Each alias is one account, role and region. Run it with hop <alias>.',\n h('button', { onclick: addAliases }, '+ Aliases'),\n );\n if (state.aliases.length === 0) {\n return [\n head,\n h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'No aliases yet'),\n h('p', {}, 'Pick the accounts and roles you use and give each one a short name.'),\n h('button', { class: 'primary', onclick: addAliases }, 'Pick accounts'),\n ),\n ];\n }\n\n const rows = h('tbody', { id: 'alias-rows' });\n const count = h('span', { class: 'count' });\n const fill = () => {\n const words = aliasFilter.text.toLowerCase().split(/\\s+/).filter(Boolean);\n const visible = state.aliases.filter((a) => {\n if (aliasFilter.source && a.source !== aliasFilter.source) return false;\n if (aliasFilter.prodOnly && !a.prod) return false;\n const text = `${a.name} ${a.label ?? a.target} ${a.region ?? ''} ${a.source}`.toLowerCase();\n return words.every((w) => text.includes(w));\n });\n rows.replaceChildren(\n ...(visible.length\n ? visible.map(aliasRow)\n : [\n h(\n 'tr',\n {},\n h('td', { colspan: '5', class: 'empty' }, 'No alias matches these filters.'),\n ),\n ]),\n );\n count.textContent = `${visible.length} of ${state.aliases.length}`;\n };\n\n const search = h('input', {\n type: 'search',\n placeholder: 'Filter: name, account, role or region',\n value: aliasFilter.text,\n 'aria-label': 'Filter aliases',\n oninput: (e) => {\n aliasFilter.text = e.currentTarget.value;\n fill();\n },\n });\n const sourceSelect = h(\n 'select',\n {\n 'aria-label': 'Source',\n onchange: (e) => {\n aliasFilter.source = e.currentTarget.value;\n fill();\n },\n },\n h('option', { value: '' }, 'All sources'),\n state.sources.map((s) =>\n h('option', { value: s.id, selected: s.id === aliasFilter.source }, s.id),\n ),\n );\n const prodOnly = h(\n 'label',\n { class: 'prod-toggle' },\n h('input', {\n type: 'checkbox',\n checked: aliasFilter.prodOnly,\n onchange: (e) => {\n aliasFilter.prodOnly = e.currentTarget.checked;\n fill();\n },\n }),\n 'Production only',\n );\n\n const table = h(\n 'table',\n { class: 'aliases' },\n h(\n 'thead',\n {},\n h(\n 'tr',\n {},\n h('th', {}, 'Alias'),\n h('th', {}, 'Source'),\n h('th', {}, 'Region'),\n h('th', {}, 'Account / role'),\n h('th'),\n ),\n ),\n rows,\n );\n fill();\n return [\n head,\n h(\n 'div',\n { class: 'toolbar' },\n search,\n sourceSelect,\n prodOnly,\n h('div', { class: 'spacer' }),\n count,\n ),\n h('section', { class: 'card' }, table),\n ];\n}\n\n/** When several sources exist, ask which one to pick accounts from. */\nfunction openSourceChooser() {\n showDialog(\n 'Add aliases from',\n h(\n 'div',\n { class: 'choices' },\n state.sources.map((source) =>\n h(\n 'button',\n { class: 'choice', onclick: () => openAliasPicker(source) },\n h(\n 'span',\n {},\n h('strong', {}, source.id),\n h('small', {}, source.settings.startUrl ?? source.kindLabel),\n ),\n ),\n ),\n ),\n );\n}\n\n// ---------- Sources page ----------\n\nfunction sourcesPage() {\n const head = pageHead('Sources', 'The SSO portals and personal accounts you sign in to.');\n if (state.sources.length === 0) return [head, firstSourceHero()];\n return [head, ...state.sources.map(sourceCard)];\n}\n\n// ---------- AI assistants page ----------\n\nfunction assistantsPage() {\n return [assistantsCard()];\n}\n\n// ---------- Tools page ----------\n\nfunction snippet(command) {\n return h(\n 'div',\n { class: 'snippet' },\n h('code', {}, command),\n h(\n 'button',\n {\n class: 'link',\n title: 'Copy',\n 'aria-label': `Copy ${command}`,\n onclick: () =>\n navigator.clipboard\n ?.writeText(command)\n .then(() => toast('Copied.'))\n .catch(() =>\n toast('Copy it by hand: the browser blocked the clipboard.', { error: true }),\n ),\n },\n 'Copy',\n ),\n );\n}\n\nfunction toolCard(title, text, ...commands) {\n return h(\n 'section',\n { class: 'card' },\n h('div', { class: 'card-head' }, h('h2', {}, title)),\n h(\n 'div',\n { class: 'card-body padded' },\n h('p', { class: 'card-help' }, text),\n commands.map(snippet),\n ),\n );\n}\n\nfunction toolsPage() {\n const sample = state.aliases[0]?.name ?? 'acme-dev';\n return [\n pageHead('Tools', 'More ways to use cloudhop from your terminal.'),\n h(\n 'section',\n { class: 'card', id: 'tools-cli' },\n h('div', { class: 'card-head' }, h('h2', {}, 'Cloud CLIs')),\n h('div', { class: 'card-body padded' }, h('p', { class: 'card-help' }, 'Checking\u2026')),\n ),\n toolCard(\n 'Shell integration',\n 'Pick aliases from a list, tab-complete them, and use one in your shell so plain aws and Terraform work without hop.',\n 'cloudhop shell install',\n `cloudhop use ${sample}`,\n 'hop',\n ),\n toolCard(\n 'Run in many accounts',\n 'Run one command in many aliases at once: names, lists, wildcards or /regex/. It asks before running.',\n `cloudhop each '${sample.split('-')[0]}-*' -- s3 ls`,\n ),\n toolCard(\n 'Share with your team',\n 'Export your portals and aliases (no secrets) and let teammates import them in one command.',\n 'cloudhop team export --out team.yaml',\n 'cloudhop team import team.yaml',\n ),\n ];\n}\n\nasync function loadTools() {\n const card = document.querySelector('#tools-cli .card-body');\n if (!card) return;\n try {\n const entries = await api('GET', '/api/doctor');\n card.replaceChildren(\n ...entries.map((e) =>\n h(\n 'div',\n { class: 'tool-row' },\n h('span', { class: `badge ${e.installed && !e.outdated ? 'valid' : 'expired'}` }, e.cli),\n h(\n 'span',\n {},\n !e.installed\n ? 'Not installed.'\n : e.outdated\n ? `${e.version} is older than ${e.minVersion}.`\n : `Version ${e.version}.`,\n ),\n e.installed && !e.outdated ? null : snippet('cloudhop doctor'),\n ),\n ),\n );\n } catch (error) {\n card.replaceChildren(h('p', { class: 'card-help' }, error.message));\n }\n}\n\n// ---------- AI assistants ----------\n\nlet assistantScope = 'user';\ntry {\n assistantScope =\n localStorage.getItem('cloudhop-assistant-scope') === 'project' ? 'project' : 'user';\n} catch {}\nlet assistantsData;\n\nconst SKILL_TEXT = {\n current: ['valid', 'Up to date'],\n outdated: ['refreshable', 'Outdated'],\n modified: ['refreshable', 'Edited by you'],\n missing: ['', 'Not installed'],\n};\nconst MCP_TEXT = { registered: 'On', missing: 'Off', unknown: 'Check in the app' };\n\nfunction assistantsCard() {\n return h('section', { class: 'card', id: 'assistants', hidden: true });\n}\n\nasync function loadAssistants() {\n try {\n assistantsData = await api('GET', '/api/assistants');\n renderAssistants();\n } catch {\n document.getElementById('assistants')?.remove();\n }\n}\n\nfunction setAssistantScope(scope) {\n assistantScope = scope;\n try {\n localStorage.setItem('cloudhop-assistant-scope', scope);\n } catch {}\n renderAssistants();\n}\n\nfunction assistantAction(label, run, { danger = false, confirmText } = {}) {\n return h(\n 'button',\n {\n class: danger ? 'danger' : undefined,\n onclick: (e) => {\n if (confirmText && !confirm(confirmText)) return;\n busy(e.currentTarget, async () => {\n try {\n await run();\n } catch (error) {\n fail(error);\n }\n await loadAssistants();\n });\n },\n },\n label,\n );\n}\n\nfunction reportInstall(results) {\n for (const r of results) {\n if (r.mcp === 'manual') {\n toast(`${r.name}: skill installed. Add the MCP server by hand.`, { hint: r.mcpManual });\n } else {\n toast(`${r.name} now knows cloudhop. Start a new chat so it picks it up.`);\n }\n }\n}\n\nfunction renderAssistants() {\n const el = document.getElementById('assistants');\n if (!el || !assistantsData) return;\n const rows = assistantScope === 'user' ? assistantsData.user : assistantsData.projectScope;\n const scope = assistantScope;\n const install = (tools) =>\n api('POST', '/api/assistants', { tools, scope, mcp: true }).then((r) =>\n reportInstall(r.results),\n );\n const missingDetected = rows\n .filter((r) => r.detected && r.skill === 'missing')\n .map((r) => r.assistant);\n\n const seg = h(\n 'div',\n { class: 'seg', role: 'group', 'aria-label': 'Where' },\n h(\n 'button',\n { 'aria-pressed': String(scope === 'user'), onclick: () => setAssistantScope('user') },\n 'For me',\n ),\n h(\n 'button',\n { 'aria-pressed': String(scope === 'project'), onclick: () => setAssistantScope('project') },\n 'This project',\n ),\n );\n\n const head = h(\n 'div',\n { class: 'card-head' },\n h('h2', {}, 'AI assistants'),\n h(\n 'span',\n { class: 'meta' },\n scope === 'user'\n ? 'Installed in your home folder, for every project'\n : assistantsData.project,\n ),\n h('div', { class: 'spacer' }),\n h(\n 'div',\n { class: 'actions' },\n seg,\n missingDetected.length > 1\n ? assistantAction('Install for all detected', () => install(missingDetected))\n : null,\n ),\n );\n\n const table = h(\n 'table',\n { class: 'assistants' },\n h(\n 'thead',\n {},\n h(\n 'tr',\n {},\n h('th', {}, 'Assistant'),\n h('th', {}, 'Skill'),\n h('th', {}, 'MCP server'),\n h('th'),\n ),\n ),\n h(\n 'tbody',\n {},\n rows.map((r) => {\n const [badgeClass, skillText] = SKILL_TEXT[r.skill];\n const actions = [];\n if (r.skill === 'missing')\n actions.push(assistantAction('Install', () => install([r.assistant])));\n if (r.skill === 'outdated') {\n actions.push(\n assistantAction('Update', () =>\n api('POST', '/api/assistants/update', {}).then(() =>\n toast(`${r.name}: skill updated.`),\n ),\n ),\n );\n }\n if (r.skill === 'modified') {\n actions.push(\n assistantAction(\n 'Replace',\n () =>\n api('POST', '/api/assistants/update', { force: true }).then(() =>\n toast('Edited skills replaced.'),\n ),\n {\n confirmText: 'Replace your edited copies with the current cloudhop skill?',\n },\n ),\n );\n }\n if (r.skill !== 'missing' || r.mcp === 'registered') {\n actions.push(\n assistantAction(\n 'Remove',\n () => api('POST', '/api/assistants/remove', { tools: [r.assistant], scope }),\n {\n danger: true,\n confirmText: `Remove the cloudhop skill and MCP server from ${r.name}?`,\n },\n ),\n );\n }\n return h(\n 'tr',\n {},\n h(\n 'td',\n { class: 'alias' },\n r.name,\n r.detected ? null : h('small', { class: 'muted-inline' }, ' not detected'),\n ),\n h('td', {}, h('span', { class: `badge ${badgeClass}`, title: r.skillPath }, skillText)),\n h('td', {}, r.skill === 'missing' && r.mcp !== 'registered' ? '' : MCP_TEXT[r.mcp]),\n h(\n 'td',\n { style: 'text-align:right' },\n h('div', { class: 'actions', style: 'justify-content:flex-end' }, actions),\n ),\n );\n }),\n ),\n );\n\n el.replaceChildren(\n head,\n h(\n 'div',\n { class: 'card-body' },\n h(\n 'p',\n { class: 'card-help' },\n 'Teach your AI assistant to use cloudhop: it asks you which alias to work with, runs commands with hop, and confirms before changing anything. Includes the cloudhop MCP server.',\n ),\n table,\n ),\n );\n el.hidden = false;\n}\n\nfunction renderChooseClouds() {\n const boxes = state.providers.map((p) =>\n h(\n 'label',\n { class: 'choice' },\n h('input', { type: 'checkbox', value: p.id, checked: true }),\n h('span', {}, p.name),\n ),\n );\n const save = h(\n 'button',\n {\n class: 'primary',\n onclick: (e) =>\n busy(e.currentTarget, async () => {\n const ids = boxes\n .map((b) => b.querySelector('input'))\n .filter((i) => i.checked)\n .map((i) => i.value);\n await api('PUT', '/api/providers', { enabled: ids }).catch(fail);\n await refresh();\n }),\n },\n 'Continue',\n );\n $main.replaceChildren(\n h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'Which clouds do you use?'),\n h('p', {}, 'Azure and Google Cloud are coming soon.'),\n h(\n 'div',\n { class: 'choices', style: 'max-width:320px;margin:0 auto 16px;text-align:left' },\n boxes,\n ),\n save,\n ),\n );\n}\n\nfunction sourceCard(source) {\n const aliases = state.aliases.filter((a) => a.source === source.id);\n const where = source.settings.startUrl ?? source.kindLabel;\n const head = h(\n 'div',\n { class: 'card-head' },\n h('h2', {}, source.id),\n h('span', { class: `badge ${source.session}` }, SESSION_TEXT[source.session] ?? source.session),\n h('span', { class: 'meta' }, `${source.providerName} \xB7 ${where}`),\n h('div', { class: 'spacer' }),\n h(\n 'div',\n { class: 'actions' },\n h('button', { onclick: () => openAliasPicker(source) }, '+ Aliases'),\n h(\n 'button',\n {\n title: 'Sign in again and refresh its accounts',\n onclick: (e) =>\n busy(e.currentTarget, async () => {\n toast('Opening the browser to sign in\u2026');\n const report = await api(\n 'POST',\n `/api/sources/${encodeURIComponent(source.id)}/login`,\n ).catch(fail);\n if (report)\n toast(`${source.id}: signed in, ${report.targets.length} account/role(s).`);\n await refresh();\n }),\n },\n 'Sign in',\n ),\n h(\n 'button',\n {\n title: 'Refresh the accounts and roles you have access to',\n onclick: (e) =>\n busy(e.currentTarget, async () => {\n const report = await api(\n 'POST',\n `/api/sources/${encodeURIComponent(source.id)}/sync`,\n ).catch(fail);\n if (report) toast(syncMessage(source.id, report));\n await refresh();\n }),\n },\n 'Sync',\n ),\n h(\n 'button',\n {\n class: 'danger',\n onclick: (e) => {\n const count = aliases.length;\n const message = `Remove \"${source.id}\"${count ? ` and its ${count} alias(es)` : ''}?`;\n if (!confirm(message)) return;\n busy(e.currentTarget, async () => {\n await api('DELETE', `/api/sources/${encodeURIComponent(source.id)}`).catch(fail);\n await refresh();\n });\n },\n },\n 'Remove',\n ),\n ),\n );\n\n const prodCount = aliases.filter((a) => a.prod).length;\n const body =\n aliases.length === 0\n ? h(\n 'div',\n { class: 'empty' },\n 'No aliases yet. ',\n h(\n 'button',\n { class: 'primary', onclick: () => openAliasPicker(source) },\n 'Pick accounts',\n ),\n )\n : h(\n 'div',\n { class: 'source-summary' },\n h(\n 'span',\n {},\n `${aliases.length} alias(es)`,\n prodCount ? h('span', { class: 'prod-tag' }, `${prodCount} PROD`) : null,\n ),\n h(\n 'button',\n { class: 'link', onclick: () => go('aliases', { source: source.id }) },\n 'View aliases \u2192',\n ),\n );\n return h('section', { class: 'card' }, head, h('div', { class: 'card-body' }, body));\n}\n\nfunction syncMessage(id, report) {\n const parts = [`${report.targets.length} available`];\n if (report.added) parts.push(`${report.added} new`);\n if (report.removed) parts.push(`${report.removed} removed`);\n const orphaned = report.orphanedAliases.length\n ? ` No longer available: ${report.orphanedAliases.join(', ')}.`\n : '';\n return `${id}: ${parts.join(', ')}.${orphaned}`;\n}\n\nfunction aliasRow(alias) {\n const input = h('input', {\n value: alias.region ?? '',\n 'aria-label': `Region for ${alias.name}`,\n class: 'mono',\n spellcheck: 'false',\n });\n const save = h('button', { hidden: true }, 'Save');\n const commit = () =>\n busy(save, async () => {\n const region = input.value.trim();\n const done = await api('PATCH', `/api/aliases/${encodeURIComponent(alias.name)}`, {\n region,\n }).catch(fail);\n if (done) toast(`${alias.name} now runs in ${region}.`);\n await refresh();\n });\n input.addEventListener('input', () => {\n save.hidden = input.value.trim() === (alias.region ?? '');\n });\n input.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' && !save.hidden) commit();\n if (e.key === 'Escape') {\n input.value = alias.region ?? '';\n save.hidden = true;\n }\n });\n save.addEventListener('click', commit);\n\n const patch = (body, message) =>\n api('PATCH', `/api/aliases/${encodeURIComponent(alias.name)}`, body)\n .then(() => message && toast(message))\n .catch(fail)\n .then(refresh);\n const iconButton = (label, title, onclick, extra = {}) =>\n h('button', { class: 'link', title, 'aria-label': title, onclick, ...extra }, label);\n\n const actions = h(\n 'div',\n { class: 'row-actions' },\n h(\n 'button',\n {\n title: `Open the AWS console as ${alias.name}, already signed in`,\n disabled: alias.missing,\n onclick: (e) =>\n busy(e.currentTarget, async () => {\n const result = await api(\n 'POST',\n `/api/aliases/${encodeURIComponent(alias.name)}/console`,\n {},\n ).catch(fail);\n if (result) {\n toast(\n result.via === 'signed-in'\n ? `Opened the console for ${alias.name}.`\n : result.via === 'portal'\n ? `Opened ${alias.name} through the SSO portal.`\n : `Opened the console sign-in page for ${alias.name}.`,\n );\n }\n }),\n },\n 'Console',\n ),\n h(\n 'label',\n { class: 'prod-toggle', title: 'Production: hop and AI assistants ask before any change' },\n h('input', {\n type: 'checkbox',\n checked: alias.prod,\n 'aria-label': `${alias.name} is production`,\n onchange: (e) =>\n patch(\n { prod: e.currentTarget.checked },\n e.currentTarget.checked\n ? `${alias.name} is now production.`\n : `${alias.name} is no longer production.`,\n ),\n }),\n 'Prod',\n ),\n iconButton('\u270E', `Rename ${alias.name}`, () => {\n const to = prompt(`New name for \"${alias.name}\"`, alias.name)?.trim();\n if (to && to !== alias.name) patch({ name: to }, `${alias.name} is now ${to}.`);\n }),\n iconButton(\n '\u2715',\n `Remove ${alias.name}`,\n (e) => {\n if (!confirm(`Remove alias \"${alias.name}\"?`)) return;\n busy(e.currentTarget, async () => {\n await api('DELETE', `/api/aliases/${encodeURIComponent(alias.name)}`).catch(fail);\n await refresh();\n });\n },\n { class: 'link danger-hover' },\n ),\n );\n\n return h(\n 'tr',\n { class: alias.prod ? 'prod' : undefined },\n h(\n 'td',\n { class: 'alias', title: `hop ${alias.name} <command>` },\n alias.name,\n alias.prod ? h('span', { class: 'prod-tag' }, 'PROD') : null,\n ),\n h('td', { class: 'muted' }, alias.source),\n h('td', {}, h('div', { class: 'region' }, input, save)),\n h(\n 'td',\n {},\n alias.missing\n ? h('span', { class: 'missing' }, `${alias.target} (no longer available)`)\n : alias.label,\n ),\n h('td', { style: 'text-align:right' }, actions),\n );\n}\n\n// ---------- dialogs ----------\n\nfunction showDialog(title, body, footer) {\n $dialog.replaceChildren(\n h(\n 'div',\n { class: 'dlg-head' },\n h('h2', {}, title),\n h('button', { class: 'link', 'aria-label': 'Close', onclick: () => $dialog.close() }, '\u2715'),\n ),\n h('div', { class: 'dlg-body' }, body),\n footer ? h('div', { class: 'dlg-foot' }, footer) : null,\n );\n if (!$dialog.open) $dialog.showModal();\n}\n\nfunction suggestSourceId(settings, kind) {\n const host = /^https:\\/\\/([^./]+)\\./.exec(settings.startUrl ?? '')?.[1];\n return slug(host ?? (kind === 'console-login' ? 'personal' : 'main'));\n}\n\nconst slug = (text) =>\n text\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 63) || 'main';\n\n/** `previous` refills the form after a failed sign-in: { provider, kind, id, settings, error }. */\nfunction openAddSource(previous) {\n const providers = state.providers.filter((p) => state.enabledProviders.includes(p.id));\n let provider = providers.find((p) => p.id === previous?.provider) ?? providers[0];\n let kind = provider.kinds.find((k) => k.kind === previous?.kind) ?? provider.kinds[0];\n\n const form = h('form', { onsubmit: (e) => e.preventDefault() });\n const nameInput = h('input', {\n id: 'f-name',\n required: true,\n class: 'mono',\n spellcheck: 'false',\n });\n let nameTouched = Boolean(previous);\n nameInput.addEventListener('input', () => {\n nameTouched = true;\n });\n\n const fieldInputs = {};\n const fieldsBox = h('div');\n const nameLabel = h('label', { for: 'f-name' });\n\n function updateSuggestion() {\n if (nameTouched) return;\n const settings = Object.fromEntries(\n Object.entries(fieldInputs).map(([k, i]) => [k, i.value.trim()]),\n );\n const taken = new Set(state.sources.map((s) => s.id));\n let id = suggestSourceId(settings, kind.kind);\n for (let i = 2; taken.has(id); i++) id = `${suggestSourceId(settings, kind.kind)}-${i}`;\n nameInput.value = id;\n }\n\n function renderFields() {\n for (const key of Object.keys(fieldInputs)) delete fieldInputs[key];\n fieldsBox.replaceChildren(\n ...kind.fields.map((f) => {\n const input = h('input', {\n id: `f-${f.key}`,\n placeholder: f.placeholder ?? f.defaultValue ?? '',\n value: previous?.settings?.[f.key] ?? f.defaultValue ?? '',\n required: true,\n class: 'mono',\n spellcheck: 'false',\n oninput: updateSuggestion,\n });\n fieldInputs[f.key] = input;\n return h('div', { class: 'field' }, h('label', { for: input.id }, f.label), input);\n }),\n );\n nameLabel.textContent = kind.nameLabel ?? 'Short name for this source';\n updateSuggestion();\n }\n\n const providerSelect =\n providers.length > 1\n ? h(\n 'div',\n { class: 'field' },\n h('label', { for: 'f-provider' }, 'Cloud'),\n h(\n 'select',\n {\n id: 'f-provider',\n onchange: (e) => {\n provider = providers.find((p) => p.id === e.target.value);\n kind = provider.kinds[0];\n renderKinds();\n },\n },\n providers.map((p) => h('option', { value: p.id, selected: p === provider }, p.name)),\n ),\n )\n : null;\n\n const kindsBox = h('div', { class: 'choices' });\n function renderKinds() {\n kindsBox.replaceChildren(\n ...provider.kinds.map((k) =>\n h(\n 'label',\n { class: 'choice' },\n h('input', {\n type: 'radio',\n name: 'kind',\n value: k.kind,\n checked: k === kind,\n onchange: () => {\n kind = k;\n renderFields();\n },\n }),\n h('span', {}, h('strong', {}, k.label), k.hint ? h('small', {}, k.hint) : null),\n ),\n ),\n );\n renderFields();\n }\n\n form.append(\n previous?.error\n ? h(\n 'div',\n { class: 'notice error' },\n h('strong', {}, 'Could not sign in, so nothing was saved. '),\n previous.error.message,\n previous.error.hint ? h('div', { class: 'help' }, previous.error.hint) : null,\n )\n : '',\n providerSelect ?? '',\n h('div', { class: 'field' }, h('label', {}, 'What do you want to add?')),\n kindsBox,\n fieldsBox,\n h(\n 'div',\n { class: 'field' },\n nameLabel,\n nameInput,\n h(\n 'span',\n { class: 'help' },\n 'Lowercase letters, numbers and dashes. You will see it in ',\n h('code', {}, 'cloudhop ls'),\n '.',\n ),\n ),\n );\n renderKinds();\n if (previous) nameInput.value = previous.id;\n\n const submit = h('button', { class: 'primary', type: 'submit' }, 'Add and sign in');\n form.addEventListener('submit', async () => {\n if (!form.reportValidity()) return;\n const body = {\n provider: provider.id,\n kind: kind.kind,\n id: nameInput.value.trim(),\n settings: Object.fromEntries(\n Object.entries(fieldInputs).map(([k, i]) => [k, i.value.trim()]),\n ),\n };\n showDialog(\n 'Sign in',\n h(\n 'div',\n { class: 'waiting' },\n h('div', { class: 'spinner' }),\n h('strong', {}, 'Finish signing in in the browser tab that just opened.'),\n h('p', { class: 'help' }, 'This page continues on its own once you approve the request.'),\n ),\n );\n try {\n const report = await api('POST', '/api/sources', body);\n await refresh();\n const source = state.sources.find((s) => s.id === body.id);\n toast(`Signed in to ${body.id}. ${report.targets.length} account/role(s) found.`);\n openAliasPicker(source, report.targets);\n } catch (error) {\n // Nothing was saved: reopen the form with what was typed so it can be fixed.\n await refresh();\n openAddSource({ ...body, error });\n }\n });\n\n showDialog('Add source', form, [\n h('button', { type: 'button', onclick: () => $dialog.close() }, 'Cancel'),\n submit,\n ]);\n submit.setAttribute('form', 'add-form');\n form.id = 'add-form';\n}\n\nasync function openAliasPicker(source, preloaded) {\n let targets = preloaded;\n if (!targets) {\n showDialog(\n `Aliases for ${source.id}`,\n h('div', { class: 'waiting' }, h('div', { class: 'spinner' }), 'Loading accounts\u2026'),\n );\n try {\n targets = (await api('GET', `/api/sources/${encodeURIComponent(source.id)}/targets`)).targets;\n } catch (error) {\n $dialog.close();\n return fail(error);\n }\n }\n\n const used = new Map(\n state.aliases.filter((a) => a.source === source.id).map((a) => [a.target, a.name]),\n );\n const taken = new Set(state.aliases.map((a) => a.name));\n const defaultRegion = source.settings.region ?? '';\n const filter = h('input', {\n type: 'search',\n placeholder: `Filter ${targets.length} account/role(s)\u2026`,\n style: 'width:100%;margin-bottom:12px',\n });\n\n const rows = targets.map((t) => {\n const existing = used.get(t.id);\n let name = t.suggestedAlias;\n for (let i = 2; taken.has(name); i++) name = `${t.suggestedAlias}-${i}`;\n const check = h('input', {\n type: 'checkbox',\n disabled: Boolean(existing),\n 'aria-label': t.label,\n });\n const aliasInput = h('input', {\n value: name,\n class: 'mono',\n spellcheck: 'false',\n 'aria-label': `Alias for ${t.label}`,\n placeholder: 'alias',\n });\n const regionInput = h('input', {\n value: defaultRegion,\n class: 'mono',\n spellcheck: 'false',\n 'aria-label': `Region for ${t.label}`,\n placeholder: 'region',\n });\n // Only the checkbox and its text sit in the label, so clicking the inputs never toggles it.\n const el = h(\n 'div',\n { class: 'target' },\n h(\n 'label',\n { class: 'pick' },\n check,\n h(\n 'span',\n {},\n t.label,\n existing ? h('small', { class: 'already' }, `Already: ${existing}`) : null,\n ),\n ),\n h('div', { class: 'inputs' }, aliasInput, regionInput),\n );\n return { target: t, el, check, aliasInput, regionInput };\n });\n\n filter.addEventListener('input', () => {\n const q = filter.value.trim().toLowerCase();\n for (const row of rows) row.el.hidden = q !== '' && !row.target.label.toLowerCase().includes(q);\n });\n\n const save = h('button', { class: 'primary' }, 'Create aliases');\n save.addEventListener('click', () =>\n busy(save, async () => {\n const aliases = rows\n .filter((r) => r.check.checked)\n .map((r) => ({\n source: source.id,\n target: r.target.id,\n name: r.aliasInput.value.trim(),\n region: r.regionInput.value.trim(),\n }));\n if (aliases.length === 0) return toast('Pick at least one account/role.', { error: true });\n try {\n const result = await api('POST', '/api/aliases', { aliases });\n $dialog.close();\n toast(`Created ${result.created.join(', ')}. Try: hop ${result.created[0]}`);\n await refresh();\n } catch (error) {\n fail(error);\n }\n }),\n );\n\n const body =\n targets.length === 0\n ? h('p', { class: 'empty' }, 'This source has no accounts or roles assigned to you.')\n : h(\n 'div',\n {},\n filter,\n h(\n 'div',\n { class: 'targets' },\n rows.map((r) => r.el),\n ),\n );\n showDialog(`Aliases for ${source.id}`, body, [\n h('span', { class: 'note' }, 'Check an account/role, then name its alias and region.'),\n h('button', { onclick: () => $dialog.close() }, 'Cancel'),\n targets.length ? save : null,\n ]);\n}\n\n$addSource.addEventListener('click', () => openAddSource());\n\nif (!token) {\n $main.replaceChildren(\n h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'Open the link from your terminal'),\n h('p', {}, 'Run cloudhop ui and open the link it prints. It includes a one-time access key.'),\n ),\n );\n} else {\n readRoute();\n refresh();\n}\n";
13480
14190
 
13481
14191
  // ../ui/src/assets/index.html
13482
14192
  var assets_default = `<!doctype html>
@@ -13877,18 +14587,18 @@ async function startUiServer({
13877
14587
  sendJson(res, 400, { error: error.message });
13878
14588
  }
13879
14589
  });
13880
- await new Promise((resolve, reject) => {
14590
+ await new Promise((resolve3, reject) => {
13881
14591
  server.once("error", reject);
13882
- server.listen(port, "127.0.0.1", resolve);
14592
+ server.listen(port, "127.0.0.1", resolve3);
13883
14593
  });
13884
14594
  const actualPort = server.address().port;
13885
14595
  allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${actualPort}`, `localhost:${actualPort}`]);
13886
14596
  return {
13887
14597
  // The token travels in the fragment, so it never reaches server logs or Referer headers.
13888
14598
  url: `http://127.0.0.1:${actualPort}/#token=${token}`,
13889
- close: () => new Promise((resolve) => {
14599
+ close: () => new Promise((resolve3) => {
13890
14600
  server.closeAllConnections();
13891
- server.close(() => resolve());
14601
+ server.close(() => resolve3());
13892
14602
  })
13893
14603
  };
13894
14604
  }
@@ -13904,23 +14614,23 @@ async function uiCommand(app, assistants, { flags }) {
13904
14614
  console.log("Only this computer can open it, and the link works only while this runs.");
13905
14615
  console.log("Press Ctrl-C to stop.");
13906
14616
  if (!flags.noOpen) await new NodeBrowserOpener().open(server.url);
13907
- await new Promise((resolve) => {
13908
- process.once("SIGINT", resolve);
13909
- process.once("SIGTERM", resolve);
14617
+ await new Promise((resolve3) => {
14618
+ process.once("SIGINT", resolve3);
14619
+ process.once("SIGTERM", resolve3);
13910
14620
  });
13911
14621
  await server.close();
13912
14622
  }
13913
14623
 
13914
14624
  // src/container.ts
13915
- import { homedir as homedir4 } from "os";
13916
- import { join as join8 } from "path";
14625
+ import { homedir as homedir5 } from "os";
14626
+ import { join as join10 } from "path";
13917
14627
 
13918
14628
  // ../provider-aws/src/aws-provider.ts
13919
- import { join as join7 } from "path";
14629
+ import { join as join9 } from "path";
13920
14630
 
13921
14631
  // ../provider-aws/src/aws-config.ts
13922
- import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
13923
- import { dirname as dirname4 } from "path";
14632
+ import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile7 } from "fs/promises";
14633
+ import { dirname as dirname6 } from "path";
13924
14634
  var BLOCK_START = "# >>> cloudhop managed >>> (edit with `cloudhop`, not by hand)";
13925
14635
  var BLOCK_END = "# <<< cloudhop managed <<<";
13926
14636
  function renderBlock(sections) {
@@ -13967,13 +14677,13 @@ function replaceManagedBlock(content, sections) {
13967
14677
  async function writeManagedBlock(path2, sections) {
13968
14678
  let content = "";
13969
14679
  try {
13970
- content = await readFile5(path2, "utf8");
14680
+ content = await readFile6(path2, "utf8");
13971
14681
  } catch {
13972
14682
  }
13973
14683
  const next = replaceManagedBlock(content, sections);
13974
14684
  if (next === content) return;
13975
- await mkdir4(dirname4(path2), { recursive: true, mode: 448 });
13976
- await writeFile4(path2, next, { mode: 384 });
14685
+ await mkdir5(dirname6(path2), { recursive: true, mode: 448 });
14686
+ await writeFile7(path2, next, { mode: 384 });
13977
14687
  }
13978
14688
 
13979
14689
  // ../provider-aws/src/console.ts
@@ -13998,17 +14708,17 @@ function consoleDestination(region, service) {
13998
14708
  async function federatedConsoleUrl(credentials, destination, region, fetchFn) {
13999
14709
  const {
14000
14710
  AWS_ACCESS_KEY_ID: id,
14001
- AWS_SECRET_ACCESS_KEY: key,
14711
+ AWS_SECRET_ACCESS_KEY: key2,
14002
14712
  AWS_SESSION_TOKEN: token
14003
14713
  } = credentials;
14004
- if (!id || !key || !token) return void 0;
14714
+ if (!id || !key2 || !token) return void 0;
14005
14715
  const { signin } = partitionHosts(region);
14006
14716
  const response = await fetchFn(`${signin}/federation`, {
14007
14717
  method: "POST",
14008
14718
  headers: { "content-type": "application/x-www-form-urlencoded" },
14009
14719
  body: new URLSearchParams({
14010
14720
  Action: "getSigninToken",
14011
- Session: JSON.stringify({ sessionId: id, sessionKey: key, sessionToken: token })
14721
+ Session: JSON.stringify({ sessionId: id, sessionKey: key2, sessionToken: token })
14012
14722
  }).toString()
14013
14723
  }).catch(() => void 0);
14014
14724
  if (!response?.ok) return void 0;
@@ -14024,7 +14734,9 @@ async function federatedConsoleUrl(credentials, destination, region, fetchFn) {
14024
14734
  }
14025
14735
  function portalShortcutUrl(startUrl, accountId, roleName, destination) {
14026
14736
  const params = new URLSearchParams({ account_id: accountId, role_name: roleName, destination });
14027
- return `${startUrl.replace(/\/+$/, "")}/#/console?${params}`;
14737
+ let base = startUrl;
14738
+ while (base.endsWith("/")) base = base.slice(0, -1);
14739
+ return `${base}/#/console?${params}`;
14028
14740
  }
14029
14741
 
14030
14742
  // ../provider-aws/src/http.ts
@@ -14089,7 +14801,7 @@ var PortalClient = class {
14089
14801
  );
14090
14802
  return body.roleCredentials;
14091
14803
  }
14092
- async paginate(path2, params, key) {
14804
+ async paginate(path2, params, key2) {
14093
14805
  const items = [];
14094
14806
  let nextToken;
14095
14807
  do {
@@ -14100,7 +14812,7 @@ var PortalClient = class {
14100
14812
  `${this.endpoint}${path2}?${query}`,
14101
14813
  this.headers()
14102
14814
  );
14103
- items.push(...page[key] ?? []);
14815
+ items.push(...page[key2] ?? []);
14104
14816
  nextToken = page.nextToken;
14105
14817
  } while (nextToken);
14106
14818
  return items;
@@ -14111,7 +14823,7 @@ var PortalClient = class {
14111
14823
  };
14112
14824
 
14113
14825
  // ../provider-aws/src/sso/sso-login.ts
14114
- import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
14826
+ import { createHash as createHash4, randomBytes as randomBytes2 } from "crypto";
14115
14827
 
14116
14828
  // ../provider-aws/src/sso/loopback.ts
14117
14829
  import { createServer as createServer2 } from "http";
@@ -14122,8 +14834,8 @@ var PAGE = (title, body) => `<!doctype html><meta charset="utf-8"><title>cloudho
14122
14834
  async function startLoopbackServer() {
14123
14835
  let resolveRequest = () => {
14124
14836
  };
14125
- const request = new Promise((resolve) => {
14126
- resolveRequest = resolve;
14837
+ const request = new Promise((resolve3) => {
14838
+ resolveRequest = resolve3;
14127
14839
  });
14128
14840
  const server = createServer2((req, res) => {
14129
14841
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -14138,7 +14850,7 @@ async function startLoopbackServer() {
14138
14850
  );
14139
14851
  resolveRequest(url);
14140
14852
  });
14141
- await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
14853
+ await new Promise((resolve3) => server.listen(0, "127.0.0.1", resolve3));
14142
14854
  const { port } = server.address();
14143
14855
  return {
14144
14856
  redirectUri: `http://127.0.0.1:${port}${CALLBACK_PATH}`,
@@ -14244,24 +14956,24 @@ var OidcClient = class {
14244
14956
  };
14245
14957
 
14246
14958
  // ../provider-aws/src/sso/token-cache.ts
14247
- import { createHash as createHash2 } from "crypto";
14248
- import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
14249
- import { join as join6 } from "path";
14959
+ import { createHash as createHash3 } from "crypto";
14960
+ import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
14961
+ import { join as join8 } from "path";
14250
14962
  var EXPIRY_MARGIN_MS = 5 * 60 * 1e3;
14251
14963
  function tokenCachePath(awsDir, sessionName) {
14252
- const hash = createHash2("sha1").update(sessionName).digest("hex");
14253
- return join6(awsDir, "sso", "cache", `${hash}.json`);
14964
+ const hash = createHash3("sha1").update(sessionName).digest("hex");
14965
+ return join8(awsDir, "sso", "cache", `${hash}.json`);
14254
14966
  }
14255
14967
  async function readToken(awsDir, sessionName) {
14256
14968
  try {
14257
- return JSON.parse(await readFile6(tokenCachePath(awsDir, sessionName), "utf8"));
14969
+ return JSON.parse(await readFile7(tokenCachePath(awsDir, sessionName), "utf8"));
14258
14970
  } catch {
14259
14971
  return void 0;
14260
14972
  }
14261
14973
  }
14262
14974
  async function writeToken(awsDir, sessionName, token) {
14263
- await mkdir5(join6(awsDir, "sso", "cache"), { recursive: true, mode: 448 });
14264
- await writeFile5(tokenCachePath(awsDir, sessionName), JSON.stringify(token, null, 2), {
14975
+ await mkdir6(join8(awsDir, "sso", "cache"), { recursive: true, mode: 448 });
14976
+ await writeFile8(tokenCachePath(awsDir, sessionName), JSON.stringify(token, null, 2), {
14265
14977
  mode: 384
14266
14978
  });
14267
14979
  }
@@ -14304,7 +15016,7 @@ async function authorizationCodeLogin(options) {
14304
15016
  clientId: client.clientId,
14305
15017
  redirectUri: server.redirectUri,
14306
15018
  state,
14307
- codeChallenge: base64url(createHash3("sha256").update(codeVerifier).digest())
15019
+ codeChallenge: base64url(createHash4("sha256").update(codeVerifier).digest())
14308
15020
  });
14309
15021
  options.interaction.notify("Opening your browser to log in\u2026");
14310
15022
  options.interaction.notify(`If it doesn't open, visit: ${url}`);
@@ -14330,7 +15042,7 @@ async function deviceCodeLogin(options) {
14330
15042
  let intervalMs = (device.interval ?? 5) * 1e3;
14331
15043
  const deadline = Date.now() + device.expiresIn * 1e3;
14332
15044
  while (Date.now() < deadline) {
14333
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
15045
+ await new Promise((resolve3) => setTimeout(resolve3, intervalMs));
14334
15046
  try {
14335
15047
  return toCachedToken(
14336
15048
  options,
@@ -14446,6 +15158,7 @@ var AwsProvider = class {
14446
15158
  displayName = "AWS";
14447
15159
  cli = { command: "aws", displayName: "AWS CLI v2", minVersion: "2.32.0" };
14448
15160
  identityArgs = ["sts", "get-caller-identity"];
15161
+ shellVars = [...CREDENTIAL_VARS, "AWS_PROFILE", "AWS_REGION", "AWS_DEFAULT_REGION"];
14449
15162
  sourceKinds = [
14450
15163
  {
14451
15164
  kind: SSO_KIND,
@@ -14554,7 +15267,7 @@ var AwsProvider = class {
14554
15267
  }
14555
15268
  });
14556
15269
  }
14557
- await writeManagedBlock(join7(this.options.awsDir, "config"), sections);
15270
+ await writeManagedBlock(join9(this.options.awsDir, "config"), sections);
14558
15271
  }
14559
15272
  async processEnv(resolved, mode) {
14560
15273
  const region = resolved.region;
@@ -14628,9 +15341,9 @@ var AwsProvider = class {
14628
15341
  if (!token) throw new CloudhopError("LOGIN_FAILED", `Not logged in to ${source.id}.`);
14629
15342
  return new PortalClient(this.setting(source, "region"), token.accessToken, this.fetchFn);
14630
15343
  }
14631
- setting(source, key) {
14632
- const value = source.settings[key];
14633
- if (!value) throw new CloudhopError("SOURCE_NOT_FOUND", `Source "${source.id}" has no ${key}.`);
15344
+ setting(source, key2) {
15345
+ const value = source.settings[key2];
15346
+ if (!value) throw new CloudhopError("SOURCE_NOT_FOUND", `Source "${source.id}" has no ${key2}.`);
14634
15347
  return value;
14635
15348
  }
14636
15349
  };
@@ -14639,7 +15352,7 @@ var AwsProvider = class {
14639
15352
  var browser = new NodeBrowserOpener();
14640
15353
  function createApp(env = process.env) {
14641
15354
  const runner = new NodeProcessRunner();
14642
- const awsDir = env.CLOUDHOP_AWS_DIR ?? join8(homedir4(), ".aws");
15355
+ const awsDir = env.CLOUDHOP_AWS_DIR ?? join10(homedir5(), ".aws");
14643
15356
  return new CloudhopApp({
14644
15357
  config: new YamlConfigStore(),
14645
15358
  providers: [
@@ -14658,33 +15371,39 @@ function createApp(env = process.env) {
14658
15371
  }
14659
15372
  function createAssistants(version, env = process.env) {
14660
15373
  return new AssistantsInstaller({
14661
- places: { home: homedir4(), project: process.cwd() },
15374
+ places: { home: homedir5(), project: process.cwd() },
14662
15375
  configDir: defaultConfigDir(env),
14663
15376
  runner: new NodeProcessRunner(),
14664
15377
  cloudhopVersion: version
14665
15378
  });
14666
15379
  }
15380
+ function createTrustedFolders(env = process.env) {
15381
+ return new TrustedFolders(defaultConfigDir(env));
15382
+ }
14667
15383
 
14668
15384
  // src/update-check.ts
14669
- import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile6 } from "fs/promises";
14670
- import { join as join9 } from "path";
15385
+ import { mkdir as mkdir7, readFile as readFile8, writeFile as writeFile9 } from "fs/promises";
15386
+ import { join as join11 } from "path";
14671
15387
  var DAY_MS = 24 * 60 * 60 * 1e3;
14672
15388
  var REGISTRY_URL = "https://registry.npmjs.org/cloudhop/latest";
15389
+ var VERSION = /^\d{1,6}\.\d{1,6}\.\d{1,6}(?:-[0-9A-Za-z.]{1,40})?$/;
15390
+ var validVersion = (value) => typeof value === "string" && VERSION.test(value) ? value : void 0;
14673
15391
  function startUpdateCheck(configDir, current) {
14674
15392
  const env = process.env;
14675
15393
  if (env.CLOUDHOP_NO_UPDATE_CHECK === "1" || env.CI || !process.stderr.isTTY)
14676
15394
  return async () => {
14677
15395
  };
14678
- const file = join9(configDir, "update-check.json");
15396
+ const file = join11(configDir, "update-check.json");
14679
15397
  const work = (async () => {
14680
15398
  let cache = { checkedAt: 0 };
14681
15399
  try {
14682
- cache = JSON.parse(await readFile7(file, "utf8"));
15400
+ cache = JSON.parse(await readFile8(file, "utf8"));
14683
15401
  } catch {
14684
15402
  }
14685
- if (cache.latest && compareVersions(cache.latest, current) > 0) {
15403
+ const cached = validVersion(cache.latest);
15404
+ if (cached && compareVersions(cached, current) > 0) {
14686
15405
  process.stderr.write(
14687
- `\x1B[2mcloudhop \u203A\x1B[22m cloudhop ${cache.latest} is available (you have ${current}): npm i -g cloudhop
15406
+ `\x1B[2mcloudhop \u203A\x1B[22m cloudhop ${cached} is available (you have ${current}): npm i -g cloudhop
14688
15407
  `
14689
15408
  );
14690
15409
  }
@@ -14694,19 +15413,24 @@ function startUpdateCheck(configDir, current) {
14694
15413
  signal: AbortSignal.timeout(1500)
14695
15414
  });
14696
15415
  if (!response.ok) return;
14697
- const { version } = await response.json();
14698
- if (typeof version !== "string") return;
14699
- await mkdir6(configDir, { recursive: true });
14700
- await writeFile6(file, JSON.stringify({ checkedAt: Date.now(), latest: version }));
15416
+ const latest = validVersion((await response.json()).version);
15417
+ if (!latest) return;
15418
+ await mkdir7(configDir, { recursive: true });
15419
+ await writeFile9(file, JSON.stringify({ checkedAt: Date.now(), latest }));
14701
15420
  })().catch(() => {
14702
15421
  });
14703
- return () => Promise.race([work, new Promise((resolve) => setTimeout(resolve, 300))]);
15422
+ return () => Promise.race([work, new Promise((resolve3) => setTimeout(resolve3, 300))]);
14704
15423
  }
14705
15424
 
14706
15425
  // src/cli.ts
14707
- var HELP = `cloudhop ${"1.2.1"}
15426
+ var HELP = `cloudhop ${"1.3.0"}
14708
15427
  Hop between cloud accounts, roles and regions with one command.
14709
15428
 
15429
+ Quick start
15430
+ cloudhop add Sign in to an SSO portal or personal account, name aliases
15431
+ cloudhop shell Set up your terminal (once)
15432
+ cloudhop use <alias> Switch this terminal to an alias, then type plain aws ...
15433
+
14710
15434
  Usage
14711
15435
  hop <alias> [args...] Run the cloud CLI as <alias> (no args: who am I)
14712
15436
  hop Pick the alias from a list, then type the command
@@ -14715,8 +15439,12 @@ Usage
14715
15439
  Open the web console as <alias>, already signed in
14716
15440
  cloudhop each <selection...> [--source <s>] [--prod|--no-prod] [--yes] [--json] -- <args...>
14717
15441
  Run one command in many aliases (names, a,b lists, 'globex-*', /regex/)
14718
- cloudhop use [alias] | --off Use an alias in this shell (aws, terraform... without hop)
14719
- cloudhop shell [install] Shell integration: cloudhop use, tab completion, cloudhop_prompt
15442
+ cloudhop use [alias] [--save] Use an alias in this terminal: then plain aws, terraform... run as it
15443
+ (--save: remember it for this folder in .cloudhoprc)
15444
+ cloudhop use --off [cloud] Stop using the alias of every cloud, or just one (aws)
15445
+ cloudhop allow | deny [folder] Trust (or stop trusting) a folder's .cloudhoprc
15446
+ cloudhop shell Set up your terminal once: cloudhop use, .cloudhoprc on cd, completion
15447
+ cloudhop shell starship Prompt segments for Starship (alias in use, PROD in red)
14720
15448
  cloudhop exec <alias> [--region <r>] -- <cmd...>
14721
15449
  Run any tool with the alias' temporary credentials
14722
15450
 
@@ -14784,7 +15512,7 @@ async function main(argv) {
14784
15512
  return 0;
14785
15513
  }
14786
15514
  if (command === "--version" || command === "-v" || command === "version") {
14787
- console.log("1.2.1");
15515
+ console.log("1.3.0");
14788
15516
  return 0;
14789
15517
  }
14790
15518
  const app = createApp();
@@ -14794,16 +15522,24 @@ async function main(argv) {
14794
15522
  return 0;
14795
15523
  }
14796
15524
  if (command === "shell") {
14797
- await shellCommand(local);
15525
+ const clis = app.availableProviders().map((p3) => ({ command: p3.cli.command, provider: p3.id }));
15526
+ await shellCommand(local, clis);
14798
15527
  return 0;
14799
15528
  }
15529
+ if (command === "use" && local.some((a2) => a2 === "--emit" || a2.startsWith("--emit="))) {
15530
+ return useCommand(app, createTrustedFolders(), local);
15531
+ }
14800
15532
  if (command === "mcp") {
14801
- await serveMcp({ app, version: "1.2.1" });
15533
+ await serveMcp({
15534
+ app,
15535
+ version: "1.3.0",
15536
+ context: () => shellContext(createTrustedFolders())
15537
+ });
14802
15538
  return 0;
14803
15539
  }
14804
- const assistants = createAssistants("1.2.1");
15540
+ const assistants = createAssistants("1.3.0");
14805
15541
  await refreshAssistantSkills(assistants, command);
14806
- const finishUpdateCheck = startUpdateCheck(defaultConfigDir(), "1.2.1");
15542
+ const finishUpdateCheck = startUpdateCheck(defaultConfigDir(), "1.3.0");
14807
15543
  try {
14808
15544
  return await dispatch(app, assistants, command, args, local);
14809
15545
  } finally {
@@ -14852,7 +15588,7 @@ async function dispatch(app, assistants, command, args, local) {
14852
15588
  return 0;
14853
15589
  case "ls":
14854
15590
  case "list":
14855
- await listCommand(app, local);
15591
+ await listCommand(app, local, () => shellContext(createTrustedFolders()));
14856
15592
  return 0;
14857
15593
  case "rm":
14858
15594
  case "remove":
@@ -14873,7 +15609,10 @@ async function dispatch(app, assistants, command, args, local) {
14873
15609
  await teamCommand(app, local);
14874
15610
  return 0;
14875
15611
  case "use":
14876
- await useCommand(app, local);
15612
+ return useCommand(app, createTrustedFolders(), local);
15613
+ case "allow":
15614
+ case "deny":
15615
+ await allowCommand(app, createTrustedFolders(), local, command === "allow");
14877
15616
  return 0;
14878
15617
  case "each":
14879
15618
  return eachCommand(app, args);