cloudhop 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -4
- package/dist/cli.js +1525 -752
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -26,9 +26,9 @@ var __export = (target, all) => {
|
|
|
26
26
|
};
|
|
27
27
|
var __copyProps = (to, from, except, desc) => {
|
|
28
28
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
29
|
-
for (let
|
|
30
|
-
if (!__hasOwnProp.call(to,
|
|
31
|
-
__defProp(to,
|
|
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_(
|
|
122
|
-
const ctrl = callVisitor(
|
|
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(
|
|
125
|
-
return visit_(
|
|
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_(
|
|
170
|
-
const ctrl = await callVisitor(
|
|
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(
|
|
173
|
-
return visitAsync_(
|
|
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(
|
|
223
|
+
function callVisitor(key2, node, visitor, path2) {
|
|
224
224
|
if (typeof visitor === "function")
|
|
225
|
-
return visitor(
|
|
225
|
+
return visitor(key2, node, path2);
|
|
226
226
|
if (identity.isMap(node))
|
|
227
|
-
return visitor.Map?.(
|
|
227
|
+
return visitor.Map?.(key2, node, path2);
|
|
228
228
|
if (identity.isSeq(node))
|
|
229
|
-
return visitor.Seq?.(
|
|
229
|
+
return visitor.Seq?.(key2, node, path2);
|
|
230
230
|
if (identity.isPair(node))
|
|
231
|
-
return visitor.Pair?.(
|
|
231
|
+
return visitor.Pair?.(key2, node, path2);
|
|
232
232
|
if (identity.isScalar(node))
|
|
233
|
-
return visitor.Scalar?.(
|
|
233
|
+
return visitor.Scalar?.(key2, node, path2);
|
|
234
234
|
if (identity.isAlias(node))
|
|
235
|
-
return visitor.Alias?.(
|
|
235
|
+
return visitor.Alias?.(key2, node, path2);
|
|
236
236
|
return void 0;
|
|
237
237
|
}
|
|
238
|
-
function replaceNode(
|
|
238
|
+
function replaceNode(key2, path2, node) {
|
|
239
239
|
const parent = path2[path2.length - 1];
|
|
240
240
|
if (identity.isCollection(parent)) {
|
|
241
|
-
parent.items[
|
|
241
|
+
parent.items[key2] = node;
|
|
242
242
|
} else if (identity.isPair(parent)) {
|
|
243
|
-
if (
|
|
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,
|
|
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,
|
|
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 [
|
|
908
|
-
const node = this.get(
|
|
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(
|
|
912
|
+
this.set(key2, collectionFromPath(this.schema, rest, value));
|
|
913
913
|
else
|
|
914
|
-
throw new Error(`Expected YAML collection at ${
|
|
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 [
|
|
922
|
+
const [key2, ...rest] = path2;
|
|
923
923
|
if (rest.length === 0)
|
|
924
|
-
return this.delete(
|
|
925
|
-
const node = this.get(
|
|
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 ${
|
|
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 [
|
|
938
|
-
const node = this.get(
|
|
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 [
|
|
956
|
+
const [key2, ...rest] = path2;
|
|
957
957
|
if (rest.length === 0)
|
|
958
|
-
return this.has(
|
|
959
|
-
const node = this.get(
|
|
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 [
|
|
967
|
+
const [key2, ...rest] = path2;
|
|
968
968
|
if (rest.length === 0) {
|
|
969
|
-
this.set(
|
|
969
|
+
this.set(key2, value);
|
|
970
970
|
} else {
|
|
971
|
-
const node = this.get(
|
|
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(
|
|
975
|
+
this.set(key2, collectionFromPath(this.schema, rest, value));
|
|
976
976
|
else
|
|
977
|
-
throw new Error(`Expected YAML collection at ${
|
|
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(
|
|
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(
|
|
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 && (!
|
|
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(
|
|
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,
|
|
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 [
|
|
1736
|
+
for (const [key2, value2] of srcMap) {
|
|
1737
1737
|
if (map instanceof Map) {
|
|
1738
|
-
if (!map.has(
|
|
1739
|
-
map.set(
|
|
1738
|
+
if (!map.has(key2))
|
|
1739
|
+
map.set(key2, value2);
|
|
1740
1740
|
} else if (map instanceof Set) {
|
|
1741
|
-
map.add(
|
|
1742
|
-
} else if (!Object.prototype.hasOwnProperty.call(map,
|
|
1743
|
-
Object.defineProperty(map,
|
|
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(
|
|
1773
|
-
|
|
1774
|
-
else if (merge.isMergeKey(ctx,
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
1835
|
-
const k = createNode.createNode(
|
|
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(
|
|
1840
|
+
constructor(key2, value = null) {
|
|
1841
1841
|
Object.defineProperty(this, identity.NODE_TYPE, { value: identity.PAIR });
|
|
1842
|
-
this.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(
|
|
1848
|
-
|
|
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(
|
|
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,
|
|
2028
|
-
const k = identity.isScalar(
|
|
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 ===
|
|
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 = (
|
|
2054
|
+
const add = (key2, value) => {
|
|
2055
2055
|
if (typeof replacer === "function")
|
|
2056
|
-
value = replacer.call(obj,
|
|
2057
|
-
else if (Array.isArray(replacer) && !replacer.includes(
|
|
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(
|
|
2060
|
+
map.items.push(Pair.createPair(key2, value, ctx));
|
|
2061
2061
|
};
|
|
2062
2062
|
if (obj instanceof Map) {
|
|
2063
|
-
for (const [
|
|
2064
|
-
add(
|
|
2063
|
+
for (const [key2, value] of obj)
|
|
2064
|
+
add(key2, value);
|
|
2065
2065
|
} else if (obj && typeof obj === "object") {
|
|
2066
|
-
for (const
|
|
2067
|
-
add(
|
|
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(
|
|
2108
|
-
const it = findPair(this.items,
|
|
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(
|
|
2115
|
-
const it = findPair(this.items,
|
|
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(
|
|
2120
|
-
return !!findPair(this.items,
|
|
2119
|
+
has(key2) {
|
|
2120
|
+
return !!findPair(this.items, key2);
|
|
2121
2121
|
}
|
|
2122
|
-
set(
|
|
2123
|
-
this.add(new Pair.Pair(
|
|
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(
|
|
2213
|
-
const idx = asItemIndex(
|
|
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(
|
|
2220
|
-
const idx = asItemIndex(
|
|
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(
|
|
2233
|
-
const idx = asItemIndex(
|
|
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(
|
|
2244
|
-
const idx = asItemIndex(
|
|
2243
|
+
set(key2, value) {
|
|
2244
|
+
const idx = asItemIndex(key2);
|
|
2245
2245
|
if (typeof idx !== "number")
|
|
2246
|
-
throw new Error(`Expected a valid index, not ${
|
|
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
|
|
2281
|
-
it = replacer.call(obj,
|
|
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(
|
|
2290
|
-
let idx = identity.isScalar(
|
|
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
|
|
2704
|
+
let key2, value;
|
|
2705
2705
|
if (Array.isArray(it)) {
|
|
2706
2706
|
if (it.length === 2) {
|
|
2707
|
-
|
|
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
|
-
|
|
2715
|
-
value = it[
|
|
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
|
-
|
|
2720
|
+
key2 = it;
|
|
2721
2721
|
}
|
|
2722
|
-
pairs2.items.push(Pair.createPair(
|
|
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
|
|
2769
|
+
let key2, value;
|
|
2770
2770
|
if (identity.isPair(pair)) {
|
|
2771
|
-
|
|
2772
|
-
value = toJS.toJS(pair.value,
|
|
2771
|
+
key2 = toJS.toJS(pair.key, "", ctx);
|
|
2772
|
+
value = toJS.toJS(pair.value, key2, ctx);
|
|
2773
2773
|
} else {
|
|
2774
|
-
|
|
2774
|
+
key2 = toJS.toJS(pair, "", ctx);
|
|
2775
2775
|
}
|
|
2776
|
-
if (map.has(
|
|
2776
|
+
if (map.has(key2))
|
|
2777
2777
|
throw new Error("Ordered maps must not include duplicate keys");
|
|
2778
|
-
map.set(
|
|
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(
|
|
2801
|
-
if (seenKeys.includes(
|
|
2802
|
-
onError(`Ordered maps must not include duplicate keys: ${
|
|
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(
|
|
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(
|
|
2989
|
+
add(key2) {
|
|
2990
2990
|
let pair;
|
|
2991
|
-
if (identity.isPair(
|
|
2992
|
-
pair =
|
|
2993
|
-
else if (
|
|
2994
|
-
pair = new Pair.Pair(
|
|
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(
|
|
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(
|
|
3006
|
-
const pair = YAMLMap.findPair(this.items,
|
|
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(
|
|
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,
|
|
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(
|
|
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((
|
|
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((
|
|
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(
|
|
3544
|
-
const k = this.createNode(
|
|
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(
|
|
3553
|
-
return assertCollection(this.contents) ? this.contents.delete(
|
|
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(
|
|
3574
|
-
return identity.isCollection(this.contents) ? this.contents.get(
|
|
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(
|
|
3590
|
-
return identity.isCollection(this.contents) ? this.contents.has(
|
|
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(
|
|
3604
|
+
set(key2, value) {
|
|
3605
3605
|
if (this.contents == null) {
|
|
3606
|
-
this.contents = Collection.collectionFromPath(this.schema, [
|
|
3606
|
+
this.contents = Collection.collectionFromPath(this.schema, [key2], value);
|
|
3607
3607
|
} else if (assertCollection(this.contents)) {
|
|
3608
|
-
this.contents.set(
|
|
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(
|
|
3917
|
-
if (!
|
|
3916
|
+
function containsNewline(key2) {
|
|
3917
|
+
if (!key2)
|
|
3918
3918
|
return null;
|
|
3919
|
-
switch (
|
|
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 (
|
|
3924
|
+
if (key2.source.includes("\n"))
|
|
3925
3925
|
return true;
|
|
3926
|
-
if (
|
|
3927
|
-
for (const st of
|
|
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
|
|
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:
|
|
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 (
|
|
4019
|
-
if (
|
|
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
|
|
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(
|
|
4035
|
-
onError(
|
|
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 =
|
|
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,
|
|
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: !
|
|
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:
|
|
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(
|
|
4243
|
+
if (!isMap && ctx.options.strict && utilContainsNewline.containsNewline(key2))
|
|
4244
4244
|
onError(
|
|
4245
|
-
|
|
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 =
|
|
4293
|
-
if (isBlock(
|
|
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
|
|
5444
|
-
if (
|
|
5445
|
-
delete token[
|
|
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
|
|
5494
|
-
if (
|
|
5495
|
-
delete token[
|
|
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 (
|
|
5555
|
-
res += stringifyToken(
|
|
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
|
|
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/
|
|
7434
|
-
import {
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
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, "-")
|
|
7651
|
-
return slug || "alias";
|
|
7501
|
+
const slug = parts.join("-").toLowerCase().replace(/access$/i, "").replace(/[^a-z0-9]+/g, "-");
|
|
7502
|
+
return trimDashes(slug).slice(0, 63) || "alias";
|
|
7503
|
+
}
|
|
7504
|
+
function trimDashes(text4) {
|
|
7505
|
+
let start = 0;
|
|
7506
|
+
let end = text4.length;
|
|
7507
|
+
while (start < end && text4[start] === "-") start++;
|
|
7508
|
+
while (end > start && text4[end - 1] === "-") end--;
|
|
7509
|
+
return text4.slice(start, end);
|
|
7652
7510
|
}
|
|
7653
7511
|
var slugTokens = (text4) => text4.toLowerCase().replace(/access$/i, "").split(/[^a-z0-9]+/).filter(Boolean);
|
|
7654
7512
|
var ROLE_WORDS = {
|
|
@@ -8031,7 +7889,11 @@ ${result.stderr}`)) {
|
|
|
8031
7889
|
* A URL that opens the web console as the alias, signing in first if the
|
|
8032
7890
|
* session expired. The URL is a short-lived secret: open it, never print it.
|
|
8033
7891
|
*/
|
|
8034
|
-
async consoleUrl(name, {
|
|
7892
|
+
async consoleUrl(name, {
|
|
7893
|
+
service,
|
|
7894
|
+
region,
|
|
7895
|
+
interactive: interactive2 = true
|
|
7896
|
+
} = {}) {
|
|
8035
7897
|
const resolved = await this.usableAlias(name, region ? { region } : {});
|
|
8036
7898
|
const provider = this.provider(resolved.source.provider);
|
|
8037
7899
|
if (!provider.consoleUrl) {
|
|
@@ -8047,7 +7909,7 @@ ${result.stderr}`)) {
|
|
|
8047
7909
|
"Examples: s3, ec2, lambda, cloudwatch."
|
|
8048
7910
|
);
|
|
8049
7911
|
}
|
|
8050
|
-
await this.ensureSession(resolved.source.id);
|
|
7912
|
+
await this.ensureSession(resolved.source.id, { interactive: interactive2 });
|
|
8051
7913
|
return provider.consoleUrl(resolved, service);
|
|
8052
7914
|
}
|
|
8053
7915
|
/**
|
|
@@ -8074,6 +7936,29 @@ ${result.stderr}`)) {
|
|
|
8074
7936
|
await this.ensureSession(resolved.source.id);
|
|
8075
7937
|
return provider.processEnv(resolved, mode);
|
|
8076
7938
|
}
|
|
7939
|
+
/**
|
|
7940
|
+
* What `cloudhop use` puts in a shell: the alias' CLI environment (profile
|
|
7941
|
+
* and region, never credentials) and every variable of that provider to
|
|
7942
|
+
* clear first. With `login: false` it never signs in, so entering a folder
|
|
7943
|
+
* never opens a browser: the first command does it if needed.
|
|
7944
|
+
*/
|
|
7945
|
+
async shellEnv(name, { login }) {
|
|
7946
|
+
const resolved = await this.usableAlias(name, {});
|
|
7947
|
+
const provider = this.provider(resolved.source.provider);
|
|
7948
|
+
if (login) await this.ensureSession(resolved.source.id);
|
|
7949
|
+
return {
|
|
7950
|
+
provider: provider.id,
|
|
7951
|
+
clear: provider.shellVars,
|
|
7952
|
+
env: await provider.processEnv(resolved, "cli"),
|
|
7953
|
+
region: resolved.region,
|
|
7954
|
+
label: resolved.target?.label,
|
|
7955
|
+
prod: resolved.alias.prod === true
|
|
7956
|
+
};
|
|
7957
|
+
}
|
|
7958
|
+
/** The variables `cloudhop use --off <provider>` removes from a shell. */
|
|
7959
|
+
shellVars(provider) {
|
|
7960
|
+
return this.provider(provider).shellVars;
|
|
7961
|
+
}
|
|
8077
7962
|
// ── Health ───────────────────────────────────────────────────────────────
|
|
8078
7963
|
async doctor() {
|
|
8079
7964
|
const config = await this.deps.config.load();
|
|
@@ -8197,11 +8082,260 @@ ${result.stderr}`)) {
|
|
|
8197
8082
|
var PROVIDER_IDS = ["aws", "azure", "gcp"];
|
|
8198
8083
|
var emptyConfig = () => ({ providers: [], sources: [], aliases: [] });
|
|
8199
8084
|
|
|
8085
|
+
// ../core/src/domain/folder.ts
|
|
8086
|
+
var FOLDER_FILE = ".cloudhoprc";
|
|
8087
|
+
var bad = (why) => new CloudhopError(
|
|
8088
|
+
"INVALID_NAME",
|
|
8089
|
+
`Not a valid ${FOLDER_FILE}: ${why}.`,
|
|
8090
|
+
"Write one line per cloud, like `aws: my-alias`."
|
|
8091
|
+
);
|
|
8092
|
+
function parseFolderFile(text4) {
|
|
8093
|
+
const result = {};
|
|
8094
|
+
for (const raw of text4.split(/\r?\n/)) {
|
|
8095
|
+
const line = raw.replace(/#.*/, "").trim();
|
|
8096
|
+
if (!line) continue;
|
|
8097
|
+
const match = /^([a-z]+)\s*[:=]\s*(\S+)$/.exec(line);
|
|
8098
|
+
const key2 = match ? match[1] : "any";
|
|
8099
|
+
const alias = match ? match[2] : line;
|
|
8100
|
+
if (key2 !== "any" && !PROVIDER_IDS.includes(key2)) throw bad(`unknown cloud "${key2}"`);
|
|
8101
|
+
if (result[key2]) throw bad(`"${key2}" appears twice`);
|
|
8102
|
+
assertValidName("alias", alias);
|
|
8103
|
+
result[key2] = alias;
|
|
8104
|
+
}
|
|
8105
|
+
if (Object.keys(result).length === 0) throw bad("it names no alias");
|
|
8106
|
+
return result;
|
|
8107
|
+
}
|
|
8108
|
+
function formatFolderFile(aliases) {
|
|
8109
|
+
const lines = Object.entries(aliases).map(
|
|
8110
|
+
([key2, alias]) => key2 === "any" ? alias : `${key2}: ${alias}`
|
|
8111
|
+
);
|
|
8112
|
+
return `# cloudhop: aliases for this folder (https://dayepesb.github.io/cloudhop/guides/shell/)
|
|
8113
|
+
${lines.join("\n")}
|
|
8114
|
+
`;
|
|
8115
|
+
}
|
|
8116
|
+
|
|
8117
|
+
// ../adapters-node/src/folder-files.ts
|
|
8118
|
+
function findFolderFile(start) {
|
|
8119
|
+
let dir = resolve(start);
|
|
8120
|
+
for (; ; ) {
|
|
8121
|
+
const file = join(dir, FOLDER_FILE);
|
|
8122
|
+
try {
|
|
8123
|
+
if (statSync(file).isFile()) return file;
|
|
8124
|
+
} catch {
|
|
8125
|
+
}
|
|
8126
|
+
const parent = dirname(dir);
|
|
8127
|
+
if (parent === dir) return void 0;
|
|
8128
|
+
dir = parent;
|
|
8129
|
+
}
|
|
8130
|
+
}
|
|
8131
|
+
var key = (path2) => {
|
|
8132
|
+
try {
|
|
8133
|
+
return realpathSync.native(path2);
|
|
8134
|
+
} catch {
|
|
8135
|
+
return resolve(path2);
|
|
8136
|
+
}
|
|
8137
|
+
};
|
|
8138
|
+
var digest = (content) => createHash("sha256").update(content).digest("hex");
|
|
8139
|
+
var TrustedFolders = class {
|
|
8140
|
+
file;
|
|
8141
|
+
constructor(configDir) {
|
|
8142
|
+
this.file = join(configDir, "trusted-folders.json");
|
|
8143
|
+
}
|
|
8144
|
+
isTrusted(path2, content) {
|
|
8145
|
+
return this.load().files[key(path2)] === digest(content);
|
|
8146
|
+
}
|
|
8147
|
+
async trust(path2, content) {
|
|
8148
|
+
const data = this.load();
|
|
8149
|
+
data.files[key(path2)] = digest(content);
|
|
8150
|
+
await this.save(data);
|
|
8151
|
+
}
|
|
8152
|
+
/** Returns false when the file was not trusted. */
|
|
8153
|
+
async forget(path2) {
|
|
8154
|
+
const data = this.load();
|
|
8155
|
+
const file = key(path2);
|
|
8156
|
+
if (!(file in data.files)) return false;
|
|
8157
|
+
delete data.files[file];
|
|
8158
|
+
await this.save(data);
|
|
8159
|
+
return true;
|
|
8160
|
+
}
|
|
8161
|
+
load() {
|
|
8162
|
+
try {
|
|
8163
|
+
const data = JSON.parse(readFileSync(this.file, "utf8"));
|
|
8164
|
+
return data?.version === 1 && data.files ? data : { version: 1, files: {} };
|
|
8165
|
+
} catch {
|
|
8166
|
+
return { version: 1, files: {} };
|
|
8167
|
+
}
|
|
8168
|
+
}
|
|
8169
|
+
async save(data) {
|
|
8170
|
+
await mkdir(dirname(this.file), { recursive: true });
|
|
8171
|
+
await writeFile(this.file, `${JSON.stringify(data, null, 2)}
|
|
8172
|
+
`, { mode: 384 });
|
|
8173
|
+
}
|
|
8174
|
+
};
|
|
8175
|
+
async function readFolderFile(path2) {
|
|
8176
|
+
return readFile(path2, "utf8").catch(() => void 0);
|
|
8177
|
+
}
|
|
8178
|
+
|
|
8179
|
+
// ../adapters-node/src/node-browser-opener.ts
|
|
8180
|
+
import { spawn } from "child_process";
|
|
8181
|
+
var NodeBrowserOpener = class {
|
|
8182
|
+
async open(url) {
|
|
8183
|
+
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", '""', url.replace(/&/g, "^&")]] : ["xdg-open", [url]];
|
|
8184
|
+
await new Promise((resolve3) => {
|
|
8185
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
8186
|
+
child.on("error", () => resolve3());
|
|
8187
|
+
child.on("spawn", () => {
|
|
8188
|
+
child.unref();
|
|
8189
|
+
resolve3();
|
|
8190
|
+
});
|
|
8191
|
+
});
|
|
8192
|
+
}
|
|
8193
|
+
};
|
|
8194
|
+
|
|
8195
|
+
// ../adapters-node/src/node-process-runner.ts
|
|
8196
|
+
import { spawn as spawn2 } from "child_process";
|
|
8197
|
+
var STDERR_TAIL_BYTES = 8 * 1024;
|
|
8198
|
+
function buildEnv(overrides) {
|
|
8199
|
+
const env = { ...process.env };
|
|
8200
|
+
for (const [key2, value] of Object.entries(overrides ?? {})) {
|
|
8201
|
+
if (value === void 0) delete env[key2];
|
|
8202
|
+
else env[key2] = value;
|
|
8203
|
+
}
|
|
8204
|
+
return env;
|
|
8205
|
+
}
|
|
8206
|
+
var useShell = process.platform === "win32";
|
|
8207
|
+
var NodeProcessRunner = class {
|
|
8208
|
+
run(command, args, options = {}) {
|
|
8209
|
+
return new Promise((resolve3, reject) => {
|
|
8210
|
+
const child = spawn2(command, args, {
|
|
8211
|
+
env: buildEnv(options.env),
|
|
8212
|
+
stdio: ["inherit", "inherit", options.captureStderr ? "pipe" : "inherit"],
|
|
8213
|
+
shell: useShell
|
|
8214
|
+
});
|
|
8215
|
+
let tail = "";
|
|
8216
|
+
child.stderr?.on("data", (chunk) => {
|
|
8217
|
+
process.stderr.write(chunk);
|
|
8218
|
+
tail = (tail + chunk.toString()).slice(-STDERR_TAIL_BYTES);
|
|
8219
|
+
});
|
|
8220
|
+
const ignore = () => {
|
|
8221
|
+
};
|
|
8222
|
+
process.on("SIGINT", ignore);
|
|
8223
|
+
process.on("SIGTERM", ignore);
|
|
8224
|
+
const cleanup = () => {
|
|
8225
|
+
process.off("SIGINT", ignore);
|
|
8226
|
+
process.off("SIGTERM", ignore);
|
|
8227
|
+
};
|
|
8228
|
+
child.on("error", (error) => {
|
|
8229
|
+
cleanup();
|
|
8230
|
+
reject(error);
|
|
8231
|
+
});
|
|
8232
|
+
child.on("close", (code, signal) => {
|
|
8233
|
+
cleanup();
|
|
8234
|
+
resolve3({ exitCode: code ?? (signal ? 128 + 2 : 1), stderrTail: tail });
|
|
8235
|
+
});
|
|
8236
|
+
});
|
|
8237
|
+
}
|
|
8238
|
+
capture(command, args, options = {}) {
|
|
8239
|
+
return new Promise((resolve3) => {
|
|
8240
|
+
const child = spawn2(command, args, { env: buildEnv(options.env), shell: useShell });
|
|
8241
|
+
child.stdin.end();
|
|
8242
|
+
let stdout2 = "";
|
|
8243
|
+
let stderr = "";
|
|
8244
|
+
child.stdout.on("data", (c3) => {
|
|
8245
|
+
stdout2 += c3.toString();
|
|
8246
|
+
});
|
|
8247
|
+
child.stderr.on("data", (c3) => {
|
|
8248
|
+
stderr += c3.toString();
|
|
8249
|
+
});
|
|
8250
|
+
child.on("error", (error) => resolve3({ exitCode: 127, stdout: stdout2, stderr: error.message }));
|
|
8251
|
+
child.on("close", (code) => resolve3({ exitCode: code ?? 1, stdout: stdout2, stderr }));
|
|
8252
|
+
});
|
|
8253
|
+
}
|
|
8254
|
+
async probe(command) {
|
|
8255
|
+
const result = await this.capture(command, ["--version"]);
|
|
8256
|
+
if (result.exitCode !== 0) return { installed: false };
|
|
8257
|
+
const output = `${result.stdout} ${result.stderr}`;
|
|
8258
|
+
const version = output.match(/(\d+\.\d+\.\d+)/)?.[1];
|
|
8259
|
+
return { installed: true, ...version ? { version } : {} };
|
|
8260
|
+
}
|
|
8261
|
+
};
|
|
8262
|
+
|
|
8263
|
+
// ../adapters-node/src/system-tool-installer.ts
|
|
8264
|
+
var TOOLS = {
|
|
8265
|
+
aws: {
|
|
8266
|
+
manualUrl: "https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html",
|
|
8267
|
+
recipes: {
|
|
8268
|
+
darwin: [{ via: "Homebrew", command: "brew", args: ["install", "awscli"] }],
|
|
8269
|
+
win32: [
|
|
8270
|
+
{ via: "winget", command: "winget", args: ["install", "--exact", "--id", "Amazon.AWSCLI"] }
|
|
8271
|
+
]
|
|
8272
|
+
}
|
|
8273
|
+
},
|
|
8274
|
+
az: {
|
|
8275
|
+
manualUrl: "https://learn.microsoft.com/cli/azure/install-azure-cli",
|
|
8276
|
+
recipes: {
|
|
8277
|
+
darwin: [{ via: "Homebrew", command: "brew", args: ["install", "azure-cli"] }],
|
|
8278
|
+
win32: [
|
|
8279
|
+
{
|
|
8280
|
+
via: "winget",
|
|
8281
|
+
command: "winget",
|
|
8282
|
+
args: ["install", "--exact", "--id", "Microsoft.AzureCLI"]
|
|
8283
|
+
}
|
|
8284
|
+
]
|
|
8285
|
+
}
|
|
8286
|
+
},
|
|
8287
|
+
gcloud: {
|
|
8288
|
+
manualUrl: "https://cloud.google.com/sdk/docs/install",
|
|
8289
|
+
recipes: {
|
|
8290
|
+
darwin: [
|
|
8291
|
+
{ via: "Homebrew", command: "brew", args: ["install", "--cask", "google-cloud-sdk"] }
|
|
8292
|
+
],
|
|
8293
|
+
win32: [
|
|
8294
|
+
{
|
|
8295
|
+
via: "winget",
|
|
8296
|
+
command: "winget",
|
|
8297
|
+
args: ["install", "--exact", "--id", "Google.CloudSDK"]
|
|
8298
|
+
}
|
|
8299
|
+
]
|
|
8300
|
+
}
|
|
8301
|
+
}
|
|
8302
|
+
};
|
|
8303
|
+
var SystemToolInstaller = class {
|
|
8304
|
+
constructor(runner, platform = process.platform) {
|
|
8305
|
+
this.runner = runner;
|
|
8306
|
+
this.platform = platform;
|
|
8307
|
+
}
|
|
8308
|
+
runner;
|
|
8309
|
+
platform;
|
|
8310
|
+
available = /* @__PURE__ */ new Map();
|
|
8311
|
+
recipeFor(command) {
|
|
8312
|
+
const tool = TOOLS[command];
|
|
8313
|
+
if (!tool) return void 0;
|
|
8314
|
+
const recipe = tool.recipes[this.platform]?.[0];
|
|
8315
|
+
return recipe ? { ...recipe, manualUrl: tool.manualUrl } : { via: "official installer", command: "", args: [], manualUrl: tool.manualUrl };
|
|
8316
|
+
}
|
|
8317
|
+
async install(recipe) {
|
|
8318
|
+
if (!recipe.command) return false;
|
|
8319
|
+
if (!await this.hasCommand(recipe.command)) return false;
|
|
8320
|
+
const { exitCode } = await this.runner.run(recipe.command, recipe.args);
|
|
8321
|
+
return exitCode === 0;
|
|
8322
|
+
}
|
|
8323
|
+
async hasCommand(command) {
|
|
8324
|
+
if (!this.available.has(command)) {
|
|
8325
|
+
this.available.set(command, (await this.runner.probe(command)).installed);
|
|
8326
|
+
}
|
|
8327
|
+
return this.available.get(command) ?? false;
|
|
8328
|
+
}
|
|
8329
|
+
};
|
|
8330
|
+
|
|
8200
8331
|
// ../adapters-node/src/yaml-config-store.ts
|
|
8332
|
+
import { mkdir as mkdir2, readFile as readFile2, rename, rm, writeFile as writeFile2 } from "fs/promises";
|
|
8333
|
+
import { homedir } from "os";
|
|
8334
|
+
import { join as join2 } from "path";
|
|
8201
8335
|
var import_yaml = __toESM(require_dist(), 1);
|
|
8202
8336
|
function defaultConfigDir(env = process.env) {
|
|
8203
8337
|
if (env.CLOUDHOP_HOME) return env.CLOUDHOP_HOME;
|
|
8204
|
-
return
|
|
8338
|
+
return join2(env.XDG_CONFIG_HOME || join2(homedir(), ".config"), "cloudhop");
|
|
8205
8339
|
}
|
|
8206
8340
|
var HEADER = "# cloudhop configuration. Contains no secrets.\n# Docs: https://github.com/dayepesb/cloudhop\n";
|
|
8207
8341
|
var YamlConfigStore = class {
|
|
@@ -8210,10 +8344,10 @@ var YamlConfigStore = class {
|
|
|
8210
8344
|
}
|
|
8211
8345
|
dir;
|
|
8212
8346
|
get configPath() {
|
|
8213
|
-
return
|
|
8347
|
+
return join2(this.dir, "config.yaml");
|
|
8214
8348
|
}
|
|
8215
8349
|
get targetsPath() {
|
|
8216
|
-
return
|
|
8350
|
+
return join2(this.dir, "targets.json");
|
|
8217
8351
|
}
|
|
8218
8352
|
async load() {
|
|
8219
8353
|
const raw = await this.read(this.configPath);
|
|
@@ -8261,7 +8395,7 @@ var YamlConfigStore = class {
|
|
|
8261
8395
|
}
|
|
8262
8396
|
async read(path2) {
|
|
8263
8397
|
try {
|
|
8264
|
-
return await
|
|
8398
|
+
return await readFile2(path2, "utf8");
|
|
8265
8399
|
} catch (error) {
|
|
8266
8400
|
if (error.code === "ENOENT") return void 0;
|
|
8267
8401
|
throw error;
|
|
@@ -8269,9 +8403,9 @@ var YamlConfigStore = class {
|
|
|
8269
8403
|
}
|
|
8270
8404
|
/** Writes through a temp file so a crash never leaves a half-written config. */
|
|
8271
8405
|
async write(path2, content) {
|
|
8272
|
-
await
|
|
8406
|
+
await mkdir2(this.dir, { recursive: true, mode: 448 });
|
|
8273
8407
|
const tmp = `${path2}.${process.pid}.tmp`;
|
|
8274
|
-
await
|
|
8408
|
+
await writeFile2(tmp, content, { mode: 384 });
|
|
8275
8409
|
await rename(tmp, path2);
|
|
8276
8410
|
}
|
|
8277
8411
|
};
|
|
@@ -8284,17 +8418,17 @@ var MAX_OUTPUT = 1e5;
|
|
|
8284
8418
|
var cap = (text4) => text4.length > MAX_OUTPUT ? `${text4.slice(0, MAX_OUTPUT)}
|
|
8285
8419
|
\u2026 [truncated: ${text4.length - MAX_OUTPUT} more characters. Narrow the command with --query or filters.]` : text4;
|
|
8286
8420
|
var WINDOWS_SHELL_CHARS = /[&|<>^%!"\r\n]/;
|
|
8287
|
-
var text = (args,
|
|
8288
|
-
const value = args[
|
|
8421
|
+
var text = (args, key2) => {
|
|
8422
|
+
const value = args[key2];
|
|
8289
8423
|
if (typeof value !== "string" || !value.trim()) {
|
|
8290
|
-
throw new CloudhopError("INVALID_NAME", `"${
|
|
8424
|
+
throw new CloudhopError("INVALID_NAME", `"${key2}" is required.`);
|
|
8291
8425
|
}
|
|
8292
8426
|
return value.trim();
|
|
8293
8427
|
};
|
|
8294
|
-
var stringArray = (args,
|
|
8295
|
-
const value = args[
|
|
8428
|
+
var stringArray = (args, key2) => {
|
|
8429
|
+
const value = args[key2];
|
|
8296
8430
|
if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) {
|
|
8297
|
-
throw new CloudhopError("INVALID_NAME", `"${
|
|
8431
|
+
throw new CloudhopError("INVALID_NAME", `"${key2}" must be an array of strings.`);
|
|
8298
8432
|
}
|
|
8299
8433
|
return value;
|
|
8300
8434
|
};
|
|
@@ -8304,15 +8438,15 @@ var aliasInput = {
|
|
|
8304
8438
|
description: "The cloudhop alias to use, as listed by list_aliases. Ask the user which one."
|
|
8305
8439
|
}
|
|
8306
8440
|
};
|
|
8307
|
-
function createTools(app) {
|
|
8441
|
+
function createTools(app, context) {
|
|
8308
8442
|
return [
|
|
8309
8443
|
{
|
|
8310
8444
|
name: "list_aliases",
|
|
8311
8445
|
title: "List cloudhop aliases",
|
|
8312
|
-
description: "Lists every cloudhop source (SSO portal or personal account) with its session state, and every alias: name, account/role label, region and whether a sign-in is needed. Call this first, then ask the user which alias to work with. Never contains secrets.",
|
|
8446
|
+
description: "Lists every cloudhop source (SSO portal or personal account) with its session state, and every alias: name, account/role label, region and whether a sign-in is needed. Also hints: `folder` (the aliases this project pins in its .cloudhoprc, and whether the user trusted it) and `inUse` (aliases the user switched their terminal to). Call this first, then ask the user which alias to work with, offering the hinted one first. Never contains secrets.",
|
|
8313
8447
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
8314
8448
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
8315
|
-
run: () => app.overview()
|
|
8449
|
+
run: async () => ({ ...await app.overview(), ...await context?.() })
|
|
8316
8450
|
},
|
|
8317
8451
|
{
|
|
8318
8452
|
name: "alias_status",
|
|
@@ -8337,16 +8471,45 @@ function createTools(app) {
|
|
|
8337
8471
|
required: ["alias"],
|
|
8338
8472
|
additionalProperties: false
|
|
8339
8473
|
},
|
|
8340
|
-
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
8474
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
8475
|
+
run: async (args) => {
|
|
8476
|
+
const alias = text(args, "alias");
|
|
8477
|
+
const result = await app.captureAsAlias(alias, await app.identityArgs(alias));
|
|
8478
|
+
return {
|
|
8479
|
+
alias,
|
|
8480
|
+
exitCode: result.exitCode,
|
|
8481
|
+
output: cap(result.stdout),
|
|
8482
|
+
errors: cap(result.stderr)
|
|
8483
|
+
};
|
|
8484
|
+
}
|
|
8485
|
+
},
|
|
8486
|
+
{
|
|
8487
|
+
name: "open_console",
|
|
8488
|
+
title: "Open the cloud console",
|
|
8489
|
+
description: "Opens the cloud's web console in the user's browser as the alias, already signed in, optionally on one service (AWS: s3, ec2, lambda, cloudwatch...). Returns how the user got in, never the sign-in URL. Use it when the user wants to see something in the console. Never opens a browser to sign in: an expired session returns LOGIN_REQUIRED.",
|
|
8490
|
+
inputSchema: {
|
|
8491
|
+
type: "object",
|
|
8492
|
+
properties: {
|
|
8493
|
+
...aliasInput,
|
|
8494
|
+
service: { type: "string", description: 'Optional service page, e.g. "s3" or "ec2".' }
|
|
8495
|
+
},
|
|
8496
|
+
required: ["alias"],
|
|
8497
|
+
additionalProperties: false
|
|
8498
|
+
},
|
|
8499
|
+
annotations: {
|
|
8500
|
+
readOnlyHint: false,
|
|
8501
|
+
destructiveHint: false,
|
|
8502
|
+
idempotentHint: true,
|
|
8503
|
+
openWorldHint: true
|
|
8504
|
+
},
|
|
8341
8505
|
run: async (args) => {
|
|
8342
8506
|
const alias = text(args, "alias");
|
|
8343
|
-
const
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
};
|
|
8507
|
+
const service = typeof args.service === "string" && args.service.trim() ? args.service.trim() : void 0;
|
|
8508
|
+
const via = await app.openConsole(alias, {
|
|
8509
|
+
...service ? { service } : {},
|
|
8510
|
+
interactive: false
|
|
8511
|
+
});
|
|
8512
|
+
return { alias, opened: true, via };
|
|
8350
8513
|
}
|
|
8351
8514
|
},
|
|
8352
8515
|
{
|
|
@@ -8443,17 +8606,19 @@ function createTools(app) {
|
|
|
8443
8606
|
|
|
8444
8607
|
// ../mcp/src/server.ts
|
|
8445
8608
|
var MCP_INSTRUCTIONS = `cloudhop gives each cloud account + role + region a short alias and signs the user in only when a session expired.
|
|
8446
|
-
Before the first cloud operation in a conversation: call list_aliases, show the user the aliases (name, account/role, region) and ask which one to use. Never guess the alias.
|
|
8609
|
+
Before the first cloud operation in a conversation: call list_aliases, show the user the aliases (name, account/role, region) and ask which one to use. Never guess the alias. If list_aliases has folder.aliases (the project's .cloudhoprc) or inUse, offer that alias first, but still ask.
|
|
8447
8610
|
Use run_cloud_cli for cloud CLI commands, never --profile. Read-only commands may run once the alias is chosen; before anything that creates, changes or deletes resources, show the alias, account, region and exact command, and wait for the user's explicit OK. Aliases with prod: true are production: run_cloud_cli refuses changes there until the user approves and you pass confirm_production: true.
|
|
8611
|
+
To show the user something in the web console, call open_console (it opens their browser, already signed in).
|
|
8448
8612
|
If a tool returns LOGIN_REQUIRED or alias_status says browser-sign-in: tell the user their browser will open, then call login (it waits up to 5 minutes for them to approve). Never print or ask for credentials.`;
|
|
8449
8613
|
var SUPPORTED_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
|
|
8450
8614
|
function serveMcp({
|
|
8451
8615
|
app,
|
|
8452
8616
|
version,
|
|
8453
8617
|
input = process.stdin,
|
|
8454
|
-
output = process.stdout
|
|
8618
|
+
output = process.stdout,
|
|
8619
|
+
context
|
|
8455
8620
|
}) {
|
|
8456
|
-
const tools = createTools(app);
|
|
8621
|
+
const tools = createTools(app, context);
|
|
8457
8622
|
const byName = new Map(tools.map((t2) => [t2.name, t2]));
|
|
8458
8623
|
const send2 = (message) => output.write(`${JSON.stringify(message)}
|
|
8459
8624
|
`);
|
|
@@ -8519,7 +8684,7 @@ function serveMcp({
|
|
|
8519
8684
|
if (!isNotification) fail(id, -32601, `Method not found: ${method}`);
|
|
8520
8685
|
}
|
|
8521
8686
|
};
|
|
8522
|
-
return new Promise((
|
|
8687
|
+
return new Promise((resolve3) => {
|
|
8523
8688
|
const lines = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
|
|
8524
8689
|
const pending = /* @__PURE__ */ new Set();
|
|
8525
8690
|
lines.on("line", (line) => {
|
|
@@ -8539,7 +8704,7 @@ function serveMcp({
|
|
|
8539
8704
|
task.finally(() => pending.delete(task));
|
|
8540
8705
|
});
|
|
8541
8706
|
lines.on("close", () => {
|
|
8542
|
-
Promise.allSettled([...pending]).then(() =>
|
|
8707
|
+
Promise.allSettled([...pending]).then(() => resolve3());
|
|
8543
8708
|
});
|
|
8544
8709
|
});
|
|
8545
8710
|
}
|
|
@@ -8554,7 +8719,9 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
8554
8719
|
"noMcp",
|
|
8555
8720
|
"force",
|
|
8556
8721
|
"prod",
|
|
8557
|
-
"off"
|
|
8722
|
+
"off",
|
|
8723
|
+
"save",
|
|
8724
|
+
"leave"
|
|
8558
8725
|
]);
|
|
8559
8726
|
var camel = (name) => name.replace(/-([a-z])/g, (_2, c3) => c3.toUpperCase());
|
|
8560
8727
|
function parseArgs(argv) {
|
|
@@ -9975,7 +10142,7 @@ import { styleText as styleText2, stripVTControlCharacters } from "util";
|
|
|
9975
10142
|
import process$1 from "process";
|
|
9976
10143
|
var import_sisteransi2 = __toESM(require_src(), 1);
|
|
9977
10144
|
import { existsSync, lstatSync, readdirSync } from "fs";
|
|
9978
|
-
import { dirname, join as
|
|
10145
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
9979
10146
|
function isUnicodeSupported() {
|
|
9980
10147
|
if (process$1.platform !== "win32") {
|
|
9981
10148
|
return process$1.env.TERM !== "linux";
|
|
@@ -10913,10 +11080,10 @@ var path = (e) => {
|
|
|
10913
11080
|
return [];
|
|
10914
11081
|
try {
|
|
10915
11082
|
let i2;
|
|
10916
|
-
existsSync(t2) ? lstatSync(t2).isDirectory() && (!e.directory || t2.endsWith("/")) ? i2 = t2 : i2 =
|
|
11083
|
+
existsSync(t2) ? lstatSync(t2).isDirectory() && (!e.directory || t2.endsWith("/")) ? i2 = t2 : i2 = dirname2(t2) : i2 = dirname2(t2);
|
|
10917
11084
|
const c3 = t2.length > 1 && t2.endsWith("/") ? t2.slice(0, -1) : t2;
|
|
10918
11085
|
return readdirSync(i2).map((r2) => {
|
|
10919
|
-
const n3 =
|
|
11086
|
+
const n3 = join3(i2, r2), m3 = lstatSync(n3);
|
|
10920
11087
|
return {
|
|
10921
11088
|
name: r2,
|
|
10922
11089
|
path: n3,
|
|
@@ -11673,16 +11840,16 @@ import { homedir as homedir2 } from "os";
|
|
|
11673
11840
|
import { isAbsolute, relative } from "path";
|
|
11674
11841
|
|
|
11675
11842
|
// ../assistants/src/installer.ts
|
|
11676
|
-
import { createHash } from "crypto";
|
|
11843
|
+
import { createHash as createHash2 } from "crypto";
|
|
11677
11844
|
import { existsSync as existsSync2 } from "fs";
|
|
11678
|
-
import { mkdir as
|
|
11679
|
-
import { dirname as
|
|
11845
|
+
import { mkdir as mkdir3, readFile as readFile3, rename as rename2, rm as rm2, rmdir, writeFile as writeFile3 } from "fs/promises";
|
|
11846
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
11680
11847
|
|
|
11681
11848
|
// ../assistants/src/assets/skill.md
|
|
11682
|
-
var skill_default = "---\nname: cloudhop\ndescription: Run AWS CLI commands and other cloud tools as a specific account, role and region through cloudhop aliases (the `hop` and `cloudhop` commands). Use whenever the user asks to do anything in their AWS accounts, inspect or change cloud resources, run aws/terraform/cdk against an account, list their cloud accounts or aliases, or mentions hop or cloudhop.\n---\n\n# Working with cloud accounts through cloudhop\n\nThe user manages their cloud access with **cloudhop**. Each **alias** (for\nexample `acme-dev`) points to one account, one role and one region. cloudhop\nsigns the user in through their browser only when a session has expired. There\nare no access keys: never ask for, create, print or store credentials.\n\n## 1. Pick the alias with the user, every time\n\nBefore the first cloud operation in a conversation:\n\n1. List the aliases. Use the `list_aliases` tool if the cloudhop MCP server is\n connected; otherwise run `cloudhop ls --json`.\n2. Show the user the options (alias, account / role, region) and **ask which\n one to use**. Don't guess, even if one name looks right, and don't pick\n one just because there is only one: confirm it.\n3. Use that alias for the rest of the conversation. Ask again if the user\n moves to another environment, account or task.\n\n## 2. Check the session before running anything\n\nRun `cloudhop status <alias> --json` (or the `alias_status` tool) and read\n`next`:\n\n| `next` | Meaning | What to do |\n| ----------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `ready` | Session valid | Go ahead |\n| `silent-renewal` | Expired but renewable without the user | Go ahead; cloudhop renews it |\n| `browser-sign-in` | The user must sign in | Tell the user their browser will open and they need to approve, then run `cloudhop login <source>` (or the `login` tool) and wait up to 5 minutes |\n| `unknown` | Managed by the cloud CLI (personal accounts) | Go ahead; if it fails, run `cloudhop login <source>` |\n\nDon't start several cloud commands in parallel while a sign-in is pending.\n\n## 3. Run commands through the alias\n\n- **AWS CLI**: `hop <alias> <aws arguments>`, e.g. `hop acme-dev s3 ls`.\n It is exactly `aws <arguments>` with the alias' account, role and region.\n Never add `--profile`. Use `--region <r>` only to override the region once.\n With the MCP server, use `run_cloud_cli` with `args` (no program name).\n- **Who am I**: `hop <alias>` with no arguments (or the `whoami` tool).\n- **The same command in several aliases** (after the user chose them):\n `cloudhop each a,b,c --yes -- <aws arguments>`; add `--json` to parse the\n results.\n- **The AWS web console**: `cloudhop console <alias> [service]` opens it in\n the user's browser, already signed in. Never try to print or fetch its URL.\n- **Other tools** (Terraform, CDK, SDK scripts):\n `cloudhop exec <alias> -- <command>`, e.g.\n `cloudhop exec acme-dev -- terraform plan`.\n- Prefer `--output json` and `--query` to keep output small.\n- Don't export `AWS_PROFILE`, `AWS_ACCESS_KEY_ID` or similar variables, don't\n run commands that print credentials (`env`, `printenv`, `export-credentials`)\n and never edit `~/.aws/config`, `~/.aws/credentials` or cloudhop's own files.\n\n## 4. Confirm before changing anything\n\n- Read-only commands (`list`, `describe`, `get`, `s3 ls`, `logs tail`) can run\n once the user has chosen the alias.\n- Before any command that **creates, changes, deletes or costs money**, show\n the alias, account, region and the exact command, and wait for the user's\n explicit \"yes\". One confirmation covers one command, not a series.\n- **Production aliases** have `\"prod\": true` in `cloudhop ls --json` (and\n `PROD` in `cloudhop ls`). Confirm every change there and suggest a dry run\n (`--dryrun`, `terraform plan`) first. cloudhop enforces it: without a\n terminal, `hop`/`cloudhop exec` refuse changes on production with\n `PROD_CONFIRMATION_REQUIRED`. After the user explicitly approves the exact\n command, rerun it with `CLOUDHOP_CONFIRM_PROD=1` in that command's\n environment (with MCP: `confirm_production: true`). Never set it on your own.\n- Treat aliases or accounts that merely look like production (`prd`, `live`)\n the same way, even if they are not marked.\n- Don't change cloudhop itself (`cloudhop add`, `rm`, `region`, `login` for\n another source) unless the user asked for it.\n\n## 5. When something goes wrong\n\n| You see | Do this |\n| --------------------------------------------------- | ------------------------------------------------------------------------------ |\n| \"Opening your browser to log in\" | Tell the user to approve in the browser; the command continues by itself |\n| The command was cut off while waiting for a sign-in | Run `cloudhop login <source>` with a timeout of at least 5 minutes, then retry |\n| `LOGIN_REQUIRED` | Same as `browser-sign-in` above |\n| No browser on this machine (remote, container) | Run `cloudhop login <source> --device-code` and show the user the URL and code |\n| `Alias \"x\" not found` | Run `cloudhop ls` and ask the user again |\n| AWS CLI missing or too old | Suggest `cloudhop doctor`; don't install anything without asking |\n| `AccessDenied` | The role lacks permission: tell the user; don't try other aliases on your own |\n\nMore: https://dayepesb.github.io/cloudhop/guides/hop-like-aws/\n";
|
|
11849
|
+
var skill_default = "---\nname: cloudhop\ndescription: Run AWS CLI commands and other cloud tools as a specific account, role and region through cloudhop aliases (the `hop` and `cloudhop` commands). Use whenever the user asks to do anything in their AWS accounts, inspect or change cloud resources, run aws/terraform/cdk against an account, list their cloud accounts or aliases, or mentions hop or cloudhop.\n---\n\n# Working with cloud accounts through cloudhop\n\nThe user manages their cloud access with **cloudhop**. Each **alias** (for\nexample `acme-dev`) points to one account, one role and one region. cloudhop\nsigns the user in through their browser only when a session has expired. There\nare no access keys: never ask for, create, print or store credentials.\n\n## How the user works (to guide them)\n\nWhen the user asks how to set up or use cloudhop themselves, describe this\npath; these are commands for **them** to run in their own terminal:\n\n1. **Once**: `npm install --global cloudhop`, then `cloudhop add` (signs in\n to an SSO portal or personal account and names each account/role as an\n alias), then `cloudhop shell` (adds one line to their shell's startup\n file and, with Starship, the alias segment to `starship.toml`; they open\n a new terminal afterwards). `cloudhop setup` does all of it in one\n guided run. Works in zsh, bash (Git Bash too), fish and PowerShell;\n in Windows `cmd.exe`, `cloudhop use` opens a new `cmd` with the alias in\n its prompt.\n2. **Every day**: `cloudhop use <alias>` switches that terminal to an\n account; after that they type plain `aws ...`, Terraform\n or SDK commands. `cloudhop use <other>` switches accounts and\n `cloudhop use --off` goes back. Expired sessions sign in by themselves,\n and on production aliases `aws` asks before changes. Their prompt shows\n the alias in use, in red for production, and `cloudhop status` tells\n them which alias their terminal uses.\n3. **Per project**: `cloudhop use <alias> --save` writes a `.cloudhoprc` in\n the folder, so `cd` into it switches the terminal; teammates run\n `cloudhop allow` once after cloning (and again after the file changes);\n `cloudhop deny` withdraws it. A file does nothing until it is trusted.\n4. **One-offs**: `hop <alias> <aws args>` runs one command in another\n account without switching; `cloudhop each` runs it in many;\n `cloudhop console <alias> [service]` opens the web console signed in.\n\nWhy `cloudhop shell` is needed: a program can't change the environment of\nthe terminal that started it, only the shell can.\nWithout it, `cloudhop use` opens a new shell instead; `hop` always works.\n\nSwitches, for the user: `CLOUDHOP_WRAP=0` keeps `aws` as the plain AWS\nCLI, `CLOUDHOP_AUTO_USE=0` ignores `.cloudhoprc` files, `CLOUDHOP_PROMPT=0`\nhides the prompt label. `cloudhop shell starship` prints the Starship\nsegment for people who place it by hand.\n\nWhen their terminal doesn't do what they expect:\n\n| They say | Suggest |\n| --- | --- |\n| \"Which account am I in?\" | `cloudhop status` (first line) |\n| `cloudhop use` opened a new shell / says it needs the integration | `cloudhop shell`, then a new terminal |\n| No label in the prompt | Run `cloudhop shell` again (it adds what's missing), then a new terminal |\n| `cd` into the project doesn't switch | `cloudhop allow` in that folder; check the alias name exists (`cloudhop ls`) |\n| `aws` runs in the wrong account | `cloudhop status`, then `cloudhop use <alias>` or `cloudhop use --off` |\n\nThat is the user's way. **You** work differently, below: you always name the\nalias with `hop <alias>`, because your shell doesn't have their integration.\n\n## 1. Pick the alias with the user, every time\n\nBefore the first cloud operation in a conversation:\n\n1. List the aliases. Use the `list_aliases` tool if the cloudhop MCP server is\n connected; otherwise run `cloudhop ls --json`.\n2. Show the user the options (alias, account / role, region) and **ask which\n one to use**. Don't guess, even if one name looks right, and don't pick\n one just because there is only one: confirm it.\n - The listing may carry hints: `folder.aliases` is what this project pins\n in its `.cloudhoprc` (`folder.trusted` says whether the user approved\n it), and `inUse` is what the user switched their terminal to with\n `cloudhop use` (`cloudhop status --json` has `inUse` too). Offer that\n alias first (\"This project uses\n `license-qa`, use it?\"), but still wait for the answer.\n3. Use that alias for the rest of the conversation. Ask again if the user\n moves to another environment, account or task.\n\n## 2. Check the session before running anything\n\nRun `cloudhop status <alias> --json` (or the `alias_status` tool) and read\n`next`:\n\n| `next` | Meaning | What to do |\n| ----------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `ready` | Session valid | Go ahead |\n| `silent-renewal` | Expired but renewable without the user | Go ahead; cloudhop renews it |\n| `browser-sign-in` | The user must sign in | Tell the user their browser will open and they need to approve, then run `cloudhop login <source>` (or the `login` tool) and wait up to 5 minutes |\n| `unknown` | Managed by the cloud CLI (personal accounts) | Go ahead; if it fails, run `cloudhop login <source>` |\n\nDon't start several cloud commands in parallel while a sign-in is pending.\n\n## 3. Run commands through the alias\n\n- **AWS CLI**: `hop <alias> <aws arguments>`, e.g. `hop acme-dev s3 ls`.\n It is exactly `aws <arguments>` with the alias' account, role and region.\n Never add `--profile`. Use `--region <r>` only to override the region once.\n With the MCP server, use `run_cloud_cli` with `args` (no program name).\n- **Who am I**: `hop <alias>` with no arguments (or the `whoami` tool).\n- **The same command in several aliases** (after the user chose them):\n `cloudhop each a,b,c --yes -- <aws arguments>`; add `--json` to parse the\n results.\n- **The AWS web console**: `cloudhop console <alias> [service]` (or the\n `open_console` tool) opens it in the user's browser, already signed in.\n Never try to print or fetch its URL.\n- **Always name the alias**, even when the project has a `.cloudhoprc` or\n the user's terminal uses an alias: your shell doesn't have the user's\n cloudhop integration, so a plain `aws` may run as anything. Use\n `hop <alias> ...`, never bare `aws`.\n- Never run the interactive forms meant for a person at a terminal: `hop` or\n `hop -` without an alias, `cloudhop console` without an alias,\n `cloudhop use` (it changes or opens an interactive shell), `cloudhop\n allow`/`deny` (trusting a folder is the user's decision), `cloudhop setup`,\n or `cloudhop add`/`alias` without flags. Nobody can answer their prompts in\n your terminal.\n- **Other tools** (Terraform, CDK, SDK scripts):\n `cloudhop exec <alias> -- <command>`, e.g.\n `cloudhop exec acme-dev -- terraform plan`.\n- Prefer `--output json` and `--query` to keep output small.\n- Don't export `AWS_PROFILE`, `AWS_ACCESS_KEY_ID` or similar variables, don't\n run commands that print credentials (`env`, `printenv`, `export-credentials`)\n and never edit `~/.aws/config`, `~/.aws/credentials` or cloudhop's own files.\n\n## 4. Confirm before changing anything\n\n- Read-only commands (`list`, `describe`, `get`, `s3 ls`, `logs tail`) can run\n once the user has chosen the alias.\n- Before any command that **creates, changes, deletes or costs money**, show\n the alias, account, region and the exact command, and wait for the user's\n explicit \"yes\". One confirmation covers one command, not a series.\n- **Production aliases** have `\"prod\": true` in `cloudhop ls --json` (and\n `PROD` in `cloudhop ls`). Confirm every change there and suggest a dry run\n (`--dryrun`, `terraform plan`) first. cloudhop enforces it: without a\n terminal, `hop`/`cloudhop exec` refuse changes on production with\n `PROD_CONFIRMATION_REQUIRED`. After the user explicitly approves the exact\n command, rerun it with `CLOUDHOP_CONFIRM_PROD=1` in that command's\n environment (with MCP: `confirm_production: true`). Never set it on your own.\n- Treat aliases or accounts that merely look like production (`prd`, `live`)\n the same way, even if they are not marked.\n- Don't change cloudhop itself (`cloudhop add`, `rm`, `mv`, `region`, `prod`,\n `team import`, `agent`, `login` for another source) or write a\n `.cloudhoprc` unless the user asked for it. Never unmark a production\n alias to get around a confirmation.\n\n## 5. When something goes wrong\n\n| You see | Do this |\n| --------------------------------------------------- | ------------------------------------------------------------------------------ |\n| \"Opening your browser to log in\" | Tell the user to approve in the browser; the command continues by itself |\n| The command was cut off while waiting for a sign-in | Run `cloudhop login <source>` with a timeout of at least 5 minutes, then retry |\n| `LOGIN_REQUIRED` | Same as `browser-sign-in` above |\n| No browser on this machine (remote, container) | Run `cloudhop login <source> --device-code` and show the user the URL and code |\n| `Alias \"x\" not found` | Run `cloudhop ls` and ask the user again |\n| AWS CLI missing or too old | Suggest `cloudhop doctor`; don't install anything without asking |\n| `AccessDenied` | The role lacks permission: tell the user; don't try other aliases on your own |\n\nMore: https://dayepesb.github.io/cloudhop/guides/hop-like-aws/\n";
|
|
11683
11850
|
|
|
11684
11851
|
// ../assistants/src/targets.ts
|
|
11685
|
-
import { join as
|
|
11852
|
+
import { join as join4 } from "path";
|
|
11686
11853
|
var ASSISTANT_IDS = [
|
|
11687
11854
|
"claude",
|
|
11688
11855
|
"kiro",
|
|
@@ -11697,9 +11864,9 @@ var ASSISTANTS = {
|
|
|
11697
11864
|
claude: {
|
|
11698
11865
|
id: "claude",
|
|
11699
11866
|
name: "Claude Code",
|
|
11700
|
-
markers: ({ home }) => [
|
|
11701
|
-
skillDir: (scope, { home, project }) =>
|
|
11702
|
-
mcp: (scope, { project }) => scope === "project" ? { kind: "json", file:
|
|
11867
|
+
markers: ({ home }) => [join4(home, ".claude")],
|
|
11868
|
+
skillDir: (scope, { home, project }) => join4(scope === "user" ? home : project, ".claude", "skills", SKILL_NAME),
|
|
11869
|
+
mcp: (scope, { project }) => scope === "project" ? { kind: "json", file: join4(project, ".mcp.json"), key: "mcpServers", withType: false } : {
|
|
11703
11870
|
kind: "cli",
|
|
11704
11871
|
command: "claude",
|
|
11705
11872
|
add: ["mcp", "add", "--scope", "user", "cloudhop", "--", MCP_COMMAND, ...MCP_ARGS],
|
|
@@ -11710,11 +11877,11 @@ var ASSISTANTS = {
|
|
|
11710
11877
|
kiro: {
|
|
11711
11878
|
id: "kiro",
|
|
11712
11879
|
name: "Kiro",
|
|
11713
|
-
markers: ({ home }) => [
|
|
11714
|
-
skillDir: (scope, { home, project }) =>
|
|
11880
|
+
markers: ({ home }) => [join4(home, ".kiro")],
|
|
11881
|
+
skillDir: (scope, { home, project }) => join4(scope === "user" ? home : project, ".kiro", "skills", SKILL_NAME),
|
|
11715
11882
|
mcp: (scope, { home, project }) => ({
|
|
11716
11883
|
kind: "json",
|
|
11717
|
-
file:
|
|
11884
|
+
file: join4(scope === "user" ? home : project, ".kiro", "settings", "mcp.json"),
|
|
11718
11885
|
key: "mcpServers",
|
|
11719
11886
|
withType: false
|
|
11720
11887
|
})
|
|
@@ -11722,11 +11889,11 @@ var ASSISTANTS = {
|
|
|
11722
11889
|
copilot: {
|
|
11723
11890
|
id: "copilot",
|
|
11724
11891
|
name: "GitHub Copilot (VS Code)",
|
|
11725
|
-
markers: ({ home }) => [
|
|
11726
|
-
skillDir: (scope, { home, project }) => scope === "user" ?
|
|
11892
|
+
markers: ({ home }) => [join4(home, ".copilot"), join4(home, ".vscode")],
|
|
11893
|
+
skillDir: (scope, { home, project }) => scope === "user" ? join4(home, ".copilot", "skills", SKILL_NAME) : join4(project, ".github", "skills", SKILL_NAME),
|
|
11727
11894
|
mcp: (scope, { project }) => scope === "project" ? {
|
|
11728
11895
|
kind: "json",
|
|
11729
|
-
file:
|
|
11896
|
+
file: join4(project, ".vscode", "mcp.json"),
|
|
11730
11897
|
key: "servers",
|
|
11731
11898
|
withType: true
|
|
11732
11899
|
} : {
|
|
@@ -11748,11 +11915,11 @@ var ASSISTANTS = {
|
|
|
11748
11915
|
cursor: {
|
|
11749
11916
|
id: "cursor",
|
|
11750
11917
|
name: "Cursor",
|
|
11751
|
-
markers: ({ home }) => [
|
|
11752
|
-
skillDir: (scope, { home, project }) =>
|
|
11918
|
+
markers: ({ home }) => [join4(home, ".cursor")],
|
|
11919
|
+
skillDir: (scope, { home, project }) => join4(scope === "user" ? home : project, ".cursor", "skills", SKILL_NAME),
|
|
11753
11920
|
mcp: (scope, { home, project }) => ({
|
|
11754
11921
|
kind: "json",
|
|
11755
|
-
file:
|
|
11922
|
+
file: join4(scope === "user" ? home : project, ".cursor", "mcp.json"),
|
|
11756
11923
|
key: "mcpServers",
|
|
11757
11924
|
withType: true
|
|
11758
11925
|
})
|
|
@@ -11760,29 +11927,29 @@ var ASSISTANTS = {
|
|
|
11760
11927
|
codex: {
|
|
11761
11928
|
id: "codex",
|
|
11762
11929
|
name: "Codex",
|
|
11763
|
-
markers: ({ home }) => [
|
|
11764
|
-
skillDir: (scope, { home, project }) =>
|
|
11930
|
+
markers: ({ home }) => [join4(home, ".codex"), join4(home, ".agents")],
|
|
11931
|
+
skillDir: (scope, { home, project }) => join4(scope === "user" ? home : project, ".agents", "skills", SKILL_NAME),
|
|
11765
11932
|
mcp: (scope, { home, project }) => ({
|
|
11766
11933
|
kind: "toml",
|
|
11767
|
-
file:
|
|
11934
|
+
file: join4(scope === "user" ? home : project, ".codex", "config.toml")
|
|
11768
11935
|
})
|
|
11769
11936
|
}
|
|
11770
11937
|
};
|
|
11771
11938
|
|
|
11772
11939
|
// ../assistants/src/installer.ts
|
|
11773
|
-
var sha = (text4) =>
|
|
11940
|
+
var sha = (text4) => createHash2("sha256").update(text4).digest("hex");
|
|
11774
11941
|
async function writeAtomic(file, content) {
|
|
11775
|
-
await
|
|
11942
|
+
await mkdir3(dirname3(file), { recursive: true });
|
|
11776
11943
|
const tmp = `${file}.cloudhop-${process.pid}.tmp`;
|
|
11777
|
-
await
|
|
11944
|
+
await writeFile3(tmp, content);
|
|
11778
11945
|
await rename2(tmp, file);
|
|
11779
11946
|
}
|
|
11780
11947
|
async function removeFileAndEmptyDir(file) {
|
|
11781
11948
|
await rm2(file, { force: true });
|
|
11782
|
-
await rmdir(
|
|
11949
|
+
await rmdir(dirname3(file)).catch(() => {
|
|
11783
11950
|
});
|
|
11784
11951
|
}
|
|
11785
|
-
var readText = (file) =>
|
|
11952
|
+
var readText = (file) => readFile3(file, "utf8").catch(() => void 0);
|
|
11786
11953
|
var AssistantsInstaller = class {
|
|
11787
11954
|
constructor(options) {
|
|
11788
11955
|
this.options = options;
|
|
@@ -11806,7 +11973,7 @@ var AssistantsInstaller = class {
|
|
|
11806
11973
|
return Promise.all(
|
|
11807
11974
|
ASSISTANT_IDS.map(async (id) => {
|
|
11808
11975
|
const assistant = ASSISTANTS[id];
|
|
11809
|
-
const skillPath =
|
|
11976
|
+
const skillPath = join5(assistant.skillDir(scope, this.options.places), "SKILL.md");
|
|
11810
11977
|
const entry = manifest.find((e) => e.skillFile === skillPath);
|
|
11811
11978
|
return {
|
|
11812
11979
|
assistant: id,
|
|
@@ -11825,7 +11992,7 @@ var AssistantsInstaller = class {
|
|
|
11825
11992
|
const manifest = await this.readManifest();
|
|
11826
11993
|
for (const id of ids) {
|
|
11827
11994
|
const assistant = ASSISTANTS[id];
|
|
11828
|
-
const skillPath =
|
|
11995
|
+
const skillPath = join5(assistant.skillDir(scope, this.options.places), "SKILL.md");
|
|
11829
11996
|
const content = this.skillContent;
|
|
11830
11997
|
await writeAtomic(skillPath, content);
|
|
11831
11998
|
const kept = manifest.filter((e) => e.skillFile !== skillPath);
|
|
@@ -11850,9 +12017,9 @@ var AssistantsInstaller = class {
|
|
|
11850
12017
|
for (const id of ids) {
|
|
11851
12018
|
const assistant = ASSISTANTS[id];
|
|
11852
12019
|
const dir = assistant.skillDir(scope, this.options.places);
|
|
11853
|
-
const skillPath =
|
|
12020
|
+
const skillPath = join5(dir, "SKILL.md");
|
|
11854
12021
|
await rm2(dir, { recursive: true, force: true });
|
|
11855
|
-
await rmdir(
|
|
12022
|
+
await rmdir(dirname3(dir)).then(() => rmdir(dirname3(dirname3(dir)))).catch(() => {
|
|
11856
12023
|
});
|
|
11857
12024
|
manifest = manifest.filter((e) => e.skillFile !== skillPath);
|
|
11858
12025
|
const outcome = await this.removeMcp(assistant.mcp(scope, this.options.places));
|
|
@@ -12006,7 +12173,7 @@ args = [${MCP_ARGS.map((a2) => `"${a2}"`).join(", ")}]
|
|
|
12006
12173
|
}
|
|
12007
12174
|
// ───────────── manifest ─────────────
|
|
12008
12175
|
get manifestPath() {
|
|
12009
|
-
return
|
|
12176
|
+
return join5(this.options.configDir, "assistants.json");
|
|
12010
12177
|
}
|
|
12011
12178
|
async readManifest() {
|
|
12012
12179
|
const text4 = await readText(this.manifestPath);
|
|
@@ -12237,9 +12404,16 @@ async function consoleCommand(app, args) {
|
|
|
12237
12404
|
}
|
|
12238
12405
|
|
|
12239
12406
|
// src/commands/each.ts
|
|
12407
|
+
var MAX_PATTERN = 200;
|
|
12240
12408
|
function matcher(selector) {
|
|
12241
12409
|
const regex = /^\/(.+)\/([a-z]*)$/.exec(selector);
|
|
12242
12410
|
if (regex) {
|
|
12411
|
+
if (selector.length > MAX_PATTERN) {
|
|
12412
|
+
throw new CloudhopError(
|
|
12413
|
+
"INVALID_NAME",
|
|
12414
|
+
`That regular expression is too long (max ${MAX_PATTERN}).`
|
|
12415
|
+
);
|
|
12416
|
+
}
|
|
12243
12417
|
try {
|
|
12244
12418
|
const re = new RegExp(regex[1], regex[2]);
|
|
12245
12419
|
return (name) => re.test(name);
|
|
@@ -12337,56 +12511,831 @@ ${listing}
|
|
|
12337
12511
|
))
|
|
12338
12512
|
return 130;
|
|
12339
12513
|
}
|
|
12340
|
-
if (risky.length > 0 && process.env.CLOUDHOP_CONFIRM_PROD !== "1") {
|
|
12341
|
-
if (!tty) {
|
|
12514
|
+
if (risky.length > 0 && process.env.CLOUDHOP_CONFIRM_PROD !== "1") {
|
|
12515
|
+
if (!tty) {
|
|
12516
|
+
throw new CloudhopError(
|
|
12517
|
+
"PROD_CONFIRMATION_REQUIRED",
|
|
12518
|
+
`This may change resources in production: ${risky.join(", ")}.`,
|
|
12519
|
+
"Run it in a terminal to confirm, or set CLOUDHOP_CONFIRM_PROD=1 once a person has approved it."
|
|
12520
|
+
);
|
|
12521
|
+
}
|
|
12522
|
+
dist_exports.log.warn(
|
|
12523
|
+
`${paint("red", bold("PRODUCTION"))} ${risky.join(", ")}: this may change resources.`
|
|
12524
|
+
);
|
|
12525
|
+
if (!answer(await dist_exports.confirm({ message: "Also run it in production?", initialValue: false })))
|
|
12526
|
+
return 130;
|
|
12527
|
+
}
|
|
12528
|
+
for (const source of new Set(selected.map((a2) => a2.source))) {
|
|
12529
|
+
await app.ensureSession(source, { interactive: tty });
|
|
12530
|
+
}
|
|
12531
|
+
const parallel = Math.max(1, Math.min(16, Number(flags.parallel) || 4));
|
|
12532
|
+
const outcomes = await mapLimit(selected, parallel, async (a2) => {
|
|
12533
|
+
try {
|
|
12534
|
+
const r2 = await app.captureAsAlias(a2.name, cliArgs);
|
|
12535
|
+
const outcome = { alias: a2.name, exitCode: r2.exitCode, stdout: r2.stdout, stderr: r2.stderr };
|
|
12536
|
+
if (!flags.json) printOutcome(outcome, width);
|
|
12537
|
+
return outcome;
|
|
12538
|
+
} catch (error) {
|
|
12539
|
+
const outcome = { alias: a2.name, exitCode: 1, stdout: "", stderr: error.message };
|
|
12540
|
+
if (!flags.json) printOutcome(outcome, width);
|
|
12541
|
+
return outcome;
|
|
12542
|
+
}
|
|
12543
|
+
});
|
|
12544
|
+
const failed = outcomes.filter((o2) => o2.exitCode !== 0);
|
|
12545
|
+
if (flags.json) {
|
|
12546
|
+
console.log(JSON.stringify({ version: 1, command: cliArgs, results: outcomes }, null, 2));
|
|
12547
|
+
} else {
|
|
12548
|
+
const summary = `${outcomes.length - failed.length} ok${failed.length ? `, ${failed.length} failed: ${failed.map((f2) => f2.alias).join(", ")}` : ""}`;
|
|
12549
|
+
process.stderr.write(`${failed.length ? paint("red", summary) : paint("green", summary)}
|
|
12550
|
+
`);
|
|
12551
|
+
}
|
|
12552
|
+
return failed.length ? 1 : 0;
|
|
12553
|
+
}
|
|
12554
|
+
function printOutcome({ alias, exitCode, stdout: stdout2, stderr }, width) {
|
|
12555
|
+
const tag = paint(exitCode === 0 ? "sky" : "red", `[${alias}]`.padEnd(width + 2));
|
|
12556
|
+
for (const line of stdout2.replace(/\n$/, "").split("\n"))
|
|
12557
|
+
if (line) process.stdout.write(`${tag} ${line}
|
|
12558
|
+
`);
|
|
12559
|
+
for (const line of stderr.replace(/\n$/, "").split("\n"))
|
|
12560
|
+
if (line) process.stderr.write(`${tag} ${paint("dim", line)}
|
|
12561
|
+
`);
|
|
12562
|
+
if (exitCode !== 0) process.stderr.write(`${tag} ${paint("red", `exit ${exitCode}`)}
|
|
12563
|
+
`);
|
|
12564
|
+
}
|
|
12565
|
+
|
|
12566
|
+
// src/commands/use.ts
|
|
12567
|
+
import { spawnSync } from "child_process";
|
|
12568
|
+
import { writeFile as writeFile5 } from "fs/promises";
|
|
12569
|
+
import { homedir as homedir4 } from "os";
|
|
12570
|
+
import { dirname as dirname5, join as join7, resolve as resolve2 } from "path";
|
|
12571
|
+
|
|
12572
|
+
// src/commands/shell.ts
|
|
12573
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
12574
|
+
import { appendFile, mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
12575
|
+
import { homedir as homedir3 } from "os";
|
|
12576
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
12577
|
+
|
|
12578
|
+
// src/commands/starship.ts
|
|
12579
|
+
var BEGIN = "# >>> cloudhop >>> (added by `cloudhop shell`; remove this block and the ${env_var.cloudhop_*} segments in `format` to undo)";
|
|
12580
|
+
var END = "# <<< cloudhop <<<";
|
|
12581
|
+
var SKY = "#3bb8f5";
|
|
12582
|
+
var ON_SKY = "#0b1124";
|
|
12583
|
+
var RED = "#be123c";
|
|
12584
|
+
var STARSHIP_SEGMENTS = PROVIDER_IDS.map(
|
|
12585
|
+
(p3) => `\${env_var.cloudhop_${p3}}\${env_var.cloudhop_${p3}_prod}`
|
|
12586
|
+
).join("");
|
|
12587
|
+
function starshipModules(powerline) {
|
|
12588
|
+
const segment = (provider, prod) => {
|
|
12589
|
+
if (!powerline) {
|
|
12590
|
+
return prod ? `[${provider} $env_value PROD ](bold #fb7185)` : `[${provider} $env_value ](bold ${SKY})`;
|
|
12591
|
+
}
|
|
12592
|
+
const { glyph, color } = powerline;
|
|
12593
|
+
const bg = prod ? RED : SKY;
|
|
12594
|
+
const fg = prod ? "#ffffff" : ON_SKY;
|
|
12595
|
+
return `[${glyph}](fg:${color} bg:${bg})[ ${provider} $env_value${prod ? " PROD" : ""} ](bold fg:${fg} bg:${bg})[${glyph}](fg:${bg} bg:${color})`;
|
|
12596
|
+
};
|
|
12597
|
+
return PROVIDER_IDS.map((p3) => {
|
|
12598
|
+
const variable = `CLOUDHOP_${p3.toUpperCase()}`;
|
|
12599
|
+
return `[env_var.cloudhop_${p3}]
|
|
12600
|
+
variable = '${variable}_SEGMENT'
|
|
12601
|
+
format = '${segment(p3, false)}'
|
|
12602
|
+
|
|
12603
|
+
[env_var.cloudhop_${p3}_prod]
|
|
12604
|
+
variable = '${variable}_PROD_SEGMENT'
|
|
12605
|
+
format = '${segment(p3, true)}'`;
|
|
12606
|
+
}).join("\n\n");
|
|
12607
|
+
}
|
|
12608
|
+
var hasCloudhopStarship = (text4) => text4.includes("[env_var.cloudhop_aws]");
|
|
12609
|
+
var CLOSING_GROUP = /\[\s*([-])\s*\]\(\s*fg:\s*(#[0-9a-fA-F]{3,8}|[a-z_]+)\s*\)/g;
|
|
12610
|
+
function addToStarshipConfig(text4) {
|
|
12611
|
+
if (hasCloudhopStarship(text4)) return text4;
|
|
12612
|
+
let result = text4;
|
|
12613
|
+
let powerline;
|
|
12614
|
+
const format = findTopLevelFormat(result);
|
|
12615
|
+
if (format && !/\$all\b|\$\{?env_var\}?(?![._])/.test(format.content)) {
|
|
12616
|
+
const { content, multiline: multiline2 } = format;
|
|
12617
|
+
const continuation = multiline2 ? "\\\n" : "";
|
|
12618
|
+
const closing = [...content.matchAll(CLOSING_GROUP)].at(-1);
|
|
12619
|
+
const lineBreak = [content.lastIndexOf("$line_break"), content.lastIndexOf("$character")].find(
|
|
12620
|
+
(i2) => i2 >= 0
|
|
12621
|
+
);
|
|
12622
|
+
let updated;
|
|
12623
|
+
if (closing?.index !== void 0 && (lineBreak === void 0 || closing.index < lineBreak)) {
|
|
12624
|
+
powerline = { glyph: closing[1], color: closing[2] };
|
|
12625
|
+
updated = `${content.slice(0, closing.index)}${STARSHIP_SEGMENTS}${continuation}${content.slice(closing.index)}`;
|
|
12626
|
+
} else if (lineBreak !== void 0) {
|
|
12627
|
+
updated = `${content.slice(0, lineBreak)}${STARSHIP_SEGMENTS}${continuation}${content.slice(lineBreak)}`;
|
|
12628
|
+
} else {
|
|
12629
|
+
const body = content.trimEnd();
|
|
12630
|
+
const tail = content.slice(body.length);
|
|
12631
|
+
const joiner = multiline2 && body.endsWith("\\") ? "\n" : "";
|
|
12632
|
+
updated = `${body}${joiner}${STARSHIP_SEGMENTS}${multiline2 ? "\\" : ""}${tail}`;
|
|
12633
|
+
}
|
|
12634
|
+
result = result.slice(0, format.start) + updated + result.slice(format.end);
|
|
12635
|
+
}
|
|
12636
|
+
const separator = result.length === 0 || result.endsWith("\n") ? "" : "\n";
|
|
12637
|
+
return `${result}${separator}
|
|
12638
|
+
${BEGIN}
|
|
12639
|
+
${starshipModules(powerline)}
|
|
12640
|
+
${END}
|
|
12641
|
+
`;
|
|
12642
|
+
}
|
|
12643
|
+
function findTopLevelFormat(text4) {
|
|
12644
|
+
const firstTable = text4.search(/^[ \t]*\[/m);
|
|
12645
|
+
const head = firstTable === -1 ? text4 : text4.slice(0, firstTable);
|
|
12646
|
+
const match = /^format\s*=\s*("""|'''|"|')/m.exec(head);
|
|
12647
|
+
if (!match) return void 0;
|
|
12648
|
+
const quote2 = match[1];
|
|
12649
|
+
const start = match.index + match[0].length;
|
|
12650
|
+
const end = text4.indexOf(quote2, start);
|
|
12651
|
+
if (end === -1) return void 0;
|
|
12652
|
+
return { start, end, content: text4.slice(start, end), multiline: quote2.length === 3 };
|
|
12653
|
+
}
|
|
12654
|
+
|
|
12655
|
+
// src/commands/shell.ts
|
|
12656
|
+
var SHELLS = ["zsh", "bash", "fish", "pwsh"];
|
|
12657
|
+
var ALIAS_COMMANDS = "use status console region mv prod rm exec";
|
|
12658
|
+
var SOURCE_COMMANDS = "alias login sync ls";
|
|
12659
|
+
var aliasVar = (provider) => `CLOUDHOP_${provider.toUpperCase()}_ALIAS`;
|
|
12660
|
+
var prodVar = (provider) => `CLOUDHOP_${provider.toUpperCase()}_PROD`;
|
|
12661
|
+
function posixScript(shell, clis) {
|
|
12662
|
+
const wrappers = clis.map(
|
|
12663
|
+
({ command, provider }) => `${command}() {
|
|
12664
|
+
case " $* " in *" --profile"*) command ${command} "$@"; return $?;; esac
|
|
12665
|
+
if [ -n "$${aliasVar(provider)}" ] && [ "$CLOUDHOP_WRAP" != 0 ]; then
|
|
12666
|
+
command cloudhop "$${aliasVar(provider)}" "$@"
|
|
12667
|
+
else
|
|
12668
|
+
command ${command} "$@"
|
|
12669
|
+
fi
|
|
12670
|
+
}`
|
|
12671
|
+
).join("\n");
|
|
12672
|
+
const prompt = PROVIDER_IDS.map(
|
|
12673
|
+
(p3) => ` [ -n "$${aliasVar(p3)}" ] && out="$out\${out:+ }${p3}:$${aliasVar(p3)}\${${prodVar(p3)}:+ PROD}"`
|
|
12674
|
+
).join("\n");
|
|
12675
|
+
const hook = shell === "zsh" ? `autoload -Uz add-zsh-hook && add-zsh-hook chpwd _cloudhop_cd` : `case ";$PROMPT_COMMAND;" in *";_cloudhop_cd;"*) ;; *) PROMPT_COMMAND="_cloudhop_cd;_cloudhop_show_prompt\${PROMPT_COMMAND:+;$PROMPT_COMMAND}";; esac`;
|
|
12676
|
+
const promptHook = shell === "zsh" ? `_cloudhop_show_prompt() {
|
|
12677
|
+
[ "$CLOUDHOP_PROMPT" = 0 ] && return 0
|
|
12678
|
+
case "$PROMPT$RPROMPT" in *starship*|*oh-my-posh*|*_cloudhop_rprompt*) return 0 ;; esac
|
|
12679
|
+
setopt prompt_subst
|
|
12680
|
+
RPROMPT='$(_cloudhop_rprompt)'"\${RPROMPT:+ $RPROMPT}"
|
|
12681
|
+
}
|
|
12682
|
+
# Sky for the alias, rose for production, like the CLI.
|
|
12683
|
+
_cloudhop_rprompt() {
|
|
12684
|
+
local p; p="$(cloudhop_prompt)"; [ -n "$p" ] || return 0
|
|
12685
|
+
case "$p" in *PROD*) printf '%%F{204}%s%%f' "$p" ;; *) printf '%%F{75}%s%%f' "$p" ;; esac
|
|
12686
|
+
}
|
|
12687
|
+
add-zsh-hook precmd _cloudhop_show_prompt` : `# \\001 and \\002 tell readline the colors take no room, so long lines still wrap right.
|
|
12688
|
+
_cloudhop_ps1() {
|
|
12689
|
+
local p c=75; p="$(cloudhop_prompt)"; [ -n "$p" ] || return 0
|
|
12690
|
+
case "$p" in *PROD*) c=204 ;; esac
|
|
12691
|
+
printf '\\001\\033[38;5;%sm\\002[%s]\\001\\033[0m\\002 ' "$c" "$p"
|
|
12692
|
+
}
|
|
12693
|
+
_cloudhop_show_prompt() {
|
|
12694
|
+
[ "$CLOUDHOP_PROMPT" = 0 ] && return 0
|
|
12695
|
+
case "$PS1" in *starship*|*oh-my-posh*|*_cloudhop_ps1*) return 0 ;; esac
|
|
12696
|
+
PS1='$(_cloudhop_ps1)'"$PS1"
|
|
12697
|
+
}`;
|
|
12698
|
+
const completion = shell === "zsh" ? `_cloudhop_words() { command cloudhop __complete "$1" 2>/dev/null }
|
|
12699
|
+
_hop() { (( CURRENT == 2 )) && { local -a a; a=(\${(f)"$(_cloudhop_words aliases)"}); compadd -a a; } }
|
|
12700
|
+
_cloudhop() {
|
|
12701
|
+
local -a w
|
|
12702
|
+
if (( CURRENT == 2 )); then w=(\${(f)"$(_cloudhop_words commands)"}); compadd -a w; return; fi
|
|
12703
|
+
(( CURRENT == 3 )) || return
|
|
12704
|
+
case " ${ALIAS_COMMANDS} " in *" $words[2] "*) w=(\${(f)"$(_cloudhop_words aliases)"});; esac
|
|
12705
|
+
case " ${SOURCE_COMMANDS} " in *" $words[2] "*) w=(\${(f)"$(_cloudhop_words sources)"});; esac
|
|
12706
|
+
compadd -a w
|
|
12707
|
+
}
|
|
12708
|
+
(( $+functions[compdef] )) && { compdef _hop hop; compdef _cloudhop cloudhop; }` : `_cloudhop_complete() {
|
|
12709
|
+
local cur=\${COMP_WORDS[COMP_CWORD]} cmd=\${COMP_WORDS[0]##*/} kind=""
|
|
12710
|
+
if [ "$cmd" = hop ]; then [ "$COMP_CWORD" -eq 1 ] && kind=aliases
|
|
12711
|
+
elif [ "$COMP_CWORD" -eq 1 ]; then kind=commands
|
|
12712
|
+
elif [ "$COMP_CWORD" -eq 2 ]; then
|
|
12713
|
+
case " ${ALIAS_COMMANDS} " in *" \${COMP_WORDS[1]} "*) kind=aliases;; esac
|
|
12714
|
+
case " ${SOURCE_COMMANDS} " in *" \${COMP_WORDS[1]} "*) kind=sources;; esac
|
|
12715
|
+
fi
|
|
12716
|
+
[ -n "$kind" ] && COMPREPLY=($(compgen -W "$(command cloudhop __complete $kind 2>/dev/null)" -- "$cur"))
|
|
12717
|
+
}
|
|
12718
|
+
complete -o default -F _cloudhop_complete hop cloudhop`;
|
|
12719
|
+
return `# cloudhop shell integration (${shell}). Load it${shell === "zsh" ? " after compinit" : ""}: ${LOAD_LINE[shell]}
|
|
12720
|
+
# - \`cloudhop use <alias>\` switches this shell; \`cloudhop use --off\` goes back.
|
|
12721
|
+
# - A folder with a trusted .cloudhoprc switches on cd (off: CLOUDHOP_AUTO_USE=0).
|
|
12722
|
+
# - With an alias in use, \`${clis.map((c3) => c3.command).join("`, `") || "aws"}\` signs in when needed and asks before changing production (off: CLOUDHOP_WRAP=0).
|
|
12723
|
+
# - Your prompt shows the alias in use (off: CLOUDHOP_PROMPT=0).
|
|
12724
|
+
cloudhop() {
|
|
12725
|
+
case "$1" in
|
|
12726
|
+
use)
|
|
12727
|
+
local out; out="$(command cloudhop use --emit ${shell} "\${@:2}")" || return $?
|
|
12728
|
+
eval "$out"; _cloudhop_rc_seen=\${CLOUDHOP_RC:-$_cloudhop_rc_seen} ;;
|
|
12729
|
+
allow|deny) command cloudhop "$@" || return $?; _cloudhop_rc_seen=-; _cloudhop_cd ;;
|
|
12730
|
+
*) command cloudhop "$@" ;;
|
|
12731
|
+
esac
|
|
12732
|
+
}
|
|
12733
|
+
_cloudhop_cd() {
|
|
12734
|
+
[ "$CLOUDHOP_AUTO_USE" = 0 ] && return
|
|
12735
|
+
[ "$PWD" = "$_cloudhop_pwd" ] && [ "$_cloudhop_rc_seen" != - ] && return
|
|
12736
|
+
_cloudhop_pwd=$PWD
|
|
12737
|
+
local dir=$PWD rc=
|
|
12738
|
+
while :; do
|
|
12739
|
+
if [ -f "$dir/.cloudhoprc" ]; then rc="$dir/.cloudhoprc"; break; fi
|
|
12740
|
+
{ [ -z "$dir" ] || [ "$dir" = / ]; } && break
|
|
12741
|
+
dir="\${dir%/*}"
|
|
12742
|
+
done
|
|
12743
|
+
[ "$rc" = "$_cloudhop_rc_seen" ] && return
|
|
12744
|
+
_cloudhop_rc_seen=$rc
|
|
12745
|
+
local out
|
|
12746
|
+
if [ -n "$rc" ]; then out="$(command cloudhop use --emit ${shell} --from "$rc")"
|
|
12747
|
+
elif [ -n "$CLOUDHOP_RC" ]; then out="$(command cloudhop use --emit ${shell} --leave)"
|
|
12748
|
+
else return 0; fi
|
|
12749
|
+
eval "$out"
|
|
12750
|
+
}
|
|
12751
|
+
${hook}
|
|
12752
|
+
_cloudhop_cd
|
|
12753
|
+
${wrappers}
|
|
12754
|
+
${completion}
|
|
12755
|
+
# The aliases in use, e.g. "aws:acme-dev". Shown in your prompt by itself
|
|
12756
|
+
# (off: CLOUDHOP_PROMPT=0); Starship gets its own segments from \`cloudhop shell\`.
|
|
12757
|
+
cloudhop_prompt() {
|
|
12758
|
+
local out=
|
|
12759
|
+
${prompt}
|
|
12760
|
+
[ -n "$out" ] && printf '%s' "$out"
|
|
12761
|
+
}
|
|
12762
|
+
${promptHook}
|
|
12763
|
+
`;
|
|
12764
|
+
}
|
|
12765
|
+
function fishScript(clis) {
|
|
12766
|
+
const wrappers = clis.map(
|
|
12767
|
+
({ command, provider }) => `function ${command} --wraps ${command}
|
|
12768
|
+
if test -n "$${aliasVar(provider)}" -a "$CLOUDHOP_WRAP" != 0; and not string match -q -- '--profile*' $argv
|
|
12769
|
+
command cloudhop $${aliasVar(provider)} $argv
|
|
12770
|
+
else
|
|
12771
|
+
command ${command} $argv
|
|
12772
|
+
end
|
|
12773
|
+
end`
|
|
12774
|
+
).join("\n");
|
|
12775
|
+
const prompt = PROVIDER_IDS.map(
|
|
12776
|
+
(p3) => ` test -n "$${aliasVar(p3)}"; and set out $out "${p3}:$${aliasVar(p3)}"(test -n "$${prodVar(p3)}"; and echo ' PROD')`
|
|
12777
|
+
).join("\n");
|
|
12778
|
+
return `# cloudhop shell integration (fish): ${LOAD_LINE.fish}
|
|
12779
|
+
# - \`cloudhop use <alias>\` switches this shell; \`cloudhop use --off\` goes back.
|
|
12780
|
+
# - A folder with a trusted .cloudhoprc switches on cd (off: set -gx CLOUDHOP_AUTO_USE 0).
|
|
12781
|
+
# - With an alias in use, the cloud CLI signs in when needed and asks before changing production (off: set -gx CLOUDHOP_WRAP 0).
|
|
12782
|
+
function cloudhop
|
|
12783
|
+
switch "$argv[1]"
|
|
12784
|
+
case use
|
|
12785
|
+
set -l out (command cloudhop use --emit fish $argv[2..-1]); or return $status
|
|
12786
|
+
printf '%s\\n' $out | source
|
|
12787
|
+
set -q CLOUDHOP_RC; and set -g _cloudhop_rc_seen $CLOUDHOP_RC
|
|
12788
|
+
case allow deny
|
|
12789
|
+
command cloudhop $argv; or return $status
|
|
12790
|
+
set -g _cloudhop_rc_seen -
|
|
12791
|
+
_cloudhop_cd
|
|
12792
|
+
case '*'
|
|
12793
|
+
command cloudhop $argv
|
|
12794
|
+
end
|
|
12795
|
+
end
|
|
12796
|
+
function _cloudhop_cd --on-variable PWD
|
|
12797
|
+
test "$CLOUDHOP_AUTO_USE" = 0; and return
|
|
12798
|
+
set -l dir $PWD
|
|
12799
|
+
set -l rc ''
|
|
12800
|
+
while true
|
|
12801
|
+
if test -f "$dir/.cloudhoprc"; set rc "$dir/.cloudhoprc"; break; end
|
|
12802
|
+
if test -z "$dir" -o "$dir" = /; break; end
|
|
12803
|
+
set dir (string replace -r '/[^/]*$' '' -- $dir)
|
|
12804
|
+
end
|
|
12805
|
+
test "$rc" = "$_cloudhop_rc_seen"; and return
|
|
12806
|
+
set -g _cloudhop_rc_seen $rc
|
|
12807
|
+
if test -n "$rc"
|
|
12808
|
+
command cloudhop use --emit fish --from $rc | source
|
|
12809
|
+
else if test -n "$CLOUDHOP_RC"
|
|
12810
|
+
command cloudhop use --emit fish --leave | source
|
|
12811
|
+
end
|
|
12812
|
+
end
|
|
12813
|
+
_cloudhop_cd
|
|
12814
|
+
${wrappers}
|
|
12815
|
+
complete -c hop -f -n 'test (count (commandline -opc)) -eq 1' -a '(command cloudhop __complete aliases 2>/dev/null)'
|
|
12816
|
+
complete -c cloudhop -f -n 'test (count (commandline -opc)) -eq 1' -a '(command cloudhop __complete commands 2>/dev/null)'
|
|
12817
|
+
complete -c cloudhop -f -n '__fish_seen_subcommand_from ${ALIAS_COMMANDS}; and test (count (commandline -opc)) -eq 2' -a '(command cloudhop __complete aliases 2>/dev/null)'
|
|
12818
|
+
complete -c cloudhop -f -n '__fish_seen_subcommand_from ${SOURCE_COMMANDS}; and test (count (commandline -opc)) -eq 2' -a '(command cloudhop __complete sources 2>/dev/null)'
|
|
12819
|
+
# The aliases in use, e.g. "aws:acme-dev", at the right of the prompt unless
|
|
12820
|
+
# something else (like Starship) draws it (off: set -gx CLOUDHOP_PROMPT 0).
|
|
12821
|
+
function cloudhop_prompt
|
|
12822
|
+
set -l out
|
|
12823
|
+
${prompt}
|
|
12824
|
+
test -n "$out"; and printf '%s' (string join ' ' $out)
|
|
12825
|
+
end
|
|
12826
|
+
if not functions -q fish_right_prompt
|
|
12827
|
+
function fish_right_prompt
|
|
12828
|
+
test "$CLOUDHOP_PROMPT" = 0; and return
|
|
12829
|
+
set -l label (cloudhop_prompt)
|
|
12830
|
+
test -n "$label"; or return
|
|
12831
|
+
if string match -q '*PROD*' -- $label; set_color fb7185; else; set_color 3bb8f5; end
|
|
12832
|
+
printf '%s' $label; set_color normal
|
|
12833
|
+
end
|
|
12834
|
+
end
|
|
12835
|
+
`;
|
|
12836
|
+
}
|
|
12837
|
+
function pwshScript(clis) {
|
|
12838
|
+
const wrappers = clis.map(
|
|
12839
|
+
({
|
|
12840
|
+
command,
|
|
12841
|
+
provider
|
|
12842
|
+
}) => `$script:CloudhopCli_${provider} = (Get-Command ${command} -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1).Source
|
|
12843
|
+
function ${command} {
|
|
12844
|
+
if ($env:${aliasVar(provider)} -and $env:CLOUDHOP_WRAP -ne '0' -and -not ($args -like '--profile*')) { & $script:CloudhopExe $env:${aliasVar(provider)} @args }
|
|
12845
|
+
elseif ($script:CloudhopCli_${provider}) { & $script:CloudhopCli_${provider} @args }
|
|
12846
|
+
else { Write-Error '${command}: command not found' }
|
|
12847
|
+
}`
|
|
12848
|
+
).join("\n");
|
|
12849
|
+
const prompt = PROVIDER_IDS.map(
|
|
12850
|
+
(p3) => ` if ($env:${aliasVar(p3)}) { $out += "${p3}:$env:${aliasVar(p3)}$(if ($env:${prodVar(p3)}) { ' PROD' })" }`
|
|
12851
|
+
).join("\n");
|
|
12852
|
+
return `# cloudhop shell integration (PowerShell): ${LOAD_LINE.pwsh}
|
|
12853
|
+
# - \`cloudhop use <alias>\` switches this shell; \`cloudhop use --off\` goes back.
|
|
12854
|
+
# - A folder with a trusted .cloudhoprc switches on cd (off: $env:CLOUDHOP_AUTO_USE = '0').
|
|
12855
|
+
# - With an alias in use, the cloud CLI signs in when needed and asks before changing production (off: $env:CLOUDHOP_WRAP = '0').
|
|
12856
|
+
$script:CloudhopExe = (Get-Command cloudhop -CommandType Application, ExternalScript | Select-Object -First 1).Source
|
|
12857
|
+
function cloudhop {
|
|
12858
|
+
if ($args.Count -gt 0 -and $args[0] -eq 'use') {
|
|
12859
|
+
$out = & $script:CloudhopExe use --emit pwsh @($args | Select-Object -Skip 1)
|
|
12860
|
+
if ($LASTEXITCODE -eq 0) { $out | Out-String | Invoke-Expression; if ($env:CLOUDHOP_RC) { $script:CloudhopRcSeen = $env:CLOUDHOP_RC } }
|
|
12861
|
+
} elseif ($args.Count -gt 0 -and $args[0] -in 'allow', 'deny') {
|
|
12862
|
+
& $script:CloudhopExe @args
|
|
12863
|
+
if ($LASTEXITCODE -eq 0) { $script:CloudhopRcSeen = '-'; $script:CloudhopPwd = ''; _CloudhopCd }
|
|
12864
|
+
} else { & $script:CloudhopExe @args }
|
|
12865
|
+
}
|
|
12866
|
+
function _CloudhopCd {
|
|
12867
|
+
if ($env:CLOUDHOP_AUTO_USE -eq '0' -or $PWD.ProviderPath -eq $script:CloudhopPwd) { return }
|
|
12868
|
+
$script:CloudhopPwd = $PWD.ProviderPath
|
|
12869
|
+
$dir = $PWD.ProviderPath; $rc = ''
|
|
12870
|
+
while ($dir) {
|
|
12871
|
+
$file = Join-Path $dir '.cloudhoprc'
|
|
12872
|
+
if (Test-Path -LiteralPath $file -PathType Leaf) { $rc = $file; break }
|
|
12873
|
+
$dir = Split-Path -Parent $dir
|
|
12874
|
+
}
|
|
12875
|
+
if ($rc -eq $script:CloudhopRcSeen) { return }
|
|
12876
|
+
$script:CloudhopRcSeen = $rc
|
|
12877
|
+
$out = if ($rc) { & $script:CloudhopExe use --emit pwsh --from $rc } elseif ($env:CLOUDHOP_RC) { & $script:CloudhopExe use --emit pwsh --leave }
|
|
12878
|
+
if ($out) { $out | Out-String | Invoke-Expression }
|
|
12879
|
+
}
|
|
12880
|
+
$script:CloudhopPrompt = $function:prompt
|
|
12881
|
+
function prompt {
|
|
12882
|
+
_CloudhopCd
|
|
12883
|
+
$out = & $script:CloudhopPrompt
|
|
12884
|
+
# The alias in use before your prompt, unless Starship draws it (off: $env:CLOUDHOP_PROMPT = '0').
|
|
12885
|
+
if ($env:CLOUDHOP_PROMPT -ne '0' -and "$script:CloudhopPrompt" -notmatch 'starship|oh-my-posh') {
|
|
12886
|
+
$label = cloudhop_prompt
|
|
12887
|
+
if ($label) {
|
|
12888
|
+
if (-not $Host.UI.SupportsVirtualTerminal) { return "[$label] $out" }
|
|
12889
|
+
$color = if ($label -match 'PROD') { 204 } else { 75 }
|
|
12890
|
+
return "$([char]27)[38;5;$($color)m[$label]$([char]27)[0m $out"
|
|
12891
|
+
}
|
|
12892
|
+
}
|
|
12893
|
+
$out
|
|
12894
|
+
}
|
|
12895
|
+
${wrappers}
|
|
12896
|
+
Register-ArgumentCompleter -Native -CommandName hop, cloudhop -ScriptBlock {
|
|
12897
|
+
param($word, $ast)
|
|
12898
|
+
$n = $ast.CommandElements.Count - [int]($word -eq '')
|
|
12899
|
+
$kind = if ($ast.CommandElements[0].Value -match 'hop$' -and $ast.CommandElements[0].Value -notmatch 'cloudhop') { if ($n -le 1) { 'aliases' } }
|
|
12900
|
+
elseif ($n -le 1) { 'commands' }
|
|
12901
|
+
elseif ($n -eq 2 -and ' ${ALIAS_COMMANDS} ' -match " $($ast.CommandElements[1].Value) ") { 'aliases' }
|
|
12902
|
+
elseif ($n -eq 2 -and ' ${SOURCE_COMMANDS} ' -match " $($ast.CommandElements[1].Value) ") { 'sources' }
|
|
12903
|
+
if ($kind) { & $script:CloudhopExe __complete $kind | Where-Object { $_ -like "$word*" } }
|
|
12904
|
+
}
|
|
12905
|
+
function cloudhop_prompt {
|
|
12906
|
+
$out = @()
|
|
12907
|
+
${prompt}
|
|
12908
|
+
$out -join ' '
|
|
12909
|
+
}
|
|
12910
|
+
`;
|
|
12911
|
+
}
|
|
12912
|
+
function shellScript(shell, clis) {
|
|
12913
|
+
if (shell === "fish") return fishScript(clis);
|
|
12914
|
+
if (shell === "pwsh") return pwshScript(clis);
|
|
12915
|
+
return posixScript(shell, clis);
|
|
12916
|
+
}
|
|
12917
|
+
function starshipSnippet() {
|
|
12918
|
+
return `# cloudhop: the alias each cloud uses in this shell (\`cloudhop use\`, .cloudhoprc).
|
|
12919
|
+
# \`cloudhop shell\` adds this for you. By hand: append it to starship.toml and, if
|
|
12920
|
+
# your config has a top-level \`format\`, add this where you want the segments:
|
|
12921
|
+
# ${STARSHIP_SEGMENTS}
|
|
12922
|
+
|
|
12923
|
+
${starshipModules()}
|
|
12924
|
+
`;
|
|
12925
|
+
}
|
|
12926
|
+
var quote = (shell, value) => shell === "pwsh" ? `'${value.replace(/'/g, "''")}'` : `'${value.replace(/'/g, "'\\''")}'`;
|
|
12927
|
+
function shellExports(shell, env) {
|
|
12928
|
+
return Object.entries(env).map(([key2, value]) => {
|
|
12929
|
+
if (value === void 0) {
|
|
12930
|
+
if (shell === "fish") return `set -e ${key2}`;
|
|
12931
|
+
if (shell === "pwsh") return `Remove-Item Env:${key2} -ErrorAction SilentlyContinue`;
|
|
12932
|
+
return `unset ${key2}`;
|
|
12933
|
+
}
|
|
12934
|
+
if (shell === "fish") return `set -gx ${key2} ${quote(shell, value)}`;
|
|
12935
|
+
if (shell === "pwsh") return `$env:${key2} = ${quote(shell, value)}`;
|
|
12936
|
+
return `export ${key2}=${quote(shell, value)}`;
|
|
12937
|
+
}).join("\n");
|
|
12938
|
+
}
|
|
12939
|
+
function detectShell() {
|
|
12940
|
+
if (process.platform === "win32") return "pwsh";
|
|
12941
|
+
const name = (process.env.SHELL ?? "").split("/").pop();
|
|
12942
|
+
return SHELLS.includes(name) ? name : "bash";
|
|
12943
|
+
}
|
|
12944
|
+
function parseShell(value) {
|
|
12945
|
+
if (!value) return detectShell();
|
|
12946
|
+
const shell = value === "powershell" ? "pwsh" : value;
|
|
12947
|
+
if (!SHELLS.includes(shell)) {
|
|
12948
|
+
throw new CloudhopError(
|
|
12949
|
+
"INVALID_NAME",
|
|
12950
|
+
`Unknown shell "${value}".`,
|
|
12951
|
+
`Use one of: ${SHELLS.join(", ")}.`
|
|
12952
|
+
);
|
|
12953
|
+
}
|
|
12954
|
+
return shell;
|
|
12955
|
+
}
|
|
12956
|
+
var RC_FILES = {
|
|
12957
|
+
zsh: (home) => join6(home, ".zshrc"),
|
|
12958
|
+
bash: (home) => join6(home, ".bashrc"),
|
|
12959
|
+
fish: (home) => join6(home, ".config", "fish", "config.fish"),
|
|
12960
|
+
pwsh: (home) => process.platform === "win32" ? join6(home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1") : join6(home, ".config", "powershell", "Microsoft.PowerShell_profile.ps1")
|
|
12961
|
+
};
|
|
12962
|
+
var LOAD_LINE = {
|
|
12963
|
+
zsh: 'eval "$(cloudhop shell zsh)"',
|
|
12964
|
+
bash: 'eval "$(cloudhop shell bash)"',
|
|
12965
|
+
fish: "cloudhop shell fish | source",
|
|
12966
|
+
pwsh: "cloudhop shell pwsh | Out-String | Invoke-Expression"
|
|
12967
|
+
};
|
|
12968
|
+
function shellIntegrationInstalled() {
|
|
12969
|
+
const shell = detectShell();
|
|
12970
|
+
const read = (file) => {
|
|
12971
|
+
try {
|
|
12972
|
+
return readFileSync2(file, "utf8");
|
|
12973
|
+
} catch {
|
|
12974
|
+
return void 0;
|
|
12975
|
+
}
|
|
12976
|
+
};
|
|
12977
|
+
const rc = read(RC_FILES[shell](homedir3())) ?? "";
|
|
12978
|
+
if (!rc.includes(LOAD_LINE[shell])) return false;
|
|
12979
|
+
const starship = read(starshipFile());
|
|
12980
|
+
if (starship === void 0) return !rc.includes("starship init");
|
|
12981
|
+
return hasCloudhopStarship(starship);
|
|
12982
|
+
}
|
|
12983
|
+
var starshipFile = () => process.env.STARSHIP_CONFIG || join6(homedir3(), ".config", "starship.toml");
|
|
12984
|
+
async function shellCommand(args, clis) {
|
|
12985
|
+
const { positional, flags } = parseArgs(args);
|
|
12986
|
+
if (positional[0] === "starship") {
|
|
12987
|
+
process.stdout.write(starshipSnippet());
|
|
12988
|
+
return;
|
|
12989
|
+
}
|
|
12990
|
+
const setUp = positional[0] === "install" || !positional[0] && process.stdout.isTTY;
|
|
12991
|
+
if (!setUp) {
|
|
12992
|
+
process.stdout.write(shellScript(parseShell(positional[0]), clis));
|
|
12993
|
+
return;
|
|
12994
|
+
}
|
|
12995
|
+
const shell = parseShell(positional[1]);
|
|
12996
|
+
const file = RC_FILES[shell](homedir3());
|
|
12997
|
+
const line = LOAD_LINE[shell];
|
|
12998
|
+
const current = await readIfExists(file) ?? "";
|
|
12999
|
+
const starship = await starshipConfig(current);
|
|
13000
|
+
const needsLine = !current.includes(line);
|
|
13001
|
+
const needsStarship = starship !== void 0 && !hasCloudhopStarship(starship.text);
|
|
13002
|
+
const rcName = file.replace(homedir3(), "~");
|
|
13003
|
+
if (!needsLine && !needsStarship) {
|
|
13004
|
+
console.log(`Already set up in ${rcName}${starship ? " and your Starship prompt" : ""}.`);
|
|
13005
|
+
return;
|
|
13006
|
+
}
|
|
13007
|
+
const changes = [
|
|
13008
|
+
needsLine && `add one line to ${rcName}: \`cloudhop use\`, .cloudhoprc folders, the alias in your prompt`,
|
|
13009
|
+
needsStarship && `show the alias in your Starship prompt (${starship.file.replace(homedir3(), "~")}, keeping a backup)`
|
|
13010
|
+
].filter(Boolean);
|
|
13011
|
+
if (!flags.yes) {
|
|
13012
|
+
if (!interactive())
|
|
13013
|
+
throw new CloudhopError(
|
|
13014
|
+
"NOT_INTERACTIVE",
|
|
13015
|
+
`Rerun with --yes to ${changes.join(" and ")}.${needsLine ? `
|
|
13016
|
+
Or add this line to ${file} yourself:
|
|
13017
|
+
${line}` : ""}`
|
|
13018
|
+
);
|
|
13019
|
+
console.log(`cloudhop will:
|
|
13020
|
+
${changes.map((c3) => ` - ${c3}`).join("\n")}`);
|
|
13021
|
+
const ok = answer(await dist_exports.confirm({ message: "Set up your terminal?", initialValue: true }));
|
|
13022
|
+
if (!ok) return;
|
|
13023
|
+
}
|
|
13024
|
+
if (needsLine) {
|
|
13025
|
+
await mkdir4(dirname4(file), { recursive: true });
|
|
13026
|
+
const note2 = shell === "zsh" ? " # after compinit" : "";
|
|
13027
|
+
await appendFile(
|
|
13028
|
+
file,
|
|
13029
|
+
`${current && !current.endsWith("\n") ? "\n" : ""}
|
|
13030
|
+
# cloudhop: \`cloudhop use\`, .cloudhoprc folders, tab completion, prompt${note2}
|
|
13031
|
+
${line}
|
|
13032
|
+
`
|
|
13033
|
+
);
|
|
13034
|
+
console.log(`${paint("green", "\u2714")} Added to ${rcName}.`);
|
|
13035
|
+
}
|
|
13036
|
+
if (needsStarship) {
|
|
13037
|
+
await mkdir4(dirname4(starship.file), { recursive: true });
|
|
13038
|
+
if (starship.exists) await writeFile4(`${starship.file}.cloudhop-backup`, starship.text);
|
|
13039
|
+
await writeFile4(starship.file, addToStarshipConfig(starship.text));
|
|
13040
|
+
console.log(
|
|
13041
|
+
`${paint("green", "\u2714")} Your Starship prompt shows the alias in use (backup: ${starship.file.replace(homedir3(), "~")}.cloudhop-backup).`
|
|
13042
|
+
);
|
|
13043
|
+
}
|
|
13044
|
+
console.log(
|
|
13045
|
+
`Open a new terminal (or run ${paint("sky", shell === "pwsh" ? ". $PROFILE" : `exec ${shell}`)}), then:`
|
|
13046
|
+
);
|
|
13047
|
+
console.log(` ${paint("sky", "cloudhop use <alias>")} switch this terminal to an account`);
|
|
13048
|
+
console.log(` ${paint("sky", "aws s3 ls")} plain aws commands, as that alias`);
|
|
13049
|
+
}
|
|
13050
|
+
async function starshipConfig(rc) {
|
|
13051
|
+
const file = starshipFile();
|
|
13052
|
+
const text4 = await readIfExists(file);
|
|
13053
|
+
if (text4 === void 0 && !rc.includes("starship init") && !process.env.STARSHIP_SHELL) {
|
|
13054
|
+
return void 0;
|
|
13055
|
+
}
|
|
13056
|
+
return { file, text: text4 ?? "", exists: text4 !== void 0 };
|
|
13057
|
+
}
|
|
13058
|
+
async function readIfExists(file) {
|
|
13059
|
+
try {
|
|
13060
|
+
return await readFile4(file, "utf8");
|
|
13061
|
+
} catch (error) {
|
|
13062
|
+
if (error.code === "ENOENT") return void 0;
|
|
13063
|
+
throw error;
|
|
13064
|
+
}
|
|
13065
|
+
}
|
|
13066
|
+
async function completeCommand(app, kind) {
|
|
13067
|
+
if (kind === "commands") {
|
|
13068
|
+
console.log(
|
|
13069
|
+
[...RESERVED_NAMES, "each", "shell", "use"].filter((c3, i2, a2) => a2.indexOf(c3) === i2).sort().join("\n")
|
|
13070
|
+
);
|
|
13071
|
+
return;
|
|
13072
|
+
}
|
|
13073
|
+
const config = await app.config();
|
|
13074
|
+
const words = kind === "sources" ? config.sources.map((s) => s.id) : config.aliases.map((a2) => a2.name);
|
|
13075
|
+
if (words.length) console.log(words.join("\n"));
|
|
13076
|
+
}
|
|
13077
|
+
|
|
13078
|
+
// src/commands/use.ts
|
|
13079
|
+
var activeVars = (provider) => {
|
|
13080
|
+
const prefix = `CLOUDHOP_${provider.toUpperCase()}`;
|
|
13081
|
+
return {
|
|
13082
|
+
alias: `${prefix}_ALIAS`,
|
|
13083
|
+
prod: `${prefix}_PROD`,
|
|
13084
|
+
prompt: `${prefix}_PROMPT`,
|
|
13085
|
+
// One or the other, so prompt themes can style production apart (Starship's env_var).
|
|
13086
|
+
segment: `${prefix}_SEGMENT`,
|
|
13087
|
+
prodSegment: `${prefix}_PROD_SEGMENT`
|
|
13088
|
+
};
|
|
13089
|
+
};
|
|
13090
|
+
function activeAliases(env = process.env) {
|
|
13091
|
+
return Object.fromEntries(
|
|
13092
|
+
PROVIDER_IDS.filter((p3) => env[activeVars(p3).alias]).map((p3) => [p3, env[activeVars(p3).alias]])
|
|
13093
|
+
);
|
|
13094
|
+
}
|
|
13095
|
+
async function shellContext(trusted, cwd = process.cwd()) {
|
|
13096
|
+
const file = findFolderFile(cwd);
|
|
13097
|
+
const content = file ? await readFolderFile(file) : void 0;
|
|
13098
|
+
let folder;
|
|
13099
|
+
if (file && content !== void 0) {
|
|
13100
|
+
try {
|
|
13101
|
+
folder = {
|
|
13102
|
+
file,
|
|
13103
|
+
aliases: parseFolderFile(content),
|
|
13104
|
+
trusted: trusted.isTrusted(file, content)
|
|
13105
|
+
};
|
|
13106
|
+
} catch {
|
|
13107
|
+
}
|
|
13108
|
+
}
|
|
13109
|
+
return { inUse: activeAliases(), ...folder ? { folder } : {} };
|
|
13110
|
+
}
|
|
13111
|
+
var tilde2 = (path2) => path2.replace(homedir4(), "~");
|
|
13112
|
+
var folderProviders = () => (process.env.CLOUDHOP_RC_PROVIDERS ?? "").split(",").filter((p3) => PROVIDER_IDS.includes(p3));
|
|
13113
|
+
async function activate(app, alias, login) {
|
|
13114
|
+
const shellEnv = await app.shellEnv(alias, { login });
|
|
13115
|
+
const vars = activeVars(shellEnv.provider);
|
|
13116
|
+
const label = shellEnv.region ? `${alias} (${shellEnv.region})` : alias;
|
|
13117
|
+
const env = Object.fromEntries(shellEnv.clear.map((name) => [name, void 0]));
|
|
13118
|
+
Object.assign(env, shellEnv.env, {
|
|
13119
|
+
[vars.alias]: alias,
|
|
13120
|
+
[vars.prod]: shellEnv.prod ? "1" : void 0,
|
|
13121
|
+
[vars.prompt]: label,
|
|
13122
|
+
[vars.segment]: shellEnv.prod ? void 0 : label,
|
|
13123
|
+
[vars.prodSegment]: shellEnv.prod ? label : void 0,
|
|
13124
|
+
// The cloud-neutral pair from cloudhop 1.2, kept for prompts already using it.
|
|
13125
|
+
CLOUDHOP_ALIAS: alias,
|
|
13126
|
+
CLOUDHOP_PROD: shellEnv.prod ? "1" : void 0
|
|
13127
|
+
});
|
|
13128
|
+
const where = [shellEnv.label, shellEnv.region ?? "no region"].filter(Boolean).join(", ");
|
|
13129
|
+
return { provider: shellEnv.provider, env, summary: `${alias} (${where})`, prod: shellEnv.prod };
|
|
13130
|
+
}
|
|
13131
|
+
function deactivate(app, provider) {
|
|
13132
|
+
const vars = activeVars(provider);
|
|
13133
|
+
const env = Object.fromEntries(app.shellVars(provider).map((n3) => [n3, void 0]));
|
|
13134
|
+
for (const name of Object.values(vars)) env[name] = void 0;
|
|
13135
|
+
if (process.env.CLOUDHOP_ALIAS && process.env.CLOUDHOP_ALIAS === process.env[vars.alias]) {
|
|
13136
|
+
Object.assign(env, { CLOUDHOP_ALIAS: void 0, CLOUDHOP_PROD: void 0 });
|
|
13137
|
+
}
|
|
13138
|
+
return env;
|
|
13139
|
+
}
|
|
13140
|
+
function warnProduction(activation) {
|
|
13141
|
+
if (!activation.prod) return;
|
|
13142
|
+
const alias = activation.summary.split(" ")[0];
|
|
13143
|
+
notify(
|
|
13144
|
+
paint(
|
|
13145
|
+
"red",
|
|
13146
|
+
`${alias} is production: with the shell integration, \`aws\` asks before changes. Other tools (terraform, scripts) don't.`
|
|
13147
|
+
)
|
|
13148
|
+
);
|
|
13149
|
+
}
|
|
13150
|
+
async function useCommand(app, trusted, args) {
|
|
13151
|
+
const { positional, flags } = parseArgs(args);
|
|
13152
|
+
const shell = flags.emit ? parseShell(flags.emit) : void 0;
|
|
13153
|
+
if (flags.from !== void 0 || flags.leave) {
|
|
13154
|
+
if (!shell) throw new CloudhopError("INVALID_NAME", "--from and --leave need --emit <shell>.");
|
|
13155
|
+
const env2 = await folderEnv(app, trusted, flags.leave ? void 0 : flags.from);
|
|
13156
|
+
process.stdout.write(`${shellExports(shell, env2)}
|
|
13157
|
+
`);
|
|
13158
|
+
return 0;
|
|
13159
|
+
}
|
|
13160
|
+
if (flags.off) {
|
|
13161
|
+
if (!shell) throw noIntegration("Nothing to turn off: this shell has no cloudhop integration.");
|
|
13162
|
+
const which = positional[0];
|
|
13163
|
+
if (which && !PROVIDER_IDS.includes(which)) {
|
|
12342
13164
|
throw new CloudhopError(
|
|
12343
|
-
"
|
|
12344
|
-
`
|
|
12345
|
-
|
|
13165
|
+
"INVALID_NAME",
|
|
13166
|
+
`Unknown cloud "${which}".`,
|
|
13167
|
+
`Use one of: ${PROVIDER_IDS.join(", ")}, or nothing to turn them all off.`
|
|
12346
13168
|
);
|
|
12347
13169
|
}
|
|
12348
|
-
|
|
12349
|
-
|
|
13170
|
+
const providers = which ? [which] : app.availableProviders().map((provider) => provider.id);
|
|
13171
|
+
const env2 = {};
|
|
13172
|
+
for (const provider of providers) Object.assign(env2, deactivate(app, provider));
|
|
13173
|
+
const remaining = folderProviders().filter((p3) => !providers.includes(p3));
|
|
13174
|
+
Object.assign(
|
|
13175
|
+
env2,
|
|
13176
|
+
folderMarks(remaining.length ? process.env.CLOUDHOP_RC : void 0, remaining)
|
|
12350
13177
|
);
|
|
12351
|
-
|
|
12352
|
-
|
|
13178
|
+
process.stdout.write(`${shellExports(shell, env2)}
|
|
13179
|
+
`);
|
|
13180
|
+
notify(which ? `No ${which} alias in this shell anymore.` : "No alias in this shell anymore.");
|
|
13181
|
+
return 0;
|
|
12353
13182
|
}
|
|
12354
|
-
|
|
12355
|
-
|
|
13183
|
+
const alias = positional[0] ?? await pickAlias(app, "Use which alias in this shell?");
|
|
13184
|
+
const activation = await activate(app, alias, true);
|
|
13185
|
+
let folder;
|
|
13186
|
+
if (flags.save) folder = await saveFolderFile(trusted, activation.provider, alias);
|
|
13187
|
+
if (!shell) {
|
|
13188
|
+
if (!interactive()) {
|
|
13189
|
+
throw noIntegration("`cloudhop use` needs a terminal or the shell integration.");
|
|
13190
|
+
}
|
|
13191
|
+
return subshell(alias, activation);
|
|
12356
13192
|
}
|
|
12357
|
-
const
|
|
12358
|
-
const
|
|
13193
|
+
const fromFolder = folderProviders().filter((p3) => p3 !== activation.provider);
|
|
13194
|
+
const env = { ...activation.env };
|
|
13195
|
+
if (folder) Object.assign(env, folderMarks(folder, [...fromFolder, activation.provider]));
|
|
13196
|
+
else
|
|
13197
|
+
Object.assign(
|
|
13198
|
+
env,
|
|
13199
|
+
folderMarks(fromFolder.length ? process.env.CLOUDHOP_RC : void 0, fromFolder)
|
|
13200
|
+
);
|
|
13201
|
+
process.stdout.write(`${shellExports(shell, env)}
|
|
13202
|
+
`);
|
|
13203
|
+
notify(`This shell now uses ${activation.summary}.`);
|
|
13204
|
+
if (folder) notify(`Saved in ${tilde2(folder)}: this folder switches to ${alias} on cd.`);
|
|
13205
|
+
warnProduction(activation);
|
|
13206
|
+
return 0;
|
|
13207
|
+
}
|
|
13208
|
+
var folderMarks = (file, providers) => ({
|
|
13209
|
+
CLOUDHOP_RC: providers.length && file ? file : void 0,
|
|
13210
|
+
CLOUDHOP_RC_PROVIDERS: providers.length && file ? providers.join(",") : void 0
|
|
13211
|
+
});
|
|
13212
|
+
var noIntegration = (message) => new CloudhopError(
|
|
13213
|
+
"NOT_INTERACTIVE",
|
|
13214
|
+
message,
|
|
13215
|
+
`Set it up once with \`cloudhop shell install\` (or add ${LOAD_LINE[detectShell()]} to your shell's startup file), then open a new terminal.`
|
|
13216
|
+
);
|
|
13217
|
+
async function folderEnv(app, trusted, file) {
|
|
13218
|
+
const previous = folderProviders();
|
|
13219
|
+
const activations = [];
|
|
13220
|
+
const path2 = file ? resolve2(file) : void 0;
|
|
13221
|
+
const content = path2 ? await readFolderFile(path2) : void 0;
|
|
13222
|
+
if (path2 && content !== void 0) {
|
|
13223
|
+
let aliases;
|
|
12359
13224
|
try {
|
|
12360
|
-
|
|
12361
|
-
const outcome = { alias: a2.name, exitCode: r2.exitCode, stdout: r2.stdout, stderr: r2.stderr };
|
|
12362
|
-
if (!flags.json) printOutcome(outcome, width);
|
|
12363
|
-
return outcome;
|
|
13225
|
+
aliases = parseFolderFile(content);
|
|
12364
13226
|
} catch (error) {
|
|
12365
|
-
|
|
12366
|
-
|
|
12367
|
-
|
|
13227
|
+
notify(`${tilde2(path2)}: ${error.message}`);
|
|
13228
|
+
}
|
|
13229
|
+
if (aliases && !trusted.isTrusted(path2, content)) {
|
|
13230
|
+
notify(
|
|
13231
|
+
`${tilde2(path2)} wants this folder to use ${Object.values(aliases).join(", ")}. Run ${bold("cloudhop allow")} if you trust it.`
|
|
13232
|
+
);
|
|
13233
|
+
} else if (aliases) {
|
|
13234
|
+
for (const alias of Object.values(aliases)) {
|
|
13235
|
+
try {
|
|
13236
|
+
activations.push(await activate(app, alias, false));
|
|
13237
|
+
} catch (error) {
|
|
13238
|
+
notify(`${tilde2(path2)}: ${error.message}`);
|
|
13239
|
+
}
|
|
13240
|
+
}
|
|
12368
13241
|
}
|
|
12369
|
-
});
|
|
12370
|
-
const failed = outcomes.filter((o2) => o2.exitCode !== 0);
|
|
12371
|
-
if (flags.json) {
|
|
12372
|
-
console.log(JSON.stringify({ version: 1, command: cliArgs, results: outcomes }, null, 2));
|
|
12373
|
-
} else {
|
|
12374
|
-
const summary = `${outcomes.length - failed.length} ok${failed.length ? `, ${failed.length} failed: ${failed.map((f2) => f2.alias).join(", ")}` : ""}`;
|
|
12375
|
-
process.stderr.write(`${failed.length ? paint("red", summary) : paint("green", summary)}
|
|
12376
|
-
`);
|
|
12377
13242
|
}
|
|
12378
|
-
|
|
13243
|
+
const applied = activations.map((a2) => a2.provider);
|
|
13244
|
+
const env = {};
|
|
13245
|
+
const left = previous.filter((p3) => !applied.includes(p3));
|
|
13246
|
+
for (const provider of left) Object.assign(env, deactivate(app, provider));
|
|
13247
|
+
for (const activation of activations) Object.assign(env, activation.env);
|
|
13248
|
+
Object.assign(env, folderMarks(path2, applied));
|
|
13249
|
+
if (activations.length) {
|
|
13250
|
+
notify(`Using ${activations.map((a2) => a2.summary).join(" and ")}, from ${FOLDER_FILE}.`);
|
|
13251
|
+
for (const activation of activations) warnProduction(activation);
|
|
13252
|
+
} else if (left.length) {
|
|
13253
|
+
const names = left.map((p3) => process.env[activeVars(p3).alias]).filter(Boolean);
|
|
13254
|
+
notify(`Left the folder of ${names.join(", ")}: no alias in this shell.`);
|
|
13255
|
+
}
|
|
13256
|
+
return env;
|
|
12379
13257
|
}
|
|
12380
|
-
function
|
|
12381
|
-
const
|
|
12382
|
-
|
|
12383
|
-
|
|
12384
|
-
|
|
12385
|
-
|
|
12386
|
-
|
|
12387
|
-
|
|
12388
|
-
|
|
12389
|
-
|
|
13258
|
+
async function saveFolderFile(trusted, provider, alias) {
|
|
13259
|
+
const file = join7(process.cwd(), FOLDER_FILE);
|
|
13260
|
+
const content = await readFolderFile(file);
|
|
13261
|
+
let aliases = {};
|
|
13262
|
+
if (content !== void 0) {
|
|
13263
|
+
aliases = parseFolderFile(content);
|
|
13264
|
+
delete aliases.any;
|
|
13265
|
+
}
|
|
13266
|
+
aliases[provider] = alias;
|
|
13267
|
+
const text4 = formatFolderFile(aliases);
|
|
13268
|
+
await writeFile5(file, text4);
|
|
13269
|
+
await trusted.trust(file, text4);
|
|
13270
|
+
return file;
|
|
13271
|
+
}
|
|
13272
|
+
function subshell(alias, activation) {
|
|
13273
|
+
const env = { ...process.env, CLOUDHOP_SUBSHELL: alias };
|
|
13274
|
+
for (const [name, value] of Object.entries(activation.env)) {
|
|
13275
|
+
if (value === void 0) delete env[name];
|
|
13276
|
+
else env[name] = value;
|
|
13277
|
+
}
|
|
13278
|
+
const shell = process.platform === "win32" ? process.env.ComSpec ?? "cmd.exe" : process.env.SHELL ?? "/bin/sh";
|
|
13279
|
+
if (/cmd(\.exe)?$/i.test(shell)) {
|
|
13280
|
+
const label = `${activation.provider}:${alias}${activation.prod ? " PROD" : ""}`;
|
|
13281
|
+
env.PROMPT = `[${label}] ${process.env.PROMPT ?? "$P$G"}`;
|
|
13282
|
+
}
|
|
13283
|
+
if (process.env.CLOUDHOP_SUBSHELL) {
|
|
13284
|
+
notify(
|
|
13285
|
+
`You are already in a cloudhop shell (${process.env.CLOUDHOP_SUBSHELL}): this one goes inside it.`
|
|
13286
|
+
);
|
|
13287
|
+
}
|
|
13288
|
+
notify(`New shell using ${activation.summary}. Type ${bold("exit")} to go back.`);
|
|
13289
|
+
notify(
|
|
13290
|
+
paint(
|
|
13291
|
+
"dim",
|
|
13292
|
+
"Tip: `cloudhop shell install` makes `cloudhop use` switch this same terminal instead."
|
|
13293
|
+
)
|
|
13294
|
+
);
|
|
13295
|
+
warnProduction(activation);
|
|
13296
|
+
const result = spawnSync(shell, [], { stdio: "inherit", env });
|
|
13297
|
+
notify(`Back from ${alias}.`);
|
|
13298
|
+
return result.status ?? 0;
|
|
13299
|
+
}
|
|
13300
|
+
async function allowCommand(app, trusted, args, allow) {
|
|
13301
|
+
const { positional } = parseArgs(args);
|
|
13302
|
+
const given = positional[0];
|
|
13303
|
+
const file = given ? given.endsWith(FOLDER_FILE) ? resolve2(given) : join7(resolve2(given), FOLDER_FILE) : findFolderFile(process.cwd());
|
|
13304
|
+
if (!file) {
|
|
13305
|
+
throw new CloudhopError(
|
|
13306
|
+
"INVALID_NAME",
|
|
13307
|
+
`No ${FOLDER_FILE} in this folder or above it.`,
|
|
13308
|
+
"Create one with `cloudhop use <alias> --save`."
|
|
13309
|
+
);
|
|
13310
|
+
}
|
|
13311
|
+
if (!allow) {
|
|
13312
|
+
const forgotten = await trusted.forget(file);
|
|
13313
|
+
console.log(
|
|
13314
|
+
forgotten ? `${paint("green", "\u2714")} ${tilde2(file)} is no longer trusted: its folder won't switch aliases.` : `${tilde2(file)} was not trusted.`
|
|
13315
|
+
);
|
|
13316
|
+
return;
|
|
13317
|
+
}
|
|
13318
|
+
const content = await readFolderFile(file);
|
|
13319
|
+
if (content === void 0) throw new CloudhopError("INVALID_NAME", `Can't read ${file}.`);
|
|
13320
|
+
const aliases = parseFolderFile(content);
|
|
13321
|
+
const lines = [];
|
|
13322
|
+
for (const alias of Object.values(aliases)) {
|
|
13323
|
+
try {
|
|
13324
|
+
const status = await app.aliasStatus(alias);
|
|
13325
|
+
lines.push(
|
|
13326
|
+
` ${bold(alias)} ${status.label ?? status.target} ${status.region ?? ""}${status.prod ? ` ${paint("red", "PROD")}` : ""}`
|
|
13327
|
+
);
|
|
13328
|
+
} catch {
|
|
13329
|
+
lines.push(
|
|
13330
|
+
` ${bold(alias)} ${paint("yellow", "you don't have this alias: it will be skipped")}`
|
|
13331
|
+
);
|
|
13332
|
+
}
|
|
13333
|
+
}
|
|
13334
|
+
await trusted.trust(file, content);
|
|
13335
|
+
console.log(
|
|
13336
|
+
`${paint("green", "\u2714")} Trusted ${tilde2(file)}. In ${tilde2(dirname5(file))} this shell uses:`
|
|
13337
|
+
);
|
|
13338
|
+
console.log(lines.join("\n"));
|
|
12390
13339
|
}
|
|
12391
13340
|
|
|
12392
13341
|
// src/commands/manage.ts
|
|
@@ -12434,11 +13383,11 @@ var SESSION_LABEL = {
|
|
|
12434
13383
|
};
|
|
12435
13384
|
var JSON_VERSION = 1;
|
|
12436
13385
|
var printJson = (value) => console.log(JSON.stringify({ version: JSON_VERSION, ...value }, null, 2));
|
|
12437
|
-
async function listCommand(app, args = []) {
|
|
13386
|
+
async function listCommand(app, args = [], context) {
|
|
12438
13387
|
const { positional, flags } = parseArgs(args);
|
|
12439
13388
|
const sourceId = positional[0];
|
|
12440
13389
|
if (flags.json) {
|
|
12441
|
-
const overview = await app.overview();
|
|
13390
|
+
const overview = { ...await app.overview(), ...await context?.() };
|
|
12442
13391
|
if (sourceId && !overview.sources.some((s) => s.id === sourceId)) {
|
|
12443
13392
|
throw new CloudhopError("SOURCE_NOT_FOUND", `Source "${sourceId}" not found.`);
|
|
12444
13393
|
}
|
|
@@ -12617,7 +13566,16 @@ async function statusCommand(app, args) {
|
|
|
12617
13566
|
return;
|
|
12618
13567
|
}
|
|
12619
13568
|
const overview = await app.overview();
|
|
12620
|
-
|
|
13569
|
+
const inUse = activeAliases();
|
|
13570
|
+
if (flags.json) return printJson({ inUse, sources: overview.sources });
|
|
13571
|
+
const using = Object.entries(inUse).map(([cloud, alias]) => {
|
|
13572
|
+
const status = overview.aliases.find((a2) => a2.name === alias);
|
|
13573
|
+
const region = status?.region ? ` (${status.region})` : "";
|
|
13574
|
+
return `${cloud} ${bold(paint(status?.prod ? "red" : "sky", alias))}${region}${status?.prod ? ` ${paint("red", "PROD")}` : ""}`;
|
|
13575
|
+
});
|
|
13576
|
+
console.log(
|
|
13577
|
+
using.length ? `This terminal uses ${using.join(", ")}.` : paint("dim", "This terminal uses no alias (`cloudhop use <alias>` to pick one).")
|
|
13578
|
+
);
|
|
12621
13579
|
if (overview.sources.length === 0) {
|
|
12622
13580
|
console.log("Nothing here yet. Run `cloudhop setup` to get started.");
|
|
12623
13581
|
return;
|
|
@@ -12759,6 +13717,23 @@ async function setupCommand(app, assistants) {
|
|
|
12759
13717
|
const addNow = answer(await dist_exports.confirm({ message: "Add an account or SSO portal now?" }));
|
|
12760
13718
|
if (addNow) await addCommand(app, { positional: [], flags: {} });
|
|
12761
13719
|
else dist_exports.log.info("Run `cloudhop add` whenever you are ready.");
|
|
13720
|
+
if (!shellIntegrationInstalled()) {
|
|
13721
|
+
dist_exports.note(
|
|
13722
|
+
[
|
|
13723
|
+
"`hop <alias>`, `each` and `console` work as they are. To also switch a terminal",
|
|
13724
|
+
"to an alias (`cloudhop use`, then plain `aws`), switch on `cd` into a folder with",
|
|
13725
|
+
"a .cloudhoprc, see the alias in your prompt (Starship too) and complete aliases",
|
|
13726
|
+
"with <Tab>, your shell loads one line from its startup file. A program can only",
|
|
13727
|
+
"change your terminal this way."
|
|
13728
|
+
].join("\n"),
|
|
13729
|
+
"Terminal"
|
|
13730
|
+
);
|
|
13731
|
+
const install = answer(
|
|
13732
|
+
await dist_exports.confirm({ message: "Set up your terminal for cloudhop?", initialValue: true })
|
|
13733
|
+
);
|
|
13734
|
+
if (install) await shellCommand(["install", "--yes"], []);
|
|
13735
|
+
else dist_exports.log.info("Later: `cloudhop shell install`. `cloudhop use` still works, in a new shell.");
|
|
13736
|
+
}
|
|
12762
13737
|
const detected = assistants.detect();
|
|
12763
13738
|
if (detected.length > 0) {
|
|
12764
13739
|
const names = detected.map((id) => ASSISTANTS[id].name).join(", ");
|
|
@@ -12796,268 +13771,37 @@ async function doctorCommand(app) {
|
|
|
12796
13771
|
);
|
|
12797
13772
|
}
|
|
12798
13773
|
|
|
12799
|
-
// src/commands/shell.ts
|
|
12800
|
-
import { existsSync as existsSync3 } from "fs";
|
|
12801
|
-
import { appendFile, mkdir as mkdir3, readFile as readFile3 } from "fs/promises";
|
|
12802
|
-
import { homedir as homedir3 } from "os";
|
|
12803
|
-
import { dirname as dirname3, join as join5 } from "path";
|
|
12804
|
-
var SHELLS = ["zsh", "bash", "fish", "pwsh"];
|
|
12805
|
-
var ALIAS_COMMANDS = "use status console region mv prod rm exec";
|
|
12806
|
-
var SOURCE_COMMANDS = "alias login sync ls";
|
|
12807
|
-
var SCRIPTS = {
|
|
12808
|
-
zsh: `# cloudhop shell integration (zsh). Load it after compinit: eval "$(cloudhop shell zsh)"
|
|
12809
|
-
cloudhop() {
|
|
12810
|
-
if [ "$1" = "use" ]; then
|
|
12811
|
-
local out; out="$(command cloudhop use --emit zsh "\${@:2}")" || return $?
|
|
12812
|
-
eval "$out"
|
|
12813
|
-
else
|
|
12814
|
-
command cloudhop "$@"
|
|
12815
|
-
fi
|
|
12816
|
-
}
|
|
12817
|
-
_cloudhop_words() { command cloudhop __complete "$1" 2>/dev/null }
|
|
12818
|
-
_hop() { (( CURRENT == 2 )) && { local -a a; a=(\${(f)"$(_cloudhop_words aliases)"}); compadd -a a; } }
|
|
12819
|
-
_cloudhop() {
|
|
12820
|
-
local -a w
|
|
12821
|
-
if (( CURRENT == 2 )); then w=(\${(f)"$(_cloudhop_words commands)"}); compadd -a w; return; fi
|
|
12822
|
-
(( CURRENT == 3 )) || return
|
|
12823
|
-
case " ${ALIAS_COMMANDS} " in *" $words[2] "*) w=(\${(f)"$(_cloudhop_words aliases)"});; esac
|
|
12824
|
-
case " ${SOURCE_COMMANDS} " in *" $words[2] "*) w=(\${(f)"$(_cloudhop_words sources)"});; esac
|
|
12825
|
-
compadd -a w
|
|
12826
|
-
}
|
|
12827
|
-
(( $+functions[compdef] )) && { compdef _hop hop; compdef _cloudhop cloudhop; }
|
|
12828
|
-
# Add $(cloudhop_prompt) to your prompt to see the alias set with \`cloudhop use\`.
|
|
12829
|
-
cloudhop_prompt() { [ -n "$CLOUDHOP_ALIAS" ] && printf '\u2601 %s%s' "$CLOUDHOP_ALIAS" "\${CLOUDHOP_PROD:+ PROD}"; }
|
|
12830
|
-
`,
|
|
12831
|
-
bash: `# cloudhop shell integration (bash): eval "$(cloudhop shell bash)"
|
|
12832
|
-
cloudhop() {
|
|
12833
|
-
if [ "$1" = "use" ]; then
|
|
12834
|
-
local out; out="$(command cloudhop use --emit bash "\${@:2}")" || return $?
|
|
12835
|
-
eval "$out"
|
|
12836
|
-
else
|
|
12837
|
-
command cloudhop "$@"
|
|
12838
|
-
fi
|
|
12839
|
-
}
|
|
12840
|
-
_cloudhop_complete() {
|
|
12841
|
-
local cur=\${COMP_WORDS[COMP_CWORD]} cmd=\${COMP_WORDS[0]##*/} kind=""
|
|
12842
|
-
if [ "$cmd" = hop ]; then [ "$COMP_CWORD" -eq 1 ] && kind=aliases
|
|
12843
|
-
elif [ "$COMP_CWORD" -eq 1 ]; then kind=commands
|
|
12844
|
-
elif [ "$COMP_CWORD" -eq 2 ]; then
|
|
12845
|
-
case " ${ALIAS_COMMANDS} " in *" \${COMP_WORDS[1]} "*) kind=aliases;; esac
|
|
12846
|
-
case " ${SOURCE_COMMANDS} " in *" \${COMP_WORDS[1]} "*) kind=sources;; esac
|
|
12847
|
-
fi
|
|
12848
|
-
[ -n "$kind" ] && COMPREPLY=($(compgen -W "$(command cloudhop __complete $kind 2>/dev/null)" -- "$cur"))
|
|
12849
|
-
}
|
|
12850
|
-
complete -o default -F _cloudhop_complete hop cloudhop
|
|
12851
|
-
# Add $(cloudhop_prompt) to PS1 to see the alias set with \`cloudhop use\`.
|
|
12852
|
-
cloudhop_prompt() { [ -n "$CLOUDHOP_ALIAS" ] && printf '\u2601 %s%s' "$CLOUDHOP_ALIAS" "\${CLOUDHOP_PROD:+ PROD}"; }
|
|
12853
|
-
`,
|
|
12854
|
-
fish: `# cloudhop shell integration (fish): cloudhop shell fish | source
|
|
12855
|
-
function cloudhop
|
|
12856
|
-
if test "$argv[1]" = use
|
|
12857
|
-
set -l out (command cloudhop use --emit fish $argv[2..-1]); or return $status
|
|
12858
|
-
printf '%s\\n' $out | source
|
|
12859
|
-
else
|
|
12860
|
-
command cloudhop $argv
|
|
12861
|
-
end
|
|
12862
|
-
end
|
|
12863
|
-
complete -c hop -f -n 'test (count (commandline -opc)) -eq 1' -a '(command cloudhop __complete aliases 2>/dev/null)'
|
|
12864
|
-
complete -c cloudhop -f -n 'test (count (commandline -opc)) -eq 1' -a '(command cloudhop __complete commands 2>/dev/null)'
|
|
12865
|
-
complete -c cloudhop -f -n '__fish_seen_subcommand_from ${ALIAS_COMMANDS}; and test (count (commandline -opc)) -eq 2' -a '(command cloudhop __complete aliases 2>/dev/null)'
|
|
12866
|
-
complete -c cloudhop -f -n '__fish_seen_subcommand_from ${SOURCE_COMMANDS}; and test (count (commandline -opc)) -eq 2' -a '(command cloudhop __complete sources 2>/dev/null)'
|
|
12867
|
-
# Call cloudhop_prompt from fish_prompt to see the alias set with \`cloudhop use\`.
|
|
12868
|
-
function cloudhop_prompt
|
|
12869
|
-
test -n "$CLOUDHOP_ALIAS"; and printf '\u2601 %s%s' $CLOUDHOP_ALIAS (test -n "$CLOUDHOP_PROD"; and echo ' PROD')
|
|
12870
|
-
end
|
|
12871
|
-
`,
|
|
12872
|
-
pwsh: `# cloudhop shell integration (PowerShell): cloudhop shell pwsh | Out-String | Invoke-Expression
|
|
12873
|
-
$script:CloudhopExe = (Get-Command cloudhop -CommandType Application, ExternalScript | Select-Object -First 1).Source
|
|
12874
|
-
function cloudhop {
|
|
12875
|
-
if ($args.Count -gt 0 -and $args[0] -eq 'use') {
|
|
12876
|
-
$out = & $script:CloudhopExe use --emit pwsh @($args | Select-Object -Skip 1)
|
|
12877
|
-
if ($LASTEXITCODE -eq 0) { $out | Out-String | Invoke-Expression }
|
|
12878
|
-
} else { & $script:CloudhopExe @args }
|
|
12879
|
-
}
|
|
12880
|
-
Register-ArgumentCompleter -Native -CommandName hop, cloudhop -ScriptBlock {
|
|
12881
|
-
param($word, $ast)
|
|
12882
|
-
$n = $ast.CommandElements.Count - [int]($word -eq '')
|
|
12883
|
-
$kind = if ($ast.CommandElements[0].Value -match 'hop$' -and $ast.CommandElements[0].Value -notmatch 'cloudhop') { if ($n -le 1) { 'aliases' } }
|
|
12884
|
-
elseif ($n -le 1) { 'commands' }
|
|
12885
|
-
elseif ($n -eq 2 -and ' ${ALIAS_COMMANDS} ' -match " $($ast.CommandElements[1].Value) ") { 'aliases' }
|
|
12886
|
-
elseif ($n -eq 2 -and ' ${SOURCE_COMMANDS} ' -match " $($ast.CommandElements[1].Value) ") { 'sources' }
|
|
12887
|
-
if ($kind) { & $script:CloudhopExe __complete $kind | Where-Object { $_ -like "$word*" } }
|
|
12888
|
-
}
|
|
12889
|
-
function cloudhop_prompt { if ($env:CLOUDHOP_ALIAS) { "\u2601 $env:CLOUDHOP_ALIAS$(if ($env:CLOUDHOP_PROD) { ' PROD' })" } }
|
|
12890
|
-
`
|
|
12891
|
-
};
|
|
12892
|
-
var quote = (shell, value) => shell === "pwsh" ? `'${value.replace(/'/g, "''")}'` : `'${value.replace(/'/g, "'\\''")}'`;
|
|
12893
|
-
function shellExports(shell, env) {
|
|
12894
|
-
return Object.entries(env).map(([key, value]) => {
|
|
12895
|
-
if (value === void 0) {
|
|
12896
|
-
if (shell === "fish") return `set -e ${key}`;
|
|
12897
|
-
if (shell === "pwsh") return `Remove-Item Env:${key} -ErrorAction SilentlyContinue`;
|
|
12898
|
-
return `unset ${key}`;
|
|
12899
|
-
}
|
|
12900
|
-
if (shell === "fish") return `set -gx ${key} ${quote(shell, value)}`;
|
|
12901
|
-
if (shell === "pwsh") return `$env:${key} = ${quote(shell, value)}`;
|
|
12902
|
-
return `export ${key}=${quote(shell, value)}`;
|
|
12903
|
-
}).join("\n");
|
|
12904
|
-
}
|
|
12905
|
-
function detectShell() {
|
|
12906
|
-
if (process.platform === "win32") return "pwsh";
|
|
12907
|
-
const name = (process.env.SHELL ?? "").split("/").pop();
|
|
12908
|
-
return SHELLS.includes(name) ? name : "bash";
|
|
12909
|
-
}
|
|
12910
|
-
function parseShell(value) {
|
|
12911
|
-
if (!value) return detectShell();
|
|
12912
|
-
const shell = value === "powershell" ? "pwsh" : value;
|
|
12913
|
-
if (!SHELLS.includes(shell)) {
|
|
12914
|
-
throw new CloudhopError(
|
|
12915
|
-
"INVALID_NAME",
|
|
12916
|
-
`Unknown shell "${value}".`,
|
|
12917
|
-
`Use one of: ${SHELLS.join(", ")}.`
|
|
12918
|
-
);
|
|
12919
|
-
}
|
|
12920
|
-
return shell;
|
|
12921
|
-
}
|
|
12922
|
-
var RC_FILES = {
|
|
12923
|
-
zsh: (home) => join5(home, ".zshrc"),
|
|
12924
|
-
bash: (home) => join5(home, ".bashrc"),
|
|
12925
|
-
fish: (home) => join5(home, ".config", "fish", "config.fish"),
|
|
12926
|
-
pwsh: (home) => process.platform === "win32" ? join5(home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1") : join5(home, ".config", "powershell", "Microsoft.PowerShell_profile.ps1")
|
|
12927
|
-
};
|
|
12928
|
-
var LOAD_LINE = {
|
|
12929
|
-
zsh: 'eval "$(cloudhop shell zsh)"',
|
|
12930
|
-
bash: 'eval "$(cloudhop shell bash)"',
|
|
12931
|
-
fish: "cloudhop shell fish | source",
|
|
12932
|
-
pwsh: "cloudhop shell pwsh | Out-String | Invoke-Expression"
|
|
12933
|
-
};
|
|
12934
|
-
async function shellCommand(args) {
|
|
12935
|
-
const { positional, flags } = parseArgs(args);
|
|
12936
|
-
if (positional[0] !== "install") {
|
|
12937
|
-
process.stdout.write(SCRIPTS[parseShell(positional[0])]);
|
|
12938
|
-
return;
|
|
12939
|
-
}
|
|
12940
|
-
const shell = parseShell(positional[1]);
|
|
12941
|
-
const file = RC_FILES[shell](homedir3());
|
|
12942
|
-
const line = LOAD_LINE[shell];
|
|
12943
|
-
const current = existsSync3(file) ? await readFile3(file, "utf8") : "";
|
|
12944
|
-
if (current.includes(line)) {
|
|
12945
|
-
console.log(`Already set up in ${file.replace(homedir3(), "~")}.`);
|
|
12946
|
-
return;
|
|
12947
|
-
}
|
|
12948
|
-
if (!flags.yes) {
|
|
12949
|
-
if (!interactive())
|
|
12950
|
-
throw new CloudhopError(
|
|
12951
|
-
"NOT_INTERACTIVE",
|
|
12952
|
-
`Add this line to ${file} yourself, or rerun with --yes:
|
|
12953
|
-
${line}`
|
|
12954
|
-
);
|
|
12955
|
-
const ok = answer(
|
|
12956
|
-
await dist_exports.confirm({
|
|
12957
|
-
message: `Add cloudhop to ${file.replace(homedir3(), "~")}?`,
|
|
12958
|
-
initialValue: true
|
|
12959
|
-
})
|
|
12960
|
-
);
|
|
12961
|
-
if (!ok) return;
|
|
12962
|
-
}
|
|
12963
|
-
await mkdir3(dirname3(file), { recursive: true });
|
|
12964
|
-
const note2 = shell === "zsh" ? " # after compinit" : "";
|
|
12965
|
-
await appendFile(
|
|
12966
|
-
file,
|
|
12967
|
-
`${current && !current.endsWith("\n") ? "\n" : ""}
|
|
12968
|
-
# cloudhop: \`cloudhop use\`, tab completion and cloudhop_prompt${note2}
|
|
12969
|
-
${line}
|
|
12970
|
-
`
|
|
12971
|
-
);
|
|
12972
|
-
console.log(
|
|
12973
|
-
`${paint("green", "\u2714")} Added to ${file.replace(homedir3(), "~")}. Open a new terminal to use it.`
|
|
12974
|
-
);
|
|
12975
|
-
}
|
|
12976
|
-
async function useCommand(app, args) {
|
|
12977
|
-
const { positional, flags } = parseArgs(args);
|
|
12978
|
-
const shell = flags.emit ? parseShell(flags.emit) : void 0;
|
|
12979
|
-
if (!shell) {
|
|
12980
|
-
throw new CloudhopError(
|
|
12981
|
-
"NOT_INTERACTIVE",
|
|
12982
|
-
"`cloudhop use` needs the shell integration to change your current shell.",
|
|
12983
|
-
`Set it up once with \`cloudhop shell install\` (or add ${LOAD_LINE[detectShell()]} to your shell's startup file), then open a new terminal.`
|
|
12984
|
-
);
|
|
12985
|
-
}
|
|
12986
|
-
const clear = {
|
|
12987
|
-
AWS_ACCESS_KEY_ID: void 0,
|
|
12988
|
-
AWS_SECRET_ACCESS_KEY: void 0,
|
|
12989
|
-
AWS_SESSION_TOKEN: void 0
|
|
12990
|
-
};
|
|
12991
|
-
if (flags.off) {
|
|
12992
|
-
process.stdout.write(
|
|
12993
|
-
`${shellExports(shell, { ...clear, AWS_PROFILE: void 0, AWS_REGION: void 0, AWS_DEFAULT_REGION: void 0, CLOUDHOP_ALIAS: void 0, CLOUDHOP_PROD: void 0 })}
|
|
12994
|
-
`
|
|
12995
|
-
);
|
|
12996
|
-
notify("No alias in this shell anymore.");
|
|
12997
|
-
return;
|
|
12998
|
-
}
|
|
12999
|
-
const alias = positional[0] ?? await pickAlias(app, "Use which alias in this shell?");
|
|
13000
|
-
const env = await app.envForAlias(alias, "cli");
|
|
13001
|
-
const status = await app.aliasStatus(alias);
|
|
13002
|
-
process.stdout.write(
|
|
13003
|
-
`${shellExports(shell, { ...clear, ...env, CLOUDHOP_ALIAS: alias, CLOUDHOP_PROD: status.prod ? "1" : void 0 })}
|
|
13004
|
-
`
|
|
13005
|
-
);
|
|
13006
|
-
notify(
|
|
13007
|
-
`This shell now uses ${alias} (${status.label ?? status.target}, ${status.region ?? "no region"}).`
|
|
13008
|
-
);
|
|
13009
|
-
if (status.prod) {
|
|
13010
|
-
notify(
|
|
13011
|
-
paint(
|
|
13012
|
-
"red",
|
|
13013
|
-
`${alias} is production. Plain \`aws\` commands are not protected here: use \`hop ${alias}\` to be asked before changes.`
|
|
13014
|
-
)
|
|
13015
|
-
);
|
|
13016
|
-
}
|
|
13017
|
-
}
|
|
13018
|
-
async function completeCommand(app, kind) {
|
|
13019
|
-
if (kind === "commands") {
|
|
13020
|
-
console.log(
|
|
13021
|
-
[...RESERVED_NAMES, "each", "shell", "use"].filter((c3, i2, a2) => a2.indexOf(c3) === i2).sort().join("\n")
|
|
13022
|
-
);
|
|
13023
|
-
return;
|
|
13024
|
-
}
|
|
13025
|
-
const config = await app.config();
|
|
13026
|
-
const words = kind === "sources" ? config.sources.map((s) => s.id) : config.aliases.map((a2) => a2.name);
|
|
13027
|
-
if (words.length) console.log(words.join("\n"));
|
|
13028
|
-
}
|
|
13029
|
-
|
|
13030
13774
|
// src/commands/team.ts
|
|
13031
|
-
import { readFile as
|
|
13775
|
+
import { readFile as readFile5, writeFile as writeFile6 } from "fs/promises";
|
|
13032
13776
|
var import_yaml2 = __toESM(require_dist(), 1);
|
|
13033
13777
|
var MAX_BYTES = 256 * 1024;
|
|
13034
13778
|
var HEADER2 = `# cloudhop team setup: import it with \`cloudhop team import <this file or its https URL>\`.
|
|
13035
13779
|
# No secrets: portal URLs, regions, account ids and role names only.
|
|
13036
13780
|
`;
|
|
13037
13781
|
function validate(app, raw) {
|
|
13038
|
-
const
|
|
13039
|
-
if (!raw || typeof raw !== "object") throw
|
|
13782
|
+
const bad2 = (why) => new CloudhopError("INVALID_NAME", `Not a cloudhop team file: ${why}.`);
|
|
13783
|
+
if (!raw || typeof raw !== "object") throw bad2("it is empty");
|
|
13040
13784
|
const file = raw;
|
|
13041
|
-
if (file.version !== 1) throw
|
|
13785
|
+
if (file.version !== 1) throw bad2('unknown "version" (expected 1)');
|
|
13042
13786
|
const sources = file.sources ?? {};
|
|
13043
13787
|
const aliases = file.aliases ?? {};
|
|
13044
13788
|
for (const [id, source] of Object.entries(sources)) {
|
|
13045
13789
|
assertValidName("source", id);
|
|
13046
13790
|
if (!PROVIDER_IDS.includes(source?.provider))
|
|
13047
|
-
throw
|
|
13791
|
+
throw bad2(`source "${id}" has an unknown provider`);
|
|
13048
13792
|
const kind = app.provider(source.provider).sourceKinds.find((k) => k.kind === source.kind);
|
|
13049
|
-
if (!kind) throw
|
|
13793
|
+
if (!kind) throw bad2(`source "${id}" has an unknown kind`);
|
|
13050
13794
|
for (const field of kind.fields) {
|
|
13051
13795
|
const value = source.settings?.[field.key];
|
|
13052
|
-
if (typeof value !== "string" || !value) throw
|
|
13796
|
+
if (typeof value !== "string" || !value) throw bad2(`source "${id}" is missing ${field.key}`);
|
|
13053
13797
|
const problem = field.validate?.(value);
|
|
13054
|
-
if (problem) throw
|
|
13798
|
+
if (problem) throw bad2(`source "${id}": ${problem}`);
|
|
13055
13799
|
}
|
|
13056
13800
|
}
|
|
13057
13801
|
for (const [name, alias] of Object.entries(aliases)) {
|
|
13058
13802
|
assertValidName("alias", name);
|
|
13059
13803
|
if (!alias || typeof alias.target !== "string" || !sources[alias.source]) {
|
|
13060
|
-
throw
|
|
13804
|
+
throw bad2(`alias "${name}" must name a source of this file and a target`);
|
|
13061
13805
|
}
|
|
13062
13806
|
}
|
|
13063
13807
|
return { version: 1, sources, aliases };
|
|
@@ -13073,7 +13817,7 @@ async function load(location) {
|
|
|
13073
13817
|
throw new CloudhopError("INVALID_NAME", "That file is too big for a team setup.");
|
|
13074
13818
|
return text4;
|
|
13075
13819
|
}
|
|
13076
|
-
return
|
|
13820
|
+
return readFile5(location, "utf8");
|
|
13077
13821
|
}
|
|
13078
13822
|
async function teamCommand(app, args) {
|
|
13079
13823
|
const { positional, flags } = parseArgs(args);
|
|
@@ -13094,7 +13838,7 @@ async function teamCommand(app, args) {
|
|
|
13094
13838
|
};
|
|
13095
13839
|
const text4 = HEADER2 + (0, import_yaml2.stringify)(file);
|
|
13096
13840
|
if (flags.out) {
|
|
13097
|
-
await
|
|
13841
|
+
await writeFile6(flags.out, text4);
|
|
13098
13842
|
console.error(
|
|
13099
13843
|
`${paint("green", "\u2714")} Wrote ${sources.length} source(s) and ${Object.keys(file.aliases).length} alias(es) to ${flags.out}.`
|
|
13100
13844
|
);
|
|
@@ -13199,8 +13943,8 @@ async function teamCommand(app, args) {
|
|
|
13199
13943
|
}
|
|
13200
13944
|
|
|
13201
13945
|
// ../ui/src/api.ts
|
|
13202
|
-
var text3 = (body,
|
|
13203
|
-
const value = body[
|
|
13946
|
+
var text3 = (body, key2) => {
|
|
13947
|
+
const value = body[key2];
|
|
13204
13948
|
return typeof value === "string" ? value.trim() : "";
|
|
13205
13949
|
};
|
|
13206
13950
|
var badRequest = (message) => new CloudhopError("INVALID_NAME", message);
|
|
@@ -13273,8 +14017,8 @@ function createApi(app, assistants) {
|
|
|
13273
14017
|
label: kind.label,
|
|
13274
14018
|
hint: kind.hint,
|
|
13275
14019
|
nameLabel: kind.nameLabel,
|
|
13276
|
-
fields: kind.fields.map(({ key, label, placeholder, defaultValue }) => ({
|
|
13277
|
-
key,
|
|
14020
|
+
fields: kind.fields.map(({ key: key2, label, placeholder, defaultValue }) => ({
|
|
14021
|
+
key: key2,
|
|
13278
14022
|
label,
|
|
13279
14023
|
placeholder,
|
|
13280
14024
|
defaultValue
|
|
@@ -13412,8 +14156,8 @@ function createApi(app, assistants) {
|
|
|
13412
14156
|
return { removed: name };
|
|
13413
14157
|
}
|
|
13414
14158
|
};
|
|
13415
|
-
const table = Object.entries(routes).map(([
|
|
13416
|
-
const [method, pattern] =
|
|
14159
|
+
const table = Object.entries(routes).map(([key2, handler]) => {
|
|
14160
|
+
const [method, pattern] = key2.split(" ");
|
|
13417
14161
|
const regex = new RegExp(`^${pattern.replace(/:[a-z]+/g, "([^/]+)")}$`);
|
|
13418
14162
|
return { method, regex, handler };
|
|
13419
14163
|
});
|
|
@@ -13442,7 +14186,7 @@ import { randomBytes, timingSafeEqual } from "crypto";
|
|
|
13442
14186
|
import { createServer } from "http";
|
|
13443
14187
|
|
|
13444
14188
|
// ../ui/src/assets/app.js
|
|
13445
|
-
var app_default = "// cloudhop local UI. Plain DOM, no build step: served as-is by `cloudhop ui`.\n\nconst TOKEN_KEY = 'cloudhop-token';\nconst SESSION_TEXT = {\n valid: 'Session valid',\n refreshable: 'Renews on next use',\n expired: 'Expired: sign in on next use',\n unknown: 'Managed by the cloud CLI',\n};\n\n// The token arrives in the URL fragment once; keep it for this tab and clean the address bar.\nconst fromHash = new URLSearchParams(location.hash.slice(1)).get('token');\nif (fromHash) {\n try {\n sessionStorage.setItem(TOKEN_KEY, fromHash);\n } catch {}\n history.replaceState(null, '', location.pathname);\n}\nlet token = fromHash;\ntry {\n token = token ?? sessionStorage.getItem(TOKEN_KEY);\n} catch {}\n\n// ---------- pages ----------\n\nconst PAGES = {\n aliases: 'Aliases',\n sources: 'Sources',\n assistants: 'AI assistants',\n tools: 'Tools',\n};\nlet route = 'aliases';\nconst aliasFilter = { text: '', source: '', prodOnly: false };\n\n/** `#/aliases?source=acme` \u2192 route \"aliases\", filtered by that source. */\nfunction readRoute() {\n const match = /^#\\/([a-z]+)(?:\\?(.*))?$/.exec(location.hash);\n route = match && PAGES[match[1]] ? match[1] : 'aliases';\n const query = new URLSearchParams(match?.[2] ?? '');\n if (query.has('source')) aliasFilter.source = query.get('source');\n}\n\nfunction go(page, query) {\n location.hash = `#/${page}${query ? `?${new URLSearchParams(query)}` : ''}`;\n}\n\n// A fresh link opened in this same tab carries a new token: reload to pick it up.\n// Any other fragment is a page change.\naddEventListener('hashchange', () => {\n if (new URLSearchParams(location.hash.slice(1)).has('token')) {\n location.reload();\n return;\n }\n readRoute();\n if (state) render();\n});\n\nconst $main = document.getElementById('main');\nconst $dialog = document.getElementById('dialog');\nconst $toasts = document.getElementById('toasts');\nconst $addSource = document.getElementById('add-source');\nconst $nav = document.getElementById('nav');\nlet state;\n\n/** Tiny element builder: h('button', { class: 'primary', onclick }, 'Save'). */\nfunction h(tag, attrs = {}, ...children) {\n const el = document.createElement(tag);\n for (const [key, value] of Object.entries(attrs)) {\n if (value === undefined || value === null || value === false) continue;\n if (key.startsWith('on')) el.addEventListener(key.slice(2), value);\n else if (key === 'class') el.className = value;\n else if (value === true) el.setAttribute(key, '');\n else el.setAttribute(key, value);\n }\n for (const child of children.flat()) {\n if (child === undefined || child === null || child === false) continue;\n el.append(child instanceof Node ? child : document.createTextNode(String(child)));\n }\n return el;\n}\n\nclass ApiError extends Error {\n constructor(message, hint) {\n super(message);\n this.hint = hint;\n }\n}\n\n// Calls that may sign in wait for the person in the browser; everything else should answer quickly.\nconst MAY_SIGN_IN = /^\\/api\\/(sources(\\/[^/]+\\/(login|sync|targets))?|aliases\\/[^/]+\\/console)$/;\nconst QUICK_TIMEOUT_MS = 20_000;\n\nasync function api(method, path, body) {\n const waitsForPerson = MAY_SIGN_IN.test(path);\n let response;\n try {\n response = await fetch(path, {\n method,\n headers: {\n 'x-cloudhop-token': token ?? '',\n ...(body ? { 'content-type': 'application/json' } : {}),\n },\n body: body ? JSON.stringify(body) : undefined,\n signal: waitsForPerson ? undefined : AbortSignal.timeout(QUICK_TIMEOUT_MS),\n });\n } catch (error) {\n if (error.name === 'TimeoutError') {\n throw new ApiError(\n 'cloudhop ui is not responding.',\n 'Is it paused in your terminal (Ctrl-Z)? Run `fg` there, or stop it and run `cloudhop ui` again.',\n );\n }\n throw new ApiError(\n 'cloudhop ui is not running.',\n 'Run `cloudhop ui` again and open the new link it prints.',\n );\n }\n const data = await response.json().catch(() => ({}));\n if (!response.ok)\n throw new ApiError(data.error ?? `Request failed (${response.status}).`, data.hint);\n return data;\n}\n\nfunction toast(message, { error = false, hint } = {}) {\n const el = h(\n 'div',\n { class: `toast${error ? ' error' : ''}` },\n message,\n hint && h('small', {}, hint),\n );\n $toasts.append(el);\n setTimeout(() => el.remove(), error ? 9000 : 4000);\n}\n\nconst fail = (error) => toast(error.message, { error: true, hint: error.hint });\n\n/** Disables a button while its action runs. */\nasync function busy(button, action) {\n button.disabled = true;\n try {\n return await action();\n } finally {\n button.disabled = false;\n }\n}\n\nasync function refresh() {\n try {\n state = await api('GET', '/api/state');\n render();\n checkDoctor();\n } catch (error) {\n $main.replaceChildren(\n h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'Cannot reach cloudhop'),\n h('p', {}, error.message, error.hint ? ` ${error.hint}` : ''),\n ),\n );\n }\n}\n\nasync function checkDoctor() {\n try {\n const entries = await api('GET', '/api/doctor');\n const problems = entries.filter((e) => !e.installed || e.outdated);\n document.getElementById('doctor')?.remove();\n if (problems.length === 0) return;\n $main.prepend(\n h(\n 'div',\n { class: 'notice', id: 'doctor' },\n ...problems.map((e) =>\n h(\n 'div',\n {},\n e.installed\n ? `${e.cli} ${e.version} is older than ${e.minVersion}. `\n : `${e.cli} is not installed, so aliases cannot run commands yet. `,\n 'Run ',\n h('code', {}, 'cloudhop doctor'),\n ' in a terminal to install it.',\n ),\n ),\n ),\n );\n } catch {}\n}\n\n// ---------- main view ----------\n\nfunction render() {\n const enabled = state.enabledProviders;\n $addSource.hidden = enabled.length === 0;\n $nav.hidden = enabled.length === 0;\n if (enabled.length === 0) return renderChooseClouds();\n for (const link of $nav.querySelectorAll('a')) {\n if (link.dataset.page === route) link.setAttribute('aria-current', 'page');\n else link.removeAttribute('aria-current');\n }\n document.title = `${PAGES[route]} \xB7 cloudhop`;\n const pages = {\n aliases: aliasesPage,\n sources: sourcesPage,\n assistants: assistantsPage,\n tools: toolsPage,\n };\n $main.replaceChildren(...pages[route]());\n if (route === 'assistants') loadAssistants();\n if (route === 'tools') loadTools();\n}\n\nfunction pageHead(title, subtitle, ...actions) {\n return h(\n 'div',\n { class: 'page-head' },\n h('div', {}, h('h1', {}, title), subtitle ? h('p', {}, subtitle) : null),\n h('div', { class: 'actions' }, actions),\n );\n}\n\nfunction firstSourceHero() {\n return h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'Add your first account'),\n h(\n 'p',\n {},\n 'Add a company SSO portal or a personal account. cloudhop signs you in, finds every account and role you can use, and lets you give each one a short alias.',\n ),\n h('button', { class: 'primary', onclick: () => openAddSource() }, '+ Add source'),\n );\n}\n\n// ---------- Aliases page ----------\n\nfunction aliasesPage() {\n if (state.sources.length === 0) return [firstSourceHero()];\n if (aliasFilter.source && !state.sources.some((s) => s.id === aliasFilter.source))\n aliasFilter.source = '';\n\n const addAliases = () => {\n const source =\n state.sources.find((s) => s.id === aliasFilter.source) ??\n (state.sources.length === 1 ? state.sources[0] : undefined);\n if (source) openAliasPicker(source);\n else openSourceChooser();\n };\n const head = pageHead(\n 'Aliases',\n 'Each alias is one account, role and region. Run it with hop <alias>.',\n h('button', { onclick: addAliases }, '+ Aliases'),\n );\n if (state.aliases.length === 0) {\n return [\n head,\n h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'No aliases yet'),\n h('p', {}, 'Pick the accounts and roles you use and give each one a short name.'),\n h('button', { class: 'primary', onclick: addAliases }, 'Pick accounts'),\n ),\n ];\n }\n\n const rows = h('tbody', { id: 'alias-rows' });\n const count = h('span', { class: 'count' });\n const fill = () => {\n const words = aliasFilter.text.toLowerCase().split(/\\s+/).filter(Boolean);\n const visible = state.aliases.filter((a) => {\n if (aliasFilter.source && a.source !== aliasFilter.source) return false;\n if (aliasFilter.prodOnly && !a.prod) return false;\n const text = `${a.name} ${a.label ?? a.target} ${a.region ?? ''} ${a.source}`.toLowerCase();\n return words.every((w) => text.includes(w));\n });\n rows.replaceChildren(\n ...(visible.length\n ? visible.map(aliasRow)\n : [\n h(\n 'tr',\n {},\n h('td', { colspan: '5', class: 'empty' }, 'No alias matches these filters.'),\n ),\n ]),\n );\n count.textContent = `${visible.length} of ${state.aliases.length}`;\n };\n\n const search = h('input', {\n type: 'search',\n placeholder: 'Filter: name, account, role or region',\n value: aliasFilter.text,\n 'aria-label': 'Filter aliases',\n oninput: (e) => {\n aliasFilter.text = e.currentTarget.value;\n fill();\n },\n });\n const sourceSelect = h(\n 'select',\n {\n 'aria-label': 'Source',\n onchange: (e) => {\n aliasFilter.source = e.currentTarget.value;\n fill();\n },\n },\n h('option', { value: '' }, 'All sources'),\n state.sources.map((s) =>\n h('option', { value: s.id, selected: s.id === aliasFilter.source }, s.id),\n ),\n );\n const prodOnly = h(\n 'label',\n { class: 'prod-toggle' },\n h('input', {\n type: 'checkbox',\n checked: aliasFilter.prodOnly,\n onchange: (e) => {\n aliasFilter.prodOnly = e.currentTarget.checked;\n fill();\n },\n }),\n 'Production only',\n );\n\n const table = h(\n 'table',\n { class: 'aliases' },\n h(\n 'thead',\n {},\n h(\n 'tr',\n {},\n h('th', {}, 'Alias'),\n h('th', {}, 'Source'),\n h('th', {}, 'Region'),\n h('th', {}, 'Account / role'),\n h('th'),\n ),\n ),\n rows,\n );\n fill();\n return [\n head,\n h(\n 'div',\n { class: 'toolbar' },\n search,\n sourceSelect,\n prodOnly,\n h('div', { class: 'spacer' }),\n count,\n ),\n h('section', { class: 'card' }, table),\n ];\n}\n\n/** When several sources exist, ask which one to pick accounts from. */\nfunction openSourceChooser() {\n showDialog(\n 'Add aliases from',\n h(\n 'div',\n { class: 'choices' },\n state.sources.map((source) =>\n h(\n 'button',\n { class: 'choice', onclick: () => openAliasPicker(source) },\n h(\n 'span',\n {},\n h('strong', {}, source.id),\n h('small', {}, source.settings.startUrl ?? source.kindLabel),\n ),\n ),\n ),\n ),\n );\n}\n\n// ---------- Sources page ----------\n\nfunction sourcesPage() {\n const head = pageHead('Sources', 'The SSO portals and personal accounts you sign in to.');\n if (state.sources.length === 0) return [head, firstSourceHero()];\n return [head, ...state.sources.map(sourceCard)];\n}\n\n// ---------- AI assistants page ----------\n\nfunction assistantsPage() {\n return [assistantsCard()];\n}\n\n// ---------- Tools page ----------\n\nfunction snippet(command) {\n return h(\n 'div',\n { class: 'snippet' },\n h('code', {}, command),\n h(\n 'button',\n {\n class: 'link',\n title: 'Copy',\n 'aria-label': `Copy ${command}`,\n onclick: () =>\n navigator.clipboard\n ?.writeText(command)\n .then(() => toast('Copied.'))\n .catch(() =>\n toast('Copy it by hand: the browser blocked the clipboard.', { error: true }),\n ),\n },\n 'Copy',\n ),\n );\n}\n\nfunction toolCard(title, text, ...commands) {\n return h(\n 'section',\n { class: 'card' },\n h('div', { class: 'card-head' }, h('h2', {}, title)),\n h(\n 'div',\n { class: 'card-body padded' },\n h('p', { class: 'card-help' }, text),\n commands.map(snippet),\n ),\n );\n}\n\nfunction toolsPage() {\n const sample = state.aliases[0]?.name ?? 'acme-dev';\n return [\n pageHead('Tools', 'More ways to use cloudhop from your terminal.'),\n h(\n 'section',\n { class: 'card', id: 'tools-cli' },\n h('div', { class: 'card-head' }, h('h2', {}, 'Cloud CLIs')),\n h('div', { class: 'card-body padded' }, h('p', { class: 'card-help' }, 'Checking\u2026')),\n ),\n toolCard(\n 'Shell integration',\n 'Pick aliases from a list, tab-complete them, and use one in your shell so plain aws and Terraform work without hop.',\n 'cloudhop shell install',\n `cloudhop use ${sample}`,\n 'hop',\n ),\n toolCard(\n 'Run in many accounts',\n 'Run one command in many aliases at once: names, lists, wildcards or /regex/. It asks before running.',\n `cloudhop each '${sample.split('-')[0]}-*' -- s3 ls`,\n ),\n toolCard(\n 'Share with your team',\n 'Export your portals and aliases (no secrets) and let teammates import them in one command.',\n 'cloudhop team export --out team.yaml',\n 'cloudhop team import team.yaml',\n ),\n ];\n}\n\nasync function loadTools() {\n const card = document.querySelector('#tools-cli .card-body');\n if (!card) return;\n try {\n const entries = await api('GET', '/api/doctor');\n card.replaceChildren(\n ...entries.map((e) =>\n h(\n 'div',\n { class: 'tool-row' },\n h('span', { class: `badge ${e.installed && !e.outdated ? 'valid' : 'expired'}` }, e.cli),\n h(\n 'span',\n {},\n !e.installed\n ? 'Not installed.'\n : e.outdated\n ? `${e.version} is older than ${e.minVersion}.`\n : `Version ${e.version}.`,\n ),\n e.installed && !e.outdated ? null : snippet('cloudhop doctor'),\n ),\n ),\n );\n } catch (error) {\n card.replaceChildren(h('p', { class: 'card-help' }, error.message));\n }\n}\n\n// ---------- AI assistants ----------\n\nlet assistantScope = 'user';\ntry {\n assistantScope =\n localStorage.getItem('cloudhop-assistant-scope') === 'project' ? 'project' : 'user';\n} catch {}\nlet assistantsData;\n\nconst SKILL_TEXT = {\n current: ['valid', 'Up to date'],\n outdated: ['refreshable', 'Outdated'],\n modified: ['refreshable', 'Edited by you'],\n missing: ['', 'Not installed'],\n};\nconst MCP_TEXT = { registered: 'On', missing: 'Off', unknown: 'Check in the app' };\n\nfunction assistantsCard() {\n return h('section', { class: 'card', id: 'assistants', hidden: true });\n}\n\nasync function loadAssistants() {\n try {\n assistantsData = await api('GET', '/api/assistants');\n renderAssistants();\n } catch {\n document.getElementById('assistants')?.remove();\n }\n}\n\nfunction setAssistantScope(scope) {\n assistantScope = scope;\n try {\n localStorage.setItem('cloudhop-assistant-scope', scope);\n } catch {}\n renderAssistants();\n}\n\nfunction assistantAction(label, run, { danger = false, confirmText } = {}) {\n return h(\n 'button',\n {\n class: danger ? 'danger' : undefined,\n onclick: (e) => {\n if (confirmText && !confirm(confirmText)) return;\n busy(e.currentTarget, async () => {\n try {\n await run();\n } catch (error) {\n fail(error);\n }\n await loadAssistants();\n });\n },\n },\n label,\n );\n}\n\nfunction reportInstall(results) {\n for (const r of results) {\n if (r.mcp === 'manual') {\n toast(`${r.name}: skill installed. Add the MCP server by hand.`, { hint: r.mcpManual });\n } else {\n toast(`${r.name} now knows cloudhop. Start a new chat so it picks it up.`);\n }\n }\n}\n\nfunction renderAssistants() {\n const el = document.getElementById('assistants');\n if (!el || !assistantsData) return;\n const rows = assistantScope === 'user' ? assistantsData.user : assistantsData.projectScope;\n const scope = assistantScope;\n const install = (tools) =>\n api('POST', '/api/assistants', { tools, scope, mcp: true }).then((r) =>\n reportInstall(r.results),\n );\n const missingDetected = rows\n .filter((r) => r.detected && r.skill === 'missing')\n .map((r) => r.assistant);\n\n const seg = h(\n 'div',\n { class: 'seg', role: 'group', 'aria-label': 'Where' },\n h(\n 'button',\n { 'aria-pressed': String(scope === 'user'), onclick: () => setAssistantScope('user') },\n 'For me',\n ),\n h(\n 'button',\n { 'aria-pressed': String(scope === 'project'), onclick: () => setAssistantScope('project') },\n 'This project',\n ),\n );\n\n const head = h(\n 'div',\n { class: 'card-head' },\n h('h2', {}, 'AI assistants'),\n h(\n 'span',\n { class: 'meta' },\n scope === 'user'\n ? 'Installed in your home folder, for every project'\n : assistantsData.project,\n ),\n h('div', { class: 'spacer' }),\n h(\n 'div',\n { class: 'actions' },\n seg,\n missingDetected.length > 1\n ? assistantAction('Install for all detected', () => install(missingDetected))\n : null,\n ),\n );\n\n const table = h(\n 'table',\n { class: 'assistants' },\n h(\n 'thead',\n {},\n h(\n 'tr',\n {},\n h('th', {}, 'Assistant'),\n h('th', {}, 'Skill'),\n h('th', {}, 'MCP server'),\n h('th'),\n ),\n ),\n h(\n 'tbody',\n {},\n rows.map((r) => {\n const [badgeClass, skillText] = SKILL_TEXT[r.skill];\n const actions = [];\n if (r.skill === 'missing')\n actions.push(assistantAction('Install', () => install([r.assistant])));\n if (r.skill === 'outdated') {\n actions.push(\n assistantAction('Update', () =>\n api('POST', '/api/assistants/update', {}).then(() =>\n toast(`${r.name}: skill updated.`),\n ),\n ),\n );\n }\n if (r.skill === 'modified') {\n actions.push(\n assistantAction(\n 'Replace',\n () =>\n api('POST', '/api/assistants/update', { force: true }).then(() =>\n toast('Edited skills replaced.'),\n ),\n {\n confirmText: 'Replace your edited copies with the current cloudhop skill?',\n },\n ),\n );\n }\n if (r.skill !== 'missing' || r.mcp === 'registered') {\n actions.push(\n assistantAction(\n 'Remove',\n () => api('POST', '/api/assistants/remove', { tools: [r.assistant], scope }),\n {\n danger: true,\n confirmText: `Remove the cloudhop skill and MCP server from ${r.name}?`,\n },\n ),\n );\n }\n return h(\n 'tr',\n {},\n h(\n 'td',\n { class: 'alias' },\n r.name,\n r.detected ? null : h('small', { class: 'muted-inline' }, ' not detected'),\n ),\n h('td', {}, h('span', { class: `badge ${badgeClass}`, title: r.skillPath }, skillText)),\n h('td', {}, r.skill === 'missing' && r.mcp !== 'registered' ? '' : MCP_TEXT[r.mcp]),\n h(\n 'td',\n { style: 'text-align:right' },\n h('div', { class: 'actions', style: 'justify-content:flex-end' }, actions),\n ),\n );\n }),\n ),\n );\n\n el.replaceChildren(\n head,\n h(\n 'div',\n { class: 'card-body' },\n h(\n 'p',\n { class: 'card-help' },\n 'Teach your AI assistant to use cloudhop: it asks you which alias to work with, runs commands with hop, and confirms before changing anything. Includes the cloudhop MCP server.',\n ),\n table,\n ),\n );\n el.hidden = false;\n}\n\nfunction renderChooseClouds() {\n const boxes = state.providers.map((p) =>\n h(\n 'label',\n { class: 'choice' },\n h('input', { type: 'checkbox', value: p.id, checked: true }),\n h('span', {}, p.name),\n ),\n );\n const save = h(\n 'button',\n {\n class: 'primary',\n onclick: (e) =>\n busy(e.currentTarget, async () => {\n const ids = boxes\n .map((b) => b.querySelector('input'))\n .filter((i) => i.checked)\n .map((i) => i.value);\n await api('PUT', '/api/providers', { enabled: ids }).catch(fail);\n await refresh();\n }),\n },\n 'Continue',\n );\n $main.replaceChildren(\n h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'Which clouds do you use?'),\n h('p', {}, 'Azure and Google Cloud are coming soon.'),\n h(\n 'div',\n { class: 'choices', style: 'max-width:320px;margin:0 auto 16px;text-align:left' },\n boxes,\n ),\n save,\n ),\n );\n}\n\nfunction sourceCard(source) {\n const aliases = state.aliases.filter((a) => a.source === source.id);\n const where = source.settings.startUrl ?? source.kindLabel;\n const head = h(\n 'div',\n { class: 'card-head' },\n h('h2', {}, source.id),\n h('span', { class: `badge ${source.session}` }, SESSION_TEXT[source.session] ?? source.session),\n h('span', { class: 'meta' }, `${source.providerName} \xB7 ${where}`),\n h('div', { class: 'spacer' }),\n h(\n 'div',\n { class: 'actions' },\n h('button', { onclick: () => openAliasPicker(source) }, '+ Aliases'),\n h(\n 'button',\n {\n title: 'Sign in again and refresh its accounts',\n onclick: (e) =>\n busy(e.currentTarget, async () => {\n toast('Opening the browser to sign in\u2026');\n const report = await api(\n 'POST',\n `/api/sources/${encodeURIComponent(source.id)}/login`,\n ).catch(fail);\n if (report)\n toast(`${source.id}: signed in, ${report.targets.length} account/role(s).`);\n await refresh();\n }),\n },\n 'Sign in',\n ),\n h(\n 'button',\n {\n title: 'Refresh the accounts and roles you have access to',\n onclick: (e) =>\n busy(e.currentTarget, async () => {\n const report = await api(\n 'POST',\n `/api/sources/${encodeURIComponent(source.id)}/sync`,\n ).catch(fail);\n if (report) toast(syncMessage(source.id, report));\n await refresh();\n }),\n },\n 'Sync',\n ),\n h(\n 'button',\n {\n class: 'danger',\n onclick: (e) => {\n const count = aliases.length;\n const message = `Remove \"${source.id}\"${count ? ` and its ${count} alias(es)` : ''}?`;\n if (!confirm(message)) return;\n busy(e.currentTarget, async () => {\n await api('DELETE', `/api/sources/${encodeURIComponent(source.id)}`).catch(fail);\n await refresh();\n });\n },\n },\n 'Remove',\n ),\n ),\n );\n\n const prodCount = aliases.filter((a) => a.prod).length;\n const body =\n aliases.length === 0\n ? h(\n 'div',\n { class: 'empty' },\n 'No aliases yet. ',\n h(\n 'button',\n { class: 'primary', onclick: () => openAliasPicker(source) },\n 'Pick accounts',\n ),\n )\n : h(\n 'div',\n { class: 'source-summary' },\n h(\n 'span',\n {},\n `${aliases.length} alias(es)`,\n prodCount ? h('span', { class: 'prod-tag' }, `${prodCount} PROD`) : null,\n ),\n h(\n 'button',\n { class: 'link', onclick: () => go('aliases', { source: source.id }) },\n 'View aliases \u2192',\n ),\n );\n return h('section', { class: 'card' }, head, h('div', { class: 'card-body' }, body));\n}\n\nfunction syncMessage(id, report) {\n const parts = [`${report.targets.length} available`];\n if (report.added) parts.push(`${report.added} new`);\n if (report.removed) parts.push(`${report.removed} removed`);\n const orphaned = report.orphanedAliases.length\n ? ` No longer available: ${report.orphanedAliases.join(', ')}.`\n : '';\n return `${id}: ${parts.join(', ')}.${orphaned}`;\n}\n\nfunction aliasRow(alias) {\n const input = h('input', {\n value: alias.region ?? '',\n 'aria-label': `Region for ${alias.name}`,\n class: 'mono',\n spellcheck: 'false',\n });\n const save = h('button', { hidden: true }, 'Save');\n const commit = () =>\n busy(save, async () => {\n const region = input.value.trim();\n const done = await api('PATCH', `/api/aliases/${encodeURIComponent(alias.name)}`, {\n region,\n }).catch(fail);\n if (done) toast(`${alias.name} now runs in ${region}.`);\n await refresh();\n });\n input.addEventListener('input', () => {\n save.hidden = input.value.trim() === (alias.region ?? '');\n });\n input.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' && !save.hidden) commit();\n if (e.key === 'Escape') {\n input.value = alias.region ?? '';\n save.hidden = true;\n }\n });\n save.addEventListener('click', commit);\n\n const patch = (body, message) =>\n api('PATCH', `/api/aliases/${encodeURIComponent(alias.name)}`, body)\n .then(() => message && toast(message))\n .catch(fail)\n .then(refresh);\n const iconButton = (label, title, onclick, extra = {}) =>\n h('button', { class: 'link', title, 'aria-label': title, onclick, ...extra }, label);\n\n const actions = h(\n 'div',\n { class: 'row-actions' },\n h(\n 'button',\n {\n title: `Open the AWS console as ${alias.name}, already signed in`,\n disabled: alias.missing,\n onclick: (e) =>\n busy(e.currentTarget, async () => {\n const result = await api(\n 'POST',\n `/api/aliases/${encodeURIComponent(alias.name)}/console`,\n {},\n ).catch(fail);\n if (result) {\n toast(\n result.via === 'signed-in'\n ? `Opened the console for ${alias.name}.`\n : result.via === 'portal'\n ? `Opened ${alias.name} through the SSO portal.`\n : `Opened the console sign-in page for ${alias.name}.`,\n );\n }\n }),\n },\n 'Console',\n ),\n h(\n 'label',\n { class: 'prod-toggle', title: 'Production: hop and AI assistants ask before any change' },\n h('input', {\n type: 'checkbox',\n checked: alias.prod,\n 'aria-label': `${alias.name} is production`,\n onchange: (e) =>\n patch(\n { prod: e.currentTarget.checked },\n e.currentTarget.checked\n ? `${alias.name} is now production.`\n : `${alias.name} is no longer production.`,\n ),\n }),\n 'Prod',\n ),\n iconButton('\u270E', `Rename ${alias.name}`, () => {\n const to = prompt(`New name for \"${alias.name}\"`, alias.name)?.trim();\n if (to && to !== alias.name) patch({ name: to }, `${alias.name} is now ${to}.`);\n }),\n iconButton(\n '\u2715',\n `Remove ${alias.name}`,\n (e) => {\n if (!confirm(`Remove alias \"${alias.name}\"?`)) return;\n busy(e.currentTarget, async () => {\n await api('DELETE', `/api/aliases/${encodeURIComponent(alias.name)}`).catch(fail);\n await refresh();\n });\n },\n { class: 'link danger-hover' },\n ),\n );\n\n return h(\n 'tr',\n { class: alias.prod ? 'prod' : undefined },\n h(\n 'td',\n { class: 'alias', title: `hop ${alias.name} <command>` },\n alias.name,\n alias.prod ? h('span', { class: 'prod-tag' }, 'PROD') : null,\n ),\n h('td', { class: 'muted' }, alias.source),\n h('td', {}, h('div', { class: 'region' }, input, save)),\n h(\n 'td',\n {},\n alias.missing\n ? h('span', { class: 'missing' }, `${alias.target} (no longer available)`)\n : alias.label,\n ),\n h('td', { style: 'text-align:right' }, actions),\n );\n}\n\n// ---------- dialogs ----------\n\nfunction showDialog(title, body, footer) {\n $dialog.replaceChildren(\n h(\n 'div',\n { class: 'dlg-head' },\n h('h2', {}, title),\n h('button', { class: 'link', 'aria-label': 'Close', onclick: () => $dialog.close() }, '\u2715'),\n ),\n h('div', { class: 'dlg-body' }, body),\n footer ? h('div', { class: 'dlg-foot' }, footer) : null,\n );\n if (!$dialog.open) $dialog.showModal();\n}\n\nfunction suggestSourceId(settings, kind) {\n const host = /^https:\\/\\/([^./]+)\\./.exec(settings.startUrl ?? '')?.[1];\n return slug(host ?? (kind === 'console-login' ? 'personal' : 'main'));\n}\n\nconst slug = (text) =>\n text\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 63) || 'main';\n\n/** `previous` refills the form after a failed sign-in: { provider, kind, id, settings, error }. */\nfunction openAddSource(previous) {\n const providers = state.providers.filter((p) => state.enabledProviders.includes(p.id));\n let provider = providers.find((p) => p.id === previous?.provider) ?? providers[0];\n let kind = provider.kinds.find((k) => k.kind === previous?.kind) ?? provider.kinds[0];\n\n const form = h('form', { onsubmit: (e) => e.preventDefault() });\n const nameInput = h('input', {\n id: 'f-name',\n required: true,\n class: 'mono',\n spellcheck: 'false',\n });\n let nameTouched = Boolean(previous);\n nameInput.addEventListener('input', () => {\n nameTouched = true;\n });\n\n const fieldInputs = {};\n const fieldsBox = h('div');\n const nameLabel = h('label', { for: 'f-name' });\n\n function updateSuggestion() {\n if (nameTouched) return;\n const settings = Object.fromEntries(\n Object.entries(fieldInputs).map(([k, i]) => [k, i.value.trim()]),\n );\n const taken = new Set(state.sources.map((s) => s.id));\n let id = suggestSourceId(settings, kind.kind);\n for (let i = 2; taken.has(id); i++) id = `${suggestSourceId(settings, kind.kind)}-${i}`;\n nameInput.value = id;\n }\n\n function renderFields() {\n for (const key of Object.keys(fieldInputs)) delete fieldInputs[key];\n fieldsBox.replaceChildren(\n ...kind.fields.map((f) => {\n const input = h('input', {\n id: `f-${f.key}`,\n placeholder: f.placeholder ?? f.defaultValue ?? '',\n value: previous?.settings?.[f.key] ?? f.defaultValue ?? '',\n required: true,\n class: 'mono',\n spellcheck: 'false',\n oninput: updateSuggestion,\n });\n fieldInputs[f.key] = input;\n return h('div', { class: 'field' }, h('label', { for: input.id }, f.label), input);\n }),\n );\n nameLabel.textContent = kind.nameLabel ?? 'Short name for this source';\n updateSuggestion();\n }\n\n const providerSelect =\n providers.length > 1\n ? h(\n 'div',\n { class: 'field' },\n h('label', { for: 'f-provider' }, 'Cloud'),\n h(\n 'select',\n {\n id: 'f-provider',\n onchange: (e) => {\n provider = providers.find((p) => p.id === e.target.value);\n kind = provider.kinds[0];\n renderKinds();\n },\n },\n providers.map((p) => h('option', { value: p.id, selected: p === provider }, p.name)),\n ),\n )\n : null;\n\n const kindsBox = h('div', { class: 'choices' });\n function renderKinds() {\n kindsBox.replaceChildren(\n ...provider.kinds.map((k) =>\n h(\n 'label',\n { class: 'choice' },\n h('input', {\n type: 'radio',\n name: 'kind',\n value: k.kind,\n checked: k === kind,\n onchange: () => {\n kind = k;\n renderFields();\n },\n }),\n h('span', {}, h('strong', {}, k.label), k.hint ? h('small', {}, k.hint) : null),\n ),\n ),\n );\n renderFields();\n }\n\n form.append(\n previous?.error\n ? h(\n 'div',\n { class: 'notice error' },\n h('strong', {}, 'Could not sign in, so nothing was saved. '),\n previous.error.message,\n previous.error.hint ? h('div', { class: 'help' }, previous.error.hint) : null,\n )\n : '',\n providerSelect ?? '',\n h('div', { class: 'field' }, h('label', {}, 'What do you want to add?')),\n kindsBox,\n fieldsBox,\n h(\n 'div',\n { class: 'field' },\n nameLabel,\n nameInput,\n h(\n 'span',\n { class: 'help' },\n 'Lowercase letters, numbers and dashes. You will see it in ',\n h('code', {}, 'cloudhop ls'),\n '.',\n ),\n ),\n );\n renderKinds();\n if (previous) nameInput.value = previous.id;\n\n const submit = h('button', { class: 'primary', type: 'submit' }, 'Add and sign in');\n form.addEventListener('submit', async () => {\n if (!form.reportValidity()) return;\n const body = {\n provider: provider.id,\n kind: kind.kind,\n id: nameInput.value.trim(),\n settings: Object.fromEntries(\n Object.entries(fieldInputs).map(([k, i]) => [k, i.value.trim()]),\n ),\n };\n showDialog(\n 'Sign in',\n h(\n 'div',\n { class: 'waiting' },\n h('div', { class: 'spinner' }),\n h('strong', {}, 'Finish signing in in the browser tab that just opened.'),\n h('p', { class: 'help' }, 'This page continues on its own once you approve the request.'),\n ),\n );\n try {\n const report = await api('POST', '/api/sources', body);\n await refresh();\n const source = state.sources.find((s) => s.id === body.id);\n toast(`Signed in to ${body.id}. ${report.targets.length} account/role(s) found.`);\n openAliasPicker(source, report.targets);\n } catch (error) {\n // Nothing was saved: reopen the form with what was typed so it can be fixed.\n await refresh();\n openAddSource({ ...body, error });\n }\n });\n\n showDialog('Add source', form, [\n h('button', { type: 'button', onclick: () => $dialog.close() }, 'Cancel'),\n submit,\n ]);\n submit.setAttribute('form', 'add-form');\n form.id = 'add-form';\n}\n\nasync function openAliasPicker(source, preloaded) {\n let targets = preloaded;\n if (!targets) {\n showDialog(\n `Aliases for ${source.id}`,\n h('div', { class: 'waiting' }, h('div', { class: 'spinner' }), 'Loading accounts\u2026'),\n );\n try {\n targets = (await api('GET', `/api/sources/${encodeURIComponent(source.id)}/targets`)).targets;\n } catch (error) {\n $dialog.close();\n return fail(error);\n }\n }\n\n const used = new Map(\n state.aliases.filter((a) => a.source === source.id).map((a) => [a.target, a.name]),\n );\n const taken = new Set(state.aliases.map((a) => a.name));\n const defaultRegion = source.settings.region ?? '';\n const filter = h('input', {\n type: 'search',\n placeholder: `Filter ${targets.length} account/role(s)\u2026`,\n style: 'width:100%;margin-bottom:12px',\n });\n\n const rows = targets.map((t) => {\n const existing = used.get(t.id);\n let name = t.suggestedAlias;\n for (let i = 2; taken.has(name); i++) name = `${t.suggestedAlias}-${i}`;\n const check = h('input', {\n type: 'checkbox',\n disabled: Boolean(existing),\n 'aria-label': t.label,\n });\n const aliasInput = h('input', {\n value: name,\n class: 'mono',\n spellcheck: 'false',\n 'aria-label': `Alias for ${t.label}`,\n placeholder: 'alias',\n });\n const regionInput = h('input', {\n value: defaultRegion,\n class: 'mono',\n spellcheck: 'false',\n 'aria-label': `Region for ${t.label}`,\n placeholder: 'region',\n });\n // Only the checkbox and its text sit in the label, so clicking the inputs never toggles it.\n const el = h(\n 'div',\n { class: 'target' },\n h(\n 'label',\n { class: 'pick' },\n check,\n h(\n 'span',\n {},\n t.label,\n existing ? h('small', { class: 'already' }, `Already: ${existing}`) : null,\n ),\n ),\n h('div', { class: 'inputs' }, aliasInput, regionInput),\n );\n return { target: t, el, check, aliasInput, regionInput };\n });\n\n filter.addEventListener('input', () => {\n const q = filter.value.trim().toLowerCase();\n for (const row of rows) row.el.hidden = q !== '' && !row.target.label.toLowerCase().includes(q);\n });\n\n const save = h('button', { class: 'primary' }, 'Create aliases');\n save.addEventListener('click', () =>\n busy(save, async () => {\n const aliases = rows\n .filter((r) => r.check.checked)\n .map((r) => ({\n source: source.id,\n target: r.target.id,\n name: r.aliasInput.value.trim(),\n region: r.regionInput.value.trim(),\n }));\n if (aliases.length === 0) return toast('Pick at least one account/role.', { error: true });\n try {\n const result = await api('POST', '/api/aliases', { aliases });\n $dialog.close();\n toast(`Created ${result.created.join(', ')}. Try: hop ${result.created[0]}`);\n await refresh();\n } catch (error) {\n fail(error);\n }\n }),\n );\n\n const body =\n targets.length === 0\n ? h('p', { class: 'empty' }, 'This source has no accounts or roles assigned to you.')\n : h(\n 'div',\n {},\n filter,\n h(\n 'div',\n { class: 'targets' },\n rows.map((r) => r.el),\n ),\n );\n showDialog(`Aliases for ${source.id}`, body, [\n h('span', { class: 'note' }, 'Check an account/role, then name its alias and region.'),\n h('button', { onclick: () => $dialog.close() }, 'Cancel'),\n targets.length ? save : null,\n ]);\n}\n\n$addSource.addEventListener('click', () => openAddSource());\n\nif (!token) {\n $main.replaceChildren(\n h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'Open the link from your terminal'),\n h('p', {}, 'Run cloudhop ui and open the link it prints. It includes a one-time access key.'),\n ),\n );\n} else {\n readRoute();\n refresh();\n}\n";
|
|
14189
|
+
var app_default = "// cloudhop local UI. Plain DOM, no build step: served as-is by `cloudhop ui`.\n\nconst TOKEN_KEY = 'cloudhop-token';\nconst SESSION_TEXT = {\n valid: 'Session valid',\n refreshable: 'Renews on next use',\n expired: 'Expired: sign in on next use',\n unknown: 'Managed by the cloud CLI',\n};\n\n// The token arrives in the URL fragment once; keep it for this tab and clean the address bar.\nconst fromHash = new URLSearchParams(location.hash.slice(1)).get('token');\nif (fromHash) {\n try {\n sessionStorage.setItem(TOKEN_KEY, fromHash);\n } catch {}\n history.replaceState(null, '', location.pathname);\n}\nlet token = fromHash;\ntry {\n token = token ?? sessionStorage.getItem(TOKEN_KEY);\n} catch {}\n\n// ---------- pages ----------\n\nconst PAGES = {\n aliases: 'Aliases',\n sources: 'Sources',\n assistants: 'AI assistants',\n tools: 'Tools',\n};\nlet route = 'aliases';\nconst aliasFilter = { text: '', source: '', prodOnly: false };\n\n/** `#/aliases?source=acme` \u2192 route \"aliases\", filtered by that source. */\nfunction readRoute() {\n const match = /^#\\/([a-z]+)(?:\\?(.*))?$/.exec(location.hash);\n // Own keys only: \"#/constructor\" must not reach Object.prototype.\n route = match && Object.hasOwn(PAGES, match[1]) ? match[1] : 'aliases';\n const query = new URLSearchParams(match?.[2] ?? '');\n if (query.has('source')) aliasFilter.source = query.get('source');\n}\n\nfunction go(page, query) {\n location.hash = `#/${page}${query ? `?${new URLSearchParams(query)}` : ''}`;\n}\n\n// A fresh link opened in this same tab carries a new token: reload to pick it up.\n// Any other fragment is a page change.\naddEventListener('hashchange', () => {\n if (new URLSearchParams(location.hash.slice(1)).has('token')) {\n location.reload();\n return;\n }\n readRoute();\n if (state) render();\n});\n\nconst $main = document.getElementById('main');\nconst $dialog = document.getElementById('dialog');\nconst $toasts = document.getElementById('toasts');\nconst $addSource = document.getElementById('add-source');\nconst $nav = document.getElementById('nav');\nlet state;\n\n/** Tiny element builder: h('button', { class: 'primary', onclick }, 'Save'). */\nfunction h(tag, attrs = {}, ...children) {\n const el = document.createElement(tag);\n for (const [key, value] of Object.entries(attrs)) {\n if (value === undefined || value === null || value === false) continue;\n if (key.startsWith('on')) el.addEventListener(key.slice(2), value);\n else if (key === 'class') el.className = value;\n else if (value === true) el.setAttribute(key, '');\n else el.setAttribute(key, value);\n }\n for (const child of children.flat()) {\n if (child === undefined || child === null || child === false) continue;\n el.append(child instanceof Node ? child : document.createTextNode(String(child)));\n }\n return el;\n}\n\nclass ApiError extends Error {\n constructor(message, hint) {\n super(message);\n this.hint = hint;\n }\n}\n\n// Calls that may sign in wait for the person in the browser; everything else should answer quickly.\nconst MAY_SIGN_IN = /^\\/api\\/(sources(\\/[^/]+\\/(login|sync|targets))?|aliases\\/[^/]+\\/console)$/;\nconst QUICK_TIMEOUT_MS = 20_000;\n\nasync function api(method, path, body) {\n const waitsForPerson = MAY_SIGN_IN.test(path);\n let response;\n try {\n response = await fetch(path, {\n method,\n headers: {\n 'x-cloudhop-token': token ?? '',\n ...(body ? { 'content-type': 'application/json' } : {}),\n },\n body: body ? JSON.stringify(body) : undefined,\n signal: waitsForPerson ? undefined : AbortSignal.timeout(QUICK_TIMEOUT_MS),\n });\n } catch (error) {\n if (error.name === 'TimeoutError') {\n throw new ApiError(\n 'cloudhop ui is not responding.',\n 'Is it paused in your terminal (Ctrl-Z)? Run `fg` there, or stop it and run `cloudhop ui` again.',\n );\n }\n throw new ApiError(\n 'cloudhop ui is not running.',\n 'Run `cloudhop ui` again and open the new link it prints.',\n );\n }\n const data = await response.json().catch(() => ({}));\n if (!response.ok)\n throw new ApiError(data.error ?? `Request failed (${response.status}).`, data.hint);\n return data;\n}\n\nfunction toast(message, { error = false, hint } = {}) {\n const el = h(\n 'div',\n { class: `toast${error ? ' error' : ''}` },\n message,\n hint && h('small', {}, hint),\n );\n $toasts.append(el);\n setTimeout(() => el.remove(), error ? 9000 : 4000);\n}\n\nconst fail = (error) => toast(error.message, { error: true, hint: error.hint });\n\n/** Disables a button while its action runs. */\nasync function busy(button, action) {\n button.disabled = true;\n try {\n return await action();\n } finally {\n button.disabled = false;\n }\n}\n\nasync function refresh() {\n try {\n state = await api('GET', '/api/state');\n render();\n checkDoctor();\n } catch (error) {\n $main.replaceChildren(\n h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'Cannot reach cloudhop'),\n h('p', {}, error.message, error.hint ? ` ${error.hint}` : ''),\n ),\n );\n }\n}\n\nasync function checkDoctor() {\n try {\n const entries = await api('GET', '/api/doctor');\n const problems = entries.filter((e) => !e.installed || e.outdated);\n document.getElementById('doctor')?.remove();\n if (problems.length === 0) return;\n $main.prepend(\n h(\n 'div',\n { class: 'notice', id: 'doctor' },\n ...problems.map((e) =>\n h(\n 'div',\n {},\n e.installed\n ? `${e.cli} ${e.version} is older than ${e.minVersion}. `\n : `${e.cli} is not installed, so aliases cannot run commands yet. `,\n 'Run ',\n h('code', {}, 'cloudhop doctor'),\n ' in a terminal to install it.',\n ),\n ),\n ),\n );\n } catch {}\n}\n\n// ---------- main view ----------\n\nfunction render() {\n const enabled = state.enabledProviders;\n $addSource.hidden = enabled.length === 0;\n $nav.hidden = enabled.length === 0;\n if (enabled.length === 0) return renderChooseClouds();\n for (const link of $nav.querySelectorAll('a')) {\n if (link.dataset.page === route) link.setAttribute('aria-current', 'page');\n else link.removeAttribute('aria-current');\n }\n document.title = `${PAGES[route]} \xB7 cloudhop`;\n const pages = new Map([\n ['aliases', aliasesPage],\n ['sources', sourcesPage],\n ['assistants', assistantsPage],\n ['tools', toolsPage],\n ]);\n const page = pages.get(route) ?? aliasesPage;\n $main.replaceChildren(...page());\n if (route === 'assistants') loadAssistants();\n if (route === 'tools') loadTools();\n}\n\nfunction pageHead(title, subtitle, ...actions) {\n return h(\n 'div',\n { class: 'page-head' },\n h('div', {}, h('h1', {}, title), subtitle ? h('p', {}, subtitle) : null),\n h('div', { class: 'actions' }, actions),\n );\n}\n\nfunction firstSourceHero() {\n return h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'Add your first account'),\n h(\n 'p',\n {},\n 'Add a company SSO portal or a personal account. cloudhop signs you in, finds every account and role you can use, and lets you give each one a short alias.',\n ),\n h('button', { class: 'primary', onclick: () => openAddSource() }, '+ Add source'),\n );\n}\n\n// ---------- Aliases page ----------\n\nfunction aliasesPage() {\n if (state.sources.length === 0) return [firstSourceHero()];\n if (aliasFilter.source && !state.sources.some((s) => s.id === aliasFilter.source))\n aliasFilter.source = '';\n\n const addAliases = () => {\n const source =\n state.sources.find((s) => s.id === aliasFilter.source) ??\n (state.sources.length === 1 ? state.sources[0] : undefined);\n if (source) openAliasPicker(source);\n else openSourceChooser();\n };\n const head = pageHead(\n 'Aliases',\n 'Each alias is one account, role and region. Run it with hop <alias>.',\n h('button', { onclick: addAliases }, '+ Aliases'),\n );\n if (state.aliases.length === 0) {\n return [\n head,\n h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'No aliases yet'),\n h('p', {}, 'Pick the accounts and roles you use and give each one a short name.'),\n h('button', { class: 'primary', onclick: addAliases }, 'Pick accounts'),\n ),\n ];\n }\n\n const rows = h('tbody', { id: 'alias-rows' });\n const count = h('span', { class: 'count' });\n const fill = () => {\n const words = aliasFilter.text.toLowerCase().split(/\\s+/).filter(Boolean);\n const visible = state.aliases.filter((a) => {\n if (aliasFilter.source && a.source !== aliasFilter.source) return false;\n if (aliasFilter.prodOnly && !a.prod) return false;\n const text = `${a.name} ${a.label ?? a.target} ${a.region ?? ''} ${a.source}`.toLowerCase();\n return words.every((w) => text.includes(w));\n });\n rows.replaceChildren(\n ...(visible.length\n ? visible.map(aliasRow)\n : [\n h(\n 'tr',\n {},\n h('td', { colspan: '5', class: 'empty' }, 'No alias matches these filters.'),\n ),\n ]),\n );\n count.textContent = `${visible.length} of ${state.aliases.length}`;\n };\n\n const search = h('input', {\n type: 'search',\n placeholder: 'Filter: name, account, role or region',\n value: aliasFilter.text,\n 'aria-label': 'Filter aliases',\n oninput: (e) => {\n aliasFilter.text = e.currentTarget.value;\n fill();\n },\n });\n const sourceSelect = h(\n 'select',\n {\n 'aria-label': 'Source',\n onchange: (e) => {\n aliasFilter.source = e.currentTarget.value;\n fill();\n },\n },\n h('option', { value: '' }, 'All sources'),\n state.sources.map((s) =>\n h('option', { value: s.id, selected: s.id === aliasFilter.source }, s.id),\n ),\n );\n const prodOnly = h(\n 'label',\n { class: 'prod-toggle' },\n h('input', {\n type: 'checkbox',\n checked: aliasFilter.prodOnly,\n onchange: (e) => {\n aliasFilter.prodOnly = e.currentTarget.checked;\n fill();\n },\n }),\n 'Production only',\n );\n\n const table = h(\n 'table',\n { class: 'aliases' },\n h(\n 'thead',\n {},\n h(\n 'tr',\n {},\n h('th', {}, 'Alias'),\n h('th', {}, 'Source'),\n h('th', {}, 'Region'),\n h('th', {}, 'Account / role'),\n h('th'),\n ),\n ),\n rows,\n );\n fill();\n return [\n head,\n h(\n 'div',\n { class: 'toolbar' },\n search,\n sourceSelect,\n prodOnly,\n h('div', { class: 'spacer' }),\n count,\n ),\n h('section', { class: 'card' }, table),\n ];\n}\n\n/** When several sources exist, ask which one to pick accounts from. */\nfunction openSourceChooser() {\n showDialog(\n 'Add aliases from',\n h(\n 'div',\n { class: 'choices' },\n state.sources.map((source) =>\n h(\n 'button',\n { class: 'choice', onclick: () => openAliasPicker(source) },\n h(\n 'span',\n {},\n h('strong', {}, source.id),\n h('small', {}, source.settings.startUrl ?? source.kindLabel),\n ),\n ),\n ),\n ),\n );\n}\n\n// ---------- Sources page ----------\n\nfunction sourcesPage() {\n const head = pageHead('Sources', 'The SSO portals and personal accounts you sign in to.');\n if (state.sources.length === 0) return [head, firstSourceHero()];\n return [head, ...state.sources.map(sourceCard)];\n}\n\n// ---------- AI assistants page ----------\n\nfunction assistantsPage() {\n return [assistantsCard()];\n}\n\n// ---------- Tools page ----------\n\nfunction snippet(command) {\n return h(\n 'div',\n { class: 'snippet' },\n h('code', {}, command),\n h(\n 'button',\n {\n class: 'link',\n title: 'Copy',\n 'aria-label': `Copy ${command}`,\n onclick: () =>\n navigator.clipboard\n ?.writeText(command)\n .then(() => toast('Copied.'))\n .catch(() =>\n toast('Copy it by hand: the browser blocked the clipboard.', { error: true }),\n ),\n },\n 'Copy',\n ),\n );\n}\n\nfunction toolCard(title, text, ...commands) {\n return h(\n 'section',\n { class: 'card' },\n h('div', { class: 'card-head' }, h('h2', {}, title)),\n h(\n 'div',\n { class: 'card-body padded' },\n h('p', { class: 'card-help' }, text),\n commands.map(snippet),\n ),\n );\n}\n\nfunction toolsPage() {\n const sample = state.aliases[0]?.name ?? 'acme-dev';\n return [\n pageHead('Tools', 'More ways to use cloudhop from your terminal.'),\n h(\n 'section',\n { class: 'card', id: 'tools-cli' },\n h('div', { class: 'card-head' }, h('h2', {}, 'Cloud CLIs')),\n h('div', { class: 'card-body padded' }, h('p', { class: 'card-help' }, 'Checking\u2026')),\n ),\n toolCard(\n 'Shell integration',\n 'Pick aliases from a list, tab-complete them, and use one in your shell so plain aws and Terraform work without hop.',\n 'cloudhop shell install',\n `cloudhop use ${sample}`,\n 'hop',\n ),\n toolCard(\n 'Run in many accounts',\n 'Run one command in many aliases at once: names, lists, wildcards or /regex/. It asks before running.',\n `cloudhop each '${sample.split('-')[0]}-*' -- s3 ls`,\n ),\n toolCard(\n 'Share with your team',\n 'Export your portals and aliases (no secrets) and let teammates import them in one command.',\n 'cloudhop team export --out team.yaml',\n 'cloudhop team import team.yaml',\n ),\n ];\n}\n\nasync function loadTools() {\n const card = document.querySelector('#tools-cli .card-body');\n if (!card) return;\n try {\n const entries = await api('GET', '/api/doctor');\n card.replaceChildren(\n ...entries.map((e) =>\n h(\n 'div',\n { class: 'tool-row' },\n h('span', { class: `badge ${e.installed && !e.outdated ? 'valid' : 'expired'}` }, e.cli),\n h(\n 'span',\n {},\n !e.installed\n ? 'Not installed.'\n : e.outdated\n ? `${e.version} is older than ${e.minVersion}.`\n : `Version ${e.version}.`,\n ),\n e.installed && !e.outdated ? null : snippet('cloudhop doctor'),\n ),\n ),\n );\n } catch (error) {\n card.replaceChildren(h('p', { class: 'card-help' }, error.message));\n }\n}\n\n// ---------- AI assistants ----------\n\nlet assistantScope = 'user';\ntry {\n assistantScope =\n localStorage.getItem('cloudhop-assistant-scope') === 'project' ? 'project' : 'user';\n} catch {}\nlet assistantsData;\n\nconst SKILL_TEXT = {\n current: ['valid', 'Up to date'],\n outdated: ['refreshable', 'Outdated'],\n modified: ['refreshable', 'Edited by you'],\n missing: ['', 'Not installed'],\n};\nconst MCP_TEXT = { registered: 'On', missing: 'Off', unknown: 'Check in the app' };\n\nfunction assistantsCard() {\n return h('section', { class: 'card', id: 'assistants', hidden: true });\n}\n\nasync function loadAssistants() {\n try {\n assistantsData = await api('GET', '/api/assistants');\n renderAssistants();\n } catch {\n document.getElementById('assistants')?.remove();\n }\n}\n\nfunction setAssistantScope(scope) {\n assistantScope = scope;\n try {\n localStorage.setItem('cloudhop-assistant-scope', scope);\n } catch {}\n renderAssistants();\n}\n\nfunction assistantAction(label, run, { danger = false, confirmText } = {}) {\n return h(\n 'button',\n {\n class: danger ? 'danger' : undefined,\n onclick: (e) => {\n if (confirmText && !confirm(confirmText)) return;\n busy(e.currentTarget, async () => {\n try {\n await run();\n } catch (error) {\n fail(error);\n }\n await loadAssistants();\n });\n },\n },\n label,\n );\n}\n\nfunction reportInstall(results) {\n for (const r of results) {\n if (r.mcp === 'manual') {\n toast(`${r.name}: skill installed. Add the MCP server by hand.`, { hint: r.mcpManual });\n } else {\n toast(`${r.name} now knows cloudhop. Start a new chat so it picks it up.`);\n }\n }\n}\n\nfunction renderAssistants() {\n const el = document.getElementById('assistants');\n if (!el || !assistantsData) return;\n const rows = assistantScope === 'user' ? assistantsData.user : assistantsData.projectScope;\n const scope = assistantScope;\n const install = (tools) =>\n api('POST', '/api/assistants', { tools, scope, mcp: true }).then((r) =>\n reportInstall(r.results),\n );\n const missingDetected = rows\n .filter((r) => r.detected && r.skill === 'missing')\n .map((r) => r.assistant);\n\n const seg = h(\n 'div',\n { class: 'seg', role: 'group', 'aria-label': 'Where' },\n h(\n 'button',\n { 'aria-pressed': String(scope === 'user'), onclick: () => setAssistantScope('user') },\n 'For me',\n ),\n h(\n 'button',\n { 'aria-pressed': String(scope === 'project'), onclick: () => setAssistantScope('project') },\n 'This project',\n ),\n );\n\n const head = h(\n 'div',\n { class: 'card-head' },\n h('h2', {}, 'AI assistants'),\n h(\n 'span',\n { class: 'meta' },\n scope === 'user'\n ? 'Installed in your home folder, for every project'\n : assistantsData.project,\n ),\n h('div', { class: 'spacer' }),\n h(\n 'div',\n { class: 'actions' },\n seg,\n missingDetected.length > 1\n ? assistantAction('Install for all detected', () => install(missingDetected))\n : null,\n ),\n );\n\n const table = h(\n 'table',\n { class: 'assistants' },\n h(\n 'thead',\n {},\n h(\n 'tr',\n {},\n h('th', {}, 'Assistant'),\n h('th', {}, 'Skill'),\n h('th', {}, 'MCP server'),\n h('th'),\n ),\n ),\n h(\n 'tbody',\n {},\n rows.map((r) => {\n const [badgeClass, skillText] = SKILL_TEXT[r.skill];\n const actions = [];\n if (r.skill === 'missing')\n actions.push(assistantAction('Install', () => install([r.assistant])));\n if (r.skill === 'outdated') {\n actions.push(\n assistantAction('Update', () =>\n api('POST', '/api/assistants/update', {}).then(() =>\n toast(`${r.name}: skill updated.`),\n ),\n ),\n );\n }\n if (r.skill === 'modified') {\n actions.push(\n assistantAction(\n 'Replace',\n () =>\n api('POST', '/api/assistants/update', { force: true }).then(() =>\n toast('Edited skills replaced.'),\n ),\n {\n confirmText: 'Replace your edited copies with the current cloudhop skill?',\n },\n ),\n );\n }\n if (r.skill !== 'missing' || r.mcp === 'registered') {\n actions.push(\n assistantAction(\n 'Remove',\n () => api('POST', '/api/assistants/remove', { tools: [r.assistant], scope }),\n {\n danger: true,\n confirmText: `Remove the cloudhop skill and MCP server from ${r.name}?`,\n },\n ),\n );\n }\n return h(\n 'tr',\n {},\n h(\n 'td',\n { class: 'alias' },\n r.name,\n r.detected ? null : h('small', { class: 'muted-inline' }, ' not detected'),\n ),\n h('td', {}, h('span', { class: `badge ${badgeClass}`, title: r.skillPath }, skillText)),\n h('td', {}, r.skill === 'missing' && r.mcp !== 'registered' ? '' : MCP_TEXT[r.mcp]),\n h(\n 'td',\n { style: 'text-align:right' },\n h('div', { class: 'actions', style: 'justify-content:flex-end' }, actions),\n ),\n );\n }),\n ),\n );\n\n el.replaceChildren(\n head,\n h(\n 'div',\n { class: 'card-body' },\n h(\n 'p',\n { class: 'card-help' },\n 'Teach your AI assistant to use cloudhop: it asks you which alias to work with, runs commands with hop, and confirms before changing anything. Includes the cloudhop MCP server.',\n ),\n table,\n ),\n );\n el.hidden = false;\n}\n\nfunction renderChooseClouds() {\n const boxes = state.providers.map((p) =>\n h(\n 'label',\n { class: 'choice' },\n h('input', { type: 'checkbox', value: p.id, checked: true }),\n h('span', {}, p.name),\n ),\n );\n const save = h(\n 'button',\n {\n class: 'primary',\n onclick: (e) =>\n busy(e.currentTarget, async () => {\n const ids = boxes\n .map((b) => b.querySelector('input'))\n .filter((i) => i.checked)\n .map((i) => i.value);\n await api('PUT', '/api/providers', { enabled: ids }).catch(fail);\n await refresh();\n }),\n },\n 'Continue',\n );\n $main.replaceChildren(\n h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'Which clouds do you use?'),\n h('p', {}, 'Azure and Google Cloud are coming soon.'),\n h(\n 'div',\n { class: 'choices', style: 'max-width:320px;margin:0 auto 16px;text-align:left' },\n boxes,\n ),\n save,\n ),\n );\n}\n\nfunction sourceCard(source) {\n const aliases = state.aliases.filter((a) => a.source === source.id);\n const where = source.settings.startUrl ?? source.kindLabel;\n const head = h(\n 'div',\n { class: 'card-head' },\n h('h2', {}, source.id),\n h('span', { class: `badge ${source.session}` }, SESSION_TEXT[source.session] ?? source.session),\n h('span', { class: 'meta' }, `${source.providerName} \xB7 ${where}`),\n h('div', { class: 'spacer' }),\n h(\n 'div',\n { class: 'actions' },\n h('button', { onclick: () => openAliasPicker(source) }, '+ Aliases'),\n h(\n 'button',\n {\n title: 'Sign in again and refresh its accounts',\n onclick: (e) =>\n busy(e.currentTarget, async () => {\n toast('Opening the browser to sign in\u2026');\n const report = await api(\n 'POST',\n `/api/sources/${encodeURIComponent(source.id)}/login`,\n ).catch(fail);\n if (report)\n toast(`${source.id}: signed in, ${report.targets.length} account/role(s).`);\n await refresh();\n }),\n },\n 'Sign in',\n ),\n h(\n 'button',\n {\n title: 'Refresh the accounts and roles you have access to',\n onclick: (e) =>\n busy(e.currentTarget, async () => {\n const report = await api(\n 'POST',\n `/api/sources/${encodeURIComponent(source.id)}/sync`,\n ).catch(fail);\n if (report) toast(syncMessage(source.id, report));\n await refresh();\n }),\n },\n 'Sync',\n ),\n h(\n 'button',\n {\n class: 'danger',\n onclick: (e) => {\n const count = aliases.length;\n const message = `Remove \"${source.id}\"${count ? ` and its ${count} alias(es)` : ''}?`;\n if (!confirm(message)) return;\n busy(e.currentTarget, async () => {\n await api('DELETE', `/api/sources/${encodeURIComponent(source.id)}`).catch(fail);\n await refresh();\n });\n },\n },\n 'Remove',\n ),\n ),\n );\n\n const prodCount = aliases.filter((a) => a.prod).length;\n const body =\n aliases.length === 0\n ? h(\n 'div',\n { class: 'empty' },\n 'No aliases yet. ',\n h(\n 'button',\n { class: 'primary', onclick: () => openAliasPicker(source) },\n 'Pick accounts',\n ),\n )\n : h(\n 'div',\n { class: 'source-summary' },\n h(\n 'span',\n {},\n `${aliases.length} alias(es)`,\n prodCount ? h('span', { class: 'prod-tag' }, `${prodCount} PROD`) : null,\n ),\n h(\n 'button',\n { class: 'link', onclick: () => go('aliases', { source: source.id }) },\n 'View aliases \u2192',\n ),\n );\n return h('section', { class: 'card' }, head, h('div', { class: 'card-body' }, body));\n}\n\nfunction syncMessage(id, report) {\n const parts = [`${report.targets.length} available`];\n if (report.added) parts.push(`${report.added} new`);\n if (report.removed) parts.push(`${report.removed} removed`);\n const orphaned = report.orphanedAliases.length\n ? ` No longer available: ${report.orphanedAliases.join(', ')}.`\n : '';\n return `${id}: ${parts.join(', ')}.${orphaned}`;\n}\n\nfunction aliasRow(alias) {\n const input = h('input', {\n value: alias.region ?? '',\n 'aria-label': `Region for ${alias.name}`,\n class: 'mono',\n spellcheck: 'false',\n });\n const save = h('button', { hidden: true }, 'Save');\n const commit = () =>\n busy(save, async () => {\n const region = input.value.trim();\n const done = await api('PATCH', `/api/aliases/${encodeURIComponent(alias.name)}`, {\n region,\n }).catch(fail);\n if (done) toast(`${alias.name} now runs in ${region}.`);\n await refresh();\n });\n input.addEventListener('input', () => {\n save.hidden = input.value.trim() === (alias.region ?? '');\n });\n input.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' && !save.hidden) commit();\n if (e.key === 'Escape') {\n input.value = alias.region ?? '';\n save.hidden = true;\n }\n });\n save.addEventListener('click', commit);\n\n const patch = (body, message) =>\n api('PATCH', `/api/aliases/${encodeURIComponent(alias.name)}`, body)\n .then(() => message && toast(message))\n .catch(fail)\n .then(refresh);\n const iconButton = (label, title, onclick, extra = {}) =>\n h('button', { class: 'link', title, 'aria-label': title, onclick, ...extra }, label);\n\n const actions = h(\n 'div',\n { class: 'row-actions' },\n h(\n 'button',\n {\n title: `Open the AWS console as ${alias.name}, already signed in`,\n disabled: alias.missing,\n onclick: (e) =>\n busy(e.currentTarget, async () => {\n const result = await api(\n 'POST',\n `/api/aliases/${encodeURIComponent(alias.name)}/console`,\n {},\n ).catch(fail);\n if (result) {\n toast(\n result.via === 'signed-in'\n ? `Opened the console for ${alias.name}.`\n : result.via === 'portal'\n ? `Opened ${alias.name} through the SSO portal.`\n : `Opened the console sign-in page for ${alias.name}.`,\n );\n }\n }),\n },\n 'Console',\n ),\n h(\n 'label',\n { class: 'prod-toggle', title: 'Production: hop and AI assistants ask before any change' },\n h('input', {\n type: 'checkbox',\n checked: alias.prod,\n 'aria-label': `${alias.name} is production`,\n onchange: (e) =>\n patch(\n { prod: e.currentTarget.checked },\n e.currentTarget.checked\n ? `${alias.name} is now production.`\n : `${alias.name} is no longer production.`,\n ),\n }),\n 'Prod',\n ),\n iconButton('\u270E', `Rename ${alias.name}`, () => {\n const to = prompt(`New name for \"${alias.name}\"`, alias.name)?.trim();\n if (to && to !== alias.name) patch({ name: to }, `${alias.name} is now ${to}.`);\n }),\n iconButton(\n '\u2715',\n `Remove ${alias.name}`,\n (e) => {\n if (!confirm(`Remove alias \"${alias.name}\"?`)) return;\n busy(e.currentTarget, async () => {\n await api('DELETE', `/api/aliases/${encodeURIComponent(alias.name)}`).catch(fail);\n await refresh();\n });\n },\n { class: 'link danger-hover' },\n ),\n );\n\n return h(\n 'tr',\n { class: alias.prod ? 'prod' : undefined },\n h(\n 'td',\n { class: 'alias', title: `hop ${alias.name} <command>` },\n alias.name,\n alias.prod ? h('span', { class: 'prod-tag' }, 'PROD') : null,\n ),\n h('td', { class: 'muted' }, alias.source),\n h('td', {}, h('div', { class: 'region' }, input, save)),\n h(\n 'td',\n {},\n alias.missing\n ? h('span', { class: 'missing' }, `${alias.target} (no longer available)`)\n : alias.label,\n ),\n h('td', { style: 'text-align:right' }, actions),\n );\n}\n\n// ---------- dialogs ----------\n\nfunction showDialog(title, body, footer) {\n $dialog.replaceChildren(\n h(\n 'div',\n { class: 'dlg-head' },\n h('h2', {}, title),\n h('button', { class: 'link', 'aria-label': 'Close', onclick: () => $dialog.close() }, '\u2715'),\n ),\n h('div', { class: 'dlg-body' }, body),\n footer ? h('div', { class: 'dlg-foot' }, footer) : null,\n );\n if (!$dialog.open) $dialog.showModal();\n}\n\nfunction suggestSourceId(settings, kind) {\n const host = /^https:\\/\\/([^./]+)\\./.exec(settings.startUrl ?? '')?.[1];\n return slug(host ?? (kind === 'console-login' ? 'personal' : 'main'));\n}\n\nconst slug = (text) =>\n text\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 63) || 'main';\n\n/** `previous` refills the form after a failed sign-in: { provider, kind, id, settings, error }. */\nfunction openAddSource(previous) {\n const providers = state.providers.filter((p) => state.enabledProviders.includes(p.id));\n let provider = providers.find((p) => p.id === previous?.provider) ?? providers[0];\n let kind = provider.kinds.find((k) => k.kind === previous?.kind) ?? provider.kinds[0];\n\n const form = h('form', { onsubmit: (e) => e.preventDefault() });\n const nameInput = h('input', {\n id: 'f-name',\n required: true,\n class: 'mono',\n spellcheck: 'false',\n });\n let nameTouched = Boolean(previous);\n nameInput.addEventListener('input', () => {\n nameTouched = true;\n });\n\n const fieldInputs = {};\n const fieldsBox = h('div');\n const nameLabel = h('label', { for: 'f-name' });\n\n function updateSuggestion() {\n if (nameTouched) return;\n const settings = Object.fromEntries(\n Object.entries(fieldInputs).map(([k, i]) => [k, i.value.trim()]),\n );\n const taken = new Set(state.sources.map((s) => s.id));\n let id = suggestSourceId(settings, kind.kind);\n for (let i = 2; taken.has(id); i++) id = `${suggestSourceId(settings, kind.kind)}-${i}`;\n nameInput.value = id;\n }\n\n function renderFields() {\n for (const key of Object.keys(fieldInputs)) delete fieldInputs[key];\n fieldsBox.replaceChildren(\n ...kind.fields.map((f) => {\n const input = h('input', {\n id: `f-${f.key}`,\n placeholder: f.placeholder ?? f.defaultValue ?? '',\n value: previous?.settings?.[f.key] ?? f.defaultValue ?? '',\n required: true,\n class: 'mono',\n spellcheck: 'false',\n oninput: updateSuggestion,\n });\n fieldInputs[f.key] = input;\n return h('div', { class: 'field' }, h('label', { for: input.id }, f.label), input);\n }),\n );\n nameLabel.textContent = kind.nameLabel ?? 'Short name for this source';\n updateSuggestion();\n }\n\n const providerSelect =\n providers.length > 1\n ? h(\n 'div',\n { class: 'field' },\n h('label', { for: 'f-provider' }, 'Cloud'),\n h(\n 'select',\n {\n id: 'f-provider',\n onchange: (e) => {\n provider = providers.find((p) => p.id === e.target.value);\n kind = provider.kinds[0];\n renderKinds();\n },\n },\n providers.map((p) => h('option', { value: p.id, selected: p === provider }, p.name)),\n ),\n )\n : null;\n\n const kindsBox = h('div', { class: 'choices' });\n function renderKinds() {\n kindsBox.replaceChildren(\n ...provider.kinds.map((k) =>\n h(\n 'label',\n { class: 'choice' },\n h('input', {\n type: 'radio',\n name: 'kind',\n value: k.kind,\n checked: k === kind,\n onchange: () => {\n kind = k;\n renderFields();\n },\n }),\n h('span', {}, h('strong', {}, k.label), k.hint ? h('small', {}, k.hint) : null),\n ),\n ),\n );\n renderFields();\n }\n\n form.append(\n previous?.error\n ? h(\n 'div',\n { class: 'notice error' },\n h('strong', {}, 'Could not sign in, so nothing was saved. '),\n previous.error.message,\n previous.error.hint ? h('div', { class: 'help' }, previous.error.hint) : null,\n )\n : '',\n providerSelect ?? '',\n h('div', { class: 'field' }, h('label', {}, 'What do you want to add?')),\n kindsBox,\n fieldsBox,\n h(\n 'div',\n { class: 'field' },\n nameLabel,\n nameInput,\n h(\n 'span',\n { class: 'help' },\n 'Lowercase letters, numbers and dashes. You will see it in ',\n h('code', {}, 'cloudhop ls'),\n '.',\n ),\n ),\n );\n renderKinds();\n if (previous) nameInput.value = previous.id;\n\n const submit = h('button', { class: 'primary', type: 'submit' }, 'Add and sign in');\n form.addEventListener('submit', async () => {\n if (!form.reportValidity()) return;\n const body = {\n provider: provider.id,\n kind: kind.kind,\n id: nameInput.value.trim(),\n settings: Object.fromEntries(\n Object.entries(fieldInputs).map(([k, i]) => [k, i.value.trim()]),\n ),\n };\n showDialog(\n 'Sign in',\n h(\n 'div',\n { class: 'waiting' },\n h('div', { class: 'spinner' }),\n h('strong', {}, 'Finish signing in in the browser tab that just opened.'),\n h('p', { class: 'help' }, 'This page continues on its own once you approve the request.'),\n ),\n );\n try {\n const report = await api('POST', '/api/sources', body);\n await refresh();\n const source = state.sources.find((s) => s.id === body.id);\n toast(`Signed in to ${body.id}. ${report.targets.length} account/role(s) found.`);\n openAliasPicker(source, report.targets);\n } catch (error) {\n // Nothing was saved: reopen the form with what was typed so it can be fixed.\n await refresh();\n openAddSource({ ...body, error });\n }\n });\n\n showDialog('Add source', form, [\n h('button', { type: 'button', onclick: () => $dialog.close() }, 'Cancel'),\n submit,\n ]);\n submit.setAttribute('form', 'add-form');\n form.id = 'add-form';\n}\n\nasync function openAliasPicker(source, preloaded) {\n let targets = preloaded;\n if (!targets) {\n showDialog(\n `Aliases for ${source.id}`,\n h('div', { class: 'waiting' }, h('div', { class: 'spinner' }), 'Loading accounts\u2026'),\n );\n try {\n targets = (await api('GET', `/api/sources/${encodeURIComponent(source.id)}/targets`)).targets;\n } catch (error) {\n $dialog.close();\n return fail(error);\n }\n }\n\n const used = new Map(\n state.aliases.filter((a) => a.source === source.id).map((a) => [a.target, a.name]),\n );\n const taken = new Set(state.aliases.map((a) => a.name));\n const defaultRegion = source.settings.region ?? '';\n const filter = h('input', {\n type: 'search',\n placeholder: `Filter ${targets.length} account/role(s)\u2026`,\n style: 'width:100%;margin-bottom:12px',\n });\n\n const rows = targets.map((t) => {\n const existing = used.get(t.id);\n let name = t.suggestedAlias;\n for (let i = 2; taken.has(name); i++) name = `${t.suggestedAlias}-${i}`;\n const check = h('input', {\n type: 'checkbox',\n disabled: Boolean(existing),\n 'aria-label': t.label,\n });\n const aliasInput = h('input', {\n value: name,\n class: 'mono',\n spellcheck: 'false',\n 'aria-label': `Alias for ${t.label}`,\n placeholder: 'alias',\n });\n const regionInput = h('input', {\n value: defaultRegion,\n class: 'mono',\n spellcheck: 'false',\n 'aria-label': `Region for ${t.label}`,\n placeholder: 'region',\n });\n // Only the checkbox and its text sit in the label, so clicking the inputs never toggles it.\n const el = h(\n 'div',\n { class: 'target' },\n h(\n 'label',\n { class: 'pick' },\n check,\n h(\n 'span',\n {},\n t.label,\n existing ? h('small', { class: 'already' }, `Already: ${existing}`) : null,\n ),\n ),\n h('div', { class: 'inputs' }, aliasInput, regionInput),\n );\n return { target: t, el, check, aliasInput, regionInput };\n });\n\n filter.addEventListener('input', () => {\n const q = filter.value.trim().toLowerCase();\n for (const row of rows) row.el.hidden = q !== '' && !row.target.label.toLowerCase().includes(q);\n });\n\n const save = h('button', { class: 'primary' }, 'Create aliases');\n save.addEventListener('click', () =>\n busy(save, async () => {\n const aliases = rows\n .filter((r) => r.check.checked)\n .map((r) => ({\n source: source.id,\n target: r.target.id,\n name: r.aliasInput.value.trim(),\n region: r.regionInput.value.trim(),\n }));\n if (aliases.length === 0) return toast('Pick at least one account/role.', { error: true });\n try {\n const result = await api('POST', '/api/aliases', { aliases });\n $dialog.close();\n toast(`Created ${result.created.join(', ')}. Try: hop ${result.created[0]}`);\n await refresh();\n } catch (error) {\n fail(error);\n }\n }),\n );\n\n const body =\n targets.length === 0\n ? h('p', { class: 'empty' }, 'This source has no accounts or roles assigned to you.')\n : h(\n 'div',\n {},\n filter,\n h(\n 'div',\n { class: 'targets' },\n rows.map((r) => r.el),\n ),\n );\n showDialog(`Aliases for ${source.id}`, body, [\n h('span', { class: 'note' }, 'Check an account/role, then name its alias and region.'),\n h('button', { onclick: () => $dialog.close() }, 'Cancel'),\n targets.length ? save : null,\n ]);\n}\n\n$addSource.addEventListener('click', () => openAddSource());\n\nif (!token) {\n $main.replaceChildren(\n h(\n 'div',\n { class: 'hero' },\n h('h2', {}, 'Open the link from your terminal'),\n h('p', {}, 'Run cloudhop ui and open the link it prints. It includes a one-time access key.'),\n ),\n );\n} else {\n readRoute();\n refresh();\n}\n";
|
|
13446
14190
|
|
|
13447
14191
|
// ../ui/src/assets/index.html
|
|
13448
14192
|
var assets_default = `<!doctype html>
|
|
@@ -13843,18 +14587,18 @@ async function startUiServer({
|
|
|
13843
14587
|
sendJson(res, 400, { error: error.message });
|
|
13844
14588
|
}
|
|
13845
14589
|
});
|
|
13846
|
-
await new Promise((
|
|
14590
|
+
await new Promise((resolve3, reject) => {
|
|
13847
14591
|
server.once("error", reject);
|
|
13848
|
-
server.listen(port, "127.0.0.1",
|
|
14592
|
+
server.listen(port, "127.0.0.1", resolve3);
|
|
13849
14593
|
});
|
|
13850
14594
|
const actualPort = server.address().port;
|
|
13851
14595
|
allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${actualPort}`, `localhost:${actualPort}`]);
|
|
13852
14596
|
return {
|
|
13853
14597
|
// The token travels in the fragment, so it never reaches server logs or Referer headers.
|
|
13854
14598
|
url: `http://127.0.0.1:${actualPort}/#token=${token}`,
|
|
13855
|
-
close: () => new Promise((
|
|
14599
|
+
close: () => new Promise((resolve3) => {
|
|
13856
14600
|
server.closeAllConnections();
|
|
13857
|
-
server.close(() =>
|
|
14601
|
+
server.close(() => resolve3());
|
|
13858
14602
|
})
|
|
13859
14603
|
};
|
|
13860
14604
|
}
|
|
@@ -13870,23 +14614,23 @@ async function uiCommand(app, assistants, { flags }) {
|
|
|
13870
14614
|
console.log("Only this computer can open it, and the link works only while this runs.");
|
|
13871
14615
|
console.log("Press Ctrl-C to stop.");
|
|
13872
14616
|
if (!flags.noOpen) await new NodeBrowserOpener().open(server.url);
|
|
13873
|
-
await new Promise((
|
|
13874
|
-
process.once("SIGINT",
|
|
13875
|
-
process.once("SIGTERM",
|
|
14617
|
+
await new Promise((resolve3) => {
|
|
14618
|
+
process.once("SIGINT", resolve3);
|
|
14619
|
+
process.once("SIGTERM", resolve3);
|
|
13876
14620
|
});
|
|
13877
14621
|
await server.close();
|
|
13878
14622
|
}
|
|
13879
14623
|
|
|
13880
14624
|
// src/container.ts
|
|
13881
|
-
import { homedir as
|
|
13882
|
-
import { join as
|
|
14625
|
+
import { homedir as homedir5 } from "os";
|
|
14626
|
+
import { join as join10 } from "path";
|
|
13883
14627
|
|
|
13884
14628
|
// ../provider-aws/src/aws-provider.ts
|
|
13885
|
-
import { join as
|
|
14629
|
+
import { join as join9 } from "path";
|
|
13886
14630
|
|
|
13887
14631
|
// ../provider-aws/src/aws-config.ts
|
|
13888
|
-
import { mkdir as
|
|
13889
|
-
import { dirname as
|
|
14632
|
+
import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile7 } from "fs/promises";
|
|
14633
|
+
import { dirname as dirname6 } from "path";
|
|
13890
14634
|
var BLOCK_START = "# >>> cloudhop managed >>> (edit with `cloudhop`, not by hand)";
|
|
13891
14635
|
var BLOCK_END = "# <<< cloudhop managed <<<";
|
|
13892
14636
|
function renderBlock(sections) {
|
|
@@ -13933,13 +14677,13 @@ function replaceManagedBlock(content, sections) {
|
|
|
13933
14677
|
async function writeManagedBlock(path2, sections) {
|
|
13934
14678
|
let content = "";
|
|
13935
14679
|
try {
|
|
13936
|
-
content = await
|
|
14680
|
+
content = await readFile6(path2, "utf8");
|
|
13937
14681
|
} catch {
|
|
13938
14682
|
}
|
|
13939
14683
|
const next = replaceManagedBlock(content, sections);
|
|
13940
14684
|
if (next === content) return;
|
|
13941
|
-
await
|
|
13942
|
-
await
|
|
14685
|
+
await mkdir5(dirname6(path2), { recursive: true, mode: 448 });
|
|
14686
|
+
await writeFile7(path2, next, { mode: 384 });
|
|
13943
14687
|
}
|
|
13944
14688
|
|
|
13945
14689
|
// ../provider-aws/src/console.ts
|
|
@@ -13964,17 +14708,17 @@ function consoleDestination(region, service) {
|
|
|
13964
14708
|
async function federatedConsoleUrl(credentials, destination, region, fetchFn) {
|
|
13965
14709
|
const {
|
|
13966
14710
|
AWS_ACCESS_KEY_ID: id,
|
|
13967
|
-
AWS_SECRET_ACCESS_KEY:
|
|
14711
|
+
AWS_SECRET_ACCESS_KEY: key2,
|
|
13968
14712
|
AWS_SESSION_TOKEN: token
|
|
13969
14713
|
} = credentials;
|
|
13970
|
-
if (!id || !
|
|
14714
|
+
if (!id || !key2 || !token) return void 0;
|
|
13971
14715
|
const { signin } = partitionHosts(region);
|
|
13972
14716
|
const response = await fetchFn(`${signin}/federation`, {
|
|
13973
14717
|
method: "POST",
|
|
13974
14718
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
13975
14719
|
body: new URLSearchParams({
|
|
13976
14720
|
Action: "getSigninToken",
|
|
13977
|
-
Session: JSON.stringify({ sessionId: id, sessionKey:
|
|
14721
|
+
Session: JSON.stringify({ sessionId: id, sessionKey: key2, sessionToken: token })
|
|
13978
14722
|
}).toString()
|
|
13979
14723
|
}).catch(() => void 0);
|
|
13980
14724
|
if (!response?.ok) return void 0;
|
|
@@ -13990,7 +14734,9 @@ async function federatedConsoleUrl(credentials, destination, region, fetchFn) {
|
|
|
13990
14734
|
}
|
|
13991
14735
|
function portalShortcutUrl(startUrl, accountId, roleName, destination) {
|
|
13992
14736
|
const params = new URLSearchParams({ account_id: accountId, role_name: roleName, destination });
|
|
13993
|
-
|
|
14737
|
+
let base = startUrl;
|
|
14738
|
+
while (base.endsWith("/")) base = base.slice(0, -1);
|
|
14739
|
+
return `${base}/#/console?${params}`;
|
|
13994
14740
|
}
|
|
13995
14741
|
|
|
13996
14742
|
// ../provider-aws/src/http.ts
|
|
@@ -14055,7 +14801,7 @@ var PortalClient = class {
|
|
|
14055
14801
|
);
|
|
14056
14802
|
return body.roleCredentials;
|
|
14057
14803
|
}
|
|
14058
|
-
async paginate(path2, params,
|
|
14804
|
+
async paginate(path2, params, key2) {
|
|
14059
14805
|
const items = [];
|
|
14060
14806
|
let nextToken;
|
|
14061
14807
|
do {
|
|
@@ -14066,7 +14812,7 @@ var PortalClient = class {
|
|
|
14066
14812
|
`${this.endpoint}${path2}?${query}`,
|
|
14067
14813
|
this.headers()
|
|
14068
14814
|
);
|
|
14069
|
-
items.push(...page[
|
|
14815
|
+
items.push(...page[key2] ?? []);
|
|
14070
14816
|
nextToken = page.nextToken;
|
|
14071
14817
|
} while (nextToken);
|
|
14072
14818
|
return items;
|
|
@@ -14077,7 +14823,7 @@ var PortalClient = class {
|
|
|
14077
14823
|
};
|
|
14078
14824
|
|
|
14079
14825
|
// ../provider-aws/src/sso/sso-login.ts
|
|
14080
|
-
import { createHash as
|
|
14826
|
+
import { createHash as createHash4, randomBytes as randomBytes2 } from "crypto";
|
|
14081
14827
|
|
|
14082
14828
|
// ../provider-aws/src/sso/loopback.ts
|
|
14083
14829
|
import { createServer as createServer2 } from "http";
|
|
@@ -14088,8 +14834,8 @@ var PAGE = (title, body) => `<!doctype html><meta charset="utf-8"><title>cloudho
|
|
|
14088
14834
|
async function startLoopbackServer() {
|
|
14089
14835
|
let resolveRequest = () => {
|
|
14090
14836
|
};
|
|
14091
|
-
const request = new Promise((
|
|
14092
|
-
resolveRequest =
|
|
14837
|
+
const request = new Promise((resolve3) => {
|
|
14838
|
+
resolveRequest = resolve3;
|
|
14093
14839
|
});
|
|
14094
14840
|
const server = createServer2((req, res) => {
|
|
14095
14841
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
@@ -14104,7 +14850,7 @@ async function startLoopbackServer() {
|
|
|
14104
14850
|
);
|
|
14105
14851
|
resolveRequest(url);
|
|
14106
14852
|
});
|
|
14107
|
-
await new Promise((
|
|
14853
|
+
await new Promise((resolve3) => server.listen(0, "127.0.0.1", resolve3));
|
|
14108
14854
|
const { port } = server.address();
|
|
14109
14855
|
return {
|
|
14110
14856
|
redirectUri: `http://127.0.0.1:${port}${CALLBACK_PATH}`,
|
|
@@ -14210,24 +14956,24 @@ var OidcClient = class {
|
|
|
14210
14956
|
};
|
|
14211
14957
|
|
|
14212
14958
|
// ../provider-aws/src/sso/token-cache.ts
|
|
14213
|
-
import { createHash as
|
|
14214
|
-
import { mkdir as
|
|
14215
|
-
import { join as
|
|
14959
|
+
import { createHash as createHash3 } from "crypto";
|
|
14960
|
+
import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
|
|
14961
|
+
import { join as join8 } from "path";
|
|
14216
14962
|
var EXPIRY_MARGIN_MS = 5 * 60 * 1e3;
|
|
14217
14963
|
function tokenCachePath(awsDir, sessionName) {
|
|
14218
|
-
const hash =
|
|
14219
|
-
return
|
|
14964
|
+
const hash = createHash3("sha1").update(sessionName).digest("hex");
|
|
14965
|
+
return join8(awsDir, "sso", "cache", `${hash}.json`);
|
|
14220
14966
|
}
|
|
14221
14967
|
async function readToken(awsDir, sessionName) {
|
|
14222
14968
|
try {
|
|
14223
|
-
return JSON.parse(await
|
|
14969
|
+
return JSON.parse(await readFile7(tokenCachePath(awsDir, sessionName), "utf8"));
|
|
14224
14970
|
} catch {
|
|
14225
14971
|
return void 0;
|
|
14226
14972
|
}
|
|
14227
14973
|
}
|
|
14228
14974
|
async function writeToken(awsDir, sessionName, token) {
|
|
14229
|
-
await
|
|
14230
|
-
await
|
|
14975
|
+
await mkdir6(join8(awsDir, "sso", "cache"), { recursive: true, mode: 448 });
|
|
14976
|
+
await writeFile8(tokenCachePath(awsDir, sessionName), JSON.stringify(token, null, 2), {
|
|
14231
14977
|
mode: 384
|
|
14232
14978
|
});
|
|
14233
14979
|
}
|
|
@@ -14270,7 +15016,7 @@ async function authorizationCodeLogin(options) {
|
|
|
14270
15016
|
clientId: client.clientId,
|
|
14271
15017
|
redirectUri: server.redirectUri,
|
|
14272
15018
|
state,
|
|
14273
|
-
codeChallenge: base64url(
|
|
15019
|
+
codeChallenge: base64url(createHash4("sha256").update(codeVerifier).digest())
|
|
14274
15020
|
});
|
|
14275
15021
|
options.interaction.notify("Opening your browser to log in\u2026");
|
|
14276
15022
|
options.interaction.notify(`If it doesn't open, visit: ${url}`);
|
|
@@ -14296,7 +15042,7 @@ async function deviceCodeLogin(options) {
|
|
|
14296
15042
|
let intervalMs = (device.interval ?? 5) * 1e3;
|
|
14297
15043
|
const deadline = Date.now() + device.expiresIn * 1e3;
|
|
14298
15044
|
while (Date.now() < deadline) {
|
|
14299
|
-
await new Promise((
|
|
15045
|
+
await new Promise((resolve3) => setTimeout(resolve3, intervalMs));
|
|
14300
15046
|
try {
|
|
14301
15047
|
return toCachedToken(
|
|
14302
15048
|
options,
|
|
@@ -14412,6 +15158,7 @@ var AwsProvider = class {
|
|
|
14412
15158
|
displayName = "AWS";
|
|
14413
15159
|
cli = { command: "aws", displayName: "AWS CLI v2", minVersion: "2.32.0" };
|
|
14414
15160
|
identityArgs = ["sts", "get-caller-identity"];
|
|
15161
|
+
shellVars = [...CREDENTIAL_VARS, "AWS_PROFILE", "AWS_REGION", "AWS_DEFAULT_REGION"];
|
|
14415
15162
|
sourceKinds = [
|
|
14416
15163
|
{
|
|
14417
15164
|
kind: SSO_KIND,
|
|
@@ -14520,7 +15267,7 @@ var AwsProvider = class {
|
|
|
14520
15267
|
}
|
|
14521
15268
|
});
|
|
14522
15269
|
}
|
|
14523
|
-
await writeManagedBlock(
|
|
15270
|
+
await writeManagedBlock(join9(this.options.awsDir, "config"), sections);
|
|
14524
15271
|
}
|
|
14525
15272
|
async processEnv(resolved, mode) {
|
|
14526
15273
|
const region = resolved.region;
|
|
@@ -14594,9 +15341,9 @@ var AwsProvider = class {
|
|
|
14594
15341
|
if (!token) throw new CloudhopError("LOGIN_FAILED", `Not logged in to ${source.id}.`);
|
|
14595
15342
|
return new PortalClient(this.setting(source, "region"), token.accessToken, this.fetchFn);
|
|
14596
15343
|
}
|
|
14597
|
-
setting(source,
|
|
14598
|
-
const value = source.settings[
|
|
14599
|
-
if (!value) throw new CloudhopError("SOURCE_NOT_FOUND", `Source "${source.id}" has no ${
|
|
15344
|
+
setting(source, key2) {
|
|
15345
|
+
const value = source.settings[key2];
|
|
15346
|
+
if (!value) throw new CloudhopError("SOURCE_NOT_FOUND", `Source "${source.id}" has no ${key2}.`);
|
|
14600
15347
|
return value;
|
|
14601
15348
|
}
|
|
14602
15349
|
};
|
|
@@ -14605,7 +15352,7 @@ var AwsProvider = class {
|
|
|
14605
15352
|
var browser = new NodeBrowserOpener();
|
|
14606
15353
|
function createApp(env = process.env) {
|
|
14607
15354
|
const runner = new NodeProcessRunner();
|
|
14608
|
-
const awsDir = env.CLOUDHOP_AWS_DIR ??
|
|
15355
|
+
const awsDir = env.CLOUDHOP_AWS_DIR ?? join10(homedir5(), ".aws");
|
|
14609
15356
|
return new CloudhopApp({
|
|
14610
15357
|
config: new YamlConfigStore(),
|
|
14611
15358
|
providers: [
|
|
@@ -14624,33 +15371,39 @@ function createApp(env = process.env) {
|
|
|
14624
15371
|
}
|
|
14625
15372
|
function createAssistants(version, env = process.env) {
|
|
14626
15373
|
return new AssistantsInstaller({
|
|
14627
|
-
places: { home:
|
|
15374
|
+
places: { home: homedir5(), project: process.cwd() },
|
|
14628
15375
|
configDir: defaultConfigDir(env),
|
|
14629
15376
|
runner: new NodeProcessRunner(),
|
|
14630
15377
|
cloudhopVersion: version
|
|
14631
15378
|
});
|
|
14632
15379
|
}
|
|
15380
|
+
function createTrustedFolders(env = process.env) {
|
|
15381
|
+
return new TrustedFolders(defaultConfigDir(env));
|
|
15382
|
+
}
|
|
14633
15383
|
|
|
14634
15384
|
// src/update-check.ts
|
|
14635
|
-
import { mkdir as
|
|
14636
|
-
import { join as
|
|
15385
|
+
import { mkdir as mkdir7, readFile as readFile8, writeFile as writeFile9 } from "fs/promises";
|
|
15386
|
+
import { join as join11 } from "path";
|
|
14637
15387
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
14638
15388
|
var REGISTRY_URL = "https://registry.npmjs.org/cloudhop/latest";
|
|
15389
|
+
var VERSION = /^\d{1,6}\.\d{1,6}\.\d{1,6}(?:-[0-9A-Za-z.]{1,40})?$/;
|
|
15390
|
+
var validVersion = (value) => typeof value === "string" && VERSION.test(value) ? value : void 0;
|
|
14639
15391
|
function startUpdateCheck(configDir, current) {
|
|
14640
15392
|
const env = process.env;
|
|
14641
15393
|
if (env.CLOUDHOP_NO_UPDATE_CHECK === "1" || env.CI || !process.stderr.isTTY)
|
|
14642
15394
|
return async () => {
|
|
14643
15395
|
};
|
|
14644
|
-
const file =
|
|
15396
|
+
const file = join11(configDir, "update-check.json");
|
|
14645
15397
|
const work = (async () => {
|
|
14646
15398
|
let cache = { checkedAt: 0 };
|
|
14647
15399
|
try {
|
|
14648
|
-
cache = JSON.parse(await
|
|
15400
|
+
cache = JSON.parse(await readFile8(file, "utf8"));
|
|
14649
15401
|
} catch {
|
|
14650
15402
|
}
|
|
14651
|
-
|
|
15403
|
+
const cached = validVersion(cache.latest);
|
|
15404
|
+
if (cached && compareVersions(cached, current) > 0) {
|
|
14652
15405
|
process.stderr.write(
|
|
14653
|
-
`\x1B[2mcloudhop \u203A\x1B[22m cloudhop ${
|
|
15406
|
+
`\x1B[2mcloudhop \u203A\x1B[22m cloudhop ${cached} is available (you have ${current}): npm i -g cloudhop
|
|
14654
15407
|
`
|
|
14655
15408
|
);
|
|
14656
15409
|
}
|
|
@@ -14660,19 +15413,24 @@ function startUpdateCheck(configDir, current) {
|
|
|
14660
15413
|
signal: AbortSignal.timeout(1500)
|
|
14661
15414
|
});
|
|
14662
15415
|
if (!response.ok) return;
|
|
14663
|
-
const
|
|
14664
|
-
if (
|
|
14665
|
-
await
|
|
14666
|
-
await
|
|
15416
|
+
const latest = validVersion((await response.json()).version);
|
|
15417
|
+
if (!latest) return;
|
|
15418
|
+
await mkdir7(configDir, { recursive: true });
|
|
15419
|
+
await writeFile9(file, JSON.stringify({ checkedAt: Date.now(), latest }));
|
|
14667
15420
|
})().catch(() => {
|
|
14668
15421
|
});
|
|
14669
|
-
return () => Promise.race([work, new Promise((
|
|
15422
|
+
return () => Promise.race([work, new Promise((resolve3) => setTimeout(resolve3, 300))]);
|
|
14670
15423
|
}
|
|
14671
15424
|
|
|
14672
15425
|
// src/cli.ts
|
|
14673
|
-
var HELP = `cloudhop ${"1.
|
|
15426
|
+
var HELP = `cloudhop ${"1.3.0"}
|
|
14674
15427
|
Hop between cloud accounts, roles and regions with one command.
|
|
14675
15428
|
|
|
15429
|
+
Quick start
|
|
15430
|
+
cloudhop add Sign in to an SSO portal or personal account, name aliases
|
|
15431
|
+
cloudhop shell Set up your terminal (once)
|
|
15432
|
+
cloudhop use <alias> Switch this terminal to an alias, then type plain aws ...
|
|
15433
|
+
|
|
14676
15434
|
Usage
|
|
14677
15435
|
hop <alias> [args...] Run the cloud CLI as <alias> (no args: who am I)
|
|
14678
15436
|
hop Pick the alias from a list, then type the command
|
|
@@ -14681,8 +15439,12 @@ Usage
|
|
|
14681
15439
|
Open the web console as <alias>, already signed in
|
|
14682
15440
|
cloudhop each <selection...> [--source <s>] [--prod|--no-prod] [--yes] [--json] -- <args...>
|
|
14683
15441
|
Run one command in many aliases (names, a,b lists, 'globex-*', /regex/)
|
|
14684
|
-
cloudhop use [alias]
|
|
14685
|
-
|
|
15442
|
+
cloudhop use [alias] [--save] Use an alias in this terminal: then plain aws, terraform... run as it
|
|
15443
|
+
(--save: remember it for this folder in .cloudhoprc)
|
|
15444
|
+
cloudhop use --off [cloud] Stop using the alias of every cloud, or just one (aws)
|
|
15445
|
+
cloudhop allow | deny [folder] Trust (or stop trusting) a folder's .cloudhoprc
|
|
15446
|
+
cloudhop shell Set up your terminal once: cloudhop use, .cloudhoprc on cd, completion
|
|
15447
|
+
cloudhop shell starship Prompt segments for Starship (alias in use, PROD in red)
|
|
14686
15448
|
cloudhop exec <alias> [--region <r>] -- <cmd...>
|
|
14687
15449
|
Run any tool with the alias' temporary credentials
|
|
14688
15450
|
|
|
@@ -14750,7 +15512,7 @@ async function main(argv) {
|
|
|
14750
15512
|
return 0;
|
|
14751
15513
|
}
|
|
14752
15514
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
14753
|
-
console.log("1.
|
|
15515
|
+
console.log("1.3.0");
|
|
14754
15516
|
return 0;
|
|
14755
15517
|
}
|
|
14756
15518
|
const app = createApp();
|
|
@@ -14760,16 +15522,24 @@ async function main(argv) {
|
|
|
14760
15522
|
return 0;
|
|
14761
15523
|
}
|
|
14762
15524
|
if (command === "shell") {
|
|
14763
|
-
|
|
15525
|
+
const clis = app.availableProviders().map((p3) => ({ command: p3.cli.command, provider: p3.id }));
|
|
15526
|
+
await shellCommand(local, clis);
|
|
14764
15527
|
return 0;
|
|
14765
15528
|
}
|
|
15529
|
+
if (command === "use" && local.some((a2) => a2 === "--emit" || a2.startsWith("--emit="))) {
|
|
15530
|
+
return useCommand(app, createTrustedFolders(), local);
|
|
15531
|
+
}
|
|
14766
15532
|
if (command === "mcp") {
|
|
14767
|
-
await serveMcp({
|
|
15533
|
+
await serveMcp({
|
|
15534
|
+
app,
|
|
15535
|
+
version: "1.3.0",
|
|
15536
|
+
context: () => shellContext(createTrustedFolders())
|
|
15537
|
+
});
|
|
14768
15538
|
return 0;
|
|
14769
15539
|
}
|
|
14770
|
-
const assistants = createAssistants("1.
|
|
15540
|
+
const assistants = createAssistants("1.3.0");
|
|
14771
15541
|
await refreshAssistantSkills(assistants, command);
|
|
14772
|
-
const finishUpdateCheck = startUpdateCheck(defaultConfigDir(), "1.
|
|
15542
|
+
const finishUpdateCheck = startUpdateCheck(defaultConfigDir(), "1.3.0");
|
|
14773
15543
|
try {
|
|
14774
15544
|
return await dispatch(app, assistants, command, args, local);
|
|
14775
15545
|
} finally {
|
|
@@ -14818,7 +15588,7 @@ async function dispatch(app, assistants, command, args, local) {
|
|
|
14818
15588
|
return 0;
|
|
14819
15589
|
case "ls":
|
|
14820
15590
|
case "list":
|
|
14821
|
-
await listCommand(app, local);
|
|
15591
|
+
await listCommand(app, local, () => shellContext(createTrustedFolders()));
|
|
14822
15592
|
return 0;
|
|
14823
15593
|
case "rm":
|
|
14824
15594
|
case "remove":
|
|
@@ -14839,7 +15609,10 @@ async function dispatch(app, assistants, command, args, local) {
|
|
|
14839
15609
|
await teamCommand(app, local);
|
|
14840
15610
|
return 0;
|
|
14841
15611
|
case "use":
|
|
14842
|
-
|
|
15612
|
+
return useCommand(app, createTrustedFolders(), local);
|
|
15613
|
+
case "allow":
|
|
15614
|
+
case "deny":
|
|
15615
|
+
await allowCommand(app, createTrustedFolders(), local, command === "allow");
|
|
14843
15616
|
return 0;
|
|
14844
15617
|
case "each":
|
|
14845
15618
|
return eachCommand(app, args);
|