smart-home-engine 1.42.0 → 1.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/web/assets/index-Crx1MbU9.js +290 -0
- package/dist/web/assets/index-dFdInRyC.css +1 -0
- package/dist/web/assets/{tsMode-C0zJ2TUA.js → tsMode-B4v7bdgd.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/service/she-servicectl +128 -1
- package/src/lib/services-host.js +1 -1
- package/src/web/config-api.js +47 -0
- package/src/web/services-api.js +55 -0
- package/dist/web/assets/index-55TivCbD.css +0 -1
- package/dist/web/assets/index-fKlQBrQH.js +0 -287
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{m as O}from"./monaco-langs-DKJI_o9J.js";import{t as I}from"./index-
|
|
1
|
+
import{m as O}from"./monaco-langs-DKJI_o9J.js";import{t as I}from"./index-Crx1MbU9.js";/*!-----------------------------------------------------------------------------
|
|
2
2
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
3
|
* Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
|
|
4
4
|
* Released under the MIT license
|
package/dist/web/index.html
CHANGED
|
@@ -172,10 +172,10 @@
|
|
|
172
172
|
}
|
|
173
173
|
})();
|
|
174
174
|
</script>
|
|
175
|
-
<script type="module" crossorigin src="/assets/index-
|
|
175
|
+
<script type="module" crossorigin src="/assets/index-Crx1MbU9.js"></script>
|
|
176
176
|
<link rel="modulepreload" crossorigin href="/assets/monaco-langs-DKJI_o9J.js">
|
|
177
177
|
<link rel="stylesheet" crossorigin href="/assets/monaco-langs-DyX1CsEw.css">
|
|
178
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
178
|
+
<link rel="stylesheet" crossorigin href="/assets/index-dFdInRyC.css">
|
|
179
179
|
</head>
|
|
180
180
|
<body>
|
|
181
181
|
<div id="app"></div>
|
package/package.json
CHANGED
package/service/she-servicectl
CHANGED
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
# she-servicectl files <adapter> <instance> JSON listing of /etc/<adapter>/ and /var/lib/<adapter>/<instance>/
|
|
32
32
|
# she-servicectl file <adapter> <instance> read|write <path> a file inside those two directories (write: content on stdin)
|
|
33
33
|
# she-servicectl asset <adapter> <relpath> a file shipped in the adapter package (example, schema)
|
|
34
|
+
# she-servicectl node update [--lts] install/refresh tj/n (downloaded, no npm), then n stable (or n lts) — JSON with the version before and after
|
|
35
|
+
# she-servicectl restart-all restart every running instance on this host (after a node update, say)
|
|
34
36
|
# she-servicectl self-update replace this script with the one on stdin (she ships it)
|
|
35
37
|
# she-servicectl remove-key drop the public key on stdin from the calling user's authorized_keys
|
|
36
38
|
# she-servicectl teardown [--force] remove she from this host: that key, the sudoers rule, this script,
|
|
@@ -43,10 +45,17 @@
|
|
|
43
45
|
|
|
44
46
|
set -eu
|
|
45
47
|
|
|
46
|
-
VERSION=
|
|
48
|
+
VERSION=13
|
|
47
49
|
BROKER_ENV=/etc/mqtt-interfaces/broker.env
|
|
48
50
|
UNIT_DIR=/etc/systemd/system
|
|
49
51
|
|
|
52
|
+
# tj/n, pinned: n is a self-contained bash script, so it is installed by downloading that one
|
|
53
|
+
# file — not with `npm install -g n`, which needs a node that may not be there yet and would
|
|
54
|
+
# drop n into whatever prefix the current node uses (where the node n installs then shadows it).
|
|
55
|
+
N_VERSION=10.2.0
|
|
56
|
+
N_BIN=/usr/local/bin/n
|
|
57
|
+
N_URL=https://raw.githubusercontent.com/tj/n/v$N_VERSION/bin/n
|
|
58
|
+
|
|
50
59
|
die() { printf 'she-servicectl: %s\n' "$*" >&2; exit 2; }
|
|
51
60
|
|
|
52
61
|
# ── validation ──────────────────────────────────────────────────────────────────
|
|
@@ -104,6 +113,11 @@ need_any() {
|
|
|
104
113
|
|
|
105
114
|
# ── helpers ─────────────────────────────────────────────────────────────────────
|
|
106
115
|
|
|
116
|
+
# a JSON string, or null when the value is empty
|
|
117
|
+
json_str() {
|
|
118
|
+
[ -n "$1" ] && printf '"%s"' "$(json_escape "$1")" || printf 'null'
|
|
119
|
+
}
|
|
120
|
+
|
|
107
121
|
json_escape() {
|
|
108
122
|
# minimal JSON string escaping for the few free-form values (timestamps, paths)
|
|
109
123
|
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' | tr -d '\n\r'
|
|
@@ -651,6 +665,117 @@ cmd_teardown() {
|
|
|
651
665
|
printf 'removed %s\n' "$self"
|
|
652
666
|
}
|
|
653
667
|
|
|
668
|
+
# ── node ────────────────────────────────────────────────────────────────────────
|
|
669
|
+
# Node itself is managed with tj/n (https://github.com/tj/n). Running services keep the
|
|
670
|
+
# binary they were started with, so restart-all is what a version change needs afterwards.
|
|
671
|
+
|
|
672
|
+
# Download the pinned n and put it at $1. Checked the way self-update checks this script:
|
|
673
|
+
# interpreter line, VERSION, and a syntax check — before anything is moved into place.
|
|
674
|
+
fetch_n() {
|
|
675
|
+
tmp=$(mktemp) || die "mktemp failed"
|
|
676
|
+
if command -v curl >/dev/null 2>&1; then
|
|
677
|
+
curl -fsSL -o "$tmp" "$N_URL" || { rm -f "$tmp"; die "could not download n $N_VERSION from $N_URL"; }
|
|
678
|
+
elif command -v wget >/dev/null 2>&1; then
|
|
679
|
+
wget -qO "$tmp" "$N_URL" || { rm -f "$tmp"; die "could not download n $N_VERSION from $N_URL"; }
|
|
680
|
+
else
|
|
681
|
+
rm -f "$tmp"
|
|
682
|
+
die "neither curl nor wget on this host — cannot fetch n (and n needs one of them itself)"
|
|
683
|
+
fi
|
|
684
|
+
head -n 1 "$tmp" | grep -q '^#!.*bash' || { rm -f "$tmp"; die "downloaded file is not the n script"; }
|
|
685
|
+
grep -q "^VERSION=\"\{0,1\}$N_VERSION" "$tmp" || { rm -f "$tmp"; die "downloaded n is not version $N_VERSION"; }
|
|
686
|
+
bash -n "$tmp" 2>/dev/null || { rm -f "$tmp"; die "downloaded n failed the syntax check"; }
|
|
687
|
+
install -m 0755 "$tmp" "$1" || { rm -f "$tmp"; die "could not install n to $1"; }
|
|
688
|
+
rm -f "$tmp"
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
cmd_node() {
|
|
692
|
+
[ $# -ge 1 ] || die "usage: node update [--lts|--stable]"
|
|
693
|
+
action=$1
|
|
694
|
+
shift
|
|
695
|
+
[ "$action" = update ] || die "unknown node action: $action"
|
|
696
|
+
spec=stable
|
|
697
|
+
while [ $# -gt 0 ]; do
|
|
698
|
+
case "$1" in
|
|
699
|
+
--lts) spec=lts ;;
|
|
700
|
+
--stable) spec=stable ;;
|
|
701
|
+
*) die "unknown option: $1" ;;
|
|
702
|
+
esac
|
|
703
|
+
shift
|
|
704
|
+
done
|
|
705
|
+
|
|
706
|
+
command -v bash >/dev/null 2>&1 || die "n is a bash script and this host has no bash (Alpine: apk add bash curl)"
|
|
707
|
+
N_PREFIX=${N_PREFIX:-/usr/local}
|
|
708
|
+
export N_PREFIX
|
|
709
|
+
|
|
710
|
+
before=$(node --version 2>/dev/null || true)
|
|
711
|
+
|
|
712
|
+
have=""
|
|
713
|
+
if [ -x "$N_BIN" ]; then have=$("$N_BIN" --version 2>/dev/null || true); fi
|
|
714
|
+
n_installed=false
|
|
715
|
+
if [ "$have" != "$N_VERSION" ]; then
|
|
716
|
+
fetch_n "$N_BIN"
|
|
717
|
+
n_installed=true
|
|
718
|
+
fi
|
|
719
|
+
|
|
720
|
+
"$N_BIN" install "$spec" >&2 || die "n install $spec failed"
|
|
721
|
+
|
|
722
|
+
hash -r 2>/dev/null || true
|
|
723
|
+
installed_path="$N_PREFIX/bin/node"
|
|
724
|
+
installed=$("$installed_path" --version 2>/dev/null || true)
|
|
725
|
+
active_path=$(command -v node 2>/dev/null || true)
|
|
726
|
+
after=$(node --version 2>/dev/null || true)
|
|
727
|
+
# n put a node in N_PREFIX, but another one (distro package, nvm, an /opt/nodeXX wrapper)
|
|
728
|
+
# still wins on PATH — that is the version the adapters keep running on.
|
|
729
|
+
mismatch=false
|
|
730
|
+
if [ -n "$installed" ] && [ "$after" != "$installed" ]; then mismatch=true; fi
|
|
731
|
+
|
|
732
|
+
printf '{"spec":"%s","before":%s,"after":%s,"installed":%s,"activePath":%s,"installedPath":"%s","mismatch":%s,"n":"%s","nInstalled":%s}\n' \
|
|
733
|
+
"$spec" \
|
|
734
|
+
"$(json_str "$before")" "$(json_str "$after")" "$(json_str "$installed")" "$(json_str "$active_path")" \
|
|
735
|
+
"$(json_escape "$installed_path")" "$mismatch" "$N_VERSION" "$n_installed"
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
# Restart every instance that is currently running — units that are stopped or disabled
|
|
739
|
+
# on purpose stay that way.
|
|
740
|
+
cmd_restart_all() {
|
|
741
|
+
[ $# -eq 0 ] || die "usage: restart-all"
|
|
742
|
+
first=1
|
|
743
|
+
printf '{"restarted":['
|
|
744
|
+
for unit in "$UNIT_DIR"/*@.service; do
|
|
745
|
+
[ -f "$unit" ] || continue
|
|
746
|
+
a=$(basename "$unit" @.service)
|
|
747
|
+
valid_adapter "$a" || continue
|
|
748
|
+
installed_adapter "$a" || continue
|
|
749
|
+
for envf in "/etc/$a"/*.env; do
|
|
750
|
+
[ -f "$envf" ] || continue
|
|
751
|
+
i=$(basename "$envf" .env)
|
|
752
|
+
valid_instance "$i" || continue
|
|
753
|
+
systemctl is-active --quiet "$a@$i.service" || continue
|
|
754
|
+
if err=$(systemctl restart "$a@$i.service" 2>&1); then ok=true; else ok=false; fi
|
|
755
|
+
[ $first -eq 1 ] || printf ','
|
|
756
|
+
first=0
|
|
757
|
+
printf '{"adapter":"%s","instance":"%s","ok":%s,"error":%s}' \
|
|
758
|
+
"$a" "$i" "$ok" \
|
|
759
|
+
"$([ "$ok" = true ] && printf 'null' || printf '"%s"' "$(json_escape "$err")")"
|
|
760
|
+
done
|
|
761
|
+
done
|
|
762
|
+
for unit in "$UNIT_DIR"/*.service; do
|
|
763
|
+
[ -f "$unit" ] || continue
|
|
764
|
+
a=$(basename "$unit" .service)
|
|
765
|
+
case "$a" in *@) continue ;; esac
|
|
766
|
+
valid_adapter "$a" || continue
|
|
767
|
+
legacy_unit "$a" || continue
|
|
768
|
+
systemctl is-active --quiet "$a.service" || continue
|
|
769
|
+
if err=$(systemctl restart "$a.service" 2>&1); then ok=true; else ok=false; fi
|
|
770
|
+
[ $first -eq 1 ] || printf ','
|
|
771
|
+
first=0
|
|
772
|
+
printf '{"adapter":"%s","instance":"-","ok":%s,"error":%s}' \
|
|
773
|
+
"$a" "$ok" \
|
|
774
|
+
"$([ "$ok" = true ] && printf 'null' || printf '"%s"' "$(json_escape "$err")")"
|
|
775
|
+
done
|
|
776
|
+
printf ']}\n'
|
|
777
|
+
}
|
|
778
|
+
|
|
654
779
|
cmd_npm() {
|
|
655
780
|
[ $# -ge 2 ] || die "usage: npm <adapter> version|origin|install|update|uninstall [--purge]"
|
|
656
781
|
need_adapter "$1"
|
|
@@ -697,6 +822,8 @@ case "$c" in
|
|
|
697
822
|
install) cmd_install "$@" ;;
|
|
698
823
|
uninstall) cmd_uninstall "$@" ;;
|
|
699
824
|
npm) cmd_npm "$@" ;;
|
|
825
|
+
node) cmd_node "$@" ;;
|
|
826
|
+
restart-all) cmd_restart_all "$@" ;;
|
|
700
827
|
files) cmd_files "$@" ;;
|
|
701
828
|
file) cmd_file "$@" ;;
|
|
702
829
|
asset) cmd_asset "$@" ;;
|
package/src/lib/services-host.js
CHANGED
|
@@ -26,7 +26,7 @@ const { execFile, spawn } = require('child_process');
|
|
|
26
26
|
const sshDeploy = require('./ssh-deploy');
|
|
27
27
|
|
|
28
28
|
const HELPER = '/usr/local/bin/she-servicectl';
|
|
29
|
-
const HELPER_VERSION =
|
|
29
|
+
const HELPER_VERSION = 13; // must match VERSION in service/she-servicectl (test/unit/helper-version.test.js)
|
|
30
30
|
/** Default identity for services.hosts[].ssh — one key for all managed hosts (I5). */
|
|
31
31
|
const DEFAULT_SERVICES_IDENTITY = '~/.she/ssh/services_id_ed25519';
|
|
32
32
|
/** The helper as shipped with this she version (copied to remote hosts by the deploy route). */
|
package/src/web/config-api.js
CHANGED
|
@@ -50,4 +50,51 @@ router.put('/', (req, res) => {
|
|
|
50
50
|
}
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* PATCH / — write a single subtree, leaving the rest of the file as it is on disk.
|
|
55
|
+
*
|
|
56
|
+
* Body: { path: 'broker.ssh', value: … }. A page that edits one branch of the config
|
|
57
|
+
* must not send the whole document back: it would carry a snapshot taken when the page
|
|
58
|
+
* was opened and silently revert anything another page wrote in the meantime (that is
|
|
59
|
+
* how a freshly configured services.hosts list disappeared when the broker SSH page was
|
|
60
|
+
* saved afterwards). Omitting `value` deletes the key.
|
|
61
|
+
*/
|
|
62
|
+
router.patch('/', (req, res) => {
|
|
63
|
+
const configPath = req.app.locals.configPath || DEFAULT_CONFIG_PATH;
|
|
64
|
+
try {
|
|
65
|
+
const { path: dotted, value } = req.body || {};
|
|
66
|
+
if (typeof dotted !== 'string' || !dotted) return res.status(400).json({ error: 'path required' });
|
|
67
|
+
const segments = dotted.split('.');
|
|
68
|
+
if (segments.length > 5 || !segments.every((k) => /^[A-Za-z0-9_-]+$/.test(k))) {
|
|
69
|
+
return res.status(400).json({ error: `invalid config path: ${dotted}` });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let config = {};
|
|
73
|
+
try {
|
|
74
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
75
|
+
} catch {
|
|
76
|
+
/* ok — file may not exist yet */
|
|
77
|
+
}
|
|
78
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) config = {};
|
|
79
|
+
|
|
80
|
+
const before = JSON.stringify(config[segments[0]]);
|
|
81
|
+
let node = config;
|
|
82
|
+
for (const key of segments.slice(0, -1)) {
|
|
83
|
+
if (!node[key] || typeof node[key] !== 'object' || Array.isArray(node[key])) node[key] = {};
|
|
84
|
+
node = node[key];
|
|
85
|
+
}
|
|
86
|
+
const last = segments[segments.length - 1];
|
|
87
|
+
if (value === undefined) delete node[last];
|
|
88
|
+
else node[last] = value;
|
|
89
|
+
|
|
90
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
91
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
92
|
+
|
|
93
|
+
const restartRequired = !FRONTEND_ONLY_KEYS.has(segments[0]) && before !== JSON.stringify(config[segments[0]]);
|
|
94
|
+
res.json({ ok: true, restartRequired, configPath });
|
|
95
|
+
} catch (err) {
|
|
96
|
+
res.status(500).json({ error: err.message });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
53
100
|
module.exports = { router, DEFAULT_CONFIG_PATH };
|
package/src/web/services-api.js
CHANGED
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
* GET /she/services/ssh/pubkey public key of the services identity (I5)
|
|
30
30
|
* POST /she/services/ssh/keygen generate it
|
|
31
31
|
* POST /she/services/hosts/:host/test run `she-servicectl version` → ok / code
|
|
32
|
+
* POST /she/services/hosts/:host/node/update { channel: stable|lts } update Node.js with tj/n (helper v13)
|
|
33
|
+
* POST /she/services/hosts/:host/instances/restart-all restart every running instance (after a node update)
|
|
32
34
|
* POST /she/services/hosts/:host/helper/deploy scp the helper to a remote host, install it, print the sudoers line
|
|
33
35
|
* POST /she/services/hosts/:host/helper/remove { mode: key|all, force? } remove she from the host (I11), drop the host entry
|
|
34
36
|
* POST /she/services/setup/token mint a one-time token + the curl | bash command (I9)
|
|
@@ -1030,6 +1032,59 @@ router.post('/hosts/:host/adapters/:adapter/update', async (req, res) => {
|
|
|
1030
1032
|
}
|
|
1031
1033
|
});
|
|
1032
1034
|
|
|
1035
|
+
/** The two node commands need helper v13; older hosts answer with "unknown command". */
|
|
1036
|
+
function nodeHostError(res, err) {
|
|
1037
|
+
if (/unknown command/i.test(err.message || '')) {
|
|
1038
|
+
return res.status(400).json({
|
|
1039
|
+
error: 'the she-servicectl helper on this host is too old for Node.js management — update it on the Hosts tab',
|
|
1040
|
+
code: 'HELPER_OUTDATED',
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
return hostError(res, err);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// POST /she/services/hosts/:host/node/update { channel?: 'stable' | 'lts' }
|
|
1047
|
+
router.post('/hosts/:host/node/update', async (req, res) => {
|
|
1048
|
+
const entry = resolve(req, res);
|
|
1049
|
+
if (!entry) return;
|
|
1050
|
+
const channel = (req.body && req.body.channel) || 'stable';
|
|
1051
|
+
if (channel !== 'stable' && channel !== 'lts') return res.status(400).json({ error: "channel must be 'stable' or 'lts'" });
|
|
1052
|
+
try {
|
|
1053
|
+
// downloading and unpacking a node build on a small host takes a while
|
|
1054
|
+
const { stdout } = await entry.driver.exec(['node', 'update', '--' + channel], { timeout: 900000 });
|
|
1055
|
+
let result = {};
|
|
1056
|
+
try {
|
|
1057
|
+
result = JSON.parse(stdout);
|
|
1058
|
+
} catch {
|
|
1059
|
+
return res.status(500).json({ error: 'helper returned unexpected output', output: stdout });
|
|
1060
|
+
}
|
|
1061
|
+
invalidateHosts();
|
|
1062
|
+
res.json({ ok: true, ...result, restartRequired: result.before !== result.after });
|
|
1063
|
+
} catch (err) {
|
|
1064
|
+
nodeHostError(res, err);
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
// POST /she/services/hosts/:host/instances/restart-all
|
|
1069
|
+
router.post('/hosts/:host/instances/restart-all', async (req, res) => {
|
|
1070
|
+
const entry = resolve(req, res);
|
|
1071
|
+
if (!entry) return;
|
|
1072
|
+
try {
|
|
1073
|
+
const { stdout } = await entry.driver.exec(['restart-all'], { timeout: 300000 });
|
|
1074
|
+
let result = { restarted: [] };
|
|
1075
|
+
try {
|
|
1076
|
+
result = JSON.parse(stdout);
|
|
1077
|
+
} catch {
|
|
1078
|
+
return res.status(500).json({ error: 'helper returned unexpected output', output: stdout });
|
|
1079
|
+
}
|
|
1080
|
+
const restarted = (result.restarted || []).filter((r) => r.ok);
|
|
1081
|
+
const failed = (result.restarted || []).filter((r) => !r.ok);
|
|
1082
|
+
res.json({ ok: failed.length === 0, restarted, failed });
|
|
1083
|
+
} catch (err) {
|
|
1084
|
+
nodeHostError(res, err);
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1033
1088
|
// POST /she/services/hosts/:host/units/:adapter/-/migrate { name } — legacy <adapter>.service → <adapter>@<name>
|
|
1034
1089
|
router.post('/hosts/:host/units/:adapter/' + LEGACY + '/migrate', async (req, res) => {
|
|
1035
1090
|
const entry = resolve(req, res, { adapter: true });
|