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