cloudhop 1.2.1 → 1.3.1

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 +1582 -743
  3. package/package.json +3 -3
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 = {
@@ -7930,6 +7788,7 @@ var CloudhopApp = class {
7930
7788
  region: resolved.region,
7931
7789
  attributes: resolved.target?.attributes ?? {},
7932
7790
  prod: alias.prod === true,
7791
+ default: alias.default === true,
7933
7792
  session: source.session,
7934
7793
  next: source.next
7935
7794
  };
@@ -7974,6 +7833,38 @@ var CloudhopApp = class {
7974
7833
  }
7975
7834
  await this.deps.config.save(config);
7976
7835
  }
7836
+ /**
7837
+ * Makes an alias the one new terminals of its cloud start with, replacing
7838
+ * that cloud's previous default. Returns the cloud.
7839
+ */
7840
+ async setDefaultAlias(name) {
7841
+ const config = await this.deps.config.load();
7842
+ const alias = this.findAlias(config, name);
7843
+ const provider = this.findSource(config, alias.source).provider;
7844
+ for (const other of config.aliases) {
7845
+ if (this.findSource(config, other.source).provider === provider) delete other.default;
7846
+ }
7847
+ alias.default = true;
7848
+ await this.deps.config.save(config);
7849
+ return provider;
7850
+ }
7851
+ /** Clears the default alias of a cloud, or of every cloud. Returns the aliases it cleared. */
7852
+ async clearDefaultAlias(provider) {
7853
+ const config = await this.deps.config.load();
7854
+ const cleared = config.aliases.filter(
7855
+ (a2) => a2.default && (!provider || this.findSource(config, a2.source).provider === provider)
7856
+ );
7857
+ for (const alias of cleared) delete alias.default;
7858
+ if (cleared.length) await this.deps.config.save(config);
7859
+ return cleared.map((a2) => a2.name);
7860
+ }
7861
+ /** The alias new terminals start with, per cloud. */
7862
+ async defaultAliases() {
7863
+ const config = await this.deps.config.load();
7864
+ return Object.fromEntries(
7865
+ config.aliases.filter((a2) => a2.default).map((a2) => [this.findSource(config, a2.source).provider, a2.name])
7866
+ );
7867
+ }
7977
7868
  /** Changes an alias' region; matching the source's region clears the override. */
