cloudhop 1.2.0 → 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 +1525 -752
  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 = {
@@ -8031,7 +7889,11 @@ ${result.stderr}`)) {
8031
7889
  * A URL that opens the web console as the alias, signing in first if the
8032
7890
  * session expired. The URL is a short-lived secret: open it, never print it.
8033
7891
  */
8034
- async consoleUrl(name, { service, region } = {}) {
7892
+ async consoleUrl(name, {
7893
+ service,
7894
+ region,
7895
+ interactive: interactive2 = true
7896
+ } = {}) {
8035
7897
  const resolved = await this.usableAlias(name, region ? { region } : {});
8036
7898
  const provider = this.provider(resolved.source.provider);
8037
7899
  if (!provider.consoleUrl) {
@@ -8047,7 +7909,7 @@ ${result.stderr}`)) {
8047
7909
  "Examples: s3, ec2, lambda, cloudwatch."
8048
7910
  );
8049
7911
  }
8050
- await this.ensureSession(resolved.source.id);
7912
+ await this.ensureSession(resolved.source.id, { interactive: interactive2 });
8051
7913
  return provider.consoleUrl(resolved, service);
8052
7914
  }
8053
7915
  /**
@@ -8074,6 +7936,29 @@ ${result.stderr}`)) {
8074
7936
  await this.ensureSession(resolved.source.id);
8075
7937
  return provider.processEnv(resolved, mode);
8076
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
+ }
8077
7962
  // ── Health ───────────────────────────────────────────────────────────────
8078
7963
  async doctor() {
8079
7964
  const config = await this.deps.config.load();
@@ -8197,11 +8082,260 @@ ${result.stderr}`)) {
8197
8082
  var PROVIDER_IDS = ["aws", "azure", "gcp"];
8198
8083
  var emptyConfig = () => ({ providers: [], sources: [], aliases: [] });
8199
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;
8328
+ }
8329
+ };
8330
+
8200
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";
8201
8335
  var import_yaml = __toESM(require_dist(), 1);
8202
8336
  function defaultConfigDir(env = process.env) {
8203
8337
  if (env.CLOUDHOP_HOME) return env.CLOUDHOP_HOME;
8204
- return join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "cloudhop");
8338
+ return join2(env.XDG_CONFIG_HOME || join2(homedir(), ".config"), "cloudhop");
8205
8339
  }
8206
8340
  var HEADER = "# cloudhop configuration. Contains no secrets.\n# Docs: https://github.com/dayepesb/cloudhop\n";
8207
8341
  var YamlConfigStore = class {
@@ -8210,10 +8344,10 @@ var YamlConfigStore = class {
8210
8344
  }
8211
8345
  dir;
8212
8346
  get configPath() {
8213
- return join(this.dir, "config.yaml");
8347
+ return join2(this.dir, "config.yaml");
8214
8348
  }
8215
8349
  get targetsPath() {
8216
- return join(this.dir, "targets.json");
8350
+ return join2(this.dir, "targets.json");
8217
8351
  }
8218
8352
  async load() {
8219
8353
  const raw = await this.read(this.configPath);
@@ -8261,7 +8395,7 @@ var YamlConfigStore = class {
8261
8395
  }
8262
8396
  async read(path2) {
8263
8397
  try {
8264
- return await readFile(path2, "utf8");
8398
+ return await readFile2(path2, "utf8");
8265
8399
  } catch (error) {
8266
8400
  if (error.code === "ENOENT") return void 0;
8267
8401
  throw error;
@@ -8269,9 +8403,9 @@ var YamlConfigStore = class {
8269
8403
  }
8270
8404
  /** Writes through a temp file so a crash never leaves a half-written config. */
8271
8405
  async write(path2, content) {
8272
- await mkdir(this.dir, { recursive: true, mode: 448 });
8406
+ await mkdir2(this.dir, { recursive: true, mode: 448 });
8273
8407
  const tmp = `${path2}.${process.pid}.tmp`;
8274
- await writeFile(tmp, content, { mode: 384 });
8408
+ await writeFile2(tmp, content, { mode: 384 });
8275
8409
  await rename(tmp, path2);
8276
8410
  }
8277
8411
  };
@@ -8284,17 +8418,17 @@ var MAX_OUTPUT = 1e5;
8284
8418
  var cap = (text4) => text4.length > MAX_OUTPUT ? `${text4.slice(0, MAX_OUTPUT)}
8285
8419
  \u2026 [truncated: ${text4.length - MAX_OUTPUT} more characters. Narrow the command with --query or filters.]` : text4;