7978
7869
  async setAliasRegion(name, region) {
7979
7870
  const config = await this.deps.config.load();
@@ -8078,6 +7969,29 @@ ${result.stderr}`)) {
8078
7969
  await this.ensureSession(resolved.source.id);
8079
7970
  return provider.processEnv(resolved, mode);
8080
7971
  }
7972
+ /**
7973
+ * What `cloudhop use` puts in a shell: the alias' CLI environment (profile
7974
+ * and region, never credentials) and every variable of that provider to
7975
+ * clear first. With `login: false` it never signs in, so entering a folder
7976
+ * never opens a browser: the first command does it if needed.
7977
+ */
7978
+ async shellEnv(name, { login }) {
7979
+ const resolved = await this.usableAlias(name, {});
7980
+ const provider = this.provider(resolved.source.provider);
7981
+ if (login) await this.ensureSession(resolved.source.id);
7982
+ return {
7983
+ provider: provider.id,
7984
+ clear: provider.shellVars,
7985
+ env: await provider.processEnv(resolved, "cli"),
7986
+ region: resolved.region,
7987
+ label: resolved.target?.label,
7988
+ prod: resolved.alias.prod === true
7989
+ };
7990
+ }
7991
+ /** The variables `cloudhop use --off <provider>` removes from a shell. */
7992
+ shellVars(provider) {
7993
+ return this.provider(provider).shellVars;
7994
+ }
8081
7995
  // ── Health ───────────────────────────────────────────────────────────────
8082
7996
  async doctor() {
8083
7997
  const config = await this.deps.config.load();
@@ -8201,11 +8115,260 @@ ${result.stderr}`)) {
8201
8115
  var PROVIDER_IDS = ["aws", "azure", "gcp"];
8202
8116
  var emptyConfig = () => ({ providers: [], sources: [], aliases: [] });
8203
8117
 
8118
+ // ../core/src/domain/folder.ts
8119
+ var FOLDER_FILE = ".cloudhoprc";
8120
+ var bad = (why) => new CloudhopError(
8121
+ "INVALID_NAME",
8122
+ `Not a valid ${FOLDER_FILE}: ${why}.`,
8123
+ "Write one line per cloud, like `aws: my-alias`."
8124
+ );
8125
+ function parseFolderFile(text4) {
8126
+ const result = {};
8127
+ for (const raw of text4.split(/\r?\n/)) {
8128
+ const line = raw.replace(/#.*/, "").trim();
8129
+ if (!line) continue;
8130
+ const match = /^([a-z]+)\s*[:=]\s*(\S+)$/.exec(line);
8131
+ const key2 = match ? match[1] : "any";
8132
+ const alias = match ? match[2] : line;
8133
+ if (key2 !== "any" && !PROVIDER_IDS.includes(key2)) throw bad(`unknown cloud "${key2}"`);
8134
+ if (result[key2]) throw bad(`"${key2}" appears twice`);
8135
+ assertValidName("alias", alias);
8136
+ result[key2] = alias;
8137
+ }
8138
+ if (Object.keys(result).length === 0) throw bad("it names no alias");
8139
+ return result;
8140
+ }
8141
+ function formatFolderFile(aliases) {
8142
+ const lines = Object.entries(aliases).map(
8143
+ ([key2, alias]) => key2 === "any" ? alias : `${key2}: ${alias}`
8144
+ );
8145
+ return `# cloudhop: aliases for this folder (https://dayepesb.github.io/cloudhop/guides/shell/)
8146
+ ${lines.join("\n")}
8147
+ `;
8148
+ }
8149
+
8150
+ // ../adapters-node/src/folder-files.ts
8151
+ function findFolderFile(start) {
8152
+ let dir = resolve(start);
8153
+ for (; ; ) {
8154
+ const file = join(dir, FOLDER_FILE);
8155
+ try {
8156
+ if (statSync(file).isFile()) return file;
8157
+ } catch {
8158
+ }
8159
+ const parent = dirname(dir);
8160
+ if (parent === dir) return void 0;
8161
+ dir = parent;
8162
+ }
8163
+ }
8164
+ var key = (path2) => {
8165
+ try {
8166
+ return realpathSync.native(path2);
8167
+ } catch {
8168
+ return resolve(path2);
8169
+ }
8170
+ };
8171
+ var digest = (content) => createHash("sha256").update(content).digest("hex");
8172
+ var TrustedFolders = class {
8173
+ file;
8174
+ constructor(configDir) {
8175
+ this.file = join(configDir, "trusted-folders.json");
8176
+ }
8177
+ isTrusted(path2, content) {
8178
+ return this.load().files[key(path2)] === digest(content);
8179
+ }
8180
+ async trust(path2, content) {
8181
+ const data = this.load();
8182
+ data.files[key(path2)] = digest(content);
8183
+ await this.save(data);
8184
+ }
8185
+ /** Returns false when the file was not trusted. */
8186
+ async forget(path2) {
8187
+ const data = this.load();
8188
+ const file = key(path2);
8189
+ if (!(file in data.files)) return false;
8190
+ delete data.files[file];
8191
+ await this.save(data);
8192
+ return true;
8193
+ }
8194
+ load() {
8195
+ try {
8196
+ const data = JSON.parse(readFileSync(this.file, "utf8"));
8197
+ return data?.version === 1 && data.files ? data : { version: 1, files: {} };
8198
+ } catch {
8199
+ return { version: 1, files: {} };
8200
+ }
8201
+ }
8202
+ async save(data) {
8203
+ await mkdir(dirname(this.file), { recursive: true });
8204
+ await writeFile(this.file, `${JSON.stringify(data, null, 2)}
8205
+ `, { mode: 384 });
8206
+ }
8207
+ };
8208
+ async function readFolderFile(path2) {
8209
+ return readFile(path2, "utf8").catch(() => void 0);
8210
+ }
8211
+
8212
+ // ../adapters-node/src/node-browser-opener.ts
8213
+ import { spawn } from "child_process";
8214
+ var NodeBrowserOpener = class {
8215
+ async open(url) {
8216
+ const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", '""', url.replace(/&/g, "^&")]] : ["xdg-open", [url]];
8217
+ await new Promise((resolve3) => {
8218
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
8219
+ child.on("error", () => resolve3());
8220
+ child.on("spawn", () => {
8221
+ child.unref();
8222
+ resolve3();
8223
+ });
8224
+ });
8225
+ }
8226
+ };
8227
+
8228
+ // ../adapters-node/src/node-process-runner.ts
8229
+ import { spawn as spawn2 } from "child_process";
8230
+ var STDERR_TAIL_BYTES = 8 * 1024;
8231
+ function buildEnv(overrides) {
8232
+ const env = { ...process.env };
8233
+ for (const [key2, value] of Object.entries(overrides ?? {})) {
8234
+ if (value === void 0) delete env[key2];
8235
+ else env[key2] = value;
8236
+ }
8237
+ return env;
8238
+ }
8239
+ var useShell = process.platform === "win32";
8240
+ var NodeProcessRunner = class {
8241
+ run(command, args, options = {}) {
8242
+ return new Promise((resolve3, reject) => {
8243
+ const child = spawn2(command, args, {
8244
+ env: buildEnv(options.env),
8245
+ stdio: ["inherit", "inherit", options.captureStderr ? "pipe" : "inherit"],
8246
+ shell: useShell
8247
+ });
8248
+ let tail = "";
8249
+ child.stderr?.on("data", (chunk) => {
8250
+ process.stderr.write(chunk);
8251
+ tail = (tail + chunk.toString()).slice(-STDERR_TAIL_BYTES);
8252
+ });
8253
+ const ignore = () => {
8254
+ };
8255
+ process.on("SIGINT", ignore);
8256
+ process.on("SIGTERM", ignore);
8257
+ const cleanup = () => {
8258
+ process.off("SIGINT", ignore);
8259
+ process.off("SIGTERM", ignore);
8260
+ };
8261
+ child.on("error", (error) => {
8262
+ cleanup();
8263
+ reject(error);
8264
+ });
8265
+ child.on("close", (code, signal) => {
8266
+ cleanup();
8267
+ resolve3({ exitCode: code ?? (signal ? 128 + 2 : 1), stderrTail: tail });
8268
+ });
8269
+ });
8270
+ }
8271
+ capture(command, args, options = {}) {
8272
+ return new Promise((resolve3) => {
8273
+ const child = spawn2(command, args, { env: buildEnv(options.env), shell: useShell });
8274
+ child.stdin.end();
8275
+ let stdout2 = "";
8276
+ let stderr = "";
8277
+ child.stdout.on("data", (c3) => {
8278
+ stdout2 += c3.toString();
8279
+ });
8280
+ child.stderr.on("data", (c3) => {
8281
+ stderr += c3.toString();
8282
+ });
8283
+ child.on("error", (error) => resolve3({ exitCode: 127, stdout: stdout2, stderr: error.message }));
8284
+ child.on("close", (code) => resolve3({ exitCode: code ?? 1, stdout: stdout2, stderr }));
8285
+ });
8286
+ }
8287
+ async probe(command) {
8288
+ const result = await this.capture(command, ["--version"]);
8289
+ if (result.exitCode !== 0) return { installed: false };
8290
+ const output = `${result.stdout} ${result.stderr}`;
8291
+ const version = output.match(/(\d+\.\d+\.\d+)/)?.[1];
8292
+ return { installed: true, ...version ? { version } : {} };
8293
+ }
8294
+ };
8295
+
8296
+ // ../adapters-node/src/system-tool-installer.ts
8297
+ var TOOLS = {
8298
+ aws: {
8299
+ manualUrl: "https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html",
8300
+ recipes: {
8301
+ darwin: [{ via: "Homebrew", command: "brew", args: ["install", "awscli"] }],
8302
+ win32: [
8303
+ { via: "winget", command: "winget", args: ["install", "--exact", "--id", "Amazon.AWSCLI"] }
8304
+ ]
8305
+ }
8306
+ },
8307
+ az: {
8308
+ manualUrl: "https://learn.microsoft.com/cli/azure/install-azure-cli",
8309
+ recipes: {
8310
+ darwin: [{ via: "Homebrew", command: "brew", args: ["install", "azure-cli"] }],
8311
+ win32: [
8312
+ {
8313
+ via: "winget",
8314
+ command: "winget",
8315
+ args: ["install", "--exact", "--id", "Microsoft.AzureCLI"]
8316
+ }
8317
+ ]
8318
+ }
8319
+ },
8320
+ gcloud: {
8321
+ manualUrl: "https://cloud.google.com/sdk/docs/install",
8322
+ recipes: {
8323
+ darwin: [
8324
+ { via: "Homebrew", command: "brew", args: ["install", "--cask", "google-cloud-sdk"] }
8325
+ ],
8326
+ win32: [
8327
+ {
8328
+ via: "winget",
8329
+ command: "winget",
8330
+ args: ["install", "--exact", "--id", "Google.CloudSDK"]
8331
+ }
8332
+ ]
8333
+ }
8334
+ }
8335
+ };
8336
+ var SystemToolInstaller = class {
8337
+ constructor(runner, platform = process.platform) {
8338
+ this.runner = runner;
8339
+ this.platform = platform;
8340
+ }
8341
+ runner;
8342
+ platform;
8343
+ available = /* @__PURE__ */ new Map();
8344
+ recipeFor(command) {
8345
+ const tool = TOOLS[command];
8346
+ if (!tool) return void 0;
8347
+ const recipe = tool.recipes[this.platform]?.[0];
8348
+ return recipe ? { ...recipe, manualUrl: tool.manualUrl } : { via: "official installer", command: "", args: [], manualUrl: tool.manualUrl };
8349
+ }
8350
+ async install(recipe) {
8351
+ if (!recipe.command) return false;
8352
+ if (!await this.hasCommand(recipe.command)) return false;
8353
+ const { exitCode } = await this.runner.run(recipe.command, recipe.args);
8354
+ return exitCode === 0;
8355
+ }
8356
+ async hasCommand(command) {
8357
+ if (!this.available.has(command)) {
8358
+ this.available.set(command, (await this.runner.probe(command)).installed);
8359
+ }
8360
+ return this.available.get(command) ?? false;
8361
+ }
8362
+ };
8363
+
8204
8364
  // ../adapters-node/src/yaml-config-store.ts
8365
+ import { mkdir as mkdir2, readFile as readFile2, rename, rm, writeFile as writeFile2 } from "fs/promises";
8366
+ import { homedir } from "os";
8367
+ import { join as join2 } from "path";
8205
8368
  var import_yaml = __toESM(require_dist(), 1);
8206
8369
  function defaultConfigDir(env = process.env) {
8207
8370
  if (env.CLOUDHOP_HOME) return env.CLOUDHOP_HOME;
8208
- return join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "cloudhop");
8371
+ return join2(env.XDG_CONFIG_HOME || join2(homedir(), ".config"), "cloudhop");
8209
8372
  }
8210
8373
  var HEADER = "# cloudhop configuration. Contains no secrets.\n# Docs: https://github.com/dayepesb/cloudhop\n";
8211
8374
  var YamlConfigStore = class {
@@ -8214,10 +8377,10 @@ var YamlConfigStore = class {
8214
8377
  }
8215
8378
  dir;
8216
8379
  get configPath() {
8217
- return join(this.dir, "config.yaml");
8380
+ return join2(this.dir, "config.yaml");
8218
8381
  }
8219
8382
  get targetsPath() {
8220
- return join(this.dir, "targets.json");
8383
+ return join2(this.dir, "targets.json");
8221
8384
  }
8222
8385
  async load() {
8223
8386
  const raw = await this.read(this.configPath);
@@ -8265,7 +8428,7 @@ var YamlConfigStore = class {
8265
8428
  }
8266
8429
  async read(path2) {
8267
8430
  try {
8268
- return await readFile(path2, "utf8");
8431
+ return await readFile2(path2, "utf8");
8269
8432
  } catch (error) {
8270
8433
  if (error.code === "ENOENT") return void 0;
8271
8434
  throw error;
@@ -8273,9 +8436,9 @@ var YamlConfigStore = class {
8273
8436
  }
8274
8437
  /** Writes through a temp file so a crash never leaves a half-written config. */
8275
8438
  async write(path2, content) {
8276
- await mkdir(this.dir, { recursive: true, mode: 448 });
8439
+ await mkdir2(this.dir, { recursive: true, mode: 448 });
8277
8440
  const tmp = `${path2}.${process.pid}.tmp`;
8278
- await writeFile(tmp, content, { mode: 384 });
8441
+ await writeFile2(tmp, content, { mode: 384 });
8279
8442
  await rename(tmp, path2);
8280
8443
  }
8281
8444
  };
@@ -8288,17 +8451,17 @@ var MAX_OUTPUT = 1e5;
8288
8451
  var cap = (text4) => text4.length > MAX_OUTPUT ? `${text4.slice(0, MAX_OUTPUT)}
8289
8452
  \u2026 [truncated: ${text4.length - MAX_OUTPUT} more characters. Narrow the command with --query or filters.]` : text4;
8290
8453
  var WINDOWS_SHELL_CHARS = /[&|<>^%!"\r\n]/;
8291
- var text = (args, key) => {
8292
- const value = args[key];
8454
+ var text = (args, key2) => {
8455
+ const value = args[key2];
8293
8456
  if (typeof value !== "string" || !value.trim()) {
8294
- throw new CloudhopError("INVALID_NAME", `"${key}" is required.`);
8457
+ throw new CloudhopError("INVALID_NAME", `"${key2}" is required.`);
8295
8458
  }
8296
8459
  return value.trim();
8297
8460
  };
8298
- var stringArray = (args, key) => {
8299
- const value = args[key];
8461
+ var stringArray = (args, key2) => {
8462
+ const value = args[key2];
8300
8463
  if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) {
8301
- throw new CloudhopError("INVALID_NAME", `"${key}" must be an array of strings.`);
8464
+ throw new CloudhopError("INVALID_NAME", `"${key2}" must be an array of strings.`);
8302
8465
  }
8303
8466
  return value;
8304
8467
  };
@@ -8308,15 +8471,15 @@ var aliasInput = {
8308
8471
  description: "The cloudhop alias to use, as listed by list_aliases. Ask the user which one."
8309
8472
  }
8310
8473
  };
8311
- function createTools(app) {
8474
+ function createTools(app, context) {
8312
8475
  return [
8313
8476
  {
8314
8477
  name: "list_aliases",
8315
8478
  title: "List cloudhop aliases",
8316
- description: "Lists every cloudhop source (SSO portal or personal account) with its session state, and every alias: name, account/role label, region and whether a sign-in is needed. Call this first, then ask the user which alias to work with. Never contains secrets.",
8479
+ 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); `default: true` marks the alias new terminals of the user start with. Call this first, then ask the user which alias to work with, offering the hinted one first. Never contains secrets.",
8317
8480
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
8318
8481
  annotations: { readOnlyHint: true, openWorldHint: false },
8319
- run: () => app.overview()
8482
+ run: async () => ({ ...await app.overview(), ...await context?.() })
8320
8483
  },
8321
8484
  {
8322
8485
  name: "alias_status",
@@ -8476,7 +8639,7 @@ function createTools(app) {
8476
8639
 
8477
8640
  // ../mcp/src/server.ts
8478
8641
  var MCP_INSTRUCTIONS = `cloudhop gives each cloud account + role + region a short alias and signs the user in only when a session expired.
8479
- Before the first cloud operation in a conversation: call list_aliases, show the user the aliases (name, account/role, region) and ask which one to use. Never guess the alias.
8642
+ 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), inUse or an alias with default: true, offer that alias first, but still ask.
8480
8643
  Use run_cloud_cli for cloud CLI commands, never --profile. Read-only commands may run once the alias is chosen; before anything that creates, changes or deletes resources, show the alias, account, region and exact command, and wait for the user's explicit OK. Aliases with prod: true are production: run_cloud_cli refuses changes there until the user approves and you pass confirm_production: true.
8481
8644
  To show the user something in the web console, call open_console (it opens their browser, already signed in).
8482
8645
  If a tool returns LOGIN_REQUIRED or alias_status says browser-sign-in: tell the user their browser will open, then call login (it waits up to 5 minutes for them to approve). Never print or ask for credentials.`;
@@ -8485,9 +8648,10 @@ function serveMcp({
8485
8648
  app,
8486
8649
  version,
8487
8650
  input = process.stdin,
8488
- output = process.stdout
8651
+ output = process.stdout,
8652
+ context
8489
8653
  }) {
8490
- const tools = createTools(app);
8654
+ const tools = createTools(app, context);
8491
8655
  const byName = new Map(tools.map((t2) => [t2.name, t2]));
8492
8656
  const send2 = (message) => output.write(`${JSON.stringify(message)}
8493
8657
  `);
@@ -8553,7 +8717,7 @@ function serveMcp({
8553
8717
  if (!isNotification) fail(id, -32601, `Method not found: ${method}`);
8554
8718
  }
8555
8719
  };
8556
- return new Promise((resolve) => {
8720
+ return new Promise((resolve3) => {
8557
8721
  const lines = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
8558
8722
  const pending = /* @__PURE__ */ new Set();
8559
8723
  lines.on("line", (line) => {
@@ -8573,7 +8737,7 @@ function serveMcp({
8573
8737
  task.finally(() => pending.delete(task));
8574
8738
  });
8575
8739
  lines.on("close", () => {
8576
- Promise.allSettled([...pending]).then(() => resolve());
8740
+ Promise.allSettled([...pending]).then(() => resolve3());
8577
8741
  });
8578
8742
  });
8579
8743
  }
@@ -8588,7 +8752,11 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
8588
8752
  "noMcp",
8589
8753
  "force",
8590
8754
  "prod",
8591
- "off"
8755
+ "off",
8756
+ "save",
8757
+ "leave",
8758
+ "default",
8759
+ "noDefault"
8592
8760
  ]);
8593
8761
  var camel = (name) => name.replace(/-([a-z])/g, (_2, c3) => c3.toUpperCase());
8594
8762
  function parseArgs(argv) {
@@ -10009,7 +10177,7 @@ import { styleText as styleText2, stripVTControlCharacters } from "util";
10009
10177
  import process$1 from "process";
10010
10178
  var import_sisteransi2 = __toESM(require_src(), 1);
10011
10179
  import { existsSync, lstatSync, readdirSync } from "fs";
10012
- import { dirname, join as join2 } from "path";
10180
+ import { dirname as dirname2, join as join3 } from "path";
10013
10181
  function isUnicodeSupported() {
10014
10182
  if (process$1.platform !== "win32") {
10015
10183
  return process$1.env.TERM !== "linux";
@@ -10947,10 +11115,10 @@ var path = (e) => {
10947
11115
  return [];
10948
11116
  try {
10949
11117
  let i2;
10950
- existsSync(t2) ? lstatSync(t2).isDirectory() && (!e.directory || t2.endsWith("/")) ? i2 = t2 : i2 = dirname(t2) : i2 = dirname(t2);
11118
+ existsSync(t2) ? lstatSync(t2).isDirectory() && (!e.directory || t2.endsWith("/")) ? i2 = t2 : i2 = dirname2(t2) : i2 = dirname2(t2);
10951
11119
  const c3 = t2.length > 1 && t2.endsWith("/") ? t2.slice(0, -1) : t2;
10952
11120
  return readdirSync(i2).map((r2) => {
10953
- const n3 = join2(i2, r2), m3 = lstatSync(n3);
11121
+ const n3 = join3(i2, r2), m3 = lstatSync(n3);
10954
11122
  return {
10955
11123
  name: r2,
10956
11124
  path: n3,
@@ -11707,16 +11875,16 @@ import { homedir as homedir2 } from "os";
11707
11875
  import { isAbsolute, relative } from "path";
11708
11876
 
11709
11877
  // ../assistants/src/installer.ts
11710
- import { createHash } from "crypto";
11878
+ import { createHash as createHash2 } from "crypto";
11711
11879
  import { existsSync as existsSync2 } from "fs";
11712
- import { mkdir as mkdir2, readFile as readFile2, rename as rename2, rm as rm2, rmdir, writeFile as writeFile2 } from "fs/promises";
11713
- import { dirname as dirname2, join as join4 } from "path";
11880
+ import { mkdir as mkdir3, readFile as readFile3, rename as rename2, rm as rm2, rmdir, writeFile as writeFile3 } from "fs/promises";
11881
+ import { dirname as dirname3, join as join5 } from "path";
11714
11882
 
11715
11883
  // ../assistants/src/assets/skill.md
11716
- var skill_default = "---\nname: cloudhop\ndescription: Run AWS CLI commands and other cloud tools as a specific account, role and region through cloudhop aliases (the `hop` and `cloudhop` commands). Use whenever the user asks to do anything in their AWS accounts, inspect or change cloud resources, run aws/terraform/cdk against an account, list their cloud accounts or aliases, or mentions hop or cloudhop.\n---\n\n# Working with cloud accounts through cloudhop\n\nThe user manages their cloud access with **cloudhop**. Each **alias** (for\nexample `acme-dev`) points to one account, one role and one region. cloudhop\nsigns the user in through their browser only when a session has expired. There\nare no access keys: never ask for, create, print or store credentials.\n\n## 1. Pick the alias with the user, every time\n\nBefore the first cloud operation in a conversation:\n\n1. List the aliases. Use the `list_aliases` tool if the cloudhop MCP server is\n connected; otherwise run `cloudhop ls --json`.\n2. Show the user the options (alias, account / role, region) and **ask which\n one to use**. Don't guess, even if one name looks right, and don't pick\n one just because there is only one: confirm it.\n3. Use that alias for the rest of the conversation. Ask again if the user\n moves to another environment, account or task.\n\n## 2. Check the session before running anything\n\nRun `cloudhop status <alias> --json` (or the `alias_status` tool) and read\n`next`:\n\n| `next` | Meaning | What to do |\n| ----------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `ready` | Session valid | Go ahead |\n| `silent-renewal` | Expired but renewable without the user | Go ahead; cloudhop renews it |\n| `browser-sign-in` | The user must sign in | Tell the user their browser will open and they need to approve, then run `cloudhop login <source>` (or the `login` tool) and wait up to 5 minutes |\n| `unknown` | Managed by the cloud CLI (personal accounts) | Go ahead; if it fails, run `cloudhop login <source>` |\n\nDon't start several cloud commands in parallel while a sign-in is pending.\n\n## 3. Run commands through the alias\n\n- **AWS CLI**: `hop <alias> <aws arguments>`, e.g. `hop acme-dev s3 ls`.\n It is exactly `aws <arguments>` with the alias' account, role and region.\n Never add `--profile`. Use `--region <r>` only to override the region once.\n With the MCP server, use `run_cloud_cli` with `args` (no program name).\n- **Who am I**: `hop <alias>` with no arguments (or the `whoami` tool).\n- **The same command in several aliases** (after the user chose them):\n `cloudhop each a,b,c --yes -- <aws arguments>`; add `--json` to parse the\n results.\n- **The AWS web console**: `cloudhop console <alias> [service]` (or the\n `open_console` tool) opens it in the user's browser, already signed in.\n Never try to print or fetch its URL.\n- **Always name the alias.** Never run the interactive forms meant for a\n person at a terminal: `hop` or `hop -` without an alias, `cloudhop console`\n without an alias, `cloudhop use` (it only changes an interactive shell),\n `cloudhop setup`, or `cloudhop add`/`alias` without flags. Nobody can answer\n their prompts in your terminal.\n- **Other tools** (Terraform, CDK, SDK scripts):\n `cloudhop exec <alias> -- <command>`, e.g.\n `cloudhop exec acme-dev -- terraform plan`.\n- Prefer `--output json` and `--query` to keep output small.\n- Don't export `AWS_PROFILE`, `AWS_ACCESS_KEY_ID` or similar variables, don't\n run commands that print credentials (`env`, `printenv`, `export-credentials`)\n and never edit `~/.aws/config`, `~/.aws/credentials` or cloudhop's own files.\n\n## 4. Confirm before changing anything\n\n- Read-only commands (`list`, `describe`, `get`, `s3 ls`, `logs tail`) can run\n once the user has chosen the alias.\n- Before any command that **creates, changes, deletes or costs money**, show\n the alias, account, region and the exact command, and wait for the user's\n explicit \"yes\". One confirmation covers one command, not a series.\n- **Production aliases** have `\"prod\": true` in `cloudhop ls --json` (and\n `PROD` in `cloudhop ls`). Confirm every change there and suggest a dry run\n (`--dryrun`, `terraform plan`) first. cloudhop enforces it: without a\n terminal, `hop`/`cloudhop exec` refuse changes on production with\n `PROD_CONFIRMATION_REQUIRED`. After the user explicitly approves the exact\n command, rerun it with `CLOUDHOP_CONFIRM_PROD=1` in that command's\n environment (with MCP: `confirm_production: true`). Never set it on your own.\n- Treat aliases or accounts that merely look like production (`prd`, `live`)\n the same way, even if they are not marked.\n- Don't change cloudhop itself (`cloudhop add`, `rm`, `mv`, `region`, `prod`,\n `team import`, `agent`, `login` for another source) unless the user asked\n for it. Never unmark a production alias to get around a confirmation.\n\n## 5. When something goes wrong\n\n| You see | Do this |\n| --------------------------------------------------- | ------------------------------------------------------------------------------ |\n| \"Opening your browser to log in\" | Tell the user to approve in the browser; the command continues by itself |\n| The command was cut off while waiting for a sign-in | Run `cloudhop login <source>` with a timeout of at least 5 minutes, then retry |\n| `LOGIN_REQUIRED` | Same as `browser-sign-in` above |\n| No browser on this machine (remote, container) | Run `cloudhop login <source> --device-code` and show the user the URL and code |\n| `Alias \"x\" not found` | Run `cloudhop ls` and ask the user again |\n| AWS CLI missing or too old | Suggest `cloudhop doctor`; don't install anything without asking |\n| `AccessDenied` | The role lacks permission: tell the user; don't try other aliases on your own |\n\nMore: https://dayepesb.github.io/cloudhop/guides/hop-like-aws/\n";
11884
+ 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. `cloudhop use` is per terminal: a\n new terminal starts with no alias unless they choose a default with\n `cloudhop use <alias> --default` (or the **Default** box in\n `cloudhop ui`); `cloudhop use --no-default` stops it. A production\n default needs `--yes`. A `cloudhop use` typed in a terminal wins over\n the folder's `.cloudhoprc`, which wins over the default.\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| A new terminal forgot the `cloudhop use` | It's per terminal by design: `cloudhop use <alias> --default` makes every new terminal start with it |\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); an alias\n with `default: true` is the one their new terminals start with. 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` or change their default alias (`use --default`,\n `--no-default`) unless the user asked for it. Never unmark a production\n alias to get around a confirmation.\n\n## 5. When something goes wrong\n\n| You see | Do this |\n| --------------------------------------------------- | ------------------------------------------------------------------------------ |\n| \"Opening your browser to log in\" | Tell the user to approve in the browser; the command continues by itself |\n| The command was cut off while waiting for a sign-in | Run `cloudhop login <source>` with a timeout of at least 5 minutes, then retry |\n| `LOGIN_REQUIRED` | Same as `browser-sign-in` above |\n| No browser on this machine (remote, container) | Run `cloudhop login <source> --device-code` and show the user the URL and code |\n| `Alias \"x\" not found` | Run `cloudhop ls` and ask the user again |\n| AWS CLI missing or too old | Suggest `cloudhop doctor`; don't install anything without asking |\n| `AccessDenied` | The role lacks permission: tell the user; don't try other aliases on your own |\n\nMore: https://dayepesb.github.io/cloudhop/guides/hop-like-aws/\n";
11717
11885
 
11718
11886
  // ../assistants/src/targets.ts
11719
- import { join as join3 } from "path";
11887
+ import { join as join4 } from "path";
11720
11888
  var ASSISTANT_IDS = [
11721
11889
  "claude",
11722
11890
  "kiro",
@@ -11731,9 +11899,9 @@ var ASSISTANTS = {
11731
11899
  claude: {
11732
11900
  id: "claude",
11733
11901
  name: "Claude Code",
11734
- markers: ({ home }) => [join3(home, ".claude")],
11735
- skillDir: (scope, { home, project }) => join3(scope === "user" ? home : project, ".claude", "skills", SKILL_NAME),
11736
- mcp: (scope, { project }) => scope === "project" ? { kind: "json", file: join3(project, ".mcp.json"), key: "mcpServers", withType: false } : {
11902
+ markers: ({ home }) => [join4(home, ".claude")],
11903
+ skillDir: (scope, { home, project }) => join4(scope === "user" ? home : project, ".claude", "skills", SKILL_NAME),
11904
+ mcp: (scope, { project }) => scope === "project" ? { kind: "json", file: join4(project, ".mcp.json"), key: "mcpServers", withType: false } : {
11737
11905
  kind: "cli",
11738
11906
  command: "claude",
11739
11907
  add: ["mcp", "add", "--scope", "user", "cloudhop", "--", MCP_COMMAND, ...MCP_ARGS],
@@ -11744,11 +11912,11 @@ var ASSISTANTS = {
11744
11912
  kiro: {
11745
11913
  id: "kiro",
11746
11914
  name: "Kiro",
11747
- markers: ({ home }) => [join3(home, ".kiro")],
11748
- skillDir: (scope, { home, project }) => join3(scope === "user" ? home : project, ".kiro", "skills", SKILL_NAME),
11915
+ markers: ({ home }) => [join4(home, ".kiro")],
11916
+ skillDir: (scope, { home, project }) => join4(scope === "user" ? home : project, ".kiro", "skills", SKILL_NAME),
11749
11917
  mcp: (scope, { home, project }) => ({
11750
11918
  kind: "json",
11751
- file: join3(scope === "user" ? home : project, ".kiro", "settings", "mcp.json"),
11919
+ file: join4(scope === "user" ? home : project, ".kiro", "settings", "mcp.json"),
11752
11920
  key: "mcpServers",
11753
11921
  withType: false
11754
11922
  })
@@ -11756,11 +11924,11 @@ var ASSISTANTS = {
11756
11924
  copilot: {
11757
11925
  id: "copilot",
11758
11926
  name: "GitHub Copilot (VS Code)",
11759
- markers: ({ home }) => [join3(home, ".copilot"), join3(home, ".vscode")],
11760
- skillDir: (scope, { home, project }) => scope === "user" ? join3(home, ".copilot", "skills", SKILL_NAME) : join3(project, ".github", "skills", SKILL_NAME),
11927
+ markers: ({ home }) => [join4(home, ".copilot"), join4(home, ".vscode")],
11928
+ skillDir: (scope, { home, project }) => scope === "user" ? join4(home, ".copilot", "skills", SKILL_NAME) : join4(project, ".github", "skills", SKILL_NAME),
11761
11929
  mcp: (scope, { project }) => scope === "project" ? {
11762
11930
  kind: "json",
11763
- file: join3(project, ".vscode", "mcp.json"),
11931
+ file: join4(project, ".vscode", "mcp.json"),
11764
11932
  key: "servers",
11765
11933
  withType: true
11766
11934
  } : {
@@ -11782,11 +11950,11 @@ var ASSISTANTS = {
11782
11950
  cursor: {
11783
11951
  id: "cursor",
11784
11952
  name: "Cursor",
11785
- markers: ({ home }) => [join3(home, ".cursor")],
11786
- skillDir: (scope, { home, project }) => join3(scope === "user" ? home : project, ".cursor", "skills", SKILL_NAME),
11953
+ markers: ({ home }) => [join4(home, ".cursor")],
11954
+ skillDir: (scope, { home, project }) => join4(scope === "user" ? home : project, ".cursor", "skills", SKILL_NAME),
11787
11955
  mcp: (scope, { home, project }) => ({
11788
11956
  kind: "json",
11789
- file: join3(scope === "user" ? home : project, ".cursor", "mcp.json"),
11957
+ file: join4(scope === "user" ? home : project, ".cursor", "mcp.json"),
11790
11958
  key: "mcpServers",
11791
11959
  withType: true
11792
11960
  })
@@ -11794,29 +11962,29 @@ var ASSISTANTS = {
11794
11962
  codex: {
11795
11963
  id: "codex",
11796
11964
  name: "Codex",
11797
- markers: ({ home }) => [join3(home, ".codex"), join3(home, ".agents")],
11798
- skillDir: (scope, { home, project }) => join3(scope === "user" ? home : project, ".agents", "skills", SKILL_NAME),
11965
+ markers: ({ home }) => [join4(home, ".codex"), join4(home, ".agents")],
11966
+ skillDir: (scope, { home, project }) => join4(scope === "user" ? home : project, ".agents", "skills", SKILL_NAME),
11799
11967
  mcp: (scope, { home, project }) => ({
11800
11968
  kind: "toml",
11801
- file: join3(scope === "user" ? home : project, ".codex", "config.toml")
11969
+ file: join4(scope === "user" ? home : project, ".codex", "config.toml")
11802
11970
  })
11803
11971
  }
11804
11972
  };
11805
11973
 
11806
11974
  // ../assistants/src/installer.ts
11807
- var sha = (text4) => createHash("sha256").update(text4).digest("hex");
11975
+ var sha = (text4) => createHash2("sha256").update(text4).digest("hex");
11808
11976
  async function writeAtomic(file, content) {
11809
- await mkdir2(dirname2(file), { recursive: true });
11977
+ await mkdir3(dirname3(file), { recursive: true });
11810
11978
  const tmp = `${file}.cloudhop-${process.pid}.tmp`;
11811
- await writeFile2(tmp, content);
11979
+ await writeFile3(tmp, content);
11812
11980
  await rename2(tmp, file);
11813
11981
  }
11814
11982
  async function removeFileAndEmptyDir(file) {
11815
11983
  await rm2(file, { force: true });
11816
- await rmdir(dirname2(file)).catch(() => {
11984
+ await rmdir(dirname3(file)).catch(() => {
11817
11985
  });
11818
11986
  }
11819
- var readText = (file) => readFile2(file, "utf8").catch(() => void 0);
11987
+ var readText = (file) => readFile3(file, "utf8").catch(() => void 0);
11820
11988
  var AssistantsInstaller = class {
11821
11989
  constructor(options) {
11822
11990
  this.options = options;
@@ -11840,7 +12008,7 @@ var AssistantsInstaller = class {
11840
12008
  return Promise.all(
11841
12009
  ASSISTANT_IDS.map(async (id) => {
11842
12010
  const assistant = ASSISTANTS[id];
11843
- const skillPath = join4(assistant.skillDir(scope, this.options.places), "SKILL.md");
12011
+ const skillPath = join5(assistant.skillDir(scope, this.options.places), "SKILL.md");
11844
12012
  const entry = manifest.find((e) => e.skillFile === skillPath);
11845
12013
  return {
11846
12014
  assistant: id,
@@ -11859,7 +12027,7 @@ var AssistantsInstaller = class {
11859
12027
  const manifest = await this.readManifest();
11860
12028
  for (const id of ids) {
11861
12029
  const assistant = ASSISTANTS[id];
11862
- const skillPath = join4(assistant.skillDir(scope, this.options.places), "SKILL.md");
12030
+ const skillPath = join5(assistant.skillDir(scope, this.options.places), "SKILL.md");
11863
12031
  const content = this.skillContent;
11864
12032
  await writeAtomic(skillPath, content);
11865
12033
  const kept = manifest.filter((e) => e.skillFile !== skillPath);
@@ -11884,9 +12052,9 @@ var AssistantsInstaller = class {
11884
12052
  for (const id of ids) {
11885
12053
  const assistant = ASSISTANTS[id];
11886
12054
  const dir = assistant.skillDir(scope, this.options.places);
11887
- const skillPath = join4(dir, "SKILL.md");
12055
+ const skillPath = join5(dir, "SKILL.md");
11888
12056
  await rm2(dir, { recursive: true, force: true });
11889
- await rmdir(dirname2(dir)).then(() => rmdir(dirname2(dirname2(dir)))).catch(() => {
12057
+ await rmdir(dirname3(dir)).then(() => rmdir(dirname3(dirname3(dir)))).catch(() => {
11890
12058
  });
11891
12059
  manifest = manifest.filter((e) => e.skillFile !== skillPath);
11892
12060
  const outcome = await this.removeMcp(assistant.mcp(scope, this.options.places));
@@ -12040,7 +12208,7 @@ args = [${MCP_ARGS.map((a2) => `"${a2}"`).join(", ")}]
12040
12208
  }
12041
12209
  // ───────────── manifest ─────────────
12042
12210
  get manifestPath() {
12043
- return join4(this.options.configDir, "assistants.json");
12211
+ return join5(this.options.configDir, "assistants.json");
12044
12212
  }
12045
12213
  async readManifest() {
12046
12214
  const text4 = await readText(this.manifestPath);
@@ -12271,9 +12439,16 @@ async function consoleCommand(app, args) {
12271
12439
  }
12272
12440
 
12273
12441
  // src/commands/each.ts
12442
+ var MAX_PATTERN = 200;
12274
12443
  function matcher(selector) {
12275
12444
  const regex = /^\/(.+)\/([a-z]*)$/.exec(selector);
12276
12445
  if (regex) {
12446
+ if (selector.length > MAX_PATTERN) {
12447
+ throw new CloudhopError(
12448
+ "INVALID_NAME",
12449
+ `That regular expression is too long (max ${MAX_PATTERN}).`
12450
+ );
12451
+ }
12277
12452
  try {
12278
12453
  const re = new RegExp(regex[1], regex[2]);
12279
12454
  return (name) => re.test(name);
@@ -12375,52 +12550,872 @@ ${listing}
12375
12550
  if (!tty) {
12376
12551
  throw new CloudhopError(
12377
12552
  "PROD_CONFIRMATION_REQUIRED",
12378
- `This may change resources in production: ${risky.join(", ")}.`,
12379
- "Run it in a terminal to confirm, or set CLOUDHOP_CONFIRM_PROD=1 once a person has approved it."
12553
+ `This may change resources in production: ${risky.join(", ")}.`,
12554
+ "Run it in a terminal to confirm, or set CLOUDHOP_CONFIRM_PROD=1 once a person has approved it."
12555
+ );
12556
+ }
12557
+ dist_exports.log.warn(
12558
+ `${paint("red", bold("PRODUCTION"))} ${risky.join(", ")}: this may change resources.`
12559
+ );
12560
+ if (!answer(await dist_exports.confirm({ message: "Also run it in production?", initialValue: false })))
12561
+ return 130;
12562
+ }
12563
+ for (const source of new Set(selected.map((a2) => a2.source))) {
12564
+ await app.ensureSession(source, { interactive: tty });
12565
+ }
12566
+ const parallel = Math.max(1, Math.min(16, Number(flags.parallel) || 4));
12567
+ const outcomes = await mapLimit(selected, parallel, async (a2) => {
12568
+ try {
12569
+ const r2 = await app.captureAsAlias(a2.name, cliArgs);
12570
+ const outcome = { alias: a2.name, exitCode: r2.exitCode, stdout: r2.stdout, stderr: r2.stderr };
12571
+ if (!flags.json) printOutcome(outcome, width);
12572
+ return outcome;
12573
+ } catch (error) {
12574
+ const outcome = { alias: a2.name, exitCode: 1, stdout: "", stderr: error.message };
12575
+ if (!flags.json) printOutcome(outcome, width);
12576
+ return outcome;
12577
+ }
12578
+ });
12579
+ const failed = outcomes.filter((o2) => o2.exitCode !== 0);
12580
+ if (flags.json) {
12581
+ console.log(JSON.stringify({ version: 1, command: cliArgs, results: outcomes }, null, 2));
12582
+ } else {
12583
+ const summary = `${outcomes.length - failed.length} ok${failed.length ? `, ${failed.length} failed: ${failed.map((f2) => f2.alias).join(", ")}` : ""}`;
12584
+ process.stderr.write(`${failed.length ? paint("red", summary) : paint("green", summary)}
12585
+ `);
12586
+ }
12587
+ return failed.length ? 1 : 0;
12588
+ }
12589
+ function printOutcome({ alias, exitCode, stdout: stdout2, stderr }, width) {
12590
+ const tag = paint(exitCode === 0 ? "sky" : "red", `[${alias}]`.padEnd(width + 2));
12591
+ for (const line of stdout2.replace(/\n$/, "").split("\n"))
12592
+ if (line) process.stdout.write(`${tag} ${line}
12593
+ `);
12594
+ for (const line of stderr.replace(/\n$/, "").split("\n"))
12595
+ if (line) process.stderr.write(`${tag} ${paint("dim", line)}
12596
+ `);
12597
+ if (exitCode !== 0) process.stderr.write(`${tag} ${paint("red", `exit ${exitCode}`)}
12598
+ `);
12599
+ }
12600
+
12601
+ // src/commands/use.ts
12602
+ import { spawnSync } from "child_process";
12603
+ import { writeFile as writeFile5 } from "fs/promises";
12604
+ import { homedir as homedir4 } from "os";
12605
+ import { dirname as dirname5, join as join7, resolve as resolve2 } from "path";
12606
+
12607
+ // src/commands/shell.ts
12608
+ import { readFileSync as readFileSync2 } from "fs";
12609
+ import { appendFile, mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
12610
+ import { homedir as homedir3 } from "os";
12611
+ import { dirname as dirname4, join as join6 } from "path";
12612
+
12613
+ // src/commands/starship.ts
12614
+ var BEGIN = "# >>> cloudhop >>> (added by `cloudhop shell`; remove this block and the ${env_var.cloudhop_*} segments in `format` to undo)";
12615
+ var END = "# <<< cloudhop <<<";
12616
+ var SKY = "#3bb8f5";
12617
+ var ON_SKY = "#0b1124";
12618
+ var RED = "#be123c";
12619
+ var STARSHIP_SEGMENTS = PROVIDER_IDS.map(
12620
+ (p3) => `\${env_var.cloudhop_${p3}}\${env_var.cloudhop_${p3}_prod}`
12621
+ ).join("");
12622
+ function starshipModules(powerline) {
12623
+ const segment = (provider, prod) => {
12624
+ if (!powerline) {
12625
+ return prod ? `[${provider} $env_value PROD ](bold #fb7185)` : `[${provider} $env_value ](bold ${SKY})`;
12626
+ }
12627
+ const { glyph, color } = powerline;
12628
+ const bg = prod ? RED : SKY;
12629
+ const fg = prod ? "#ffffff" : ON_SKY;
12630
+ return `[${glyph}](fg:${color} bg:${bg})[ ${provider} $env_value${prod ? " PROD" : ""} ](bold fg:${fg} bg:${bg})[${glyph}](fg:${bg} bg:${color})`;
12631
+ };
12632
+ return PROVIDER_IDS.map((p3) => {
12633
+ const variable = `CLOUDHOP_${p3.toUpperCase()}`;
12634
+ return `[env_var.cloudhop_${p3}]
12635
+ variable = '${variable}_SEGMENT'
12636
+ format = '${segment(p3, false)}'
12637
+
12638
+ [env_var.cloudhop_${p3}_prod]
12639
+ variable = '${variable}_PROD_SEGMENT'
12640
+ format = '${segment(p3, true)}'`;
12641
+ }).join("\n\n");
12642
+ }
12643
+ var hasCloudhopStarship = (text4) => text4.includes("[env_var.cloudhop_aws]");
12644
+ var CLOSING_GROUP = /\[\s*([-])\s*\]\(\s*fg:\s*(#[0-9a-fA-F]{3,8}|[a-z_]+)\s*\)/g;
12645
+ function addToStarshipConfig(text4) {
12646
+ if (hasCloudhopStarship(text4)) return text4;
12647
+ let result = text4;
12648
+ let powerline;
12649
+ const format = findTopLevelFormat(result);
12650
+ if (format && !/\$all\b|\$\{?env_var\}?(?![._])/.test(format.content)) {
12651
+ const { content, multiline: multiline2 } = format;
12652
+ const continuation = multiline2 ? "\\\n" : "";
12653
+ const closing = [...content.matchAll(CLOSING_GROUP)].at(-1);
12654
+ const lineBreak = [content.lastIndexOf("$line_break"), content.lastIndexOf("$character")].find(
12655
+ (i2) => i2 >= 0
12656
+ );
12657
+ let updated;
12658
+ if (closing?.index !== void 0 && (lineBreak === void 0 || closing.index < lineBreak)) {
12659
+ powerline = { glyph: closing[1], color: closing[2] };
12660
+ updated = `${content.slice(0, closing.index)}${STARSHIP_SEGMENTS}${continuation}${content.slice(closing.index)}`;
12661
+ } else if (lineBreak !== void 0) {
12662
+ updated = `${content.slice(0, lineBreak)}${STARSHIP_SEGMENTS}${continuation}${content.slice(lineBreak)}`;
12663
+ } else {
12664
+ const body = content.trimEnd();
12665
+ const tail = content.slice(body.length);
12666
+ const joiner = multiline2 && body.endsWith("\\") ? "\n" : "";
12667
+ updated = `${body}${joiner}${STARSHIP_SEGMENTS}${multiline2 ? "\\" : ""}${tail}`;
12668
+ }
12669
+ result = result.slice(0, format.start) + updated + result.slice(format.end);
12670
+ }
12671
+ const separator = result.length === 0 || result.endsWith("\n") ? "" : "\n";
12672
+ return `${result}${separator}
12673
+ ${BEGIN}
12674
+ ${starshipModules(powerline)}
12675
+ ${END}
12676
+ `;
12677
+ }
12678
+ function findTopLevelFormat(text4) {
12679
+ const firstTable = text4.search(/^[ \t]*\[/m);
12680
+ const head = firstTable === -1 ? text4 : text4.slice(0, firstTable);
12681
+ const match = /^format\s*=\s*("""|'''|"|')/m.exec(head);
12682
+ if (!match) return void 0;
12683
+ const quote2 = match[1];
12684
+ const start = match.index + match[0].length;
12685
+ const end = text4.indexOf(quote2, start);
12686
+ if (end === -1) return void 0;
12687
+ return { start, end, content: text4.slice(start, end), multiline: quote2.length === 3 };
12688
+ }
12689
+
12690
+ // src/commands/shell.ts
12691
+ var SHELLS = ["zsh", "bash", "fish", "pwsh"];
12692
+ var ALIAS_COMMANDS = "use status console region mv prod rm exec";
12693
+ var SOURCE_COMMANDS = "alias login sync ls";
12694
+ var aliasVar = (provider) => `CLOUDHOP_${provider.toUpperCase()}_ALIAS`;
12695
+ var prodVar = (provider) => `CLOUDHOP_${provider.toUpperCase()}_PROD`;
12696
+ function posixScript(shell, clis, start) {
12697
+ const wrappers = clis.map(
12698
+ ({ command, provider }) => `${command}() {
12699
+ case " $* " in *" --profile"*) command ${command} "$@"; return $?;; esac
12700
+ if [ -n "$${aliasVar(provider)}" ] && [ "$CLOUDHOP_WRAP" != 0 ]; then
12701
+ command cloudhop "$${aliasVar(provider)}" "$@"
12702
+ else
12703
+ command ${command} "$@"
12704
+ fi
12705
+ }`
12706
+ ).join("\n");
12707
+ const prompt = PROVIDER_IDS.map(
12708
+ (p3) => ` [ -n "$${aliasVar(p3)}" ] && out="$out\${out:+ }${p3}:$${aliasVar(p3)}\${${prodVar(p3)}:+ PROD}"`
12709
+ ).join("\n");
12710
+ 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`;
12711
+ const promptHook = shell === "zsh" ? `_cloudhop_show_prompt() {
12712
+ [ "$CLOUDHOP_PROMPT" = 0 ] && return 0
12713
+ case "$PROMPT$RPROMPT" in *starship*|*oh-my-posh*|*_cloudhop_rprompt*) return 0 ;; esac
12714
+ setopt prompt_subst
12715
+ RPROMPT='$(_cloudhop_rprompt)'"\${RPROMPT:+ $RPROMPT}"
12716
+ }
12717
+ # Sky for the alias, rose for production, like the CLI.
12718
+ _cloudhop_rprompt() {
12719
+ local p; p="$(cloudhop_prompt)"; [ -n "$p" ] || return 0
12720
+ case "$p" in *PROD*) printf '%%F{204}%s%%f' "$p" ;; *) printf '%%F{75}%s%%f' "$p" ;; esac
12721
+ }
12722
+ add-zsh-hook precmd _cloudhop_show_prompt` : `# \\001 and \\002 tell readline the colors take no room, so long lines still wrap right.
12723
+ _cloudhop_ps1() {
12724
+ local p c=75; p="$(cloudhop_prompt)"; [ -n "$p" ] || return 0
12725
+ case "$p" in *PROD*) c=204 ;; esac
12726
+ printf '\\001\\033[38;5;%sm\\002[%s]\\001\\033[0m\\002 ' "$c" "$p"
12727
+ }
12728
+ _cloudhop_show_prompt() {
12729
+ [ "$CLOUDHOP_PROMPT" = 0 ] && return 0
12730
+ case "$PS1" in *starship*|*oh-my-posh*|*_cloudhop_ps1*) return 0 ;; esac
12731
+ PS1='$(_cloudhop_ps1)'"$PS1"
12732
+ }`;
12733
+ const completion = shell === "zsh" ? `_cloudhop_words() { command cloudhop __complete "$1" 2>/dev/null }
12734
+ _hop() { (( CURRENT == 2 )) && { local -a a; a=(\${(f)"$(_cloudhop_words aliases)"}); compadd -a a; } }
12735
+ _cloudhop() {
12736
+ local -a w
12737
+ if (( CURRENT == 2 )); then w=(\${(f)"$(_cloudhop_words commands)"}); compadd -a w; return; fi
12738
+ (( CURRENT == 3 )) || return
12739
+ case " ${ALIAS_COMMANDS} " in *" $words[2] "*) w=(\${(f)"$(_cloudhop_words aliases)"});; esac
12740
+ case " ${SOURCE_COMMANDS} " in *" $words[2] "*) w=(\${(f)"$(_cloudhop_words sources)"});; esac
12741
+ compadd -a w
12742
+ }
12743
+ (( $+functions[compdef] )) && { compdef _hop hop; compdef _cloudhop cloudhop; }` : `_cloudhop_complete() {
12744
+ local cur=\${COMP_WORDS[COMP_CWORD]} cmd=\${COMP_WORDS[0]##*/} kind=""
12745
+ if [ "$cmd" = hop ]; then [ "$COMP_CWORD" -eq 1 ] && kind=aliases
12746
+ elif [ "$COMP_CWORD" -eq 1 ]; then kind=commands
12747
+ elif [ "$COMP_CWORD" -eq 2 ]; then
12748
+ case " ${ALIAS_COMMANDS} " in *" \${COMP_WORDS[1]} "*) kind=aliases;; esac
12749
+ case " ${SOURCE_COMMANDS} " in *" \${COMP_WORDS[1]} "*) kind=sources;; esac
12750
+ fi
12751
+ [ -n "$kind" ] && COMPREPLY=($(compgen -W "$(command cloudhop __complete $kind 2>/dev/null)" -- "$cur"))
12752
+ }
12753
+ complete -o default -F _cloudhop_complete hop cloudhop`;
12754
+ return `# cloudhop shell integration (${shell}). Load it${shell === "zsh" ? " after compinit" : ""}: ${LOAD_LINE[shell]}
12755
+ # - \`cloudhop use <alias>\` switches this shell; \`cloudhop use --off\` goes back.
12756
+ # - A folder with a trusted .cloudhoprc switches on cd (off: CLOUDHOP_AUTO_USE=0).
12757
+ # - 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).
12758
+ # - Your prompt shows the alias in use (off: CLOUDHOP_PROMPT=0).
12759
+ cloudhop() {
12760
+ case "$1" in
12761
+ use)
12762
+ local out; out="$(command cloudhop use --emit ${shell} "\${@:2}")" || return $?
12763
+ eval "$out"; _cloudhop_rc_seen=\${CLOUDHOP_RC:-$_cloudhop_rc_seen} ;;
12764
+ allow|deny) command cloudhop "$@" || return $?; _cloudhop_rc_seen=-; _cloudhop_cd ;;
12765
+ *) command cloudhop "$@" ;;
12766
+ esac
12767
+ }
12768
+ _cloudhop_cd() {
12769
+ [ "$CLOUDHOP_AUTO_USE" = 0 ] && return
12770
+ [ "$PWD" = "$_cloudhop_pwd" ] && [ "$_cloudhop_rc_seen" != - ] && return
12771
+ _cloudhop_pwd=$PWD
12772
+ local dir=$PWD rc=
12773
+ while :; do
12774
+ if [ -f "$dir/.cloudhoprc" ]; then rc="$dir/.cloudhoprc"; break; fi
12775
+ { [ -z "$dir" ] || [ "$dir" = / ]; } && break
12776
+ dir="\${dir%/*}"
12777
+ done
12778
+ [ "$rc" = "$_cloudhop_rc_seen" ] && return
12779
+ _cloudhop_rc_seen=$rc
12780
+ local out
12781
+ if [ -n "$rc" ]; then out="$(command cloudhop use --emit ${shell} --from "$rc")"
12782
+ elif [ -n "$CLOUDHOP_RC$_cloudhop_start" ]; then out="$(command cloudhop use --emit ${shell} --leave)"
12783
+ else return 0; fi
12784
+ eval "$out"
12785
+ }
12786
+ ${hook}
12787
+ ${start ? "# New terminals start with your default alias (`cloudhop use <alias> --default`).\n_cloudhop_start=1 _cloudhop_rc_seen=-\n_cloudhop_cd\nunset _cloudhop_start" : "_cloudhop_cd"}
12788
+ ${wrappers}
12789
+ ${completion}
12790
+ # The aliases in use, e.g. "aws:acme-dev". Shown in your prompt by itself
12791
+ # (off: CLOUDHOP_PROMPT=0); Starship gets its own segments from \`cloudhop shell\`.
12792
+ cloudhop_prompt() {
12793
+ local out=
12794
+ ${prompt}
12795
+ [ -n "$out" ] && printf '%s' "$out"
12796
+ }
12797
+ ${promptHook}
12798
+ `;
12799
+ }
12800
+ function fishScript(clis, start) {
12801
+ const wrappers = clis.map(
12802
+ ({ command, provider }) => `function ${command} --wraps ${command}
12803
+ if test -n "$${aliasVar(provider)}" -a "$CLOUDHOP_WRAP" != 0; and not string match -q -- '--profile*' $argv
12804
+ command cloudhop $${aliasVar(provider)} $argv
12805
+ else
12806
+ command ${command} $argv
12807
+ end
12808
+ end`
12809
+ ).join("\n");
12810
+ const prompt = PROVIDER_IDS.map(
12811
+ (p3) => ` test -n "$${aliasVar(p3)}"; and set out $out "${p3}:$${aliasVar(p3)}"(test -n "$${prodVar(p3)}"; and echo ' PROD')`
12812
+ ).join("\n");
12813
+ return `# cloudhop shell integration (fish): ${LOAD_LINE.fish}
12814
+ # - \`cloudhop use <alias>\` switches this shell; \`cloudhop use --off\` goes back.
12815
+ # - A folder with a trusted .cloudhoprc switches on cd (off: set -gx CLOUDHOP_AUTO_USE 0).
12816
+ # - With an alias in use, the cloud CLI signs in when needed and asks before changing production (off: set -gx CLOUDHOP_WRAP 0).
12817
+ function cloudhop
12818
+ switch "$argv[1]"
12819
+ case use
12820
+ set -l out (command cloudhop use --emit fish $argv[2..-1]); or return $status
12821
+ printf '%s\\n' $out | source
12822
+ set -q CLOUDHOP_RC; and set -g _cloudhop_rc_seen $CLOUDHOP_RC
12823
+ case allow deny
12824
+ command cloudhop $argv; or return $status
12825
+ set -g _cloudhop_rc_seen -
12826
+ _cloudhop_cd
12827
+ case '*'
12828
+ command cloudhop $argv
12829
+ end
12830
+ end
12831
+ function _cloudhop_cd --on-variable PWD
12832
+ test "$CLOUDHOP_AUTO_USE" = 0; and return
12833
+ set -l dir $PWD
12834
+ set -l rc ''
12835
+ while true
12836
+ if test -f "$dir/.cloudhoprc"; set rc "$dir/.cloudhoprc"; break; end
12837
+ if test -z "$dir" -o "$dir" = /; break; end
12838
+ set dir (string replace -r '/[^/]*$' '' -- $dir)
12839
+ end
12840
+ test "$rc" = "$_cloudhop_rc_seen"; and return
12841
+ set -g _cloudhop_rc_seen $rc
12842
+ if test -n "$rc"
12843
+ command cloudhop use --emit fish --from $rc | source
12844
+ else if test -n "$CLOUDHOP_RC$_cloudhop_start"
12845
+ command cloudhop use --emit fish --leave | source
12846
+ end
12847
+ end
12848
+ ${start ? "# New terminals start with your default alias (`cloudhop use <alias> --default`).\nset -g _cloudhop_start 1\nset -g _cloudhop_rc_seen -\n_cloudhop_cd\nset -e _cloudhop_start" : "_cloudhop_cd"}
12849
+ ${wrappers}
12850
+ complete -c hop -f -n 'test (count (commandline -opc)) -eq 1' -a '(command cloudhop __complete aliases 2>/dev/null)'
12851
+ complete -c cloudhop -f -n 'test (count (commandline -opc)) -eq 1' -a '(command cloudhop __complete commands 2>/dev/null)'
12852
+ 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)'
12853
+ 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)'
12854
+ # The aliases in use, e.g. "aws:acme-dev", at the right of the prompt unless
12855
+ # something else (like Starship) draws it (off: set -gx CLOUDHOP_PROMPT 0).
12856
+ function cloudhop_prompt
12857
+ set -l out
12858
+ ${prompt}
12859
+ test -n "$out"; and printf '%s' (string join ' ' $out)
12860
+ end
12861
+ if not functions -q fish_right_prompt
12862
+ function fish_right_prompt
12863
+ test "$CLOUDHOP_PROMPT" = 0; and return
12864
+ set -l label (cloudhop_prompt)
12865
+ test -n "$label"; or return
12866
+ if string match -q '*PROD*' -- $label; set_color fb7185; else; set_color 3bb8f5; end
12867
+ printf '%s' $label; set_color normal
12868
+ end
12869
+ end
12870
+ `;
12871
+ }
12872
+ function pwshScript(clis, start) {
12873
+ const wrappers = clis.map(
12874
+ ({
12875
+ command,
12876
+ provider
12877
+ }) => `$script:CloudhopCli_${provider} = (Get-Command ${command} -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1).Source
12878
+ function ${command} {
12879
+ if ($env:${aliasVar(provider)} -and $env:CLOUDHOP_WRAP -ne '0' -and -not ($args -like '--profile*')) { & $script:CloudhopExe $env:${aliasVar(provider)} @args }
12880
+ elseif ($script:CloudhopCli_${provider}) { & $script:CloudhopCli_${provider} @args }
12881
+ else { Write-Error '${command}: command not found' }
12882
+ }`
12883
+ ).join("\n");
12884
+ const prompt = PROVIDER_IDS.map(
12885
+ (p3) => ` if ($env:${aliasVar(p3)}) { $out += "${p3}:$env:${aliasVar(p3)}$(if ($env:${prodVar(p3)}) { ' PROD' })" }`
12886
+ ).join("\n");
12887
+ return `# cloudhop shell integration (PowerShell): ${LOAD_LINE.pwsh}
12888
+ # - \`cloudhop use <alias>\` switches this shell; \`cloudhop use --off\` goes back.
12889
+ # - A folder with a trusted .cloudhoprc switches on cd (off: $env:CLOUDHOP_AUTO_USE = '0').
12890
+ # - With an alias in use, the cloud CLI signs in when needed and asks before changing production (off: $env:CLOUDHOP_WRAP = '0').
12891
+ $script:CloudhopExe = (Get-Command cloudhop -CommandType Application, ExternalScript | Select-Object -First 1).Source
12892
+ function cloudhop {
12893
+ if ($args.Count -gt 0 -and $args[0] -eq 'use') {
12894
+ $out = & $script:CloudhopExe use --emit pwsh @($args | Select-Object -Skip 1)
12895
+ if ($LASTEXITCODE -eq 0) { $out | Out-String | Invoke-Expression; if ($env:CLOUDHOP_RC) { $script:CloudhopRcSeen = $env:CLOUDHOP_RC } }
12896
+ } elseif ($args.Count -gt 0 -and $args[0] -in 'allow', 'deny') {
12897
+ & $script:CloudhopExe @args
12898
+ if ($LASTEXITCODE -eq 0) { $script:CloudhopRcSeen = '-'; $script:CloudhopPwd = ''; _CloudhopCd }
12899
+ } else { & $script:CloudhopExe @args }
12900
+ }
12901
+ function _CloudhopCd {
12902
+ if ($env:CLOUDHOP_AUTO_USE -eq '0' -or $PWD.ProviderPath -eq $script:CloudhopPwd) { return }
12903
+ $script:CloudhopPwd = $PWD.ProviderPath
12904
+ $dir = $PWD.ProviderPath; $rc = ''
12905
+ while ($dir) {
12906
+ $file = Join-Path $dir '.cloudhoprc'
12907
+ if (Test-Path -LiteralPath $file -PathType Leaf) { $rc = $file; break }
12908
+ $dir = Split-Path -Parent $dir
12909
+ }
12910
+ if ($rc -eq $script:CloudhopRcSeen) { return }
12911
+ $script:CloudhopRcSeen = $rc
12912
+ $out = if ($rc) { & $script:CloudhopExe use --emit pwsh --from $rc } elseif ($env:CLOUDHOP_RC -or $script:CloudhopStart) { & $script:CloudhopExe use --emit pwsh --leave }
12913
+ $script:CloudhopStart = $false
12914
+ if ($out) { $out | Out-String | Invoke-Expression }
12915
+ }
12916
+ ${start ? "# New terminals start with your default alias (`cloudhop use <alias> --default`).\n$script:CloudhopStart = $true\n" : ""}$script:CloudhopPrompt = $function:prompt
12917
+ function prompt {
12918
+ _CloudhopCd
12919
+ $out = & $script:CloudhopPrompt
12920
+ # The alias in use before your prompt, unless Starship draws it (off: $env:CLOUDHOP_PROMPT = '0').
12921
+ if ($env:CLOUDHOP_PROMPT -ne '0' -and "$script:CloudhopPrompt" -notmatch 'starship|oh-my-posh') {
12922
+ $label = cloudhop_prompt
12923
+ if ($label) {
12924
+ if (-not $Host.UI.SupportsVirtualTerminal) { return "[$label] $out" }
12925
+ $color = if ($label -match 'PROD') { 204 } else { 75 }
12926
+ return "$([char]27)[38;5;$($color)m[$label]$([char]27)[0m $out"
12927
+ }
12928
+ }
12929
+ $out
12930
+ }
12931
+ ${wrappers}
12932
+ Register-ArgumentCompleter -Native -CommandName hop, cloudhop -ScriptBlock {
12933
+ param($word, $ast)
12934
+ $n = $ast.CommandElements.Count - [int]($word -eq '')
12935
+ $kind = if ($ast.CommandElements[0].Value -match 'hop$' -and $ast.CommandElements[0].Value -notmatch 'cloudhop') { if ($n -le 1) { 'aliases' } }
12936
+ elseif ($n -le 1) { 'commands' }
12937
+ elseif ($n -eq 2 -and ' ${ALIAS_COMMANDS} ' -match " $($ast.CommandElements[1].Value) ") { 'aliases' }
12938
+ elseif ($n -eq 2 -and ' ${SOURCE_COMMANDS} ' -match " $($ast.CommandElements[1].Value) ") { 'sources' }
12939
+ if ($kind) { & $script:CloudhopExe __complete $kind | Where-Object { $_ -like "$word*" } }
12940
+ }
12941
+ function cloudhop_prompt {
12942
+ $out = @()
12943
+ ${prompt}
12944
+ $out -join ' '
12945
+ }
12946
+ `;
12947
+ }
12948
+ function shellScript(shell, clis, start = false) {
12949
+ if (shell === "fish") return fishScript(clis, start);
12950
+ if (shell === "pwsh") return pwshScript(clis, start);
12951
+ return posixScript(shell, clis, start);
12952
+ }
12953
+ function starshipSnippet() {
12954
+ return `# cloudhop: the alias each cloud uses in this shell (\`cloudhop use\`, .cloudhoprc).
12955
+ # \`cloudhop shell\` adds this for you. By hand: append it to starship.toml and, if
12956
+ # your config has a top-level \`format\`, add this where you want the segments:
12957
+ # ${STARSHIP_SEGMENTS}
12958
+
12959
+ ${starshipModules()}
12960
+ `;
12961
+ }
12962
+ var quote = (shell, value) => shell === "pwsh" ? `'${value.replace(/'/g, "''")}'` : `'${value.replace(/'/g, "'\\''")}'`;
12963
+ function shellExports(shell, env) {
12964
+ return Object.entries(env).map(([key2, value]) => {
12965
+ if (value === void 0) {
12966
+ if (shell === "fish") return `set -e ${key2}`;
12967
+ if (shell === "pwsh") return `Remove-Item Env:${key2} -ErrorAction SilentlyContinue`;
12968
+ return `unset ${key2}`;
12969
+ }
12970
+ if (shell === "fish") return `set -gx ${key2} ${quote(shell, value)}`;
12971
+ if (shell === "pwsh") return `$env:${key2} = ${quote(shell, value)}`;
12972
+ return `export ${key2}=${quote(shell, value)}`;
12973
+ }).join("\n");
12974
+ }
12975
+ function detectShell() {
12976
+ if (process.platform === "win32") return "pwsh";
12977
+ const name = (process.env.SHELL ?? "").split("/").pop();
12978
+ return SHELLS.includes(name) ? name : "bash";
12979
+ }
12980
+ function parseShell(value) {
12981
+ if (!value) return detectShell();
12982
+ const shell = value === "powershell" ? "pwsh" : value;
12983
+ if (!SHELLS.includes(shell)) {
12984
+ throw new CloudhopError(
12985
+ "INVALID_NAME",
12986
+ `Unknown shell "${value}".`,
12987
+ `Use one of: ${SHELLS.join(", ")}.`
12988
+ );
12989
+ }
12990
+ return shell;
12991
+ }
12992
+ var RC_FILES = {
12993
+ zsh: (home) => join6(home, ".zshrc"),
12994
+ bash: (home) => join6(home, ".bashrc"),
12995
+ fish: (home) => join6(home, ".config", "fish", "config.fish"),
12996
+ pwsh: (home) => process.platform === "win32" ? join6(home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1") : join6(home, ".config", "powershell", "Microsoft.PowerShell_profile.ps1")
12997
+ };
12998
+ var LOAD_LINE = {
12999
+ zsh: 'eval "$(cloudhop shell zsh)"',
13000
+ bash: 'eval "$(cloudhop shell bash)"',
13001
+ fish: "cloudhop shell fish | source",
13002
+ pwsh: "cloudhop shell pwsh | Out-String | Invoke-Expression"
13003
+ };
13004
+ function shellIntegrationInstalled() {
13005
+ const shell = detectShell();
13006
+ const read = (file) => {
13007
+ try {
13008
+ return readFileSync2(file, "utf8");
13009
+ } catch {
13010
+ return void 0;
13011
+ }
13012
+ };
13013
+ const rc = read(RC_FILES[shell](homedir3())) ?? "";
13014
+ if (!rc.includes(LOAD_LINE[shell])) return false;
13015
+ const starship = read(starshipFile());
13016
+ if (starship === void 0) return !rc.includes("starship init");
13017
+ return hasCloudhopStarship(starship);
13018
+ }
13019
+ var starshipFile = () => process.env.STARSHIP_CONFIG || join6(homedir3(), ".config", "starship.toml");
13020
+ async function shellCommand(args, clis, start = false) {
13021
+ const { positional, flags } = parseArgs(args);
13022
+ if (positional[0] === "starship") {
13023
+ process.stdout.write(starshipSnippet());
13024
+ return;
13025
+ }
13026
+ const setUp = positional[0] === "install" || !positional[0] && process.stdout.isTTY;
13027
+ if (!setUp) {
13028
+ process.stdout.write(shellScript(parseShell(positional[0]), clis, start));
13029
+ return;
13030
+ }
13031
+ const shell = parseShell(positional[1]);
13032
+ const file = RC_FILES[shell](homedir3());
13033
+ const line = LOAD_LINE[shell];
13034
+ const current = await readIfExists(file) ?? "";
13035
+ const starship = await starshipConfig(current);
13036
+ const needsLine = !current.includes(line);
13037
+ const needsStarship = starship !== void 0 && !hasCloudhopStarship(starship.text);
13038
+ const rcName = file.replace(homedir3(), "~");
13039
+ if (!needsLine && !needsStarship) {
13040
+ console.log(`Already set up in ${rcName}${starship ? " and your Starship prompt" : ""}.`);
13041
+ return;
13042
+ }
13043
+ const changes = [
13044
+ needsLine && `add one line to ${rcName}: \`cloudhop use\`, .cloudhoprc folders, the alias in your prompt`,
13045
+ needsStarship && `show the alias in your Starship prompt (${starship.file.replace(homedir3(), "~")}, keeping a backup)`
13046
+ ].filter(Boolean);
13047
+ if (!flags.yes) {
13048
+ if (!interactive())
13049
+ throw new CloudhopError(
13050
+ "NOT_INTERACTIVE",
13051
+ `Rerun with --yes to ${changes.join(" and ")}.${needsLine ? `
13052
+ Or add this line to ${file} yourself:
13053
+ ${line}` : ""}`
13054
+ );
13055
+ console.log(`cloudhop will:
13056
+ ${changes.map((c3) => ` - ${c3}`).join("\n")}`);
13057
+ const ok = answer(await dist_exports.confirm({ message: "Set up your terminal?", initialValue: true }));
13058
+ if (!ok) return;
13059
+ }
13060
+ if (needsLine) {
13061
+ await mkdir4(dirname4(file), { recursive: true });
13062
+ const note2 = shell === "zsh" ? " # after compinit" : "";
13063
+ await appendFile(
13064
+ file,
13065
+ `${current && !current.endsWith("\n") ? "\n" : ""}
13066
+ # cloudhop: \`cloudhop use\`, .cloudhoprc folders, tab completion, prompt${note2}
13067
+ ${line}
13068
+ `
13069
+ );
13070
+ console.log(`${paint("green", "\u2714")} Added to ${rcName}.`);
13071
+ }
13072
+ if (needsStarship) {
13073
+ await mkdir4(dirname4(starship.file), { recursive: true });
13074
+ if (starship.exists) await writeFile4(`${starship.file}.cloudhop-backup`, starship.text);
13075
+ await writeFile4(starship.file, addToStarshipConfig(starship.text));
13076
+ console.log(
13077
+ `${paint("green", "\u2714")} Your Starship prompt shows the alias in use (backup: ${starship.file.replace(homedir3(), "~")}.cloudhop-backup).`
13078
+ );
13079
+ }
13080
+ console.log(
13081
+ `Open a new terminal (or run ${paint("sky", shell === "pwsh" ? ". $PROFILE" : `exec ${shell}`)}), then:`
13082
+ );
13083
+ console.log(` ${paint("sky", "cloudhop use <alias>")} switch this terminal to an account`);
13084
+ console.log(` ${paint("sky", "aws s3 ls")} plain aws commands, as that alias`);
13085
+ }
13086
+ async function starshipConfig(rc) {
13087
+ const file = starshipFile();
13088
+ const text4 = await readIfExists(file);
13089
+ if (text4 === void 0 && !rc.includes("starship init") && !process.env.STARSHIP_SHELL) {
13090
+ return void 0;
13091
+ }
13092
+ return { file, text: text4 ?? "", exists: text4 !== void 0 };
13093
+ }
13094
+ async function readIfExists(file) {
13095
+ try {
13096
+ return await readFile4(file, "utf8");
13097
+ } catch (error) {
13098
+ if (error.code === "ENOENT") return void 0;
13099
+ throw error;
13100
+ }
13101
+ }
13102
+ async function completeCommand(app, kind) {
13103
+ if (kind === "commands") {
13104
+ console.log(
13105
+ [...RESERVED_NAMES, "each", "shell", "use"].filter((c3, i2, a2) => a2.indexOf(c3) === i2).sort().join("\n")
13106
+ );
13107
+ return;
13108
+ }
13109
+ const config = await app.config();
13110
+ const words = kind === "sources" ? config.sources.map((s) => s.id) : config.aliases.map((a2) => a2.name);
13111
+ if (words.length) console.log(words.join("\n"));
13112
+ }
13113
+
13114
+ // src/commands/use.ts
13115
+ var activeVars = (provider) => {
13116
+ const prefix = `CLOUDHOP_${provider.toUpperCase()}`;
13117
+ return {
13118
+ alias: `${prefix}_ALIAS`,
13119
+ prod: `${prefix}_PROD`,
13120
+ prompt: `${prefix}_PROMPT`,
13121
+ // One or the other, so prompt themes can style production apart (Starship's env_var).
13122
+ segment: `${prefix}_SEGMENT`,
13123
+ prodSegment: `${prefix}_PROD_SEGMENT`
13124
+ };
13125
+ };
13126
+ function activeAliases(env = process.env) {
13127
+ return Object.fromEntries(
13128
+ PROVIDER_IDS.filter((p3) => env[activeVars(p3).alias]).map((p3) => [p3, env[activeVars(p3).alias]])
13129
+ );
13130
+ }
13131
+ async function shellContext(trusted, cwd = process.cwd()) {
13132
+ const file = findFolderFile(cwd);
13133
+ const content = file ? await readFolderFile(file) : void 0;
13134
+ let folder;
13135
+ if (file && content !== void 0) {
13136
+ try {
13137
+ folder = {
13138
+ file,
13139
+ aliases: parseFolderFile(content),
13140
+ trusted: trusted.isTrusted(file, content)
13141
+ };
13142
+ } catch {
13143
+ }
13144
+ }
13145
+ return { inUse: activeAliases(), ...folder ? { folder } : {} };
13146
+ }
13147
+ var tilde2 = (path2) => path2.replace(homedir4(), "~");
13148
+ var folderProviders = () => (process.env.CLOUDHOP_RC_PROVIDERS ?? "").split(",").filter((p3) => PROVIDER_IDS.includes(p3));
13149
+ async function activate(app, alias, login) {
13150
+ const shellEnv = await app.shellEnv(alias, { login });
13151
+ const vars = activeVars(shellEnv.provider);
13152
+ const label = shellEnv.region ? `${alias} (${shellEnv.region})` : alias;
13153
+ const env = Object.fromEntries(shellEnv.clear.map((name) => [name, void 0]));
13154
+ Object.assign(env, shellEnv.env, {
13155
+ [vars.alias]: alias,
13156
+ [vars.prod]: shellEnv.prod ? "1" : void 0,
13157
+ [vars.prompt]: label,
13158
+ [vars.segment]: shellEnv.prod ? void 0 : label,
13159
+ [vars.prodSegment]: shellEnv.prod ? label : void 0,
13160
+ // The cloud-neutral pair from cloudhop 1.2, kept for prompts already using it.
13161
+ CLOUDHOP_ALIAS: alias,
13162
+ CLOUDHOP_PROD: shellEnv.prod ? "1" : void 0
13163
+ });
13164
+ const where = [shellEnv.label, shellEnv.region ?? "no region"].filter(Boolean).join(", ");
13165
+ return { provider: shellEnv.provider, env, summary: `${alias} (${where})`, prod: shellEnv.prod };
13166
+ }
13167
+ function deactivate(app, provider) {
13168
+ const vars = activeVars(provider);
13169
+ const env = Object.fromEntries(app.shellVars(provider).map((n3) => [n3, void 0]));
13170
+ for (const name of Object.values(vars)) env[name] = void 0;
13171
+ if (process.env.CLOUDHOP_ALIAS && process.env.CLOUDHOP_ALIAS === process.env[vars.alias]) {
13172
+ Object.assign(env, { CLOUDHOP_ALIAS: void 0, CLOUDHOP_PROD: void 0 });
13173
+ }
13174
+ return env;
13175
+ }
13176
+ function warnProduction(activation) {
13177
+ if (!activation.prod) return;
13178
+ const alias = activation.summary.split(" ")[0];
13179
+ notify(
13180
+ paint(
13181
+ "red",
13182
+ `${alias} is production: with the shell integration, \`aws\` asks before changes. Other tools (terraform, scripts) don't.`
13183
+ )
13184
+ );
13185
+ }
13186
+ async function useCommand(app, trusted, args) {
13187
+ const { positional, flags } = parseArgs(args);
13188
+ const shell = flags.emit ? parseShell(flags.emit) : void 0;
13189
+ if (flags.from !== void 0 || flags.leave) {
13190
+ if (!shell) throw new CloudhopError("INVALID_NAME", "--from and --leave need --emit <shell>.");
13191
+ const env2 = await folderEnv(app, trusted, flags.leave ? void 0 : flags.from);
13192
+ process.stdout.write(`${shellExports(shell, env2)}
13193
+ `);
13194
+ return 0;
13195
+ }
13196
+ if (flags.noDefault) {
13197
+ const which = positional[0];
13198
+ const cleared = await app.clearDefaultAlias(which ? cloudArg(which) : void 0);
13199
+ if (shell) process.stdout.write("\n");
13200
+ notify(
13201
+ cleared.length ? `New terminals no longer start with ${cleared.join(", ")}. This one keeps what it uses.` : "There was no default alias."
13202
+ );
13203
+ return 0;
13204
+ }
13205
+ if (flags.off) {
13206
+ if (!shell) throw noIntegration("Nothing to turn off: this shell has no cloudhop integration.");
13207
+ const which = positional[0];
13208
+ const providers = which ? [cloudArg(which)] : app.availableProviders().map((provider) => provider.id);
13209
+ const env2 = {};
13210
+ for (const provider of providers) Object.assign(env2, deactivate(app, provider));
13211
+ const remaining = folderProviders().filter((p3) => !providers.includes(p3));
13212
+ Object.assign(
13213
+ env2,
13214
+ folderMarks(remaining.length ? process.env.CLOUDHOP_RC : void 0, remaining)
13215
+ );
13216
+ process.stdout.write(`${shellExports(shell, env2)}
13217
+ `);
13218
+ notify(which ? `No ${which} alias in this shell anymore.` : "No alias in this shell anymore.");
13219
+ return 0;
13220
+ }
13221
+ const alias = positional[0] ?? await pickAlias(app, "Use which alias in this shell?");
13222
+ const activation = await activate(app, alias, true);
13223
+ if (flags.default) {
13224
+ if (activation.prod && !flags.yes) {
13225
+ throw new CloudhopError(
13226
+ "PROD_CONFIRMATION_REQUIRED",
13227
+ `${alias} is production: every new terminal would start in it.`,
13228
+ `If that's what you want, run \`cloudhop use ${alias} --default --yes\`.`
12380
13229
  );
12381
13230
  }
12382
- dist_exports.log.warn(
12383
- `${paint("red", bold("PRODUCTION"))} ${risky.join(", ")}: this may change resources.`
12384
- );
12385
- if (!answer(await dist_exports.confirm({ message: "Also run it in production?", initialValue: false })))
12386
- return 130;
13231
+ await app.setDefaultAlias(alias);
12387
13232
  }
12388
- for (const source of new Set(selected.map((a2) => a2.source))) {
12389
- await app.ensureSession(source, { interactive: tty });
13233
+ let folder;
13234
+ if (flags.save) folder = await saveFolderFile(trusted, activation.provider, alias);
13235
+ if (!shell) {
13236
+ if (!interactive()) {
13237
+ throw noIntegration("`cloudhop use` needs a terminal or the shell integration.");
13238
+ }
13239
+ return subshell(alias, activation);
12390
13240
  }
12391
- const parallel = Math.max(1, Math.min(16, Number(flags.parallel) || 4));
12392
- const outcomes = await mapLimit(selected, parallel, async (a2) => {
13241
+ const fromFolder = folderProviders().filter((p3) => p3 !== activation.provider);
13242
+ const env = { ...activation.env };
13243
+ if (folder) Object.assign(env, folderMarks(folder, [...fromFolder, activation.provider]));
13244
+ else
13245
+ Object.assign(
13246
+ env,
13247
+ folderMarks(fromFolder.length ? process.env.CLOUDHOP_RC : void 0, fromFolder)
13248
+ );
13249
+ process.stdout.write(`${shellExports(shell, env)}
13250
+ `);
13251
+ notify(`This shell now uses ${activation.summary}.`);
13252
+ if (flags.default) notify(`New terminals start with ${alias}.`);
13253
+ if (folder) notify(`Saved in ${tilde2(folder)}: this folder switches to ${alias} on cd.`);
13254
+ warnProduction(activation);
13255
+ return 0;
13256
+ }
13257
+ var folderMarks = (file, providers) => ({
13258
+ CLOUDHOP_RC: providers.length && file ? file : void 0,
13259
+ CLOUDHOP_RC_PROVIDERS: providers.length ? providers.join(",") : void 0
13260
+ });
13261
+ function cloudArg(which) {
13262
+ if (PROVIDER_IDS.includes(which)) return which;
13263
+ throw new CloudhopError(
13264
+ "INVALID_NAME",
13265
+ `Unknown cloud "${which}".`,
13266
+ `Use one of: ${PROVIDER_IDS.join(", ")}, or nothing for all of them.`
13267
+ );
13268
+ }
13269
+ var noIntegration = (message) => new CloudhopError(
13270
+ "NOT_INTERACTIVE",
13271
+ message,
13272
+ `Set it up once with \`cloudhop shell install\` (or add ${LOAD_LINE[detectShell()]} to your shell's startup file), then open a new terminal.`
13273
+ );
13274
+ async function folderEnv(app, trusted, file) {
13275
+ const previous = folderProviders();
13276
+ const activations = [];
13277
+ const path2 = file ? resolve2(file) : void 0;
13278
+ const content = path2 ? await readFolderFile(path2) : void 0;
13279
+ if (path2 && content !== void 0) {
13280
+ let aliases;
12393
13281
  try {
12394
- const r2 = await app.captureAsAlias(a2.name, cliArgs);
12395
- const outcome = { alias: a2.name, exitCode: r2.exitCode, stdout: r2.stdout, stderr: r2.stderr };
12396
- if (!flags.json) printOutcome(outcome, width);
12397
- return outcome;
13282
+ aliases = parseFolderFile(content);
12398
13283
  } catch (error) {
12399
- const outcome = { alias: a2.name, exitCode: 1, stdout: "", stderr: error.message };
12400
- if (!flags.json) printOutcome(outcome, width);
12401
- return outcome;
13284
+ notify(`${tilde2(path2)}: ${error.message}`);
13285
+ }
13286
+ if (aliases && !trusted.isTrusted(path2, content)) {
13287
+ notify(
13288
+ `${tilde2(path2)} wants this folder to use ${Object.values(aliases).join(", ")}. Run ${bold("cloudhop allow")} if you trust it.`
13289
+ );
13290
+ } else if (aliases) {
13291
+ for (const alias of Object.values(aliases)) {
13292
+ try {
13293
+ activations.push(await activate(app, alias, false));
13294
+ } catch (error) {
13295
+ notify(`${tilde2(path2)}: ${error.message}`);
13296
+ }
13297
+ }
12402
13298
  }
12403
- });
12404
- const failed = outcomes.filter((o2) => o2.exitCode !== 0);
12405
- if (flags.json) {
12406
- console.log(JSON.stringify({ version: 1, command: cliArgs, results: outcomes }, null, 2));
12407
- } else {
12408
- const summary = `${outcomes.length - failed.length} ok${failed.length ? `, ${failed.length} failed: ${failed.map((f2) => f2.alias).join(", ")}` : ""}`;
12409
- process.stderr.write(`${failed.length ? paint("red", summary) : paint("green", summary)}
12410
- `);
12411
13299
  }
12412
- return failed.length ? 1 : 0;
13300
+ const fromFolder = activations.map((a2) => a2.provider);
13301
+ const defaults = [];
13302
+ for (const [provider, alias] of Object.entries(await defaultAliases(app))) {
13303
+ if (fromFolder.includes(provider)) continue;
13304
+ const picked = process.env[activeVars(provider).alias];
13305
+ if (picked && !previous.includes(provider)) continue;
13306
+ try {
13307
+ defaults.push(await activate(app, alias, false));
13308
+ } catch (error) {
13309
+ notify(`Default alias: ${error.message}`);
13310
+ }
13311
+ }
13312
+ const applied = [...fromFolder, ...defaults.map((a2) => a2.provider)];
13313
+ const env = {};
13314
+ const left = previous.filter((p3) => !applied.includes(p3));
13315
+ for (const provider of left) Object.assign(env, deactivate(app, provider));
13316
+ for (const activation of [...activations, ...defaults]) Object.assign(env, activation.env);
13317
+ Object.assign(env, folderMarks(path2, applied));
13318
+ const back = defaults.filter((a2) => previous.includes(a2.provider));
13319
+ if (activations.length) {
13320
+ notify(`Using ${activations.map((a2) => a2.summary).join(" and ")}, from ${FOLDER_FILE}.`);
13321
+ } else if (back.length) {
13322
+ notify(`Back to your default ${back.map((a2) => a2.summary).join(" and ")}.`);
13323
+ }
13324
+ const gone = left.map((p3) => process.env[activeVars(p3).alias]).filter(Boolean);
13325
+ if (!activations.length && !back.length && gone.length) {
13326
+ notify(`Left the folder of ${gone.join(", ")}: no alias in this shell.`);
13327
+ }
13328
+ for (const activation of [...activations, ...defaults]) warnProduction(activation);
13329
+ return env;
12413
13330
  }
12414
- function printOutcome({ alias, exitCode, stdout: stdout2, stderr }, width) {
12415
- const tag = paint(exitCode === 0 ? "sky" : "red", `[${alias}]`.padEnd(width + 2));
12416
- for (const line of stdout2.replace(/\n$/, "").split("\n"))
12417
- if (line) process.stdout.write(`${tag} ${line}
12418
- `);
12419
- for (const line of stderr.replace(/\n$/, "").split("\n"))
12420
- if (line) process.stderr.write(`${tag} ${paint("dim", line)}
12421
- `);
12422
- if (exitCode !== 0) process.stderr.write(`${tag} ${paint("red", `exit ${exitCode}`)}
12423
- `);
13331
+ async function defaultAliases(app) {
13332
+ try {
13333
+ return await app.defaultAliases();
13334
+ } catch {
13335
+ return {};
13336
+ }
13337
+ }
13338
+ async function saveFolderFile(trusted, provider, alias) {
13339
+ const file = join7(process.cwd(), FOLDER_FILE);
13340
+ const content = await readFolderFile(file);
13341
+ let aliases = {};
13342
+ if (content !== void 0) {
13343
+ aliases = parseFolderFile(content);
13344
+ delete aliases.any;
13345
+ }
13346
+ aliases[provider] = alias;
13347
+ const text4 = formatFolderFile(aliases);
13348
+ await writeFile5(file, text4);
13349
+ await trusted.trust(file, text4);
13350
+ return file;
13351
+ }
13352
+ function subshell(alias, activation) {
13353
+ const env = { ...process.env, CLOUDHOP_SUBSHELL: alias };
13354
+ for (const [name, value] of Object.entries(activation.env)) {
13355
+ if (value === void 0) delete env[name];
13356
+ else env[name] = value;
13357
+ }
13358
+ const shell = process.platform === "win32" ? process.env.ComSpec ?? "cmd.exe" : process.env.SHELL ?? "/bin/sh";
13359
+ if (/cmd(\.exe)?$/i.test(shell)) {
13360
+ const label = `${activation.provider}:${alias}${activation.prod ? " PROD" : ""}`;
13361
+ env.PROMPT = `[${label}] ${process.env.PROMPT ?? "$P$G"}`;
13362
+ }
13363
+ if (process.env.CLOUDHOP_SUBSHELL) {
13364
+ notify(
13365
+ `You are already in a cloudhop shell (${process.env.CLOUDHOP_SUBSHELL}): this one goes inside it.`
13366
+ );
13367
+ }
13368
+ notify(`New shell using ${activation.summary}. Type ${bold("exit")} to go back.`);
13369
+ notify(
13370
+ paint(
13371
+ "dim",
13372
+ "Tip: `cloudhop shell install` makes `cloudhop use` switch this same terminal instead."
13373
+ )
13374
+ );
13375
+ warnProduction(activation);
13376
+ const result = spawnSync(shell, [], { stdio: "inherit", env });
13377
+ notify(`Back from ${alias}.`);
13378
+ return result.status ?? 0;
13379
+ }
13380
+ async function allowCommand(app, trusted, args, allow) {
13381
+ const { positional } = parseArgs(args);
13382
+ const given = positional[0];
13383
+ const file = given ? given.endsWith(FOLDER_FILE) ? resolve2(given) : join7(resolve2(given), FOLDER_FILE) : findFolderFile(process.cwd());
13384
+ if (!file) {
13385
+ throw new CloudhopError(
13386
+ "INVALID_NAME",
13387
+ `No ${FOLDER_FILE} in this folder or above it.`,
13388
+ "Create one with `cloudhop use <alias> --save`."
13389
+ );
13390
+ }
13391
+ if (!allow) {
13392
+ const forgotten = await trusted.forget(file);
13393
+ console.log(
13394
+ forgotten ? `${paint("green", "\u2714")} ${tilde2(file)} is no longer trusted: its folder won't switch aliases.` : `${tilde2(file)} was not trusted.`
13395
+ );
13396
+ return;
13397
+ }
13398
+ const content = await readFolderFile(file);
13399
+ if (content === void 0) throw new CloudhopError("INVALID_NAME", `Can't read ${file}.`);
13400
+ const aliases = parseFolderFile(content);
13401
+ const lines = [];
13402
+ for (const alias of Object.values(aliases)) {
13403
+ try {
13404
+ const status = await app.aliasStatus(alias);
13405
+ lines.push(
13406
+ ` ${bold(alias)} ${status.label ?? status.target} ${status.region ?? ""}${status.prod ? ` ${paint("red", "PROD")}` : ""}`
13407
+ );
13408
+ } catch {
13409
+ lines.push(
13410
+ ` ${bold(alias)} ${paint("yellow", "you don't have this alias: it will be skipped")}`
13411
+ );
13412
+ }
13413
+ }
13414
+ await trusted.trust(file, content);
13415
+ console.log(
13416
+ `${paint("green", "\u2714")} Trusted ${tilde2(file)}. In ${tilde2(dirname5(file))} this shell uses:`
13417
+ );
13418
+ console.log(lines.join("\n"));
12424
13419
  }
12425
13420
 
12426
13421
  // src/commands/manage.ts
@@ -12468,11 +13463,11 @@ var SESSION_LABEL = {
12468
13463
  };
12469
13464
  var JSON_VERSION = 1;
12470
13465
  var printJson = (value) => console.log(JSON.stringify({ version: JSON_VERSION, ...value }, null, 2));
12471
- async function listCommand(app, args = []) {
13466
+ async function listCommand(app, args = [], context) {
12472
13467
  const { positional, flags } = parseArgs(args);
12473
13468
  const sourceId = positional[0];
12474
13469
  if (flags.json) {
12475
- const overview = await app.overview();
13470
+ const overview = { ...await app.overview(), ...await context?.() };
12476
13471
  if (sourceId && !overview.sources.some((s) => s.id === sourceId)) {
12477
13472
  throw new CloudhopError("SOURCE_NOT_FOUND", `Source "${sourceId}" not found.`);
12478
13473
  }
@@ -12519,7 +13514,7 @@ async function listCommand(app, args = []) {
12519
13514
  );
12520
13515
  for (const { alias, target, region } of own) {
12521
13516
  const label = target ? target.label : paint("red", `${alias.target} (no longer available)`);
12522
- const tag = alias.prod ? ` ${paint("red", "PROD")}` : "";
13517
+ const tag = `${alias.prod ? ` ${paint("red", "PROD")}` : ""}${alias.default ? ` ${paint("dim", "default")}` : ""}`;
12523
13518
  console.log(
12524
13519
  ` ${alias.name.padEnd(nameWidth)}${(region ?? "-").padEnd(regionWidth)}${label}${tag}`
12525
13520
  );
@@ -12651,7 +13646,21 @@ async function statusCommand(app, args) {
12651
13646
  return;
12652
13647
  }
12653
13648
  const overview = await app.overview();
12654
- if (flags.json) return printJson({ sources: overview.sources });
13649
+ const inUse = activeAliases();
13650
+ const defaults = Object.fromEntries(
13651
+ overview.aliases.filter((a2) => a2.default).map((a2) => [a2.provider, a2.name])
13652
+ );
13653
+ if (flags.json) return printJson({ inUse, defaults, sources: overview.sources });
13654
+ const using = Object.entries(inUse).map(([cloud, alias]) => {
13655
+ const status = overview.aliases.find((a2) => a2.name === alias);
13656
+ const region = status?.region ? ` (${status.region})` : "";
13657
+ return `${cloud} ${bold(paint(status?.prod ? "red" : "sky", alias))}${region}${status?.prod ? ` ${paint("red", "PROD")}` : ""}`;
13658
+ });
13659
+ console.log(
13660
+ using.length ? `This terminal uses ${using.join(", ")}.` : paint("dim", "This terminal uses no alias (`cloudhop use <alias>` to pick one).")
13661
+ );
13662
+ const starts = Object.entries(defaults).map(([cloud, alias]) => `${cloud} ${alias}`);
13663
+ if (starts.length) console.log(paint("dim", `New terminals start with ${starts.join(", ")}.`));
12655
13664
  if (overview.sources.length === 0) {
12656
13665
  console.log("Nothing here yet. Run `cloudhop setup` to get started.");
12657
13666
  return;
@@ -12793,6 +13802,23 @@ async function setupCommand(app, assistants) {
12793
13802
  const addNow = answer(await dist_exports.confirm({ message: "Add an account or SSO portal now?" }));
12794
13803
  if (addNow) await addCommand(app, { positional: [], flags: {} });
12795
13804
  else dist_exports.log.info("Run `cloudhop add` whenever you are ready.");
13805
+ if (!shellIntegrationInstalled()) {
13806
+ dist_exports.note(
13807
+ [
13808
+ "`hop <alias>`, `each` and `console` work as they are. To also switch a terminal",
13809
+ "to an alias (`cloudhop use`, then plain `aws`), switch on `cd` into a folder with",
13810
+ "a .cloudhoprc, see the alias in your prompt (Starship too) and complete aliases",
13811
+ "with <Tab>, your shell loads one line from its startup file. A program can only",
13812
+ "change your terminal this way."
13813
+ ].join("\n"),
13814
+ "Terminal"
13815
+ );
13816
+ const install = answer(
13817
+ await dist_exports.confirm({ message: "Set up your terminal for cloudhop?", initialValue: true })
13818
+ );
13819
+ if (install) await shellCommand(["install", "--yes"], []);
13820
+ else dist_exports.log.info("Later: `cloudhop shell install`. `cloudhop use` still works, in a new shell.");
13821
+ }
12796
13822
  const detected = assistants.detect();
12797
13823
  if (detected.length > 0) {
12798
13824
  const names = detected.map((id) => ASSISTANTS[id].name).join(", ");
@@ -12830,268 +13856,37 @@ async function doctorCommand(app) {
12830
13856
  );
12831
13857
  }
12832
13858
 
12833
- // src/commands/shell.ts
12834
- import { existsSync as existsSync3 } from "fs";
12835
- import { appendFile, mkdir as mkdir3, readFile as readFile3 } from "fs/promises";
12836
- import { homedir as homedir3 } from "os";
12837
- import { dirname as dirname3, join as join5 } from "path";
12838
- var SHELLS = ["zsh", "bash", "fish", "pwsh"];
12839
- var ALIAS_COMMANDS = "use status console region mv prod rm exec";
12840
- var SOURCE_COMMANDS = "alias login sync ls";
12841
- var SCRIPTS = {
12842
- zsh: `# cloudhop shell integration (zsh). Load it after compinit: eval "$(cloudhop shell zsh)"
12843
- cloudhop() {
12844
- if [ "$1" = "use" ]; then
12845
- local out; out="$(command cloudhop use --emit zsh "\${@:2}")" || return $?
12846
- eval "$out"
12847
- else
12848
- command cloudhop "$@"
12849
- fi
12850
- }
12851
- _cloudhop_words() { command cloudhop __complete "$1" 2>/dev/null }
12852
- _hop() { (( CURRENT == 2 )) && { local -a a; a=(\${(f)"$(_cloudhop_words aliases)"}); compadd -a a; } }
12853
- _cloudhop() {
12854
- local -a w
12855
- if (( CURRENT == 2 )); then w=(\${(f)"$(_cloudhop_words commands)"}); compadd -a w; return; fi
12856
- (( CURRENT == 3 )) || return
12857
- case " ${ALIAS_COMMANDS} " in *" $words[2] "*) w=(\${(f)"$(_cloudhop_words aliases)"});; esac
12858
- case " ${SOURCE_COMMANDS} " in *" $words[2] "*) w=(\${(f)"$(_cloudhop_words sources)"});; esac
12859
- compadd -a w
12860
- }
12861
- (( $+functions[compdef] )) && { compdef _hop hop; compdef _cloudhop cloudhop; }
12862
- # Add $(cloudhop_prompt) to your prompt to see the alias set with \`cloudhop use\`.
12863
- cloudhop_prompt() { [ -n "$CLOUDHOP_ALIAS" ] && printf '\u2601 %s%s' "$CLOUDHOP_ALIAS" "\${CLOUDHOP_PROD:+ PROD}"; }
12864
- `,
12865
- bash: `# cloudhop shell integration (bash): eval "$(cloudhop shell bash)"
12866
- cloudhop() {
12867
- if [ "$1" = "use" ]; then
12868
- local out; out="$(command cloudhop use --emit bash "\${@:2}")" || return $?
12869
- eval "$out"
12870
- else
12871
- command cloudhop "$@"
12872
- fi
12873
- }
12874
- _cloudhop_complete() {
12875
- local cur=\${COMP_WORDS[COMP_CWORD]} cmd=\${COMP_WORDS[0]##*/} kind=""
12876
- if [ "$cmd" = hop ]; then [ "$COMP_CWORD" -eq 1 ] && kind=aliases
12877
- elif [ "$COMP_CWORD" -eq 1 ]; then kind=commands
12878
- elif [ "$COMP_CWORD" -eq 2 ]; then
12879
- case " ${ALIAS_COMMANDS} " in *" \${COMP_WORDS[1]} "*) kind=aliases;; esac
12880
- case " ${SOURCE_COMMANDS} " in *" \${COMP_WORDS[1]} "*) kind=sources;; esac
12881
- fi
12882
- [ -n "$kind" ] && COMPREPLY=($(compgen -W "$(command cloudhop __complete $kind 2>/dev/null)" -- "$cur"))
12883
- }
12884
- complete -o default -F _cloudhop_complete hop cloudhop
12885
- # Add $(cloudhop_prompt) to PS1 to see the alias set with \`cloudhop use\`.
12886
- cloudhop_prompt() { [ -n "$CLOUDHOP_ALIAS" ] && printf '\u2601 %s%s' "$CLOUDHOP_ALIAS" "\${CLOUDHOP_PROD:+ PROD}"; }
12887
- `,
12888
- fish: `# cloudhop shell integration (fish): cloudhop shell fish | source
12889
- function cloudhop
12890
- if test "$argv[1]" = use
12891
- set -l out (command cloudhop use --emit fish $argv[2..-1]); or return $status
12892
- printf '%s\\n' $out | source
12893
- else
12894
- command cloudhop $argv
12895
- end
12896
- end
12897
- complete -c hop -f -n 'test (count (commandline -opc)) -eq 1' -a '(command cloudhop __complete aliases 2>/dev/null)'
12898
- complete -c cloudhop -f -n 'test (count (commandline -opc)) -eq 1' -a '(command cloudhop __complete commands 2>/dev/null)'
12899
- complete -c cloudhop -f -n '__fish_seen_subcommand_from ${ALIAS_COMMANDS}; and test (count (commandline -opc)) -eq 2' -a '(command cloudhop __complete aliases 2>/dev/null)'
12900
- complete -c cloudhop -f -n '__fish_seen_subcommand_from ${SOURCE_COMMANDS}; and test (count (commandline -opc)) -eq 2' -a '(command cloudhop __complete sources 2>/dev/null)'
12901
- # Call cloudhop_prompt from fish_prompt to see the alias set with \`cloudhop use\`.
12902
- function cloudhop_prompt
12903
- test -n "$CLOUDHOP_ALIAS"; and printf '\u2601 %s%s' $CLOUDHOP_ALIAS (test -n "$CLOUDHOP_PROD"; and echo ' PROD')
12904
- end
12905
- `,
12906
- pwsh: `# cloudhop shell integration (PowerShell): cloudhop shell pwsh | Out-String | Invoke-Expression
12907
- $script:CloudhopExe = (Get-Command cloudhop -CommandType Application, ExternalScript | Select-Object -First 1).Source
12908
- function cloudhop {
12909
- if ($args.Count -gt 0 -and $args[0] -eq 'use') {
12910
- $out = & $script:CloudhopExe use --emit pwsh @($args | Select-Object -Skip 1)
12911
- if ($LASTEXITCODE -eq 0) { $out | Out-String | Invoke-Expression }
12912
- } else { & $script:CloudhopExe @args }
12913
- }
12914
- Register-ArgumentCompleter -Native -CommandName hop, cloudhop -ScriptBlock {
12915
- param($word, $ast)
12916
- $n = $ast.CommandElements.Count - [int]($word -eq '')
12917
- $kind = if ($ast.CommandElements[0].Value -match 'hop$' -and $ast.CommandElements[0].Value -notmatch 'cloudhop') { if ($n -le 1) { 'aliases' } }
12918
- elseif ($n -le 1) { 'commands' }
12919
- elseif ($n -eq 2 -and ' ${ALIAS_COMMANDS} ' -match " $($ast.CommandElements[1].Value) ") { 'aliases' }
12920
- elseif ($n -eq 2 -and ' ${SOURCE_COMMANDS} ' -match " $($ast.CommandElements[1].Value) ") { 'sources' }
12921
- if ($kind) { & $script:CloudhopExe __complete $kind | Where-Object { $_ -like "$word*" } }
12922
- }
12923
- function cloudhop_prompt { if ($env:CLOUDHOP_ALIAS) { "\u2601 $env:CLOUDHOP_ALIAS$(if ($env:CLOUDHOP_PROD) { ' PROD' })" } }
12924
- `
12925
- };
12926
- var quote = (shell, value) => shell === "pwsh" ? `'${value.replace(/'/g, "''")}'` : `'${value.replace(/'/g, "'\\''")}'`;
12927
- function shellExports(shell, env) {
12928
- return Object.entries(env).map(([key, value]) => {
12929
- if (value === void 0) {
12930
- if (shell === "fish") return `set -e ${key}`;
12931
- if (shell === "pwsh") return `Remove-Item Env:${key} -ErrorAction SilentlyContinue`;
12932
- return `unset ${key}`;
12933
- }
12934
- if (shell === "fish") return `set -gx ${key} ${quote(shell, value)}`;
12935
- if (shell === "pwsh") return `$env:${key} = ${quote(shell, value)}`;
12936
- return `export ${key}=${quote(shell, value)}`;
12937
- }).join("\n");
12938
- }
12939
- function detectShell() {
12940
- if (process.platform === "win32") return "pwsh";
12941
- const name = (process.env.SHELL ?? "").split("/").pop();
12942
- return SHELLS.includes(name) ? name : "bash";
12943
- }
12944
- function parseShell(value) {
12945
- if (!value) return detectShell();
12946
- const shell = value === "powershell" ? "pwsh" : value;
12947
- if (!SHELLS.includes(shell)) {
12948
- throw new CloudhopError(
12949
- "INVALID_NAME",
12950
- `Unknown shell "${value}".`,
12951
- `Use one of: ${SHELLS.join(", ")}.`
12952
- );
12953
- }
12954
- return shell;
12955
- }
12956
- var RC_FILES = {
12957
- zsh: (home) => join5(home, ".zshrc"),
12958
- bash: (home) => join5(home, ".bashrc"),
12959
- fish: (home) => join5(home, ".config", "fish", "config.fish"),
12960
- pwsh: (home) => process.platform === "win32" ? join5(home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1") : join5(home, ".config", "powershell", "Microsoft.PowerShell_profile.ps1")
12961
- };
12962
- var LOAD_LINE = {
12963
- zsh: 'eval "$(cloudhop shell zsh)"',
12964
- bash: 'eval "$(cloudhop shell bash)"',
12965
- fish: "cloudhop shell fish | source",
12966
- pwsh: "cloudhop shell pwsh | Out-String | Invoke-Expression"
12967
- };
12968
- async function shellCommand(args) {
12969
- const { positional, flags } = parseArgs(args);
12970
- if (positional[0] !== "install") {
12971
- process.stdout.write(SCRIPTS[parseShell(positional[0])]);
12972
- return;
12973
- }
12974
- const shell = parseShell(positional[1]);
12975
- const file = RC_FILES[shell](homedir3());
12976
- const line = LOAD_LINE[shell];
12977
- const current = existsSync3(file) ? await readFile3(file, "utf8") : "";
12978
- if (current.includes(line)) {
12979
- console.log(`Already set up in ${file.replace(homedir3(), "~")}.`);
12980
- return;
12981
- }
12982
- if (!flags.yes) {
12983
- if (!interactive())
12984
- throw new CloudhopError(
12985
- "NOT_INTERACTIVE",
12986
- `Add this line to ${file} yourself, or rerun with --yes:
12987
- ${line}`
12988
- );
12989
- const ok = answer(
12990
- await dist_exports.confirm({
12991
- message: `Add cloudhop to ${file.replace(homedir3(), "~")}?`,
12992
- initialValue: true
12993
- })
12994
- );
12995
- if (!ok) return;
12996
- }
12997
- await mkdir3(dirname3(file), { recursive: true });
12998
- const note2 = shell === "zsh" ? " # after compinit" : "";
12999
- await appendFile(
13000
- file,
13001
- `${current && !current.endsWith("\n") ? "\n" : ""}
13002
- # cloudhop: \`cloudhop use\`, tab completion and cloudhop_prompt${note2}
13003
- ${line}
13004
- `
13005
- );
13006
- console.log(
13007
- `${paint("green", "\u2714")} Added to ${file.replace(homedir3(), "~")}. Open a new terminal to use it.`
13008
- );
13009
- }
13010
- async function useCommand(app, args) {
13011
- const { positional, flags } = parseArgs(args);
13012
- const shell = flags.emit ? parseShell(flags.emit) : void 0;
13013
- if (!shell) {
13014
- throw new CloudhopError(
13015
- "NOT_INTERACTIVE",
13016
- "`cloudhop use` needs the shell integration to change your current shell.",
13017
- `Set it up once with \`cloudhop shell install\` (or add ${LOAD_LINE[detectShell()]} to your shell's startup file), then open a new terminal.`
13018
- );
13019
- }
13020
- const clear = {
13021
- AWS_ACCESS_KEY_ID: void 0,
13022
- AWS_SECRET_ACCESS_KEY: void 0,
13023
- AWS_SESSION_TOKEN: void 0
13024
- };
13025
- if (flags.off) {
13026
- process.stdout.write(
13027
- `${shellExports(shell, { ...clear, AWS_PROFILE: void 0, AWS_REGION: void 0, AWS_DEFAULT_REGION: void 0, CLOUDHOP_ALIAS: void 0, CLOUDHOP_PROD: void 0 })}
13028
- `
13029
- );
13030
- notify("No alias in this shell anymore.");
13031
- return;
13032
- }
13033
- const alias = positional[0] ?? await pickAlias(app, "Use which alias in this shell?");
13034
- const env = await app.envForAlias(alias, "cli");
13035
- const status = await app.aliasStatus(alias);
13036
- process.stdout.write(
13037
- `${shellExports(shell, { ...clear, ...env, CLOUDHOP_ALIAS: alias, CLOUDHOP_PROD: status.prod ? "1" : void 0 })}
13038
- `
13039
- );
13040
- notify(
13041
- `This shell now uses ${alias} (${status.label ?? status.target}, ${status.region ?? "no region"}).`
13042
- );
13043
- if (status.prod) {
13044
- notify(
13045
- paint(
13046
- "red",
13047
- `${alias} is production. Plain \`aws\` commands are not protected here: use \`hop ${alias}\` to be asked before changes.`
13048
- )
13049
- );
13050
- }
13051
- }
13052
- async function completeCommand(app, kind) {
13053
- if (kind === "commands") {
13054
- console.log(
13055
- [...RESERVED_NAMES, "each", "shell", "use"].filter((c3, i2, a2) => a2.indexOf(c3) === i2).sort().join("\n")
13056
- );
13057
- return;
13058
- }
13059
- const config = await app.config();
13060
- const words = kind === "sources" ? config.sources.map((s) => s.id) : config.aliases.map((a2) => a2.name);
13061
- if (words.length) console.log(words.join("\n"));
13062
- }
13063
-
13064
13859
  // src/commands/team.ts
13065
- import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
13860
+ import { readFile as readFile5, writeFile as writeFile6 } from "fs/promises";
13066
13861
  var import_yaml2 = __toESM(require_dist(), 1);
13067
13862
  var MAX_BYTES = 256 * 1024;
13068
13863
  var HEADER2 = `# cloudhop team setup: import it with \`cloudhop team import <this file or its https URL>\`.
13069
13864
  # No secrets: portal URLs, regions, account ids and role names only.
13070
13865
  `;
13071
13866
  function validate(app, raw) {
13072
- const bad = (why) => new CloudhopError("INVALID_NAME", `Not a cloudhop team file: ${why}.`);
13073
- if (!raw || typeof raw !== "object") throw bad("it is empty");
13867
+ const bad2 = (why) => new CloudhopError("INVALID_NAME", `Not a cloudhop team file: ${why}.`);
13868
+ if (!raw || typeof raw !== "object") throw bad2("it is empty");
13074
13869
  const file = raw;
13075
- if (file.version !== 1) throw bad('unknown "version" (expected 1)');
13870
+ if (file.version !== 1) throw bad2('unknown "version" (expected 1)');
13076
13871
  const sources = file.sources ?? {};
13077
13872
  const aliases = file.aliases ?? {};
13078
13873
  for (const [id, source] of Object.entries(sources)) {
13079
13874
  assertValidName("source", id);
13080
13875
  if (!PROVIDER_IDS.includes(source?.provider))
13081
- throw bad(`source "${id}" has an unknown provider`);
13876
+ throw bad2(`source "${id}" has an unknown provider`);
13082
13877
  const kind = app.provider(source.provider).sourceKinds.find((k) => k.kind === source.kind);
13083
- if (!kind) throw bad(`source "${id}" has an unknown kind`);
13878
+ if (!kind) throw bad2(`source "${id}" has an unknown kind`);
13084
13879
  for (const field of kind.fields) {
13085
13880
  const value = source.settings?.[field.key];
13086
- if (typeof value !== "string" || !value) throw bad(`source "${id}" is missing ${field.key}`);
13881
+ if (typeof value !== "string" || !value) throw bad2(`source "${id}" is missing ${field.key}`);
13087
13882
  const problem = field.validate?.(value);
13088
- if (problem) throw bad(`source "${id}": ${problem}`);
13883
+ if (problem) throw bad2(`source "${id}": ${problem}`);
13089
13884
  }
13090
13885
  }
13091
13886
  for (const [name, alias] of Object.entries(aliases)) {
13092
13887
  assertValidName("alias", name);
13093
13888
  if (!alias || typeof alias.target !== "string" || !sources[alias.source]) {
13094
- throw bad(`alias "${name}" must name a source of this file and a target`);
13889
+ throw bad2(`alias "${name}" must name a source of this file and a target`);
13095
13890
  }
13096
13891
  }
13097
13892
  return { version: 1, sources, aliases };
@@ -13107,7 +13902,7 @@ async function load(location) {
13107
13902
  throw new CloudhopError("INVALID_NAME", "That file is too big for a team setup.");
13108
13903
  return text4;
13109
13904
  }
13110
- return readFile4(location, "utf8");
13905
+ return readFile5(location, "utf8");
13111
13906
  }
13112
13907
  async function teamCommand(app, args) {
13113
13908
  const { positional, flags } = parseArgs(args);
@@ -13128,7 +13923,7 @@ async function teamCommand(app, args) {
13128
13923
  };
13129
13924
  const text4 = HEADER2 + (0, import_yaml2.stringify)(file);
13130
13925
  if (flags.out) {
13131
- await writeFile3(flags.out, text4);
13926
+ await writeFile6(flags.out, text4);
13132
13927
  console.error(
13133
13928
  `${paint("green", "\u2714")} Wrote ${sources.length} source(s) and ${Object.keys(file.aliases).length} alias(es) to ${flags.out}.`
13134
13929
  );
@@ -13233,8 +14028,8 @@ async function teamCommand(app, args) {
13233
14028
  }
13234
14029
 
13235
14030
  // ../ui/src/api.ts
13236
- var text3 = (body, key) => {
13237
- const value = body[key];
14031
+ var text3 = (body, key2) => {
14032
+ const value = body[key2];
13238
14033
  return typeof value === "string" ? value.trim() : "";
13239
14034
  };
13240
14035
  var badRequest = (message) => new CloudhopError("INVALID_NAME", message);
@@ -13307,8 +14102,8 @@ function createApi(app, assistants) {
13307
14102
  label: kind.label,
13308
14103
  hint: kind.hint,
13309
14104
  nameLabel: kind.nameLabel,
13310
- fields: kind.fields.map(({ key, label, placeholder, defaultValue }) => ({
13311
- key,
14105
+ fields: kind.fields.map(({ key: key2, label, placeholder, defaultValue }) => ({
14106
+ key: key2,
13312
14107
  label,
13313
14108
  placeholder,
13314
14109
  defaultValue
@@ -13323,7 +14118,8 @@ function createApi(app, assistants) {
13323
14118
  label: target?.label,
13324
14119
  missing: !target,
13325
14120
  region,
13326
- prod: alias.prod === true
14121
+ prod: alias.prod === true,
14122
+ default: alias.default === true
13327
14123
  }))
13328
14124
  };
13329
14125
  },
@@ -13418,7 +14214,10 @@ function createApi(app, assistants) {
13418
14214
  await app.addAliases(aliases);
13419
14215
  return { created: aliases.map((a2) => a2.name) };
13420
14216
  },
13421
- /** Changes an alias: any of `region`, `prod` (production mark) and `name` (rename, applied last). */
14217
+ /**
14218
+ * Changes an alias: any of `region`, `prod` (production mark), `default`
14219
+ * (new terminals start with it) and `name` (rename, applied last).
14220
+ */
13422
14221
  "PATCH /api/aliases/:name": async ([name], body) => {
13423
14222
  let current = name;
13424
14223
  if ("region" in body) {
@@ -13428,6 +14227,11 @@ function createApi(app, assistants) {
13428
14227
  await app.setAliasRegion(current, region);
13429
14228
  }
13430
14229
  if (typeof body.prod === "boolean") await app.setProduction([current], body.prod);
14230
+ if (body.default === true) await app.setDefaultAlias(current);
14231
+ if (body.default === false) {
14232
+ const resolved = await app.resolveAlias(current);
14233
+ if (resolved.alias.default) await app.clearDefaultAlias(resolved.source.provider);
14234
+ }
13431
14235
  if ("name" in body) {
13432
14236
  const to = text3(body, "name");
13433
14237
  await app.renameAlias(current, to);
@@ -13446,8 +14250,8 @@ function createApi(app, assistants) {
13446
14250
  return { removed: name };
13447
14251
  }
13448
14252
  };
13449
- const table = Object.entries(routes).map(([key, handler]) => {
13450
- const [method, pattern] = key.split(" ");
14253
+ const table = Object.entries(routes).map(([key2, handler]) => {
14254
+ const [method, pattern] = key2.split(" ");
13451
14255
  const regex = new RegExp(`^${pattern.replace(/:[a-z]+/g, "([^/]+)")}$`);
13452
14256
  return { method, regex, handler };
13453
14257
  });
@@ -13476,7 +14280,7 @@ import { randomBytes, timingSafeEqual } from "crypto";
13476
14280
  import { createServer } from "http";
13477
14281
 
13478
14282
  // ../ui/src/assets/app.js
13479
- var app_default = "// cloudhop local UI. Plain DOM, no build step: served as-is by `cloudhop ui`.\n\nconst TOKEN_KEY = 'cloudhop-token';\nconst SESSION_TEXT = {\n valid: 'Session valid',\n refreshable: 'Renews on next use',\n expired: 'Expired: sign in on next use',\n unknown: 'Managed by the cloud CLI',\n};\n\n// The token arrives in the URL fragment once; keep it for this tab and clean the address bar.\nconst fromHash = new URLSearchParams(location.hash.slice(1)).get('token');\nif (fromHash) {\n try {\n sessionStorage.setItem(TOKEN_KEY, fromHash);\n } catch {}\n history.replaceState(null, '', location.pathname);\n}\nlet token = fromHash;\ntry {\n token = token ?? sessionStorage.getItem(TOKEN_KEY);\n} catch {}\n\n// ---------- pages ----------\n\nconst PAGES = {\n aliases: 'Aliases',\n sources: 'Sources',\n assistants: 'AI assistants',\n tools: 'Tools',\n};\nlet route = 'aliases';\nconst aliasFilter = { text: '', source: '', prodOnly: false };\n\n/** `#/aliases?source=acme` \u2192 route \"aliases\", filtered by that source. */\nfunction readRoute() {\n const match = /^#\\/([a-z]+)(?:\\?(.*))?$/.exec(location.hash);\n route = match && PAGES[match[1]] ? match[1] : 'aliases';\n const query = new URLSearchParams(match?.[2] ?? '');\n if (query.has('source')) aliasFilter.source = query.get('source');\n}\n\nfunction go(page, query) {\n location.hash = `#/${page}${query ? `?${new URLSearchParams(query)}` : ''}`;\n}\n\n// A fresh link opened in this same tab carries a new token: reload to pick it up.\n// Any other fragment is a page change.\naddEventListener('hashchange', () => {\n if (new URLSearchParams(location.hash.slice(1)).has('token')) {\n location.reload();\n return;\n }\n readRoute();\n if (state) render();\n});\n\nconst $main = document.getElementById('main');\nconst $dialog = document.getElementById('dialog');\nconst $toasts = document.getElementById('toasts');\nconst $addSource = document.getElementById('add-source');\nconst $nav = document.getElementById('nav');\nlet state;\n\n/** Tiny element builder: h('button', { class: 'primary', onclick }, 'Save'). */\nfunction h(tag, attrs = {}, ...children) {\n const el = document.createElement(tag);\n for (const [key, value] of Object.entries(attrs)) {\n if (value === undefined || value === null || value === false) continue;\n if (key.startsWith('on')) el.addEventListener(key.slice(2), value);\n else if (key === 'class') el.className = value;\n else if (value === true) el.setAttribute(key, '');\n else el.setAttribute(key, value);\n }\n for (const child of children.flat()) {\n if (child === undefined || child === null || child === false) continue;\n el.append(child instanceof Node ? child : document.createTextNode(String(child)));\n }\n return el;\n}\n\nclass ApiError extends Error {\n constructor(message, hint) {\n super(message);\n this.hint = hint;\n }\n}\n\n// Calls that may sign in wait for the person in the browser; everything else should answer quickly.\nconst MAY_SIGN_IN = /^\\/api\\/(sources(\\/[^/]+\\/(login|sync|targets))?|aliases\\/[^/]+\\/console)$/;\nconst QUICK_TIMEOUT_MS = 20_000;\n\nasync function api(method, path, body) {\n const waitsForPerson = MAY_SIGN_IN.test(path);\n let response;\n try {\n response = await fetch(path, {\n method,\n headers: {\n 'x-cloudhop-token': token ?? '',\n ...(body ? { 'content-type': 'application/json' } : {}),\n },\n body: body ? JSON.stringify(body) : undefined,\n signal: waitsForPerson ? undefined : AbortSignal.timeout(QUICK_TIMEOUT_MS),\n });\n } catch (error) {\n if (error.name === 'TimeoutError') {\n throw new ApiError(\n 'cloudhop ui is not responding.',\n 'Is it paused in your terminal (Ctrl-Z)? Run `fg` there, or stop it and run `cloudhop ui` again.',\n );\n }\n throw new ApiError(\n 'cloudhop ui is not running.',\n 'Run `cloudhop ui` again and open the new link it prints.',\n );\n }\n const data = await response.json().catch(() => ({}));\n if (!response.ok)\n throw new ApiError(data.error ?? `Request failed (${response.status}).`, data.hint);\n return data;\n}\n\nfunction toast(message, { error = false, hint } = {}) {\n const el = h(\n 'div',\n { class: `toast${error ? ' error' : ''}` },\n message,\n hint && h('small', {}, hint),\n );\n $toasts.append(el);\n setTimeout(() => el.remove(), error ? 9000 : 4000);\n}\n\nconst fail = (error) => toast(error.message, { error: true, hint: error.hint });\n\n/** Disables a button while its action runs. */\nasync function busy(button, action) {\n button.disabled = true;\n try {\n return await action();\n } finally {\n button.disabled = false;\n }\n}\n\nasync function refresh() {\n try {\n state = await api('GET', '/api/state');\n render();\n checkDoctor();\n } catch (error) {\n $main.replaceChildren(\n h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'Cannot reach cloudhop'),\n h('p', {}, error.message, error.hint ? ` ${error.hint}` : ''),\n ),\n );\n }\n}\n\nasync function checkDoctor() {\n try {\n const entries = await api('GET', '/api/doctor');\n const problems = entries.filter((e) => !e.installed || e.outdated);\n document.getElementById('doctor')?.remove();\n if (problems.length === 0) return;\n $main.prepend(\n h(\n 'div',\n { class: 'notice', id: 'doctor' },\n ...problems.map((e) =>\n h(\n 'div',\n {},\n e.installed\n ? `${e.cli} ${e.version} is older than ${e.minVersion}. `\n : `${e.cli} is not installed, so aliases cannot run commands yet. `,\n 'Run ',\n h('code', {}, 'cloudhop doctor'),\n ' in a terminal to install it.',\n ),\n ),\n ),\n );\n } catch {}\n}\n\n// ---------- main view ----------\n\nfunction render() {\n const enabled = state.enabledProviders;\n $addSource.hidden = enabled.length === 0;\n $nav.hidden = enabled.length === 0;\n if (enabled.length === 0) return renderChooseClouds();\n for (const link of $nav.querySelectorAll('a')) {\n if (link.dataset.page === route) link.setAttribute('aria-current', 'page');\n else link.removeAttribute('aria-current');\n }\n document.title = `${PAGES[route]} \xB7 cloudhop`;\n const pages = {\n aliases: aliasesPage,\n sources: sourcesPage,\n assistants: assistantsPage,\n tools: toolsPage,\n };\n $main.replaceChildren(...pages[route]());\n if (route === 'assistants') loadAssistants();\n if (route === 'tools') loadTools();\n}\n\nfunction pageHead(title, subtitle, ...actions) {\n return h(\n 'div',\n { class: 'page-head' },\n h('div', {}, h('h1', {}, title), subtitle ? h('p', {}, subtitle) : null),\n h('div', { class: 'actions' }, actions),\n );\n}\n\nfunction firstSourceHero() {\n return h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'Add your first account'),\n h(\n 'p',\n {},\n 'Add a company SSO portal or a personal account. cloudhop signs you in, finds every account and role you can use, and lets you give each one a short alias.',\n ),\n h('button', { class: 'primary', onclick: () => openAddSource() }, '+ Add source'),\n );\n}\n\n// ---------- Aliases page ----------\n\nfunction aliasesPage() {\n if (state.sources.length === 0) return [firstSourceHero()];\n if (aliasFilter.source && !state.sources.some((s) => s.id === aliasFilter.source))\n aliasFilter.source = '';\n\n const addAliases = () => {\n const source =\n state.sources.find((s) => s.id === aliasFilter.source) ??\n (state.sources.length === 1 ? state.sources[0] : undefined);\n if (source) openAliasPicker(source);\n else openSourceChooser();\n };\n const head = pageHead(\n 'Aliases',\n 'Each alias is one account, role and region. Run it with hop <alias>.',\n h('button', { onclick: addAliases }, '+ Aliases'),\n );\n if (state.aliases.length === 0) {\n return [\n head,\n h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'No aliases yet'),\n h('p', {}, 'Pick the accounts and roles you use and give each one a short name.'),\n h('button', { class: 'primary', onclick: addAliases }, 'Pick accounts'),\n ),\n ];\n }\n\n const rows = h('tbody', { id: 'alias-rows' });\n const count = h('span', { class: 'count' });\n const fill = () => {\n const words = aliasFilter.text.toLowerCase().split(/\\s+/).filter(Boolean);\n const visible = state.aliases.filter((a) => {\n if (aliasFilter.source && a.source !== aliasFilter.source) return false;\n if (aliasFilter.prodOnly && !a.prod) return false;\n const text = `${a.name} ${a.label ?? a.target} ${a.region ?? ''} ${a.source}`.toLowerCase();\n return words.every((w) => text.includes(w));\n });\n rows.replaceChildren(\n ...(visible.length\n ? visible.map(aliasRow)\n : [\n h(\n 'tr',\n {},\n h('td', { colspan: '5', class: 'empty' }, 'No alias matches these filters.'),\n ),\n ]),\n );\n count.textContent = `${visible.length} of ${state.aliases.length}`;\n };\n\n const search = h('input', {\n type: 'search',\n placeholder: 'Filter: name, account, role or region',\n value: aliasFilter.text,\n 'aria-label': 'Filter aliases',\n oninput: (e) => {\n aliasFilter.text = e.currentTarget.value;\n fill();\n },\n });\n const sourceSelect = h(\n 'select',\n {\n 'aria-label': 'Source',\n onchange: (e) => {\n aliasFilter.source = e.currentTarget.value;\n fill();\n },\n },\n h('option', { value: '' }, 'All sources'),\n state.sources.map((s) =>\n h('option', { value: s.id, selected: s.id === aliasFilter.source }, s.id),\n ),\n );\n const prodOnly = h(\n 'label',\n { class: 'prod-toggle' },\n h('input', {\n type: 'checkbox',\n checked: aliasFilter.prodOnly,\n onchange: (e) => {\n aliasFilter.prodOnly = e.currentTarget.checked;\n fill();\n },\n }),\n 'Production only',\n );\n\n const table = h(\n 'table',\n { class: 'aliases' },\n h(\n 'thead',\n {},\n h(\n 'tr',\n {},\n h('th', {}, 'Alias'),\n h('th', {}, 'Source'),\n h('th', {}, 'Region'),\n h('th', {}, 'Account / role'),\n h('th'),\n ),\n ),\n rows,\n );\n fill();\n return [\n head,\n h(\n 'div',\n { class: 'toolbar' },\n search,\n sourceSelect,\n prodOnly,\n h('div', { class: 'spacer' }),\n count,\n ),\n h('section', { class: 'card' }, table),\n ];\n}\n\n/** When several sources exist, ask which one to pick accounts from. */\nfunction openSourceChooser() {\n showDialog(\n 'Add aliases from',\n h(\n 'div',\n { class: 'choices' },\n state.sources.map((source) =>\n h(\n 'button',\n { class: 'choice', onclick: () => openAliasPicker(source) },\n h(\n 'span',\n {},\n h('strong', {}, source.id),\n h('small', {}, source.settings.startUrl ?? source.kindLabel),\n ),\n ),\n ),\n ),\n );\n}\n\n// ---------- Sources page ----------\n\nfunction sourcesPage() {\n const head = pageHead('Sources', 'The SSO portals and personal accounts you sign in to.');\n if (state.sources.length === 0) return [head, firstSourceHero()];\n return [head, ...state.sources.map(sourceCard)];\n}\n\n// ---------- AI assistants page ----------\n\nfunction assistantsPage() {\n return [assistantsCard()];\n}\n\n// ---------- Tools page ----------\n\nfunction snippet(command) {\n return h(\n 'div',\n { class: 'snippet' },\n h('code', {}, command),\n h(\n 'button',\n {\n class: 'link',\n title: 'Copy',\n 'aria-label': `Copy ${command}`,\n onclick: () =>\n navigator.clipboard\n ?.writeText(command)\n .then(() => toast('Copied.'))\n .catch(() =>\n toast('Copy it by hand: the browser blocked the clipboard.', { error: true }),\n ),\n },\n 'Copy',\n ),\n );\n}\n\nfunction toolCard(title, text, ...commands) {\n return h(\n 'section',\n { class: 'card' },\n h('div', { class: 'card-head' }, h('h2', {}, title)),\n h(\n 'div',\n { class: 'card-body padded' },\n h('p', { class: 'card-help' }, text),\n commands.map(snippet),\n ),\n );\n}\n\nfunction toolsPage() {\n const sample = state.aliases[0]?.name ?? 'acme-dev';\n return [\n pageHead('Tools', 'More ways to use cloudhop from your terminal.'),\n h(\n 'section',\n { class: 'card', id: 'tools-cli' },\n h('div', { class: 'card-head' }, h('h2', {}, 'Cloud CLIs')),\n h('div', { class: 'card-body padded' }, h('p', { class: 'card-help' }, 'Checking\u2026')),\n ),\n toolCard(\n 'Shell integration',\n 'Pick aliases from a list, tab-complete them, and use one in your shell so plain aws and Terraform work without hop.',\n 'cloudhop shell install',\n `cloudhop use ${sample}`,\n 'hop',\n ),\n toolCard(\n 'Run in many accounts',\n 'Run one command in many aliases at once: names, lists, wildcards or /regex/. It asks before running.',\n `cloudhop each '${sample.split('-')[0]}-*' -- s3 ls`,\n ),\n toolCard(\n 'Share with your team',\n 'Export your portals and aliases (no secrets) and let teammates import them in one command.',\n 'cloudhop team export --out team.yaml',\n 'cloudhop team import team.yaml',\n ),\n ];\n}\n\nasync function loadTools() {\n const card = document.querySelector('#tools-cli .card-body');\n if (!card) return;\n try {\n const entries = await api('GET', '/api/doctor');\n card.replaceChildren(\n ...entries.map((e) =>\n h(\n 'div',\n { class: 'tool-row' },\n h('span', { class: `badge ${e.installed && !e.outdated ? 'valid' : 'expired'}` }, e.cli),\n h(\n 'span',\n {},\n !e.installed\n ? 'Not installed.'\n : e.outdated\n ? `${e.version} is older than ${e.minVersion}.`\n : `Version ${e.version}.`,\n ),\n e.installed && !e.outdated ? null : snippet('cloudhop doctor'),\n ),\n ),\n );\n } catch (error) {\n card.replaceChildren(h('p', { class: 'card-help' }, error.message));\n }\n}\n\n// ---------- AI assistants ----------\n\nlet assistantScope = 'user';\ntry {\n assistantScope =\n localStorage.getItem('cloudhop-assistant-scope') === 'project' ? 'project' : 'user';\n} catch {}\nlet assistantsData;\n\nconst SKILL_TEXT = {\n current: ['valid', 'Up to date'],\n outdated: ['refreshable', 'Outdated'],\n modified: ['refreshable', 'Edited by you'],\n missing: ['', 'Not installed'],\n};\nconst MCP_TEXT = { registered: 'On', missing: 'Off', unknown: 'Check in the app' };\n\nfunction assistantsCard() {\n return h('section', { class: 'card', id: 'assistants', hidden: true });\n}\n\nasync function loadAssistants() {\n try {\n assistantsData = await api('GET', '/api/assistants');\n renderAssistants();\n } catch {\n document.getElementById('assistants')?.remove();\n }\n}\n\nfunction setAssistantScope(scope) {\n assistantScope = scope;\n try {\n localStorage.setItem('cloudhop-assistant-scope', scope);\n } catch {}\n renderAssistants();\n}\n\nfunction assistantAction(label, run, { danger = false, confirmText } = {}) {\n return h(\n 'button',\n {\n class: danger ? 'danger' : undefined,\n onclick: (e) => {\n if (confirmText && !confirm(confirmText)) return;\n busy(e.currentTarget, async () => {\n try {\n await run();\n } catch (error) {\n fail(error);\n }\n await loadAssistants();\n });\n },\n },\n label,\n );\n}\n\nfunction reportInstall(results) {\n for (const r of results) {\n if (r.mcp === 'manual') {\n toast(`${r.name}: skill installed. Add the MCP server by hand.`, { hint: r.mcpManual });\n } else {\n toast(`${r.name} now knows cloudhop. Start a new chat so it picks it up.`);\n }\n }\n}\n\nfunction renderAssistants() {\n const el = document.getElementById('assistants');\n if (!el || !assistantsData) return;\n const rows = assistantScope === 'user' ? assistantsData.user : assistantsData.projectScope;\n const scope = assistantScope;\n const install = (tools) =>\n api('POST', '/api/assistants', { tools, scope, mcp: true }).then((r) =>\n reportInstall(r.results),\n );\n const missingDetected = rows\n .filter((r) => r.detected && r.skill === 'missing')\n .map((r) => r.assistant);\n\n const seg = h(\n 'div',\n { class: 'seg', role: 'group', 'aria-label': 'Where' },\n h(\n 'button',\n { 'aria-pressed': String(scope === 'user'), onclick: () => setAssistantScope('user') },\n 'For me',\n ),\n h(\n 'button',\n { 'aria-pressed': String(scope === 'project'), onclick: () => setAssistantScope('project') },\n 'This project',\n ),\n );\n\n const head = h(\n 'div',\n { class: 'card-head' },\n h('h2', {}, 'AI assistants'),\n h(\n 'span',\n { class: 'meta' },\n scope === 'user'\n ? 'Installed in your home folder, for every project'\n : assistantsData.project,\n ),\n h('div', { class: 'spacer' }),\n h(\n 'div',\n { class: 'actions' },\n seg,\n missingDetected.length > 1\n ? assistantAction('Install for all detected', () => install(missingDetected))\n : null,\n ),\n );\n\n const table = h(\n 'table',\n { class: 'assistants' },\n h(\n 'thead',\n {},\n h(\n 'tr',\n {},\n h('th', {}, 'Assistant'),\n h('th', {}, 'Skill'),\n h('th', {}, 'MCP server'),\n h('th'),\n ),\n ),\n h(\n 'tbody',\n {},\n rows.map((r) => {\n const [badgeClass, skillText] = SKILL_TEXT[r.skill];\n const actions = [];\n if (r.skill === 'missing')\n actions.push(assistantAction('Install', () => install([r.assistant])));\n if (r.skill === 'outdated') {\n actions.push(\n assistantAction('Update', () =>\n api('POST', '/api/assistants/update', {}).then(() =>\n toast(`${r.name}: skill updated.`),\n ),\n ),\n );\n }\n if (r.skill === 'modified') {\n actions.push(\n assistantAction(\n 'Replace',\n () =>\n api('POST', '/api/assistants/update', { force: true }).then(() =>\n toast('Edited skills replaced.'),\n ),\n {\n confirmText: 'Replace your edited copies with the current cloudhop skill?',\n },\n ),\n );\n }\n if (r.skill !== 'missing' || r.mcp === 'registered') {\n actions.push(\n assistantAction(\n 'Remove',\n () => api('POST', '/api/assistants/remove', { tools: [r.assistant], scope }),\n {\n danger: true,\n confirmText: `Remove the cloudhop skill and MCP server from ${r.name}?`,\n },\n ),\n );\n }\n return h(\n 'tr',\n {},\n h(\n 'td',\n { class: 'alias' },\n r.name,\n r.detected ? null : h('small', { class: 'muted-inline' }, ' not detected'),\n ),\n h('td', {}, h('span', { class: `badge ${badgeClass}`, title: r.skillPath }, skillText)),\n h('td', {}, r.skill === 'missing' && r.mcp !== 'registered' ? '' : MCP_TEXT[r.mcp]),\n h(\n 'td',\n { style: 'text-align:right' },\n h('div', { class: 'actions', style: 'justify-content:flex-end' }, actions),\n ),\n );\n }),\n ),\n );\n\n el.replaceChildren(\n head,\n h(\n 'div',\n { class: 'card-body' },\n h(\n 'p',\n { class: 'card-help' },\n 'Teach your AI assistant to use cloudhop: it asks you which alias to work with, runs commands with hop, and confirms before changing anything. Includes the cloudhop MCP server.',\n ),\n table,\n ),\n );\n el.hidden = false;\n}\n\nfunction renderChooseClouds() {\n const boxes = state.providers.map((p) =>\n h(\n 'label',\n { class: 'choice' },\n h('input', { type: 'checkbox', value: p.id, checked: true }),\n h('span', {}, p.name),\n ),\n );\n const save = h(\n 'button',\n {\n class: 'primary',\n onclick: (e) =>\n busy(e.currentTarget, async () => {\n const ids = boxes\n .map((b) => b.querySelector('input'))\n .filter((i) => i.checked)\n .map((i) => i.value);\n await api('PUT', '/api/providers', { enabled: ids }).catch(fail);\n await refresh();\n }),\n },\n 'Continue',\n );\n $main.replaceChildren(\n h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'Which clouds do you use?'),\n h('p', {}, 'Azure and Google Cloud are coming soon.'),\n h(\n 'div',\n { class: 'choices', style: 'max-width:320px;margin:0 auto 16px;text-align:left' },\n boxes,\n ),\n save,\n ),\n );\n}\n\nfunction sourceCard(source) {\n const aliases = state.aliases.filter((a) => a.source === source.id);\n const where = source.settings.startUrl ?? source.kindLabel;\n const head = h(\n 'div',\n { class: 'card-head' },\n h('h2', {}, source.id),\n h('span', { class: `badge ${source.session}` }, SESSION_TEXT[source.session] ?? source.session),\n h('span', { class: 'meta' }, `${source.providerName} \xB7 ${where}`),\n h('div', { class: 'spacer' }),\n h(\n 'div',\n { class: 'actions' },\n h('button', { onclick: () => openAliasPicker(source) }, '+ Aliases'),\n h(\n 'button',\n {\n title: 'Sign in again and refresh its accounts',\n onclick: (e) =>\n busy(e.currentTarget, async () => {\n toast('Opening the browser to sign in\u2026');\n const report = await api(\n 'POST',\n `/api/sources/${encodeURIComponent(source.id)}/login`,\n ).catch(fail);\n if (report)\n toast(`${source.id}: signed in, ${report.targets.length} account/role(s).`);\n await refresh();\n }),\n },\n 'Sign in',\n ),\n h(\n 'button',\n {\n title: 'Refresh the accounts and roles you have access to',\n onclick: (e) =>\n busy(e.currentTarget, async () => {\n const report = await api(\n 'POST',\n `/api/sources/${encodeURIComponent(source.id)}/sync`,\n ).catch(fail);\n if (report) toast(syncMessage(source.id, report));\n await refresh();\n }),\n },\n 'Sync',\n ),\n h(\n 'button',\n {\n class: 'danger',\n onclick: (e) => {\n const count = aliases.length;\n const message = `Remove \"${source.id}\"${count ? ` and its ${count} alias(es)` : ''}?`;\n if (!confirm(message)) return;\n busy(e.currentTarget, async () => {\n await api('DELETE', `/api/sources/${encodeURIComponent(source.id)}`).catch(fail);\n await refresh();\n });\n },\n },\n 'Remove',\n ),\n ),\n );\n\n const prodCount = aliases.filter((a) => a.prod).length;\n const body =\n aliases.length === 0\n ? h(\n 'div',\n { class: 'empty' },\n 'No aliases yet. ',\n h(\n 'button',\n { class: 'primary', onclick: () => openAliasPicker(source) },\n 'Pick accounts',\n ),\n )\n : h(\n 'div',\n { class: 'source-summary' },\n h(\n 'span',\n {},\n `${aliases.length} alias(es)`,\n prodCount ? h('span', { class: 'prod-tag' }, `${prodCount} PROD`) : null,\n ),\n h(\n 'button',\n { class: 'link', onclick: () => go('aliases', { source: source.id }) },\n 'View aliases \u2192',\n ),\n );\n return h('section', { class: 'card' }, head, h('div', { class: 'card-body' }, body));\n}\n\nfunction syncMessage(id, report) {\n const parts = [`${report.targets.length} available`];\n if (report.added) parts.push(`${report.added} new`);\n if (report.removed) parts.push(`${report.removed} removed`);\n const orphaned = report.orphanedAliases.length\n ? ` No longer available: ${report.orphanedAliases.join(', ')}.`\n : '';\n return `${id}: ${parts.join(', ')}.${orphaned}`;\n}\n\nfunction aliasRow(alias) {\n const input = h('input', {\n value: alias.region ?? '',\n 'aria-label': `Region for ${alias.name}`,\n class: 'mono',\n spellcheck: 'false',\n });\n const save = h('button', { hidden: true }, 'Save');\n const commit = () =>\n busy(save, async () => {\n const region = input.value.trim();\n const done = await api('PATCH', `/api/aliases/${encodeURIComponent(alias.name)}`, {\n region,\n }).catch(fail);\n if (done) toast(`${alias.name} now runs in ${region}.`);\n await refresh();\n });\n input.addEventListener('input', () => {\n save.hidden = input.value.trim() === (alias.region ?? '');\n });\n input.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' && !save.hidden) commit();\n if (e.key === 'Escape') {\n input.value = alias.region ?? '';\n save.hidden = true;\n }\n });\n save.addEventListener('click', commit);\n\n const patch = (body, message) =>\n api('PATCH', `/api/aliases/${encodeURIComponent(alias.name)}`, body)\n .then(() => message && toast(message))\n .catch(fail)\n .then(refresh);\n const iconButton = (label, title, onclick, extra = {}) =>\n h('button', { class: 'link', title, 'aria-label': title, onclick, ...extra }, label);\n\n const actions = h(\n 'div',\n { class: 'row-actions' },\n h(\n 'button',\n {\n title: `Open the AWS console as ${alias.name}, already signed in`,\n disabled: alias.missing,\n onclick: (e) =>\n busy(e.currentTarget, async () => {\n const result = await api(\n 'POST',\n `/api/aliases/${encodeURIComponent(alias.name)}/console`,\n {},\n ).catch(fail);\n if (result) {\n toast(\n result.via === 'signed-in'\n ? `Opened the console for ${alias.name}.`\n : result.via === 'portal'\n ? `Opened ${alias.name} through the SSO portal.`\n : `Opened the console sign-in page for ${alias.name}.`,\n );\n }\n }),\n },\n 'Console',\n ),\n h(\n 'label',\n { class: 'prod-toggle', title: 'Production: hop and AI assistants ask before any change' },\n h('input', {\n type: 'checkbox',\n checked: alias.prod,\n 'aria-label': `${alias.name} is production`,\n onchange: (e) =>\n patch(\n { prod: e.currentTarget.checked },\n e.currentTarget.checked\n ? `${alias.name} is now production.`\n : `${alias.name} is no longer production.`,\n ),\n }),\n 'Prod',\n ),\n iconButton('\u270E', `Rename ${alias.name}`, () => {\n const to = prompt(`New name for \"${alias.name}\"`, alias.name)?.trim();\n if (to && to !== alias.name) patch({ name: to }, `${alias.name} is now ${to}.`);\n }),\n iconButton(\n '\u2715',\n `Remove ${alias.name}`,\n (e) => {\n if (!confirm(`Remove alias \"${alias.name}\"?`)) return;\n busy(e.currentTarget, async () => {\n await api('DELETE', `/api/aliases/${encodeURIComponent(alias.name)}`).catch(fail);\n await refresh();\n });\n },\n { class: 'link danger-hover' },\n ),\n );\n\n return h(\n 'tr',\n { class: alias.prod ? 'prod' : undefined },\n h(\n 'td',\n { class: 'alias', title: `hop ${alias.name} <command>` },\n alias.name,\n alias.prod ? h('span', { class: 'prod-tag' }, 'PROD') : null,\n ),\n h('td', { class: 'muted' }, alias.source),\n h('td', {}, h('div', { class: 'region' }, input, save)),\n h(\n 'td',\n {},\n alias.missing\n ? h('span', { class: 'missing' }, `${alias.target} (no longer available)`)\n : alias.label,\n ),\n h('td', { style: 'text-align:right' }, actions),\n );\n}\n\n// ---------- dialogs ----------\n\nfunction showDialog(title, body, footer) {\n $dialog.replaceChildren(\n h(\n 'div',\n { class: 'dlg-head' },\n h('h2', {}, title),\n h('button', { class: 'link', 'aria-label': 'Close', onclick: () => $dialog.close() }, '\u2715'),\n ),\n h('div', { class: 'dlg-body' }, body),\n footer ? h('div', { class: 'dlg-foot' }, footer) : null,\n );\n if (!$dialog.open) $dialog.showModal();\n}\n\nfunction suggestSourceId(settings, kind) {\n const host = /^https:\\/\\/([^./]+)\\./.exec(settings.startUrl ?? '')?.[1];\n return slug(host ?? (kind === 'console-login' ? 'personal' : 'main'));\n}\n\nconst slug = (text) =>\n text\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 63) || 'main';\n\n/** `previous` refills the form after a failed sign-in: { provider, kind, id, settings, error }. */\nfunction openAddSource(previous) {\n const providers = state.providers.filter((p) => state.enabledProviders.includes(p.id));\n let provider = providers.find((p) => p.id === previous?.provider) ?? providers[0];\n let kind = provider.kinds.find((k) => k.kind === previous?.kind) ?? provider.kinds[0];\n\n const form = h('form', { onsubmit: (e) => e.preventDefault() });\n const nameInput = h('input', {\n id: 'f-name',\n required: true,\n class: 'mono',\n spellcheck: 'false',\n });\n let nameTouched = Boolean(previous);\n nameInput.addEventListener('input', () => {\n nameTouched = true;\n });\n\n const fieldInputs = {};\n const fieldsBox = h('div');\n const nameLabel = h('label', { for: 'f-name' });\n\n function updateSuggestion() {\n if (nameTouched) return;\n const settings = Object.fromEntries(\n Object.entries(fieldInputs).map(([k, i]) => [k, i.value.trim()]),\n );\n const taken = new Set(state.sources.map((s) => s.id));\n let id = suggestSourceId(settings, kind.kind);\n for (let i = 2; taken.has(id); i++) id = `${suggestSourceId(settings, kind.kind)}-${i}`;\n nameInput.value = id;\n }\n\n function renderFields() {\n for (const key of Object.keys(fieldInputs)) delete fieldInputs[key];\n fieldsBox.replaceChildren(\n ...kind.fields.map((f) => {\n const input = h('input', {\n id: `f-${f.key}`,\n placeholder: f.placeholder ?? f.defaultValue ?? '',\n value: previous?.settings?.[f.key] ?? f.defaultValue ?? '',\n required: true,\n class: 'mono',\n spellcheck: 'false',\n oninput: updateSuggestion,\n });\n fieldInputs[f.key] = input;\n return h('div', { class: 'field' }, h('label', { for: input.id }, f.label), input);\n }),\n );\n nameLabel.textContent = kind.nameLabel ?? 'Short name for this source';\n updateSuggestion();\n }\n\n const providerSelect =\n providers.length > 1\n ? h(\n 'div',\n { class: 'field' },\n h('label', { for: 'f-provider' }, 'Cloud'),\n h(\n 'select',\n {\n id: 'f-provider',\n onchange: (e) => {\n provider = providers.find((p) => p.id === e.target.value);\n kind = provider.kinds[0];\n renderKinds();\n },\n },\n providers.map((p) => h('option', { value: p.id, selected: p === provider }, p.name)),\n ),\n )\n : null;\n\n const kindsBox = h('div', { class: 'choices' });\n function renderKinds() {\n kindsBox.replaceChildren(\n ...provider.kinds.map((k) =>\n h(\n 'label',\n { class: 'choice' },\n h('input', {\n type: 'radio',\n name: 'kind',\n value: k.kind,\n checked: k === kind,\n onchange: () => {\n kind = k;\n renderFields();\n },\n }),\n h('span', {}, h('strong', {}, k.label), k.hint ? h('small', {}, k.hint) : null),\n ),\n ),\n );\n renderFields();\n }\n\n form.append(\n previous?.error\n ? h(\n 'div',\n { class: 'notice error' },\n h('strong', {}, 'Could not sign in, so nothing was saved. '),\n previous.error.message,\n previous.error.hint ? h('div', { class: 'help' }, previous.error.hint) : null,\n )\n : '',\n providerSelect ?? '',\n h('div', { class: 'field' }, h('label', {}, 'What do you want to add?')),\n kindsBox,\n fieldsBox,\n h(\n 'div',\n { class: 'field' },\n nameLabel,\n nameInput,\n h(\n 'span',\n { class: 'help' },\n 'Lowercase letters, numbers and dashes. You will see it in ',\n h('code', {}, 'cloudhop ls'),\n '.',\n ),\n ),\n );\n renderKinds();\n if (previous) nameInput.value = previous.id;\n\n const submit = h('button', { class: 'primary', type: 'submit' }, 'Add and sign in');\n form.addEventListener('submit', async () => {\n if (!form.reportValidity()) return;\n const body = {\n provider: provider.id,\n kind: kind.kind,\n id: nameInput.value.trim(),\n settings: Object.fromEntries(\n Object.entries(fieldInputs).map(([k, i]) => [k, i.value.trim()]),\n ),\n };\n showDialog(\n 'Sign in',\n h(\n 'div',\n { class: 'waiting' },\n h('div', { class: 'spinner' }),\n h('strong', {}, 'Finish signing in in the browser tab that just opened.'),\n h('p', { class: 'help' }, 'This page continues on its own once you approve the request.'),\n ),\n );\n try {\n const report = await api('POST', '/api/sources', body);\n await refresh();\n const source = state.sources.find((s) => s.id === body.id);\n toast(`Signed in to ${body.id}. ${report.targets.length} account/role(s) found.`);\n openAliasPicker(source, report.targets);\n } catch (error) {\n // Nothing was saved: reopen the form with what was typed so it can be fixed.\n await refresh();\n openAddSource({ ...body, error });\n }\n });\n\n showDialog('Add source', form, [\n h('button', { type: 'button', onclick: () => $dialog.close() }, 'Cancel'),\n submit,\n ]);\n submit.setAttribute('form', 'add-form');\n form.id = 'add-form';\n}\n\nasync function openAliasPicker(source, preloaded) {\n let targets = preloaded;\n if (!targets) {\n showDialog(\n `Aliases for ${source.id}`,\n h('div', { class: 'waiting' }, h('div', { class: 'spinner' }), 'Loading accounts\u2026'),\n );\n try {\n targets = (await api('GET', `/api/sources/${encodeURIComponent(source.id)}/targets`)).targets;\n } catch (error) {\n $dialog.close();\n return fail(error);\n }\n }\n\n const used = new Map(\n state.aliases.filter((a) => a.source === source.id).map((a) => [a.target, a.name]),\n );\n const taken = new Set(state.aliases.map((a) => a.name));\n const defaultRegion = source.settings.region ?? '';\n const filter = h('input', {\n type: 'search',\n placeholder: `Filter ${targets.length} account/role(s)\u2026`,\n style: 'width:100%;margin-bottom:12px',\n });\n\n const rows = targets.map((t) => {\n const existing = used.get(t.id);\n let name = t.suggestedAlias;\n for (let i = 2; taken.has(name); i++) name = `${t.suggestedAlias}-${i}`;\n const check = h('input', {\n type: 'checkbox',\n disabled: Boolean(existing),\n 'aria-label': t.label,\n });\n const aliasInput = h('input', {\n value: name,\n class: 'mono',\n spellcheck: 'false',\n 'aria-label': `Alias for ${t.label}`,\n placeholder: 'alias',\n });\n const regionInput = h('input', {\n value: defaultRegion,\n class: 'mono',\n spellcheck: 'false',\n 'aria-label': `Region for ${t.label}`,\n placeholder: 'region',\n });\n // Only the checkbox and its text sit in the label, so clicking the inputs never toggles it.\n const el = h(\n 'div',\n { class: 'target' },\n h(\n 'label',\n { class: 'pick' },\n check,\n h(\n 'span',\n {},\n t.label,\n existing ? h('small', { class: 'already' }, `Already: ${existing}`) : null,\n ),\n ),\n h('div', { class: 'inputs' }, aliasInput, regionInput),\n );\n return { target: t, el, check, aliasInput, regionInput };\n });\n\n filter.addEventListener('input', () => {\n const q = filter.value.trim().toLowerCase();\n for (const row of rows) row.el.hidden = q !== '' && !row.target.label.toLowerCase().includes(q);\n });\n\n const save = h('button', { class: 'primary' }, 'Create aliases');\n save.addEventListener('click', () =>\n busy(save, async () => {\n const aliases = rows\n .filter((r) => r.check.checked)\n .map((r) => ({\n source: source.id,\n target: r.target.id,\n name: r.aliasInput.value.trim(),\n region: r.regionInput.value.trim(),\n }));\n if (aliases.length === 0) return toast('Pick at least one account/role.', { error: true });\n try {\n const result = await api('POST', '/api/aliases', { aliases });\n $dialog.close();\n toast(`Created ${result.created.join(', ')}. Try: hop ${result.created[0]}`);\n await refresh();\n } catch (error) {\n fail(error);\n }\n }),\n );\n\n const body =\n targets.length === 0\n ? h('p', { class: 'empty' }, 'This source has no accounts or roles assigned to you.')\n : h(\n 'div',\n {},\n filter,\n h(\n 'div',\n { class: 'targets' },\n rows.map((r) => r.el),\n ),\n );\n showDialog(`Aliases for ${source.id}`, body, [\n h('span', { class: 'note' }, 'Check an account/role, then name its alias and region.'),\n h('button', { onclick: () => $dialog.close() }, 'Cancel'),\n targets.length ? save : null,\n ]);\n}\n\n$addSource.addEventListener('click', () => openAddSource());\n\nif (!token) {\n $main.replaceChildren(\n h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'Open the link from your terminal'),\n h('p', {}, 'Run cloudhop ui and open the link it prints. It includes a one-time access key.'),\n ),\n );\n} else {\n readRoute();\n refresh();\n}\n";
14283
+ 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 h(\n 'label',\n {\n class: 'prod-toggle',\n title: 'New terminals start with this alias (only one per cloud)',\n },\n h('input', {\n type: 'checkbox',\n checked: alias.default,\n 'aria-label': `New terminals start with ${alias.name}`,\n onchange: (e) => {\n const on = e.currentTarget.checked;\n if (\n on &&\n alias.prod &&\n !confirm(`${alias.name} is production. Start every new terminal in it?`)\n ) {\n e.currentTarget.checked = false;\n return;\n }\n patch(\n { default: on },\n on\n ? `New terminals start with ${alias.name}. Open a new one to see it.`\n : `New terminals no longer start with ${alias.name}.`,\n );\n },\n }),\n 'Default',\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 alias.default\n ? h(\n 'span',\n { class: 'default-tag', title: 'New terminals start with this alias' },\n 'DEFAULT',\n )\n : null,\n ),\n h('td', { class: 'muted' }, alias.source),\n h('td', {}, h('div', { class: 'region' }, input, save)),\n h(\n 'td',\n {},\n alias.missing\n ? h('span', { class: 'missing' }, `${alias.target} (no longer available)`)\n : alias.label,\n ),\n h('td', { style: 'text-align:right' }, actions),\n );\n}\n\n// ---------- dialogs ----------\n\nfunction showDialog(title, body, footer) {\n $dialog.replaceChildren(\n h(\n 'div',\n { class: 'dlg-head' },\n h('h2', {}, title),\n h('button', { class: 'link', 'aria-label': 'Close', onclick: () => $dialog.close() }, '\u2715'),\n ),\n h('div', { class: 'dlg-body' }, body),\n footer ? h('div', { class: 'dlg-foot' }, footer) : null,\n );\n if (!$dialog.open) $dialog.showModal();\n}\n\nfunction suggestSourceId(settings, kind) {\n const host = /^https:\\/\\/([^./]+)\\./.exec(settings.startUrl ?? '')?.[1];\n return slug(host ?? (kind === 'console-login' ? 'personal' : 'main'));\n}\n\nconst slug = (text) =>\n text\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 63) || 'main';\n\n/** `previous` refills the form after a failed sign-in: { provider, kind, id, settings, error }. */\nfunction openAddSource(previous) {\n const providers = state.providers.filter((p) => state.enabledProviders.includes(p.id));\n let provider = providers.find((p) => p.id === previous?.provider) ?? providers[0];\n let kind = provider.kinds.find((k) => k.kind === previous?.kind) ?? provider.kinds[0];\n\n const form = h('form', { onsubmit: (e) => e.preventDefault() });\n const nameInput = h('input', {\n id: 'f-name',\n required: true,\n class: 'mono',\n spellcheck: 'false',\n });\n let nameTouched = Boolean(previous);\n nameInput.addEventListener('input', () => {\n nameTouched = true;\n });\n\n const fieldInputs = {};\n const fieldsBox = h('div');\n const nameLabel = h('label', { for: 'f-name' });\n\n function updateSuggestion() {\n if (nameTouched) return;\n const settings = Object.fromEntries(\n Object.entries(fieldInputs).map(([k, i]) => [k, i.value.trim()]),\n );\n const taken = new Set(state.sources.map((s) => s.id));\n let id = suggestSourceId(settings, kind.kind);\n for (let i = 2; taken.has(id); i++) id = `${suggestSourceId(settings, kind.kind)}-${i}`;\n nameInput.value = id;\n }\n\n function renderFields() {\n for (const key of Object.keys(fieldInputs)) delete fieldInputs[key];\n fieldsBox.replaceChildren(\n ...kind.fields.map((f) => {\n const input = h('input', {\n id: `f-${f.key}`,\n placeholder: f.placeholder ?? f.defaultValue ?? '',\n value: previous?.settings?.[f.key] ?? f.defaultValue ?? '',\n required: true,\n class: 'mono',\n spellcheck: 'false',\n oninput: updateSuggestion,\n });\n fieldInputs[f.key] = input;\n return h('div', { class: 'field' }, h('label', { for: input.id }, f.label), input);\n }),\n );\n nameLabel.textContent = kind.nameLabel ?? 'Short name for this source';\n updateSuggestion();\n }\n\n const providerSelect =\n providers.length > 1\n ? h(\n 'div',\n { class: 'field' },\n h('label', { for: 'f-provider' }, 'Cloud'),\n h(\n 'select',\n {\n id: 'f-provider',\n onchange: (e) => {\n provider = providers.find((p) => p.id === e.target.value);\n kind = provider.kinds[0];\n renderKinds();\n },\n },\n providers.map((p) => h('option', { value: p.id, selected: p === provider }, p.name)),\n ),\n )\n : null;\n\n const kindsBox = h('div', { class: 'choices' });\n function renderKinds() {\n kindsBox.replaceChildren(\n ...provider.kinds.map((k) =>\n h(\n 'label',\n { class: 'choice' },\n h('input', {\n type: 'radio',\n name: 'kind',\n value: k.kind,\n checked: k === kind,\n onchange: () => {\n kind = k;\n renderFields();\n },\n }),\n h('span', {}, h('strong', {}, k.label), k.hint ? h('small', {}, k.hint) : null),\n ),\n ),\n );\n renderFields();\n }\n\n form.append(\n previous?.error\n ? h(\n 'div',\n { class: 'notice error' },\n h('strong', {}, 'Could not sign in, so nothing was saved. '),\n previous.error.message,\n previous.error.hint ? h('div', { class: 'help' }, previous.error.hint) : null,\n )\n : '',\n providerSelect ?? '',\n h('div', { class: 'field' }, h('label', {}, 'What do you want to add?')),\n kindsBox,\n fieldsBox,\n h(\n 'div',\n { class: 'field' },\n nameLabel,\n nameInput,\n h(\n 'span',\n { class: 'help' },\n 'Lowercase letters, numbers and dashes. You will see it in ',\n h('code', {}, 'cloudhop ls'),\n '.',\n ),\n ),\n );\n renderKinds();\n if (previous) nameInput.value = previous.id;\n\n const submit = h('button', { class: 'primary', type: 'submit' }, 'Add and sign in');\n form.addEventListener('submit', async () => {\n if (!form.reportValidity()) return;\n const body = {\n provider: provider.id,\n kind: kind.kind,\n id: nameInput.value.trim(),\n settings: Object.fromEntries(\n Object.entries(fieldInputs).map(([k, i]) => [k, i.value.trim()]),\n ),\n };\n showDialog(\n 'Sign in',\n h(\n 'div',\n { class: 'waiting' },\n h('div', { class: 'spinner' }),\n h('strong', {}, 'Finish signing in in the browser tab that just opened.'),\n h('p', { class: 'help' }, 'This page continues on its own once you approve the request.'),\n ),\n );\n try {\n const report = await api('POST', '/api/sources', body);\n await refresh();\n const source = state.sources.find((s) => s.id === body.id);\n toast(`Signed in to ${body.id}. ${report.targets.length} account/role(s) found.`);\n openAliasPicker(source, report.targets);\n } catch (error) {\n // Nothing was saved: reopen the form with what was typed so it can be fixed.\n await refresh();\n openAddSource({ ...body, error });\n }\n });\n\n showDialog('Add source', form, [\n h('button', { type: 'button', onclick: () => $dialog.close() }, 'Cancel'),\n submit,\n ]);\n submit.setAttribute('form', 'add-form');\n form.id = 'add-form';\n}\n\nasync function openAliasPicker(source, preloaded) {\n let targets = preloaded;\n if (!targets) {\n showDialog(\n `Aliases for ${source.id}`,\n h('div', { class: 'waiting' }, h('div', { class: 'spinner' }), 'Loading accounts\u2026'),\n );\n try {\n targets = (await api('GET', `/api/sources/${encodeURIComponent(source.id)}/targets`)).targets;\n } catch (error) {\n $dialog.close();\n return fail(error);\n }\n }\n\n const used = new Map(\n state.aliases.filter((a) => a.source === source.id).map((a) => [a.target, a.name]),\n );\n const taken = new Set(state.aliases.map((a) => a.name));\n const defaultRegion = source.settings.region ?? '';\n const filter = h('input', {\n type: 'search',\n placeholder: `Filter ${targets.length} account/role(s)\u2026`,\n style: 'width:100%;margin-bottom:12px',\n });\n\n const rows = targets.map((t) => {\n const existing = used.get(t.id);\n let name = t.suggestedAlias;\n for (let i = 2; taken.has(name); i++) name = `${t.suggestedAlias}-${i}`;\n const check = h('input', {\n type: 'checkbox',\n disabled: Boolean(existing),\n 'aria-label': t.label,\n });\n const aliasInput = h('input', {\n value: name,\n class: 'mono',\n spellcheck: 'false',\n 'aria-label': `Alias for ${t.label}`,\n placeholder: 'alias',\n });\n const regionInput = h('input', {\n value: defaultRegion,\n class: 'mono',\n spellcheck: 'false',\n 'aria-label': `Region for ${t.label}`,\n placeholder: 'region',\n });\n // Only the checkbox and its text sit in the label, so clicking the inputs never toggles it.\n const el = h(\n 'div',\n { class: 'target' },\n h(\n 'label',\n { class: 'pick' },\n check,\n h(\n 'span',\n {},\n t.label,\n existing ? h('small', { class: 'already' }, `Already: ${existing}`) : null,\n ),\n ),\n h('div', { class: 'inputs' }, aliasInput, regionInput),\n );\n return { target: t, el, check, aliasInput, regionInput };\n });\n\n filter.addEventListener('input', () => {\n const q = filter.value.trim().toLowerCase();\n for (const row of rows) row.el.hidden = q !== '' && !row.target.label.toLowerCase().includes(q);\n });\n\n const save = h('button', { class: 'primary' }, 'Create aliases');\n save.addEventListener('click', () =>\n busy(save, async () => {\n const aliases = rows\n .filter((r) => r.check.checked)\n .map((r) => ({\n source: source.id,\n target: r.target.id,\n name: r.aliasInput.value.trim(),\n region: r.regionInput.value.trim(),\n }));\n if (aliases.length === 0) return toast('Pick at least one account/role.', { error: true });\n try {\n const result = await api('POST', '/api/aliases', { aliases });\n $dialog.close();\n toast(`Created ${result.created.join(', ')}. Try: hop ${result.created[0]}`);\n await refresh();\n } catch (error) {\n fail(error);\n }\n }),\n );\n\n const body =\n targets.length === 0\n ? h('p', { class: 'empty' }, 'This source has no accounts or roles assigned to you.')\n : h(\n 'div',\n {},\n filter,\n h(\n 'div',\n { class: 'targets' },\n rows.map((r) => r.el),\n ),\n );\n showDialog(`Aliases for ${source.id}`, body, [\n h('span', { class: 'note' }, 'Check an account/role, then name its alias and region.'),\n h('button', { onclick: () => $dialog.close() }, 'Cancel'),\n targets.length ? save : null,\n ]);\n}\n\n$addSource.addEventListener('click', () => openAddSource());\n\nif (!token) {\n $main.replaceChildren(\n h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'Open the link from your terminal'),\n h('p', {}, 'Run cloudhop ui and open the link it prints. It includes a one-time access key.'),\n ),\n );\n} else {\n readRoute();\n refresh();\n}\n";
13480
14284
 
13481
14285
  // ../ui/src/assets/index.html
13482
14286
  var assets_default = `<!doctype html>
@@ -13745,6 +14549,7 @@ var assets_default = `<!doctype html>
13745
14549
  .row-actions { display: flex; gap: 6px; align-items: center; justify-content: flex-end; white-space: nowrap; }
13746
14550
  .prod-toggle { display: inline-flex; align-items: center; gap: 4px; font-size: 13px; color: var(--muted); cursor: pointer; }
13747
14551
  .prod-tag { display: inline-block; white-space: nowrap; margin-left: 8px; font-size: 11px; font-weight: 700; letter-spacing: 0.04em; color: var(--bad); border: 1px solid currentColor; border-radius: 4px; padding: 0 4px; vertical-align: 1px; }
14552
+ .default-tag { display: inline-block; white-space: nowrap; margin-left: 8px; font-size: 11px; font-weight: 700; letter-spacing: 0.04em; color: var(--accent-strong); border: 1px solid currentColor; border-radius: 4px; padding: 0 4px; vertical-align: 1px; }
13748
14553
  tr.prod td:first-child { box-shadow: inset 3px 0 0 var(--bad); }
13749
14554
  button.link.danger-hover:hover { color: var(--bad); }
13750
14555
  .seg { display: inline-flex; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
@@ -13877,18 +14682,18 @@ async function startUiServer({
13877
14682
  sendJson(res, 400, { error: error.message });
13878
14683
  }
13879
14684
  });
13880
- await new Promise((resolve, reject) => {
14685
+ await new Promise((resolve3, reject) => {
13881
14686
  server.once("error", reject);
13882
- server.listen(port, "127.0.0.1", resolve);
14687
+ server.listen(port, "127.0.0.1", resolve3);
13883
14688
  });
13884
14689
  const actualPort = server.address().port;
13885
14690
  allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${actualPort}`, `localhost:${actualPort}`]);
13886
14691
  return {
13887
14692
  // The token travels in the fragment, so it never reaches server logs or Referer headers.
13888
14693
  url: `http://127.0.0.1:${actualPort}/#token=${token}`,
13889
- close: () => new Promise((resolve) => {
14694
+ close: () => new Promise((resolve3) => {
13890
14695
  server.closeAllConnections();
13891
- server.close(() => resolve());
14696
+ server.close(() => resolve3());
13892
14697
  })
13893
14698
  };
13894
14699
  }
@@ -13904,23 +14709,23 @@ async function uiCommand(app, assistants, { flags }) {
13904
14709
  console.log("Only this computer can open it, and the link works only while this runs.");
13905
14710
  console.log("Press Ctrl-C to stop.");
13906
14711
  if (!flags.noOpen) await new NodeBrowserOpener().open(server.url);
13907
- await new Promise((resolve) => {
13908
- process.once("SIGINT", resolve);
13909
- process.once("SIGTERM", resolve);
14712
+ await new Promise((resolve3) => {
14713
+ process.once("SIGINT", resolve3);
14714
+ process.once("SIGTERM", resolve3);
13910
14715
  });
13911
14716
  await server.close();
13912
14717
  }
13913
14718
 
13914
14719
  // src/container.ts
13915
- import { homedir as homedir4 } from "os";
13916
- import { join as join8 } from "path";
14720
+ import { homedir as homedir5 } from "os";
14721
+ import { join as join10 } from "path";
13917
14722
 
13918
14723
  // ../provider-aws/src/aws-provider.ts
13919
- import { join as join7 } from "path";
14724
+ import { join as join9 } from "path";
13920
14725
 
13921
14726
  // ../provider-aws/src/aws-config.ts
13922
- import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
13923
- import { dirname as dirname4 } from "path";
14727
+ import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile7 } from "fs/promises";
14728
+ import { dirname as dirname6 } from "path";
13924
14729
  var BLOCK_START = "# >>> cloudhop managed >>> (edit with `cloudhop`, not by hand)";
13925
14730
  var BLOCK_END = "# <<< cloudhop managed <<<";
13926
14731
  function renderBlock(sections) {
@@ -13967,13 +14772,13 @@ function replaceManagedBlock(content, sections) {
13967
14772
  async function writeManagedBlock(path2, sections) {
13968
14773
  let content = "";
13969
14774
  try {
13970
- content = await readFile5(path2, "utf8");
14775
+ content = await readFile6(path2, "utf8");
13971
14776
  } catch {
13972
14777
  }
13973
14778
  const next = replaceManagedBlock(content, sections);
13974
14779
  if (next === content) return;
13975
- await mkdir4(dirname4(path2), { recursive: true, mode: 448 });
13976
- await writeFile4(path2, next, { mode: 384 });
14780
+ await mkdir5(dirname6(path2), { recursive: true, mode: 448 });
14781
+ await writeFile7(path2, next, { mode: 384 });
13977
14782
  }
13978
14783
 
13979
14784
  // ../provider-aws/src/console.ts
@@ -13998,17 +14803,17 @@ function consoleDestination(region, service) {
13998
14803
  async function federatedConsoleUrl(credentials, destination, region, fetchFn) {
13999
14804
  const {
14000
14805
  AWS_ACCESS_KEY_ID: id,
14001
- AWS_SECRET_ACCESS_KEY: key,
14806
+ AWS_SECRET_ACCESS_KEY: key2,
14002
14807
  AWS_SESSION_TOKEN: token
14003
14808
  } = credentials;
14004
- if (!id || !key || !token) return void 0;
14809
+ if (!id || !key2 || !token) return void 0;
14005
14810
  const { signin } = partitionHosts(region);
14006
14811
  const response = await fetchFn(`${signin}/federation`, {
14007
14812
  method: "POST",
14008
14813
  headers: { "content-type": "application/x-www-form-urlencoded" },
14009
14814
  body: new URLSearchParams({
14010
14815
  Action: "getSigninToken",
14011
- Session: JSON.stringify({ sessionId: id, sessionKey: key, sessionToken: token })
14816
+ Session: JSON.stringify({ sessionId: id, sessionKey: key2, sessionToken: token })
14012
14817
  }).toString()
14013
14818
  }).catch(() => void 0);
14014
14819
  if (!response?.ok) return void 0;
@@ -14024,7 +14829,9 @@ async function federatedConsoleUrl(credentials, destination, region, fetchFn) {
14024
14829
  }
14025
14830
  function portalShortcutUrl(startUrl, accountId, roleName, destination) {
14026
14831
  const params = new URLSearchParams({ account_id: accountId, role_name: roleName, destination });
14027
- return `${startUrl.replace(/\/+$/, "")}/#/console?${params}`;
14832
+ let base = startUrl;
14833
+ while (base.endsWith("/")) base = base.slice(0, -1);
14834
+ return `${base}/#/console?${params}`;
14028
14835
  }
14029
14836
 
14030
14837
  // ../provider-aws/src/http.ts
@@ -14089,7 +14896,7 @@ var PortalClient = class {
14089
14896
  );
14090
14897
  return body.roleCredentials;
14091
14898
  }
14092
- async paginate(path2, params, key) {
14899
+ async paginate(path2, params, key2) {
14093
14900
  const items = [];
14094
14901
  let nextToken;
14095
14902
  do {
@@ -14100,7 +14907,7 @@ var PortalClient = class {
14100
14907
  `${this.endpoint}${path2}?${query}`,
14101
14908
  this.headers()
14102
14909
  );
14103
- items.push(...page[key] ?? []);
14910
+ items.push(...page[key2] ?? []);
14104
14911
  nextToken = page.nextToken;
14105
14912
  } while (nextToken);
14106
14913
  return items;
@@ -14111,7 +14918,7 @@ var PortalClient = class {
14111
14918
  };
14112
14919
 
14113
14920
  // ../provider-aws/src/sso/sso-login.ts
14114
- import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
14921
+ import { createHash as createHash4, randomBytes as randomBytes2 } from "crypto";
14115
14922
 
14116
14923
  // ../provider-aws/src/sso/loopback.ts
14117
14924
  import { createServer as createServer2 } from "http";
@@ -14122,8 +14929,8 @@ var PAGE = (title, body) => `<!doctype html><meta charset="utf-8"><title>cloudho
14122
14929
  async function startLoopbackServer() {
14123
14930
  let resolveRequest = () => {
14124
14931
  };
14125
- const request = new Promise((resolve) => {
14126
- resolveRequest = resolve;
14932
+ const request = new Promise((resolve3) => {
14933
+ resolveRequest = resolve3;
14127
14934
  });
14128
14935
  const server = createServer2((req, res) => {
14129
14936
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -14138,7 +14945,7 @@ async function startLoopbackServer() {
14138
14945
  );
14139
14946
  resolveRequest(url);
14140
14947
  });
14141
- await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
14948
+ await new Promise((resolve3) => server.listen(0, "127.0.0.1", resolve3));
14142
14949
  const { port } = server.address();
14143
14950
  return {
14144
14951
  redirectUri: `http://127.0.0.1:${port}${CALLBACK_PATH}`,
@@ -14244,24 +15051,24 @@ var OidcClient = class {
14244
15051
  };
14245
15052
 
14246
15053
  // ../provider-aws/src/sso/token-cache.ts
14247
- import { createHash as createHash2 } from "crypto";
14248
- import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
14249
- import { join as join6 } from "path";
15054
+ import { createHash as createHash3 } from "crypto";
15055
+ import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
15056
+ import { join as join8 } from "path";
14250
15057
  var EXPIRY_MARGIN_MS = 5 * 60 * 1e3;
14251
15058
  function tokenCachePath(awsDir, sessionName) {
14252
- const hash = createHash2("sha1").update(sessionName).digest("hex");
14253
- return join6(awsDir, "sso", "cache", `${hash}.json`);
15059
+ const hash = createHash3("sha1").update(sessionName).digest("hex");
15060
+ return join8(awsDir, "sso", "cache", `${hash}.json`);
14254
15061
  }
14255
15062
  async function readToken(awsDir, sessionName) {
14256
15063
  try {
14257
- return JSON.parse(await readFile6(tokenCachePath(awsDir, sessionName), "utf8"));
15064
+ return JSON.parse(await readFile7(tokenCachePath(awsDir, sessionName), "utf8"));
14258
15065
  } catch {
14259
15066
  return void 0;
14260
15067
  }
14261
15068
  }
14262
15069
  async function writeToken(awsDir, sessionName, token) {
14263
- await mkdir5(join6(awsDir, "sso", "cache"), { recursive: true, mode: 448 });
14264
- await writeFile5(tokenCachePath(awsDir, sessionName), JSON.stringify(token, null, 2), {
15070
+ await mkdir6(join8(awsDir, "sso", "cache"), { recursive: true, mode: 448 });
15071
+ await writeFile8(tokenCachePath(awsDir, sessionName), JSON.stringify(token, null, 2), {
14265
15072
  mode: 384
14266
15073
  });
14267
15074
  }
@@ -14304,7 +15111,7 @@ async function authorizationCodeLogin(options) {
14304
15111
  clientId: client.clientId,
14305
15112
  redirectUri: server.redirectUri,
14306
15113
  state,
14307
- codeChallenge: base64url(createHash3("sha256").update(codeVerifier).digest())
15114
+ codeChallenge: base64url(createHash4("sha256").update(codeVerifier).digest())
14308
15115
  });
14309
15116
  options.interaction.notify("Opening your browser to log in\u2026");
14310
15117
  options.interaction.notify(`If it doesn't open, visit: ${url}`);
@@ -14330,7 +15137,7 @@ async function deviceCodeLogin(options) {
14330
15137
  let intervalMs = (device.interval ?? 5) * 1e3;
14331
15138
  const deadline = Date.now() + device.expiresIn * 1e3;
14332
15139
  while (Date.now() < deadline) {
14333
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
15140
+ await new Promise((resolve3) => setTimeout(resolve3, intervalMs));
14334
15141
  try {
14335
15142
  return toCachedToken(
14336
15143
  options,
@@ -14446,6 +15253,7 @@ var AwsProvider = class {
14446
15253
  displayName = "AWS";
14447
15254
  cli = { command: "aws", displayName: "AWS CLI v2", minVersion: "2.32.0" };
14448
15255
  identityArgs = ["sts", "get-caller-identity"];
15256
+ shellVars = [...CREDENTIAL_VARS, "AWS_PROFILE", "AWS_REGION", "AWS_DEFAULT_REGION"];
14449
15257
  sourceKinds = [
14450
15258
  {
14451
15259
  kind: SSO_KIND,
@@ -14554,7 +15362,7 @@ var AwsProvider = class {
14554
15362
  }
14555
15363
  });
14556
15364
  }
14557
- await writeManagedBlock(join7(this.options.awsDir, "config"), sections);
15365
+ await writeManagedBlock(join9(this.options.awsDir, "config"), sections);
14558
15366
  }
14559
15367
  async processEnv(resolved, mode) {
14560
15368
  const region = resolved.region;
@@ -14628,9 +15436,9 @@ var AwsProvider = class {
14628
15436
  if (!token) throw new CloudhopError("LOGIN_FAILED", `Not logged in to ${source.id}.`);
14629
15437
  return new PortalClient(this.setting(source, "region"), token.accessToken, this.fetchFn);
14630
15438
  }
14631
- setting(source, key) {
14632
- const value = source.settings[key];
14633
- if (!value) throw new CloudhopError("SOURCE_NOT_FOUND", `Source "${source.id}" has no ${key}.`);
15439
+ setting(source, key2) {
15440
+ const value = source.settings[key2];
15441
+ if (!value) throw new CloudhopError("SOURCE_NOT_FOUND", `Source "${source.id}" has no ${key2}.`);
14634
15442
  return value;
14635
15443
  }
14636
15444
  };
@@ -14639,7 +15447,7 @@ var AwsProvider = class {
14639
15447
  var browser = new NodeBrowserOpener();
14640
15448
  function createApp(env = process.env) {
14641
15449
  const runner = new NodeProcessRunner();
14642
- const awsDir = env.CLOUDHOP_AWS_DIR ?? join8(homedir4(), ".aws");
15450
+ const awsDir = env.CLOUDHOP_AWS_DIR ?? join10(homedir5(), ".aws");
14643
15451
  return new CloudhopApp({
14644
15452
  config: new YamlConfigStore(),
14645
15453
  providers: [
@@ -14658,33 +15466,39 @@ function createApp(env = process.env) {
14658
15466
  }
14659
15467
  function createAssistants(version, env = process.env) {
14660
15468
  return new AssistantsInstaller({
14661
- places: { home: homedir4(), project: process.cwd() },
15469
+ places: { home: homedir5(), project: process.cwd() },
14662
15470
  configDir: defaultConfigDir(env),
14663
15471
  runner: new NodeProcessRunner(),
14664
15472
  cloudhopVersion: version
14665
15473
  });
14666
15474
  }
15475
+ function createTrustedFolders(env = process.env) {
15476
+ return new TrustedFolders(defaultConfigDir(env));
15477
+ }
14667
15478
 
14668
15479
  // src/update-check.ts
14669
- import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile6 } from "fs/promises";
14670
- import { join as join9 } from "path";
15480
+ import { mkdir as mkdir7, readFile as readFile8, writeFile as writeFile9 } from "fs/promises";
15481
+ import { join as join11 } from "path";
14671
15482
  var DAY_MS = 24 * 60 * 60 * 1e3;
14672
15483
  var REGISTRY_URL = "https://registry.npmjs.org/cloudhop/latest";
15484
+ var VERSION = /^\d{1,6}\.\d{1,6}\.\d{1,6}(?:-[0-9A-Za-z.]{1,40})?$/;
15485
+ var validVersion = (value) => typeof value === "string" && VERSION.test(value) ? value : void 0;
14673
15486
  function startUpdateCheck(configDir, current) {
14674
15487
  const env = process.env;
14675
15488
  if (env.CLOUDHOP_NO_UPDATE_CHECK === "1" || env.CI || !process.stderr.isTTY)
14676
15489
  return async () => {
14677
15490
  };
14678
- const file = join9(configDir, "update-check.json");
15491
+ const file = join11(configDir, "update-check.json");
14679
15492
  const work = (async () => {
14680
15493
  let cache = { checkedAt: 0 };
14681
15494
  try {
14682
- cache = JSON.parse(await readFile7(file, "utf8"));
15495
+ cache = JSON.parse(await readFile8(file, "utf8"));
14683
15496
  } catch {
14684
15497
  }
14685
- if (cache.latest && compareVersions(cache.latest, current) > 0) {
15498
+ const cached = validVersion(cache.latest);
15499
+ if (cached && compareVersions(cached, current) > 0) {
14686
15500
  process.stderr.write(
14687
- `\x1B[2mcloudhop \u203A\x1B[22m cloudhop ${cache.latest} is available (you have ${current}): npm i -g cloudhop
15501
+ `\x1B[2mcloudhop \u203A\x1B[22m cloudhop ${cached} is available (you have ${current}): npm i -g cloudhop
14688
15502
  `
14689
15503
  );
14690
15504
  }
@@ -14694,19 +15508,24 @@ function startUpdateCheck(configDir, current) {
14694
15508
  signal: AbortSignal.timeout(1500)
14695
15509
  });
14696
15510
  if (!response.ok) return;
14697
- const { version } = await response.json();
14698
- if (typeof version !== "string") return;
14699
- await mkdir6(configDir, { recursive: true });
14700
- await writeFile6(file, JSON.stringify({ checkedAt: Date.now(), latest: version }));
15511
+ const latest = validVersion((await response.json()).version);
15512
+ if (!latest) return;
15513
+ await mkdir7(configDir, { recursive: true });
15514
+ await writeFile9(file, JSON.stringify({ checkedAt: Date.now(), latest }));
14701
15515
  })().catch(() => {
14702
15516
  });
14703
- return () => Promise.race([work, new Promise((resolve) => setTimeout(resolve, 300))]);
15517
+ return () => Promise.race([work, new Promise((resolve3) => setTimeout(resolve3, 300))]);
14704
15518
  }
14705
15519
 
14706
15520
  // src/cli.ts
14707
- var HELP = `cloudhop ${"1.2.1"}
15521
+ var HELP = `cloudhop ${"1.3.1"}
14708
15522
  Hop between cloud accounts, roles and regions with one command.
14709
15523
 
15524
+ Quick start
15525
+ cloudhop add Sign in to an SSO portal or personal account, name aliases
15526
+ cloudhop shell Set up your terminal (once)
15527
+ cloudhop use <alias> Switch this terminal to an alias, then type plain aws ...
15528
+
14710
15529
  Usage
14711
15530
  hop <alias> [args...] Run the cloud CLI as <alias> (no args: who am I)
14712
15531
  hop Pick the alias from a list, then type the command
@@ -14715,8 +15534,13 @@ Usage
14715
15534
  Open the web console as <alias>, already signed in
14716
15535
  cloudhop each <selection...> [--source <s>] [--prod|--no-prod] [--yes] [--json] -- <args...>
14717
15536
  Run one command in many aliases (names, a,b lists, 'globex-*', /regex/)
14718
- cloudhop use [alias] | --off Use an alias in this shell (aws, terraform... without hop)
14719
- cloudhop shell [install] Shell integration: cloudhop use, tab completion, cloudhop_prompt
15537
+ cloudhop use [alias] [--save] Use an alias in this terminal: then plain aws, terraform... run as it
15538
+ (--save: remember it for this folder in .cloudhoprc)
15539
+ cloudhop use --off [cloud] Stop using the alias of every cloud, or just one (aws)
15540
+ cloudhop use <alias> --default Also start every new terminal with it (--no-default: stop)
15541
+ cloudhop allow | deny [folder] Trust (or stop trusting) a folder's .cloudhoprc
15542
+ cloudhop shell Set up your terminal once: cloudhop use, .cloudhoprc on cd, completion
15543
+ cloudhop shell starship Prompt segments for Starship (alias in use, PROD in red)
14720
15544
  cloudhop exec <alias> [--region <r>] -- <cmd...>
14721
15545
  Run any tool with the alias' temporary credentials
14722
15546
 
@@ -14784,7 +15608,7 @@ async function main(argv) {
14784
15608
  return 0;
14785
15609
  }
14786
15610
  if (command === "--version" || command === "-v" || command === "version") {
14787
- console.log("1.2.1");
15611
+ console.log("1.3.1");
14788
15612
  return 0;
14789
15613
  }
14790
15614
  const app = createApp();
@@ -14794,16 +15618,28 @@ async function main(argv) {
14794
15618
  return 0;
14795
15619
  }
14796
15620
  if (command === "shell") {
14797
- await shellCommand(local);
15621
+ const clis = app.availableProviders().map((p3) => ({ command: p3.cli.command, provider: p3.id }));
15622
+ const start = await app.defaultAliases().then(
15623
+ (d) => Object.keys(d).length > 0,
15624
+ () => false
15625
+ );
15626
+ await shellCommand(local, clis, start);
14798
15627
  return 0;
14799
15628
  }
15629
+ if (command === "use" && local.some((a2) => a2 === "--emit" || a2.startsWith("--emit="))) {
15630
+ return useCommand(app, createTrustedFolders(), local);
15631
+ }
14800
15632
  if (command === "mcp") {
14801
- await serveMcp({ app, version: "1.2.1" });
15633
+ await serveMcp({
15634
+ app,
15635
+ version: "1.3.1",
15636
+ context: () => shellContext(createTrustedFolders())
15637
+ });
14802
15638
  return 0;
14803
15639
  }
14804
- const assistants = createAssistants("1.2.1");
15640
+ const assistants = createAssistants("1.3.1");
14805
15641
  await refreshAssistantSkills(assistants, command);
14806
- const finishUpdateCheck = startUpdateCheck(defaultConfigDir(), "1.2.1");
15642
+ const finishUpdateCheck = startUpdateCheck(defaultConfigDir(), "1.3.1");
14807
15643
  try {
14808
15644
  return await dispatch(app, assistants, command, args, local);
14809
15645
  } finally {
@@ -14852,7 +15688,7 @@ async function dispatch(app, assistants, command, args, local) {
14852
15688
  return 0;
14853
15689
  case "ls":
14854
15690
  case "list":
14855
- await listCommand(app, local);
15691
+ await listCommand(app, local, () => shellContext(createTrustedFolders()));
14856
15692
  return 0;
14857
15693
  case "rm":
14858
15694
  case "remove":
@@ -14873,7 +15709,10 @@ async function dispatch(app, assistants, command, args, local) {
14873
15709
  await teamCommand(app, local);
14874
15710
  return 0;
14875
15711
  case "use":
14876
- await useCommand(app, local);
15712
+ return useCommand(app, createTrustedFolders(), local);
15713
+ case "allow":
15714
+ case "deny":
15715
+ await allowCommand(app, createTrustedFolders(), local, command === "allow");
14877
15716
  return 0;
14878
15717
  case "each":
14879
15718
  return eachCommand(app, args);