8286
8420
  var WINDOWS_SHELL_CHARS = /[&|<>^%!"\r\n]/;
8287
- var text = (args, key) => {
8288
- const value = args[key];
8421
+ var text = (args, key2) => {
8422
+ const value = args[key2];
8289
8423
  if (typeof value !== "string" || !value.trim()) {
8290
- throw new CloudhopError("INVALID_NAME", `"${key}" is required.`);
8424
+ throw new CloudhopError("INVALID_NAME", `"${key2}" is required.`);
8291
8425
  }
8292
8426
  return value.trim();
8293
8427
  };
8294
- var stringArray = (args, key) => {
8295
- const value = args[key];
8428
+ var stringArray = (args, key2) => {
8429
+ const value = args[key2];
8296
8430
  if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) {
8297
- 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.`);
8298
8432
  }
8299
8433
  return value;
8300
8434
  };
@@ -8304,15 +8438,15 @@ var aliasInput = {
8304
8438
  description: "The cloudhop alias to use, as listed by list_aliases. Ask the user which one."
8305
8439
  }
8306
8440
  };
8307
- function createTools(app) {
8441
+ function createTools(app, context) {
8308
8442
  return [
8309
8443
  {
8310
8444
  name: "list_aliases",
8311
8445
  title: "List cloudhop aliases",
8312
- 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.",
8313
8447
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
8314
8448
  annotations: { readOnlyHint: true, openWorldHint: false },
8315
- run: () => app.overview()
8449
+ run: async () => ({ ...await app.overview(), ...await context?.() })
8316
8450
  },
8317
8451
  {
8318
8452
  name: "alias_status",
@@ -8337,16 +8471,45 @@ function createTools(app) {
8337
8471
  required: ["alias"],
8338
8472
  additionalProperties: false
8339
8473
  },
8340
- annotations: { readOnlyHint: true, openWorldHint: true },
8474
+ annotations: { readOnlyHint: true, openWorldHint: true },
8475
+ run: async (args) => {
8476
+ const alias = text(args, "alias");
8477
+ const result = await app.captureAsAlias(alias, await app.identityArgs(alias));
8478
+ return {
8479
+ alias,
8480
+ exitCode: result.exitCode,
8481
+ output: cap(result.stdout),
8482
+ errors: cap(result.stderr)
8483
+ };
8484
+ }
8485
+ },
8486
+ {
8487
+ name: "open_console",
8488
+ title: "Open the cloud console",
8489
+ description: "Opens the cloud's web console in the user's browser as the alias, already signed in, optionally on one service (AWS: s3, ec2, lambda, cloudwatch...). Returns how the user got in, never the sign-in URL. Use it when the user wants to see something in the console. Never opens a browser to sign in: an expired session returns LOGIN_REQUIRED.",
8490
+ inputSchema: {
8491
+ type: "object",
8492
+ properties: {
8493
+ ...aliasInput,
8494
+ service: { type: "string", description: 'Optional service page, e.g. "s3" or "ec2".' }
8495
+ },
8496
+ required: ["alias"],
8497
+ additionalProperties: false
8498
+ },
8499
+ annotations: {
8500
+ readOnlyHint: false,
8501
+ destructiveHint: false,
8502
+ idempotentHint: true,
8503
+ openWorldHint: true
8504
+ },
8341
8505
  run: async (args) => {
8342
8506
  const alias = text(args, "alias");
8343
- const result = await app.captureAsAlias(alias, await app.identityArgs(alias));
8344
- return {
8345
- alias,
8346
- exitCode: result.exitCode,
8347
- output: cap(result.stdout),
8348
- errors: cap(result.stderr)
8349
- };
8507
+ const service = typeof args.service === "string" && args.service.trim() ? args.service.trim() : void 0;
8508
+ const via = await app.openConsole(alias, {
8509
+ ...service ? { service } : {},
8510
+ interactive: false
8511
+ });
8512
+ return { alias, opened: true, via };
8350
8513
  }
8351
8514
  },
8352
8515
  {
@@ -8443,17 +8606,19 @@ function createTools(app) {
8443
8606
 
8444
8607
  // ../mcp/src/server.ts
8445
8608
  var MCP_INSTRUCTIONS = `cloudhop gives each cloud account + role + region a short alias and signs the user in only when a session expired.
8446
- 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.
8447
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.
8611
+ To show the user something in the web console, call open_console (it opens their browser, already signed in).
8448
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.`;
8449
8613
  var SUPPORTED_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
8450
8614
  function serveMcp({
8451
8615
  app,
8452
8616
  version,
8453
8617
  input = process.stdin,
8454
- output = process.stdout
8618
+ output = process.stdout,
8619
+ context
8455
8620
  }) {
8456
- const tools = createTools(app);
8621
+ const tools = createTools(app, context);
8457
8622
  const byName = new Map(tools.map((t2) => [t2.name, t2]));
8458
8623
  const send2 = (message) => output.write(`${JSON.stringify(message)}
8459
8624
  `);
@@ -8519,7 +8684,7 @@ function serveMcp({
8519
8684
  if (!isNotification) fail(id, -32601, `Method not found: ${method}`);
8520
8685
  }
8521
8686
  };
8522
- return new Promise((resolve) => {
8687
+ return new Promise((resolve3) => {
8523
8688
  const lines = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
8524
8689
  const pending = /* @__PURE__ */ new Set();
8525
8690
  lines.on("line", (line) => {
@@ -8539,7 +8704,7 @@ function serveMcp({
8539
8704
  task.finally(() => pending.delete(task));
8540
8705
  });
8541
8706
  lines.on("close", () => {
8542
- Promise.allSettled([...pending]).then(() => resolve());
8707
+ Promise.allSettled([...pending]).then(() => resolve3());
8543
8708
  });
8544
8709
  });
8545
8710
  }
@@ -8554,7 +8719,9 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
8554
8719
  "noMcp",
8555
8720
  "force",
8556
8721
  "prod",
8557
- "off"
8722
+ "off",
8723
+ "save",
8724
+ "leave"
8558
8725
  ]);
8559
8726
  var camel = (name) => name.replace(/-([a-z])/g, (_2, c3) => c3.toUpperCase());
8560
8727
  function parseArgs(argv) {
@@ -9975,7 +10142,7 @@ import { styleText as styleText2, stripVTControlCharacters } from "util";
9975
10142
  import process$1 from "process";
9976
10143
  var import_sisteransi2 = __toESM(require_src(), 1);
9977
10144
  import { existsSync, lstatSync, readdirSync } from "fs";
9978
- import { dirname, join as join2 } from "path";
10145
+ import { dirname as dirname2, join as join3 } from "path";
9979
10146
  function isUnicodeSupported() {
9980
10147
  if (process$1.platform !== "win32") {
9981
10148
  return process$1.env.TERM !== "linux";
@@ -10913,10 +11080,10 @@ var path = (e) => {
10913
11080
  return [];
10914
11081
  try {
10915
11082
  let i2;
10916
- 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);
10917
11084
  const c3 = t2.length > 1 && t2.endsWith("/") ? t2.slice(0, -1) : t2;
10918
11085
  return readdirSync(i2).map((r2) => {
10919
- const n3 = join2(i2, r2), m3 = lstatSync(n3);
11086
+ const n3 = join3(i2, r2), m3 = lstatSync(n3);
10920
11087
  return {
10921
11088
  name: r2,
10922
11089
  path: n3,
@@ -11673,16 +11840,16 @@ import { homedir as homedir2 } from "os";
11673
11840
  import { isAbsolute, relative } from "path";
11674
11841
 
11675
11842
  // ../assistants/src/installer.ts
11676
- import { createHash } from "crypto";
11843
+ import { createHash as createHash2 } from "crypto";
11677
11844
  import { existsSync as existsSync2 } from "fs";
11678
- import { mkdir as mkdir2, readFile as readFile2, rename as rename2, rm as rm2, rmdir, writeFile as writeFile2 } from "fs/promises";
11679
- 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";
11680
11847
 
11681
11848
  // ../assistants/src/assets/skill.md
11682
- 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]` opens it in\n the user's browser, already signed in. Never try to print or fetch its URL.\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`, `region`, `login` for\n another source) unless the user asked for it.\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";
11683
11850
 
11684
11851
  // ../assistants/src/targets.ts
11685
- import { join as join3 } from "path";
11852
+ import { join as join4 } from "path";
11686
11853
  var ASSISTANT_IDS = [
11687
11854
  "claude",
11688
11855
  "kiro",
@@ -11697,9 +11864,9 @@ var ASSISTANTS = {
11697
11864
  claude: {
11698
11865
  id: "claude",
11699
11866
  name: "Claude Code",
11700
- markers: ({ home }) => [join3(home, ".claude")],
11701
- skillDir: (scope, { home, project }) => join3(scope === "user" ? home : project, ".claude", "skills", SKILL_NAME),
11702
- 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 } : {
11703
11870
  kind: "cli",
11704
11871
  command: "claude",
11705
11872
  add: ["mcp", "add", "--scope", "user", "cloudhop", "--", MCP_COMMAND, ...MCP_ARGS],
@@ -11710,11 +11877,11 @@ var ASSISTANTS = {
11710
11877
  kiro: {
11711
11878
  id: "kiro",
11712
11879
  name: "Kiro",
11713
- markers: ({ home }) => [join3(home, ".kiro")],
11714
- 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),
11715
11882
  mcp: (scope, { home, project }) => ({
11716
11883
  kind: "json",
11717
- file: join3(scope === "user" ? home : project, ".kiro", "settings", "mcp.json"),
11884
+ file: join4(scope === "user" ? home : project, ".kiro", "settings", "mcp.json"),
11718
11885
  key: "mcpServers",
11719
11886
  withType: false
11720
11887
  })
@@ -11722,11 +11889,11 @@ var ASSISTANTS = {
11722
11889
  copilot: {
11723
11890
  id: "copilot",
11724
11891
  name: "GitHub Copilot (VS Code)",
11725
- markers: ({ home }) => [join3(home, ".copilot"), join3(home, ".vscode")],
11726
- 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),
11727
11894
  mcp: (scope, { project }) => scope === "project" ? {
11728
11895
  kind: "json",
11729
- file: join3(project, ".vscode", "mcp.json"),
11896
+ file: join4(project, ".vscode", "mcp.json"),
11730
11897
  key: "servers",
11731
11898
  withType: true
11732
11899
  } : {
@@ -11748,11 +11915,11 @@ var ASSISTANTS = {
11748
11915
  cursor: {
11749
11916
  id: "cursor",
11750
11917
  name: "Cursor",
11751
- markers: ({ home }) => [join3(home, ".cursor")],
11752
- 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),
11753
11920
  mcp: (scope, { home, project }) => ({
11754
11921
  kind: "json",
11755
- file: join3(scope === "user" ? home : project, ".cursor", "mcp.json"),
11922
+ file: join4(scope === "user" ? home : project, ".cursor", "mcp.json"),
11756
11923
  key: "mcpServers",
11757
11924
  withType: true
11758
11925
  })
@@ -11760,29 +11927,29 @@ var ASSISTANTS = {
11760
11927
  codex: {
11761
11928
  id: "codex",
11762
11929
  name: "Codex",
11763
- markers: ({ home }) => [join3(home, ".codex"), join3(home, ".agents")],
11764
- 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),
11765
11932
  mcp: (scope, { home, project }) => ({
11766
11933
  kind: "toml",
11767
- file: join3(scope === "user" ? home : project, ".codex", "config.toml")
11934
+ file: join4(scope === "user" ? home : project, ".codex", "config.toml")
11768
11935
  })
11769
11936
  }
11770
11937
  };
11771
11938
 
11772
11939
  // ../assistants/src/installer.ts
11773
- var sha = (text4) => createHash("sha256").update(text4).digest("hex");
11940
+ var sha = (text4) => createHash2("sha256").update(text4).digest("hex");
11774
11941
  async function writeAtomic(file, content) {
11775
- await mkdir2(dirname2(file), { recursive: true });
11942
+ await mkdir3(dirname3(file), { recursive: true });
11776
11943
  const tmp = `${file}.cloudhop-${process.pid}.tmp`;
11777
- await writeFile2(tmp, content);
11944
+ await writeFile3(tmp, content);
11778
11945
  await rename2(tmp, file);
11779
11946
  }
11780
11947
  async function removeFileAndEmptyDir(file) {
11781
11948
  await rm2(file, { force: true });
11782
- await rmdir(dirname2(file)).catch(() => {
11949
+ await rmdir(dirname3(file)).catch(() => {
11783
11950
  });
11784
11951
  }
11785
- var readText = (file) => readFile2(file, "utf8").catch(() => void 0);
11952
+ var readText = (file) => readFile3(file, "utf8").catch(() => void 0);
11786
11953
  var AssistantsInstaller = class {
11787
11954
  constructor(options) {
11788
11955
  this.options = options;
@@ -11806,7 +11973,7 @@ var AssistantsInstaller = class {
11806
11973
  return Promise.all(
11807
11974
  ASSISTANT_IDS.map(async (id) => {
11808
11975
  const assistant = ASSISTANTS[id];
11809
- const skillPath = join4(assistant.skillDir(scope, this.options.places), "SKILL.md");
11976
+ const skillPath = join5(assistant.skillDir(scope, this.options.places), "SKILL.md");
11810
11977
  const entry = manifest.find((e) => e.skillFile === skillPath);
11811
11978
  return {
11812
11979
  assistant: id,
@@ -11825,7 +11992,7 @@ var AssistantsInstaller = class {
11825
11992
  const manifest = await this.readManifest();
11826
11993
  for (const id of ids) {
11827
11994
  const assistant = ASSISTANTS[id];
11828
- const skillPath = join4(assistant.skillDir(scope, this.options.places), "SKILL.md");
11995
+ const skillPath = join5(assistant.skillDir(scope, this.options.places), "SKILL.md");
11829
11996
  const content = this.skillContent;
11830
11997
  await writeAtomic(skillPath, content);
11831
11998
  const kept = manifest.filter((e) => e.skillFile !== skillPath);
@@ -11850,9 +12017,9 @@ var AssistantsInstaller = class {
11850
12017
  for (const id of ids) {
11851
12018
  const assistant = ASSISTANTS[id];
11852
12019
  const dir = assistant.skillDir(scope, this.options.places);
11853
- const skillPath = join4(dir, "SKILL.md");
12020
+ const skillPath = join5(dir, "SKILL.md");
11854
12021
  await rm2(dir, { recursive: true, force: true });
11855
- await rmdir(dirname2(dir)).then(() => rmdir(dirname2(dirname2(dir)))).catch(() => {
12022
+ await rmdir(dirname3(dir)).then(() => rmdir(dirname3(dirname3(dir)))).catch(() => {
11856
12023
  });
11857
12024
  manifest = manifest.filter((e) => e.skillFile !== skillPath);
11858
12025
  const outcome = await this.removeMcp(assistant.mcp(scope, this.options.places));
@@ -12006,7 +12173,7 @@ args = [${MCP_ARGS.map((a2) => `"${a2}"`).join(", ")}]
12006
12173
  }
12007
12174
  // ───────────── manifest ─────────────
12008
12175
  get manifestPath() {
12009
- return join4(this.options.configDir, "assistants.json");
12176
+ return join5(this.options.configDir, "assistants.json");
12010
12177
  }
12011
12178
  async readManifest() {
12012
12179
  const text4 = await readText(this.manifestPath);
@@ -12237,9 +12404,16 @@ async function consoleCommand(app, args) {
12237
12404
  }
12238
12405
 
12239
12406
  // src/commands/each.ts
12407
+ var MAX_PATTERN = 200;
12240
12408
  function matcher(selector) {
12241
12409
  const regex = /^\/(.+)\/([a-z]*)$/.exec(selector);
12242
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
+ }
12243
12417
  try {
12244
12418
  const re = new RegExp(regex[1], regex[2]);
12245
12419
  return (name) => re.test(name);
@@ -12337,56 +12511,831 @@ ${listing}
12337
12511
  ))
12338
12512
  return 130;
12339
12513
  }
12340
- if (risky.length > 0 && process.env.CLOUDHOP_CONFIRM_PROD !== "1") {
12341
- 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)) {
12342
13164
  throw new CloudhopError(
12343
- "PROD_CONFIRMATION_REQUIRED",
12344
- `This may change resources in production: ${risky.join(", ")}.`,
12345
- "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.`
12346
13168
  );
12347
13169
  }
12348
- dist_exports.log.warn(
12349
- `${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)
12350
13177
  );
12351
- if (!answer(await dist_exports.confirm({ message: "Also run it in production?", initialValue: false })))
12352
- 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;
12353
13182
  }
12354
- for (const source of new Set(selected.map((a2) => a2.source))) {
12355
- 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);
12356
13192
  }
12357
- const parallel = Math.max(1, Math.min(16, Number(flags.parallel) || 4));
12358
- 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;
12359
13224
  try {
12360
- const r2 = await app.captureAsAlias(a2.name, cliArgs);
12361
- const outcome = { alias: a2.name, exitCode: r2.exitCode, stdout: r2.stdout, stderr: r2.stderr };
12362
- if (!flags.json) printOutcome(outcome, width);
12363
- return outcome;
13225
+ aliases = parseFolderFile(content);
12364
13226
  } catch (error) {
12365
- const outcome = { alias: a2.name, exitCode: 1, stdout: "", stderr: error.message };
12366
- if (!flags.json) printOutcome(outcome, width);
12367
- 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
+ }
12368
13241
  }
12369
- });
12370
- const failed = outcomes.filter((o2) => o2.exitCode !== 0);
12371
- if (flags.json) {
12372
- console.log(JSON.stringify({ version: 1, command: cliArgs, results: outcomes }, null, 2));
12373
- } else {
12374
- const summary = `${outcomes.length - failed.length} ok${failed.length ? `, ${failed.length} failed: ${failed.map((f2) => f2.alias).join(", ")}` : ""}`;
12375
- process.stderr.write(`${failed.length ? paint("red", summary) : paint("green", summary)}
12376
- `);
12377
13242
  }
12378
- 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;
12379
13257
  }
12380
- function printOutcome({ alias, exitCode, stdout: stdout2, stderr }, width) {
12381
- const tag = paint(exitCode === 0 ? "sky" : "red", `[${alias}]`.padEnd(width + 2));
12382
- for (const line of stdout2.replace(/\n$/, "").split("\n"))
12383
- if (line) process.stdout.write(`${tag} ${line}
12384
- `);
12385
- for (const line of stderr.replace(/\n$/, "").split("\n"))
12386
- if (line) process.stderr.write(`${tag} ${paint("dim", line)}
12387
- `);
12388
- if (exitCode !== 0) process.stderr.write(`${tag} ${paint("red", `exit ${exitCode}`)}
12389
- `);
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"));
12390
13339
  }
12391
13340
 
12392
13341
  // src/commands/manage.ts
@@ -12434,11 +13383,11 @@ var SESSION_LABEL = {
12434
13383
  };
12435
13384
  var JSON_VERSION = 1;
12436
13385
  var printJson = (value) => console.log(JSON.stringify({ version: JSON_VERSION, ...value }, null, 2));
12437
- async function listCommand(app, args = []) {
13386
+ async function listCommand(app, args = [], context) {
12438
13387
  const { positional, flags } = parseArgs(args);
12439
13388
  const sourceId = positional[0];
12440
13389
  if (flags.json) {
12441
- const overview = await app.overview();
13390
+ const overview = { ...await app.overview(), ...await context?.() };
12442
13391
  if (sourceId && !overview.sources.some((s) => s.id === sourceId)) {
12443
13392
  throw new CloudhopError("SOURCE_NOT_FOUND", `Source "${sourceId}" not found.`);
12444
13393
  }
@@ -12617,7 +13566,16 @@ async function statusCommand(app, args) {
12617
13566
  return;
12618
13567
  }
12619
13568
  const overview = await app.overview();
12620
- 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
+ );
12621
13579
  if (overview.sources.length === 0) {
12622
13580
  console.log("Nothing here yet. Run `cloudhop setup` to get started.");
12623
13581
  return;
@@ -12759,6 +13717,23 @@ async function setupCommand(app, assistants) {
12759
13717
  const addNow = answer(await dist_exports.confirm({ message: "Add an account or SSO portal now?" }));
12760
13718
  if (addNow) await addCommand(app, { positional: [], flags: {} });
12761
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
+ }
12762
13737
  const detected = assistants.detect();
12763
13738
  if (detected.length > 0) {
12764
13739
  const names = detected.map((id) => ASSISTANTS[id].name).join(", ");
@@ -12796,268 +13771,37 @@ async function doctorCommand(app) {
12796
13771
  );
12797
13772
  }
12798
13773
 
12799
- // src/commands/shell.ts
12800
- import { existsSync as existsSync3 } from "fs";
12801
- import { appendFile, mkdir as mkdir3, readFile as readFile3 } from "fs/promises";
12802
- import { homedir as homedir3 } from "os";
12803
- import { dirname as dirname3, join as join5 } from "path";
12804
- var SHELLS = ["zsh", "bash", "fish", "pwsh"];
12805
- var ALIAS_COMMANDS = "use status console region mv prod rm exec";
12806
- var SOURCE_COMMANDS = "alias login sync ls";
12807
- var SCRIPTS = {
12808
- zsh: `# cloudhop shell integration (zsh). Load it after compinit: eval "$(cloudhop shell zsh)"
12809
- cloudhop() {
12810
- if [ "$1" = "use" ]; then
12811
- local out; out="$(command cloudhop use --emit zsh "\${@:2}")" || return $?
12812
- eval "$out"
12813
- else
12814
- command cloudhop "$@"
12815
- fi
12816
- }
12817
- _cloudhop_words() { command cloudhop __complete "$1" 2>/dev/null }
12818
- _hop() { (( CURRENT == 2 )) && { local -a a; a=(\${(f)"$(_cloudhop_words aliases)"}); compadd -a a; } }
12819
- _cloudhop() {
12820
- local -a w
12821
- if (( CURRENT == 2 )); then w=(\${(f)"$(_cloudhop_words commands)"}); compadd -a w; return; fi
12822
- (( CURRENT == 3 )) || return
12823
- case " ${ALIAS_COMMANDS} " in *" $words[2] "*) w=(\${(f)"$(_cloudhop_words aliases)"});; esac
12824
- case " ${SOURCE_COMMANDS} " in *" $words[2] "*) w=(\${(f)"$(_cloudhop_words sources)"});; esac
12825
- compadd -a w
12826
- }
12827
- (( $+functions[compdef] )) && { compdef _hop hop; compdef _cloudhop cloudhop; }
12828
- # Add $(cloudhop_prompt) to your prompt to see the alias set with \`cloudhop use\`.
12829
- cloudhop_prompt() { [ -n "$CLOUDHOP_ALIAS" ] && printf '\u2601 %s%s' "$CLOUDHOP_ALIAS" "\${CLOUDHOP_PROD:+ PROD}"; }
12830
- `,
12831
- bash: `# cloudhop shell integration (bash): eval "$(cloudhop shell bash)"
12832
- cloudhop() {
12833
- if [ "$1" = "use" ]; then
12834
- local out; out="$(command cloudhop use --emit bash "\${@:2}")" || return $?
12835
- eval "$out"
12836
- else
12837
- command cloudhop "$@"
12838
- fi
12839
- }
12840
- _cloudhop_complete() {
12841
- local cur=\${COMP_WORDS[COMP_CWORD]} cmd=\${COMP_WORDS[0]##*/} kind=""
12842
- if [ "$cmd" = hop ]; then [ "$COMP_CWORD" -eq 1 ] && kind=aliases
12843
- elif [ "$COMP_CWORD" -eq 1 ]; then kind=commands
12844
- elif [ "$COMP_CWORD" -eq 2 ]; then
12845
- case " ${ALIAS_COMMANDS} " in *" \${COMP_WORDS[1]} "*) kind=aliases;; esac
12846
- case " ${SOURCE_COMMANDS} " in *" \${COMP_WORDS[1]} "*) kind=sources;; esac
12847
- fi
12848
- [ -n "$kind" ] && COMPREPLY=($(compgen -W "$(command cloudhop __complete $kind 2>/dev/null)" -- "$cur"))
12849
- }
12850
- complete -o default -F _cloudhop_complete hop cloudhop
12851
- # Add $(cloudhop_prompt) to PS1 to see the alias set with \`cloudhop use\`.
12852
- cloudhop_prompt() { [ -n "$CLOUDHOP_ALIAS" ] && printf '\u2601 %s%s' "$CLOUDHOP_ALIAS" "\${CLOUDHOP_PROD:+ PROD}"; }
12853
- `,
12854
- fish: `# cloudhop shell integration (fish): cloudhop shell fish | source
12855
- function cloudhop
12856
- if test "$argv[1]" = use
12857
- set -l out (command cloudhop use --emit fish $argv[2..-1]); or return $status
12858
- printf '%s\\n' $out | source
12859
- else
12860
- command cloudhop $argv
12861
- end
12862
- end
12863
- complete -c hop -f -n 'test (count (commandline -opc)) -eq 1' -a '(command cloudhop __complete aliases 2>/dev/null)'
12864
- complete -c cloudhop -f -n 'test (count (commandline -opc)) -eq 1' -a '(command cloudhop __complete commands 2>/dev/null)'
12865
- 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)'
12866
- 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)'
12867
- # Call cloudhop_prompt from fish_prompt to see the alias set with \`cloudhop use\`.
12868
- function cloudhop_prompt
12869
- test -n "$CLOUDHOP_ALIAS"; and printf '\u2601 %s%s' $CLOUDHOP_ALIAS (test -n "$CLOUDHOP_PROD"; and echo ' PROD')
12870
- end
12871
- `,
12872
- pwsh: `# cloudhop shell integration (PowerShell): cloudhop shell pwsh | Out-String | Invoke-Expression
12873
- $script:CloudhopExe = (Get-Command cloudhop -CommandType Application, ExternalScript | Select-Object -First 1).Source
12874
- function cloudhop {
12875
- if ($args.Count -gt 0 -and $args[0] -eq 'use') {
12876
- $out = & $script:CloudhopExe use --emit pwsh @($args | Select-Object -Skip 1)
12877
- if ($LASTEXITCODE -eq 0) { $out | Out-String | Invoke-Expression }
12878
- } else { & $script:CloudhopExe @args }
12879
- }
12880
- Register-ArgumentCompleter -Native -CommandName hop, cloudhop -ScriptBlock {
12881
- param($word, $ast)
12882
- $n = $ast.CommandElements.Count - [int]($word -eq '')
12883
- $kind = if ($ast.CommandElements[0].Value -match 'hop$' -and $ast.CommandElements[0].Value -notmatch 'cloudhop') { if ($n -le 1) { 'aliases' } }
12884
- elseif ($n -le 1) { 'commands' }
12885
- elseif ($n -eq 2 -and ' ${ALIAS_COMMANDS} ' -match " $($ast.CommandElements[1].Value) ") { 'aliases' }
12886
- elseif ($n -eq 2 -and ' ${SOURCE_COMMANDS} ' -match " $($ast.CommandElements[1].Value) ") { 'sources' }
12887
- if ($kind) { & $script:CloudhopExe __complete $kind | Where-Object { $_ -like "$word*" } }
12888
- }
12889
- function cloudhop_prompt { if ($env:CLOUDHOP_ALIAS) { "\u2601 $env:CLOUDHOP_ALIAS$(if ($env:CLOUDHOP_PROD) { ' PROD' })" } }
12890
- `
12891
- };
12892
- var quote = (shell, value) => shell === "pwsh" ? `'${value.replace(/'/g, "''")}'` : `'${value.replace(/'/g, "'\\''")}'`;
12893
- function shellExports(shell, env) {
12894
- return Object.entries(env).map(([key, value]) => {
12895
- if (value === void 0) {
12896
- if (shell === "fish") return `set -e ${key}`;
12897
- if (shell === "pwsh") return `Remove-Item Env:${key} -ErrorAction SilentlyContinue`;
12898
- return `unset ${key}`;
12899
- }
12900
- if (shell === "fish") return `set -gx ${key} ${quote(shell, value)}`;
12901
- if (shell === "pwsh") return `$env:${key} = ${quote(shell, value)}`;
12902
- return `export ${key}=${quote(shell, value)}`;
12903
- }).join("\n");
12904
- }
12905
- function detectShell() {
12906
- if (process.platform === "win32") return "pwsh";
12907
- const name = (process.env.SHELL ?? "").split("/").pop();
12908
- return SHELLS.includes(name) ? name : "bash";
12909
- }
12910
- function parseShell(value) {
12911
- if (!value) return detectShell();
12912
- const shell = value === "powershell" ? "pwsh" : value;
12913
- if (!SHELLS.includes(shell)) {
12914
- throw new CloudhopError(
12915
- "INVALID_NAME",
12916
- `Unknown shell "${value}".`,
12917
- `Use one of: ${SHELLS.join(", ")}.`
12918
- );
12919
- }
12920
- return shell;
12921
- }
12922
- var RC_FILES = {
12923
- zsh: (home) => join5(home, ".zshrc"),
12924
- bash: (home) => join5(home, ".bashrc"),
12925
- fish: (home) => join5(home, ".config", "fish", "config.fish"),
12926
- pwsh: (home) => process.platform === "win32" ? join5(home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1") : join5(home, ".config", "powershell", "Microsoft.PowerShell_profile.ps1")
12927
- };
12928
- var LOAD_LINE = {
12929
- zsh: 'eval "$(cloudhop shell zsh)"',
12930
- bash: 'eval "$(cloudhop shell bash)"',
12931
- fish: "cloudhop shell fish | source",
12932
- pwsh: "cloudhop shell pwsh | Out-String | Invoke-Expression"
12933
- };
12934
- async function shellCommand(args) {
12935
- const { positional, flags } = parseArgs(args);
12936
- if (positional[0] !== "install") {
12937
- process.stdout.write(SCRIPTS[parseShell(positional[0])]);
12938
- return;
12939
- }
12940
- const shell = parseShell(positional[1]);
12941
- const file = RC_FILES[shell](homedir3());
12942
- const line = LOAD_LINE[shell];
12943
- const current = existsSync3(file) ? await readFile3(file, "utf8") : "";
12944
- if (current.includes(line)) {
12945
- console.log(`Already set up in ${file.replace(homedir3(), "~")}.`);
12946
- return;
12947
- }
12948
- if (!flags.yes) {
12949
- if (!interactive())
12950
- throw new CloudhopError(
12951
- "NOT_INTERACTIVE",
12952
- `Add this line to ${file} yourself, or rerun with --yes:
12953
- ${line}`
12954
- );
12955
- const ok = answer(
12956
- await dist_exports.confirm({
12957
- message: `Add cloudhop to ${file.replace(homedir3(), "~")}?`,
12958
- initialValue: true
12959
- })
12960
- );
12961
- if (!ok) return;
12962
- }
12963
- await mkdir3(dirname3(file), { recursive: true });
12964
- const note2 = shell === "zsh" ? " # after compinit" : "";
12965
- await appendFile(
12966
- file,
12967
- `${current && !current.endsWith("\n") ? "\n" : ""}
12968
- # cloudhop: \`cloudhop use\`, tab completion and cloudhop_prompt${note2}
12969
- ${line}
12970
- `
12971
- );
12972
- console.log(
12973
- `${paint("green", "\u2714")} Added to ${file.replace(homedir3(), "~")}. Open a new terminal to use it.`
12974
- );
12975
- }
12976
- async function useCommand(app, args) {
12977
- const { positional, flags } = parseArgs(args);
12978
- const shell = flags.emit ? parseShell(flags.emit) : void 0;
12979
- if (!shell) {
12980
- throw new CloudhopError(
12981
- "NOT_INTERACTIVE",
12982
- "`cloudhop use` needs the shell integration to change your current shell.",
12983
- `Set it up once with \`cloudhop shell install\` (or add ${LOAD_LINE[detectShell()]} to your shell's startup file), then open a new terminal.`
12984
- );
12985
- }
12986
- const clear = {
12987
- AWS_ACCESS_KEY_ID: void 0,
12988
- AWS_SECRET_ACCESS_KEY: void 0,
12989
- AWS_SESSION_TOKEN: void 0
12990
- };
12991
- if (flags.off) {
12992
- process.stdout.write(
12993
- `${shellExports(shell, { ...clear, AWS_PROFILE: void 0, AWS_REGION: void 0, AWS_DEFAULT_REGION: void 0, CLOUDHOP_ALIAS: void 0, CLOUDHOP_PROD: void 0 })}
12994
- `
12995
- );
12996
- notify("No alias in this shell anymore.");
12997
- return;
12998
- }
12999
- const alias = positional[0] ?? await pickAlias(app, "Use which alias in this shell?");
13000
- const env = await app.envForAlias(alias, "cli");
13001
- const status = await app.aliasStatus(alias);
13002
- process.stdout.write(
13003
- `${shellExports(shell, { ...clear, ...env, CLOUDHOP_ALIAS: alias, CLOUDHOP_PROD: status.prod ? "1" : void 0 })}
13004
- `
13005
- );
13006
- notify(
13007
- `This shell now uses ${alias} (${status.label ?? status.target}, ${status.region ?? "no region"}).`
13008
- );
13009
- if (status.prod) {
13010
- notify(
13011
- paint(
13012
- "red",
13013
- `${alias} is production. Plain \`aws\` commands are not protected here: use \`hop ${alias}\` to be asked before changes.`
13014
- )
13015
- );
13016
- }
13017
- }
13018
- async function completeCommand(app, kind) {
13019
- if (kind === "commands") {
13020
- console.log(
13021
- [...RESERVED_NAMES, "each", "shell", "use"].filter((c3, i2, a2) => a2.indexOf(c3) === i2).sort().join("\n")
13022
- );
13023
- return;
13024
- }
13025
- const config = await app.config();
13026
- const words = kind === "sources" ? config.sources.map((s) => s.id) : config.aliases.map((a2) => a2.name);
13027
- if (words.length) console.log(words.join("\n"));
13028
- }
13029
-
13030
13774
  // src/commands/team.ts
13031
- import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
13775
+ import { readFile as readFile5, writeFile as writeFile6 } from "fs/promises";
13032
13776
  var import_yaml2 = __toESM(require_dist(), 1);
13033
13777
  var MAX_BYTES = 256 * 1024;
13034
13778
  var HEADER2 = `# cloudhop team setup: import it with \`cloudhop team import <this file or its https URL>\`.
13035
13779
  # No secrets: portal URLs, regions, account ids and role names only.
13036
13780
  `;
13037
13781
  function validate(app, raw) {
13038
- const bad = (why) => new CloudhopError("INVALID_NAME", `Not a cloudhop team file: ${why}.`);
13039
- 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");
13040
13784
  const file = raw;
13041
- if (file.version !== 1) throw bad('unknown "version" (expected 1)');
13785
+ if (file.version !== 1) throw bad2('unknown "version" (expected 1)');
13042
13786
  const sources = file.sources ?? {};
13043
13787
  const aliases = file.aliases ?? {};
13044
13788
  for (const [id, source] of Object.entries(sources)) {
13045
13789
  assertValidName("source", id);
13046
13790
  if (!PROVIDER_IDS.includes(source?.provider))
13047
- throw bad(`source "${id}" has an unknown provider`);
13791
+ throw bad2(`source "${id}" has an unknown provider`);
13048
13792
  const kind = app.provider(source.provider).sourceKinds.find((k) => k.kind === source.kind);
13049
- if (!kind) throw bad(`source "${id}" has an unknown kind`);
13793
+ if (!kind) throw bad2(`source "${id}" has an unknown kind`);
13050
13794
  for (const field of kind.fields) {
13051
13795
  const value = source.settings?.[field.key];
13052
- 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}`);
13053
13797
  const problem = field.validate?.(value);
13054
- if (problem) throw bad(`source "${id}": ${problem}`);
13798
+ if (problem) throw bad2(`source "${id}": ${problem}`);
13055
13799
  }
13056
13800
  }
13057
13801
  for (const [name, alias] of Object.entries(aliases)) {
13058
13802
  assertValidName("alias", name);
13059
13803
  if (!alias || typeof alias.target !== "string" || !sources[alias.source]) {
13060
- 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`);
13061
13805
  }
13062
13806
  }
13063
13807
  return { version: 1, sources, aliases };
@@ -13073,7 +13817,7 @@ async function load(location) {
13073
13817
  throw new CloudhopError("INVALID_NAME", "That file is too big for a team setup.");
13074
13818
  return text4;
13075
13819
  }
13076
- return readFile4(location, "utf8");
13820
+ return readFile5(location, "utf8");
13077
13821
  }
13078
13822
  async function teamCommand(app, args) {
13079
13823
  const { positional, flags } = parseArgs(args);
@@ -13094,7 +13838,7 @@ async function teamCommand(app, args) {
13094
13838
  };
13095
13839
  const text4 = HEADER2 + (0, import_yaml2.stringify)(file);
13096
13840
  if (flags.out) {
13097
- await writeFile3(flags.out, text4);
13841
+ await writeFile6(flags.out, text4);
13098
13842
  console.error(
13099
13843
  `${paint("green", "\u2714")} Wrote ${sources.length} source(s) and ${Object.keys(file.aliases).length} alias(es) to ${flags.out}.`
13100
13844
  );
@@ -13199,8 +13943,8 @@ async function teamCommand(app, args) {
13199
13943
  }
13200
13944
 
13201
13945
  // ../ui/src/api.ts
13202
- var text3 = (body, key) => {
13203
- const value = body[key];
13946
+ var text3 = (body, key2) => {
13947
+ const value = body[key2];
13204
13948
  return typeof value === "string" ? value.trim() : "";
13205
13949
  };
13206
13950
  var badRequest = (message) => new CloudhopError("INVALID_NAME", message);
@@ -13273,8 +14017,8 @@ function createApi(app, assistants) {
13273
14017
  label: kind.label,
13274
14018
  hint: kind.hint,
13275
14019
  nameLabel: kind.nameLabel,
13276
- fields: kind.fields.map(({ key, label, placeholder, defaultValue }) => ({
13277
- key,
14020
+ fields: kind.fields.map(({ key: key2, label, placeholder, defaultValue }) => ({
14021
+ key: key2,
13278
14022
  label,
13279
14023
  placeholder,
13280
14024
  defaultValue
@@ -13412,8 +14156,8 @@ function createApi(app, assistants) {
13412
14156
  return { removed: name };
13413
14157
  }
13414
14158
  };
13415
- const table = Object.entries(routes).map(([key, handler]) => {
13416
- const [method, pattern] = key.split(" ");
14159
+ const table = Object.entries(routes).map(([key2, handler]) => {
14160
+ const [method, pattern] = key2.split(" ");
13417
14161
  const regex = new RegExp(`^${pattern.replace(/:[a-z]+/g, "([^/]+)")}$`);
13418
14162
  return { method, regex, handler };
13419
14163
  });
@@ -13442,7 +14186,7 @@ import { randomBytes, timingSafeEqual } from "crypto";
13442
14186
  import { createServer } from "http";
13443
14187
 
13444
14188
  // ../ui/src/assets/app.js
13445
- 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";
13446
14190
 
13447
14191
  // ../ui/src/assets/index.html
13448
14192
  var assets_default = `<!doctype html>
@@ -13843,18 +14587,18 @@ async function startUiServer({
13843
14587
  sendJson(res, 400, { error: error.message });
13844
14588
  }
13845
14589
  });
13846
- await new Promise((resolve, reject) => {
14590
+ await new Promise((resolve3, reject) => {
13847
14591
  server.once("error", reject);
13848
- server.listen(port, "127.0.0.1", resolve);
14592
+ server.listen(port, "127.0.0.1", resolve3);
13849
14593
  });
13850
14594
  const actualPort = server.address().port;
13851
14595
  allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${actualPort}`, `localhost:${actualPort}`]);
13852
14596
  return {
13853
14597
  // The token travels in the fragment, so it never reaches server logs or Referer headers.
13854
14598
  url: `http://127.0.0.1:${actualPort}/#token=${token}`,
13855
- close: () => new Promise((resolve) => {
14599
+ close: () => new Promise((resolve3) => {
13856
14600
  server.closeAllConnections();
13857
- server.close(() => resolve());
14601
+ server.close(() => resolve3());
13858
14602
  })
13859
14603
  };
13860
14604
  }
@@ -13870,23 +14614,23 @@ async function uiCommand(app, assistants, { flags }) {
13870
14614
  console.log("Only this computer can open it, and the link works only while this runs.");
13871
14615
  console.log("Press Ctrl-C to stop.");
13872
14616
  if (!flags.noOpen) await new NodeBrowserOpener().open(server.url);
13873
- await new Promise((resolve) => {
13874
- process.once("SIGINT", resolve);
13875
- process.once("SIGTERM", resolve);
14617
+ await new Promise((resolve3) => {
14618
+ process.once("SIGINT", resolve3);
14619
+ process.once("SIGTERM", resolve3);
13876
14620
  });
13877
14621
  await server.close();
13878
14622
  }
13879
14623
 
13880
14624
  // src/container.ts
13881
- import { homedir as homedir4 } from "os";
13882
- import { join as join8 } from "path";
14625
+ import { homedir as homedir5 } from "os";
14626
+ import { join as join10 } from "path";
13883
14627
 
13884
14628
  // ../provider-aws/src/aws-provider.ts
13885
- import { join as join7 } from "path";
14629
+ import { join as join9 } from "path";
13886
14630
 
13887
14631
  // ../provider-aws/src/aws-config.ts
13888
- import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
13889
- 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";
13890
14634
  var BLOCK_START = "# >>> cloudhop managed >>> (edit with `cloudhop`, not by hand)";
13891
14635
  var BLOCK_END = "# <<< cloudhop managed <<<";
13892
14636
  function renderBlock(sections) {
@@ -13933,13 +14677,13 @@ function replaceManagedBlock(content, sections) {
13933
14677
  async function writeManagedBlock(path2, sections) {
13934
14678
  let content = "";
13935
14679
  try {
13936
- content = await readFile5(path2, "utf8");
14680
+ content = await readFile6(path2, "utf8");
13937
14681
  } catch {
13938
14682
  }
13939
14683
  const next = replaceManagedBlock(content, sections);
13940
14684
  if (next === content) return;
13941
- await mkdir4(dirname4(path2), { recursive: true, mode: 448 });
13942
- await writeFile4(path2, next, { mode: 384 });
14685
+ await mkdir5(dirname6(path2), { recursive: true, mode: 448 });
14686
+ await writeFile7(path2, next, { mode: 384 });
13943
14687
  }
13944
14688
 
13945
14689
  // ../provider-aws/src/console.ts
@@ -13964,17 +14708,17 @@ function consoleDestination(region, service) {
13964
14708
  async function federatedConsoleUrl(credentials, destination, region, fetchFn) {
13965
14709
  const {
13966
14710
  AWS_ACCESS_KEY_ID: id,
13967
- AWS_SECRET_ACCESS_KEY: key,
14711
+ AWS_SECRET_ACCESS_KEY: key2,
13968
14712
  AWS_SESSION_TOKEN: token
13969
14713
  } = credentials;
13970
- if (!id || !key || !token) return void 0;
14714
+ if (!id || !key2 || !token) return void 0;
13971
14715
  const { signin } = partitionHosts(region);
13972
14716
  const response = await fetchFn(`${signin}/federation`, {
13973
14717
  method: "POST",
13974
14718
  headers: { "content-type": "application/x-www-form-urlencoded" },
13975
14719
  body: new URLSearchParams({
13976
14720
  Action: "getSigninToken",
13977
- Session: JSON.stringify({ sessionId: id, sessionKey: key, sessionToken: token })
14721
+ Session: JSON.stringify({ sessionId: id, sessionKey: key2, sessionToken: token })
13978
14722
  }).toString()
13979
14723
  }).catch(() => void 0);
13980
14724
  if (!response?.ok) return void 0;
@@ -13990,7 +14734,9 @@ async function federatedConsoleUrl(credentials, destination, region, fetchFn) {
13990
14734
  }
13991
14735
  function portalShortcutUrl(startUrl, accountId, roleName, destination) {
13992
14736
  const params = new URLSearchParams({ account_id: accountId, role_name: roleName, destination });
13993
- return `${startUrl.replace(/\/+$/, "")}/#/console?${params}`;
14737
+ let base = startUrl;
14738
+ while (base.endsWith("/")) base = base.slice(0, -1);
14739
+ return `${base}/#/console?${params}`;
13994
14740
  }
13995
14741
 
13996
14742
  // ../provider-aws/src/http.ts
@@ -14055,7 +14801,7 @@ var PortalClient = class {
14055
14801
  );
14056
14802
  return body.roleCredentials;
14057
14803
  }
14058
- async paginate(path2, params, key) {
14804
+ async paginate(path2, params, key2) {
14059
14805
  const items = [];
14060
14806
  let nextToken;
14061
14807
  do {
@@ -14066,7 +14812,7 @@ var PortalClient = class {
14066
14812
  `${this.endpoint}${path2}?${query}`,
14067
14813
  this.headers()
14068
14814
  );
14069
- items.push(...page[key] ?? []);
14815
+ items.push(...page[key2] ?? []);
14070
14816
  nextToken = page.nextToken;
14071
14817
  } while (nextToken);
14072
14818
  return items;
@@ -14077,7 +14823,7 @@ var PortalClient = class {
14077
14823
  };
14078
14824
 
14079
14825
  // ../provider-aws/src/sso/sso-login.ts
14080
- import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
14826
+ import { createHash as createHash4, randomBytes as randomBytes2 } from "crypto";
14081
14827
 
14082
14828
  // ../provider-aws/src/sso/loopback.ts
14083
14829
  import { createServer as createServer2 } from "http";
@@ -14088,8 +14834,8 @@ var PAGE = (title, body) => `<!doctype html><meta charset="utf-8"><title>cloudho
14088
14834
  async function startLoopbackServer() {
14089
14835
  let resolveRequest = () => {
14090
14836
  };
14091
- const request = new Promise((resolve) => {
14092
- resolveRequest = resolve;
14837
+ const request = new Promise((resolve3) => {
14838
+ resolveRequest = resolve3;
14093
14839
  });
14094
14840
  const server = createServer2((req, res) => {
14095
14841
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -14104,7 +14850,7 @@ async function startLoopbackServer() {
14104
14850
  );
14105
14851
  resolveRequest(url);
14106
14852
  });
14107
- 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));
14108
14854
  const { port } = server.address();
14109
14855
  return {
14110
14856
  redirectUri: `http://127.0.0.1:${port}${CALLBACK_PATH}`,
@@ -14210,24 +14956,24 @@ var OidcClient = class {
14210
14956
  };
14211
14957
 
14212
14958
  // ../provider-aws/src/sso/token-cache.ts
14213
- import { createHash as createHash2 } from "crypto";
14214
- import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
14215
- 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";
14216
14962
  var EXPIRY_MARGIN_MS = 5 * 60 * 1e3;
14217
14963
  function tokenCachePath(awsDir, sessionName) {
14218
- const hash = createHash2("sha1").update(sessionName).digest("hex");
14219
- return join6(awsDir, "sso", "cache", `${hash}.json`);
14964
+ const hash = createHash3("sha1").update(sessionName).digest("hex");
14965
+ return join8(awsDir, "sso", "cache", `${hash}.json`);
14220
14966
  }
14221
14967
  async function readToken(awsDir, sessionName) {
14222
14968
  try {
14223
- return JSON.parse(await readFile6(tokenCachePath(awsDir, sessionName), "utf8"));
14969
+ return JSON.parse(await readFile7(tokenCachePath(awsDir, sessionName), "utf8"));
14224
14970
  } catch {
14225
14971
  return void 0;
14226
14972
  }
14227
14973
  }
14228
14974
  async function writeToken(awsDir, sessionName, token) {
14229
- await mkdir5(join6(awsDir, "sso", "cache"), { recursive: true, mode: 448 });
14230
- 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), {
14231
14977
  mode: 384
14232
14978
  });
14233
14979
  }
@@ -14270,7 +15016,7 @@ async function authorizationCodeLogin(options) {
14270
15016
  clientId: client.clientId,
14271
15017
  redirectUri: server.redirectUri,
14272
15018
  state,
14273
- codeChallenge: base64url(createHash3("sha256").update(codeVerifier).digest())
15019
+ codeChallenge: base64url(createHash4("sha256").update(codeVerifier).digest())
14274
15020
  });
14275
15021
  options.interaction.notify("Opening your browser to log in\u2026");
14276
15022
  options.interaction.notify(`If it doesn't open, visit: ${url}`);
@@ -14296,7 +15042,7 @@ async function deviceCodeLogin(options) {
14296
15042
  let intervalMs = (device.interval ?? 5) * 1e3;
14297
15043
  const deadline = Date.now() + device.expiresIn * 1e3;
14298
15044
  while (Date.now() < deadline) {
14299
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
15045
+ await new Promise((resolve3) => setTimeout(resolve3, intervalMs));
14300
15046
  try {
14301
15047
  return toCachedToken(
14302
15048
  options,
@@ -14412,6 +15158,7 @@ var AwsProvider = class {
14412
15158
  displayName = "AWS";
14413
15159
  cli = { command: "aws", displayName: "AWS CLI v2", minVersion: "2.32.0" };
14414
15160
  identityArgs = ["sts", "get-caller-identity"];
15161
+ shellVars = [...CREDENTIAL_VARS, "AWS_PROFILE", "AWS_REGION", "AWS_DEFAULT_REGION"];
14415
15162
  sourceKinds = [
14416
15163
  {
14417
15164
  kind: SSO_KIND,
@@ -14520,7 +15267,7 @@ var AwsProvider = class {
14520
15267
  }
14521
15268
  });
14522
15269
  }
14523
- await writeManagedBlock(join7(this.options.awsDir, "config"), sections);
15270
+ await writeManagedBlock(join9(this.options.awsDir, "config"), sections);
14524
15271
  }
14525
15272
  async processEnv(resolved, mode) {
14526
15273
  const region = resolved.region;
@@ -14594,9 +15341,9 @@ var AwsProvider = class {
14594
15341
  if (!token) throw new CloudhopError("LOGIN_FAILED", `Not logged in to ${source.id}.`);
14595
15342
  return new PortalClient(this.setting(source, "region"), token.accessToken, this.fetchFn);
14596
15343
  }
14597
- setting(source, key) {
14598
- const value = source.settings[key];
14599
- 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}.`);
14600
15347
  return value;
14601
15348
  }
14602
15349
  };
@@ -14605,7 +15352,7 @@ var AwsProvider = class {
14605
15352
  var browser = new NodeBrowserOpener();
14606
15353
  function createApp(env = process.env) {
14607
15354
  const runner = new NodeProcessRunner();
14608
- const awsDir = env.CLOUDHOP_AWS_DIR ?? join8(homedir4(), ".aws");
15355
+ const awsDir = env.CLOUDHOP_AWS_DIR ?? join10(homedir5(), ".aws");
14609
15356
  return new CloudhopApp({
14610
15357
  config: new YamlConfigStore(),
14611
15358
  providers: [
@@ -14624,33 +15371,39 @@ function createApp(env = process.env) {
14624
15371
  }
14625
15372
  function createAssistants(version, env = process.env) {
14626
15373
  return new AssistantsInstaller({
14627
- places: { home: homedir4(), project: process.cwd() },
15374
+ places: { home: homedir5(), project: process.cwd() },
14628
15375
  configDir: defaultConfigDir(env),
14629
15376
  runner: new NodeProcessRunner(),
14630
15377
  cloudhopVersion: version
14631
15378
  });
14632
15379
  }
15380
+ function createTrustedFolders(env = process.env) {
15381
+ return new TrustedFolders(defaultConfigDir(env));
15382
+ }
14633
15383
 
14634
15384
  // src/update-check.ts
14635
- import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile6 } from "fs/promises";
14636
- 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";
14637
15387
  var DAY_MS = 24 * 60 * 60 * 1e3;
14638
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;
14639
15391
  function startUpdateCheck(configDir, current) {
14640
15392
  const env = process.env;
14641
15393
  if (env.CLOUDHOP_NO_UPDATE_CHECK === "1" || env.CI || !process.stderr.isTTY)
14642
15394
  return async () => {
14643
15395
  };
14644
- const file = join9(configDir, "update-check.json");
15396
+ const file = join11(configDir, "update-check.json");
14645
15397
  const work = (async () => {
14646
15398
  let cache = { checkedAt: 0 };
14647
15399
  try {
14648
- cache = JSON.parse(await readFile7(file, "utf8"));
15400
+ cache = JSON.parse(await readFile8(file, "utf8"));
14649
15401
  } catch {
14650
15402
  }
14651
- if (cache.latest && compareVersions(cache.latest, current) > 0) {
15403
+ const cached = validVersion(cache.latest);
15404
+ if (cached && compareVersions(cached, current) > 0) {
14652
15405
  process.stderr.write(
14653
- `\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
14654
15407
  `
14655
15408
  );
14656
15409
  }
@@ -14660,19 +15413,24 @@ function startUpdateCheck(configDir, current) {
14660
15413
  signal: AbortSignal.timeout(1500)
14661
15414
  });
14662
15415
  if (!response.ok) return;
14663
- const { version } = await response.json();
14664
- if (typeof version !== "string") return;
14665
- await mkdir6(configDir, { recursive: true });
14666
- 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 }));
14667
15420
  })().catch(() => {
14668
15421
  });
14669
- return () => Promise.race([work, new Promise((resolve) => setTimeout(resolve, 300))]);
15422
+ return () => Promise.race([work, new Promise((resolve3) => setTimeout(resolve3, 300))]);
14670
15423
  }
14671
15424
 
14672
15425
  // src/cli.ts
14673
- var HELP = `cloudhop ${"1.2.0"}
15426
+ var HELP = `cloudhop ${"1.3.0"}
14674
15427
  Hop between cloud accounts, roles and regions with one command.
14675
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
+
14676
15434
  Usage
14677
15435
  hop <alias> [args...] Run the cloud CLI as <alias> (no args: who am I)
14678
15436
  hop Pick the alias from a list, then type the command
@@ -14681,8 +15439,12 @@ Usage
14681
15439
  Open the web console as <alias>, already signed in
14682
15440
  cloudhop each <selection...> [--source <s>] [--prod|--no-prod] [--yes] [--json] -- <args...>
14683
15441
  Run one command in many aliases (names, a,b lists, 'globex-*', /regex/)
14684
- cloudhop use [alias] | --off Use an alias in this shell (aws, terraform... without hop)
14685
- 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)
14686
15448
  cloudhop exec <alias> [--region <r>] -- <cmd...>
14687
15449
  Run any tool with the alias' temporary credentials
14688
15450
 
@@ -14750,7 +15512,7 @@ async function main(argv) {
14750
15512
  return 0;
14751
15513
  }
14752
15514
  if (command === "--version" || command === "-v" || command === "version") {
14753
- console.log("1.2.0");
15515
+ console.log("1.3.0");
14754
15516
  return 0;
14755
15517
  }
14756
15518
  const app = createApp();
@@ -14760,16 +15522,24 @@ async function main(argv) {
14760
15522
  return 0;
14761
15523
  }
14762
15524
  if (command === "shell") {
14763
- await shellCommand(local);
15525
+ const clis = app.availableProviders().map((p3) => ({ command: p3.cli.command, provider: p3.id }));
15526
+ await shellCommand(local, clis);
14764
15527
  return 0;
14765
15528
  }
15529
+ if (command === "use" && local.some((a2) => a2 === "--emit" || a2.startsWith("--emit="))) {
15530
+ return useCommand(app, createTrustedFolders(), local);
15531
+ }
14766
15532
  if (command === "mcp") {
14767
- await serveMcp({ app, version: "1.2.0" });
15533
+ await serveMcp({
15534
+ app,
15535
+ version: "1.3.0",
15536
+ context: () => shellContext(createTrustedFolders())
15537
+ });
14768
15538
  return 0;
14769
15539
  }
14770
- const assistants = createAssistants("1.2.0");
15540
+ const assistants = createAssistants("1.3.0");
14771
15541
  await refreshAssistantSkills(assistants, command);
14772
- const finishUpdateCheck = startUpdateCheck(defaultConfigDir(), "1.2.0");
15542
+ const finishUpdateCheck = startUpdateCheck(defaultConfigDir(), "1.3.0");
14773
15543
  try {
14774
15544
  return await dispatch(app, assistants, command, args, local);
14775
15545
  } finally {
@@ -14818,7 +15588,7 @@ async function dispatch(app, assistants, command, args, local) {
14818
15588
  return 0;
14819
15589
  case "ls":
14820
15590
  case "list":
14821
- await listCommand(app, local);
15591
+ await listCommand(app, local, () => shellContext(createTrustedFolders()));
14822
15592
  return 0;
14823
15593
  case "rm":
14824
15594
  case "remove":
@@ -14839,7 +15609,10 @@ async function dispatch(app, assistants, command, args, local) {
14839
15609
  await teamCommand(app, local);
14840
15610
  return 0;
14841
15611
  case "use":
14842
- await useCommand(app, local);
15612
+ return useCommand(app, createTrustedFolders(), local);
15613
+ case "allow":
15614
+ case "deny":
15615
+ await allowCommand(app, createTrustedFolders(), local, command === "allow");
14843
15616
  return 0;
14844
15617
  case "each":
14845
15618
  return eachCommand(app, args);