smart-home-engine 1.49.0 → 1.50.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-D2S52Ody.js → index-C1jFg5Bw.js} +118 -118
- package/dist/web/assets/{index-BTq9iige.css → index-DPDW78ru.css} +1 -1
- package/dist/web/assets/{tsMode-DPnnht_o.js → tsMode-D_ifZuZE.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/service/she-servicectl +44 -21
- package/src/lib/node-releases.js +64 -8
- package/src/lib/services-host.js +3 -1
- package/src/web/services-api.js +9 -3
|
@@ -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-C1jFg5Bw.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
|
@@ -175,10 +175,10 @@
|
|
|
175
175
|
}
|
|
176
176
|
})();
|
|
177
177
|
</script>
|
|
178
|
-
<script type="module" crossorigin src="/assets/index-
|
|
178
|
+
<script type="module" crossorigin src="/assets/index-C1jFg5Bw.js"></script>
|
|
179
179
|
<link rel="modulepreload" crossorigin href="/assets/monaco-langs-DKJI_o9J.js">
|
|
180
180
|
<link rel="stylesheet" crossorigin href="/assets/monaco-langs-DyX1CsEw.css">
|
|
181
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
181
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DPDW78ru.css">
|
|
182
182
|
</head>
|
|
183
183
|
<body>
|
|
184
184
|
<div id="app"></div>
|
package/package.json
CHANGED
package/service/she-servicectl
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
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|--latest|--stable]
|
|
34
|
+
# she-servicectl node update [--lts|--latest|--stable|--version X.Y.Z] install/refresh tj/n (downloaded, no npm), then n install <label|version> — JSON with the version before and after
|
|
35
35
|
# she-servicectl restart-all restart every running instance on this host (after a node update, say)
|
|
36
36
|
# she-servicectl self-update replace this script with the one on stdin (she ships it)
|
|
37
37
|
# she-servicectl remove-key drop the public key on stdin from the calling user's authorized_keys
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
|
|
46
46
|
set -eu
|
|
47
47
|
|
|
48
|
-
VERSION=
|
|
48
|
+
VERSION=15
|
|
49
49
|
BROKER_ENV=/etc/mqtt-interfaces/broker.env
|
|
50
50
|
UNIT_DIR=/etc/systemd/system
|
|
51
51
|
|
|
@@ -84,26 +84,41 @@ need_instance() {
|
|
|
84
84
|
valid_instance "$1" || die "invalid instance name: $1"
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
#
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
# where npm keeps globally installed packages
|
|
88
|
+
npm_roots() {
|
|
89
|
+
r=$(command -v npm >/dev/null 2>&1 && npm root -g 2>/dev/null || true)
|
|
90
|
+
printf '%s\n/usr/local/lib/node_modules\n' "$r" | grep -v '^$' | LC_ALL=C sort -u
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# package.json of an npm-installed <adapter>: from a global root, or from wherever the command
|
|
94
|
+
# behind the name lives (a unit may run the package without a binary on PATH at all)
|
|
95
|
+
adapter_pkg_json() {
|
|
96
|
+
for root in $(npm_roots); do
|
|
97
|
+
if [ -f "$root/$1/package.json" ]; then printf '%s' "$root/$1/package.json"; return 0; fi
|
|
98
|
+
done
|
|
90
99
|
dir=$(adapter_dir "$1" 2>/dev/null) || return 1
|
|
91
|
-
[
|
|
92
|
-
|
|
100
|
+
[ -f "$dir/package.json" ] || return 1
|
|
101
|
+
printf '%s' "$dir/package.json"
|
|
93
102
|
}
|
|
94
103
|
|
|
95
|
-
#
|
|
104
|
+
# an mqtt-interfaces adapter: the package says so itself, either with the mqttInterfaces block
|
|
105
|
+
# the core reads or by depending on the core. The same question npm_adapters asks — a host runs
|
|
106
|
+
# plenty of other things, npm packages among them.
|
|
107
|
+
mqtt_interfaces_pkg() {
|
|
108
|
+
pkg=$(adapter_pkg_json "$1") || return 1
|
|
109
|
+
grep -q '"mqttInterfaces"' "$pkg" 2>/dev/null && return 0
|
|
110
|
+
grep -q '"mqtt-interfaces-core"' "$pkg" 2>/dev/null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# a plain <adapter>.service of an npm-installed adapter (pre-core layout). That the unit runs the
|
|
114
|
+
# package or a binary of that name is not enough on its own: dnscrypt-proxy keeps its binary in
|
|
115
|
+
# /usr/local/bin, nut-display is an npm package — neither is an adapter.
|
|
96
116
|
legacy_unit() {
|
|
97
117
|
[ -f "$UNIT_DIR/$1.service" ] || return 1
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return 0
|
|
101
|
-
fi
|
|
102
|
-
# ExecStart=/usr/local/bin/<name> alone is not: dnscrypt-proxy, dnsproxy and the like live
|
|
103
|
-
# there too. It counts only when that binary is an npm package called <adapter>.
|
|
104
|
-
grep -Eq "/usr/local/bin/$1( |$)" "$UNIT_DIR/$1.service" 2>/dev/null || return 1
|
|
105
|
-
npm_adapter_pkg "$1"
|
|
118
|
+
grep -Eq "(/node_modules/$1/|/usr/local/bin/$1( |$))" "$UNIT_DIR/$1.service" 2>/dev/null || return 1
|
|
119
|
+
mqtt_interfaces_pkg "$1"
|
|
106
120
|
}
|
|
121
|
+
|
|
107
122
|
# the env file of a legacy unit: its EnvironmentFile= (leading - stripped), default /etc/default/<adapter>;
|
|
108
123
|
# only /etc/default/<adapter> or something inside /etc/<adapter>/ is accepted
|
|
109
124
|
legacy_env_file() {
|
|
@@ -254,10 +269,6 @@ read_env_stdin() {
|
|
|
254
269
|
|
|
255
270
|
# ── adapters installed with npm but without a template unit yet ─────────────────
|
|
256
271
|
# (a catalog install is `npm install -g <adapter>`; the unit appears with the first `--install`)
|
|
257
|
-
npm_roots() {
|
|
258
|
-
r=$(command -v npm >/dev/null 2>&1 && npm root -g 2>/dev/null || true)
|
|
259
|
-
printf '%s\n/usr/local/lib/node_modules\n' "$r" | grep -v '^$' | LC_ALL=C sort -u
|
|
260
|
-
}
|
|
261
272
|
npm_adapters() {
|
|
262
273
|
for root in $(npm_roots); do
|
|
263
274
|
for pkg in "$root"/*/package.json; do
|
|
@@ -278,9 +289,13 @@ cmd_version() {
|
|
|
278
289
|
cmd_list() {
|
|
279
290
|
hostname=$(hostname 2>/dev/null || uname -n)
|
|
280
291
|
node=$(command -v node >/dev/null 2>&1 && node --version 2>/dev/null || true)
|
|
281
|
-
|
|
292
|
+
# the machine type decides which node builds exist for this host (an old Pi has none
|
|
293
|
+
# past the armv6l/armv7l lines) — she reads it off nodejs.org's index per architecture
|
|
294
|
+
arch=$(uname -m 2>/dev/null || true)
|
|
295
|
+
printf '{"helper":%s,"hostname":"%s","node":%s,"arch":%s,"brokerEnv":%s,"adapters":[' \
|
|
282
296
|
"$VERSION" "$(json_escape "$hostname")" \
|
|
283
297
|
"$([ -n "$node" ] && printf '"%s"' "$node" || printf 'null')" \
|
|
298
|
+
"$([ -n "$arch" ] && printf '"%s"' "$(json_escape "$arch")" || printf 'null')" \
|
|
284
299
|
"$([ -f "$BROKER_ENV" ] && printf 'true' || printf 'false')"
|
|
285
300
|
first=1
|
|
286
301
|
adapters=""
|
|
@@ -716,6 +731,14 @@ cmd_node() {
|
|
|
716
731
|
--lts) spec=lts ;;
|
|
717
732
|
--latest) spec=latest ;;
|
|
718
733
|
--stable) spec=stable ;;
|
|
734
|
+
# an exact release: what a label resolves to depends on the architecture, and she
|
|
735
|
+
# resolved it against the builds that exist for this host before asking
|
|
736
|
+
--version)
|
|
737
|
+
shift
|
|
738
|
+
[ $# -gt 0 ] || die "--version needs a version"
|
|
739
|
+
printf '%s' "$1" | grep -Eq '^v?[0-9]{1,3}(\.[0-9]{1,3}){0,2}$' || die "invalid version: $1"
|
|
740
|
+
spec=$1
|
|
741
|
+
;;
|
|
719
742
|
*) die "unknown option: $1" ;;
|
|
720
743
|
esac
|
|
721
744
|
shift
|
package/src/lib/node-releases.js
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
* she asks nodejs.org once for all hosts and caches the answer, so the Hosts tab can show
|
|
8
8
|
* what a click would install and hide the button when the host already runs it. Best-effort:
|
|
9
9
|
* every failure yields the last good answer, or nulls — nothing throws.
|
|
10
|
+
*
|
|
11
|
+
* Not every release is built for every machine: an old Raspberry Pi runs armv6l/armv7l, and
|
|
12
|
+
* the newest lines have no build for those at all. index.json lists the files of each release,
|
|
13
|
+
* so `byArch` answers the same two questions per architecture — offering a host a version that
|
|
14
|
+
* does not exist for it is a promise `n install` cannot keep.
|
|
10
15
|
*/
|
|
11
16
|
|
|
12
17
|
const INDEX_URL = 'https://nodejs.org/dist/index.json';
|
|
@@ -19,7 +24,23 @@ function setFetch(fn) {
|
|
|
19
24
|
_fetch = fn;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Node's own name for a machine type, by what `uname -m` calls it — the keys of `byArch` are
|
|
29
|
+
* both, so a host's raw uname string is enough to look its releases up.
|
|
30
|
+
* @type {Record<string, string>}
|
|
31
|
+
*/
|
|
32
|
+
const UNAME_ALIASES = {
|
|
33
|
+
x86_64: 'x64',
|
|
34
|
+
amd64: 'x64',
|
|
35
|
+
aarch64: 'arm64',
|
|
36
|
+
aarch64_be: 'arm64',
|
|
37
|
+
i386: 'x86',
|
|
38
|
+
i486: 'x86',
|
|
39
|
+
i586: 'x86',
|
|
40
|
+
i686: 'x86',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** @type {{lts: string|null, ltsName: string|null, latest: string|null, byArch: object, ts: number, ok: boolean}|null} */
|
|
23
44
|
let _cache = null;
|
|
24
45
|
/** @type {Promise<object>|null} */
|
|
25
46
|
let _inflight = null;
|
|
@@ -29,15 +50,43 @@ function clearCache() {
|
|
|
29
50
|
_inflight = null;
|
|
30
51
|
}
|
|
31
52
|
|
|
53
|
+
function answer(c, fetchedAt) {
|
|
54
|
+
return { lts: c.lts, ltsName: c.ltsName, latest: c.latest, byArch: c.byArch ?? {}, fetchedAt, stale: !c.ok };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The newest release and the newest LTS release *that have a linux build for that machine*,
|
|
59
|
+
* per architecture. A release lists its downloads in `files` ("linux-armv7l", "linux-x64", …),
|
|
60
|
+
* which is exactly what `n install` will look for.
|
|
61
|
+
* @param {object[]} list index.json, newest first
|
|
62
|
+
*/
|
|
63
|
+
function byArch(list) {
|
|
64
|
+
const out = {};
|
|
65
|
+
for (const entry of list) {
|
|
66
|
+
if (!entry || typeof entry.version !== 'string' || !Array.isArray(entry.files)) continue;
|
|
67
|
+
for (const file of entry.files) {
|
|
68
|
+
if (typeof file !== 'string' || !file.startsWith('linux-')) continue;
|
|
69
|
+
const arch = file.slice('linux-'.length);
|
|
70
|
+
if (!arch || arch.includes('-')) continue; // linux-x64-musl and the like are not n's target
|
|
71
|
+
const seen = (out[arch] ??= { lts: null, latest: null });
|
|
72
|
+
if (!seen.latest) seen.latest = entry.version;
|
|
73
|
+
if (!seen.lts && entry.lts) seen.lts = entry.version;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// a host reports what uname says; let that name find the same entry
|
|
77
|
+
for (const [uname, arch] of Object.entries(UNAME_ALIASES)) {
|
|
78
|
+
if (out[arch]) out[uname] = out[arch];
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
32
83
|
/**
|
|
33
84
|
* @param {{force?: boolean, now?: number}} [opts]
|
|
34
85
|
* @returns {Promise<{lts: string|null, ltsName: string|null, latest: string|null, fetchedAt: number|null, stale: boolean}>}
|
|
35
86
|
*/
|
|
36
87
|
async function releases(opts = {}) {
|
|
37
88
|
const now = opts.now ?? Date.now();
|
|
38
|
-
if (_cache && !opts.force && now - _cache.ts < TTL)
|
|
39
|
-
return { lts: _cache.lts, ltsName: _cache.ltsName, latest: _cache.latest, fetchedAt: _cache.ts, stale: !_cache.ok };
|
|
40
|
-
}
|
|
89
|
+
if (_cache && !opts.force && now - _cache.ts < TTL) return answer(_cache, _cache.ts);
|
|
41
90
|
if (_inflight) return _inflight;
|
|
42
91
|
|
|
43
92
|
_inflight = (async () => {
|
|
@@ -49,17 +98,24 @@ async function releases(opts = {}) {
|
|
|
49
98
|
// index.json is ordered newest first; an LTS entry carries its codename in `lts`
|
|
50
99
|
const latest = typeof list[0].version === 'string' ? list[0].version : null;
|
|
51
100
|
const ltsEntry = list.find((e) => e && e.lts && typeof e.version === 'string') || null;
|
|
52
|
-
_cache = {
|
|
101
|
+
_cache = {
|
|
102
|
+
lts: ltsEntry ? ltsEntry.version : null,
|
|
103
|
+
ltsName: ltsEntry ? String(ltsEntry.lts) : null,
|
|
104
|
+
latest,
|
|
105
|
+
byArch: byArch(list),
|
|
106
|
+
ts: now,
|
|
107
|
+
ok: true,
|
|
108
|
+
};
|
|
53
109
|
} catch {
|
|
54
110
|
// keep whatever was known before, just mark it stale
|
|
55
|
-
_cache = _cache ? { ..._cache, ts: now, ok: false } : { lts: null, ltsName: null, latest: null, ts: now, ok: false };
|
|
111
|
+
_cache = _cache ? { ..._cache, ts: now, ok: false } : { lts: null, ltsName: null, latest: null, byArch: {}, ts: now, ok: false };
|
|
56
112
|
} finally {
|
|
57
113
|
_inflight = null;
|
|
58
114
|
}
|
|
59
|
-
return
|
|
115
|
+
return answer(_cache, _cache.ok ? _cache.ts : null);
|
|
60
116
|
})();
|
|
61
117
|
|
|
62
118
|
return _inflight;
|
|
63
119
|
}
|
|
64
120
|
|
|
65
|
-
module.exports = { releases, setFetch, clearCache, INDEX_URL, TTL };
|
|
121
|
+
module.exports = { releases, byArch, setFetch, clearCache, UNAME_ALIASES, INDEX_URL, TTL };
|
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 = 15; // 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). */
|
|
@@ -179,6 +179,8 @@ function parseList(stdout) {
|
|
|
179
179
|
helperOutdated: (Number(data.helper) || 0) < HELPER_VERSION,
|
|
180
180
|
hostname: data.hostname || null,
|
|
181
181
|
node: data.node || null,
|
|
182
|
+
/** uname -m of the host (helper v15+): which node builds exist for it */
|
|
183
|
+
arch: data.arch || null,
|
|
182
184
|
brokerEnv: data.brokerEnv === true,
|
|
183
185
|
adapters: Array.isArray(data.adapters) ? data.adapters : [],
|
|
184
186
|
instances: Array.isArray(data.instances) ? data.instances : [],
|
package/src/web/services-api.js
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* POST /she/services/ssh/keygen generate it
|
|
31
31
|
* POST /she/services/hosts/:host/test run `she-servicectl version` → ok / code
|
|
32
32
|
* GET /she/services/node/releases[?refresh=1] what a node update would install: the newest lts and latest release
|
|
33
|
-
* POST /she/services/hosts/:host/node/update { channel: stable|lts|latest } update Node.js with tj/n (helper v13)
|
|
33
|
+
* POST /she/services/hosts/:host/node/update { channel: stable|lts|latest, version? } update Node.js with tj/n (helper v13, exact version v15)
|
|
34
34
|
* POST /she/services/hosts/:host/instances/restart-all restart every running instance (after a node update)
|
|
35
35
|
* POST /she/services/hosts/:host/helper/deploy scp the helper to a remote host, install it, print the sudoers line
|
|
36
36
|
* POST /she/services/hosts/:host/helper/remove { mode: key|all, force? } remove she from the host (I11), drop the host entry
|
|
@@ -1056,16 +1056,22 @@ router.get('/node/releases', async (req, res) => {
|
|
|
1056
1056
|
res.json(await nodeReleases.releases({ force: req.query.refresh === '1' }));
|
|
1057
1057
|
});
|
|
1058
1058
|
|
|
1059
|
-
// POST /she/services/hosts/:host/node/update { channel?: 'lts' | 'latest' }
|
|
1059
|
+
// POST /she/services/hosts/:host/node/update { channel?: 'lts' | 'latest', version?: 'vX.Y.Z' }
|
|
1060
1060
|
router.post('/hosts/:host/node/update', async (req, res) => {
|
|
1061
1061
|
const entry = resolve(req, res);
|
|
1062
1062
|
if (!entry) return;
|
|
1063
1063
|
// n's own labels, passed through as they are: stable and lts both mean the newest LTS
|
|
1064
1064
|
const channel = (req.body && req.body.channel) || 'lts';
|
|
1065
1065
|
if (!['stable', 'lts', 'latest'].includes(channel)) return res.status(400).json({ error: "channel must be 'stable', 'lts' or 'latest'" });
|
|
1066
|
+
// A label means something else on a host whose architecture the newest lines are not built
|
|
1067
|
+
// for — n would resolve `lts` to a release that has no download for an old Pi. The caller
|
|
1068
|
+
// resolved it against that host's builds, so the exact version is what gets installed.
|
|
1069
|
+
const version = (req.body && req.body.version) || null;
|
|
1070
|
+
if (version !== null && !/^v?\d{1,3}(\.\d{1,3}){0,2}$/.test(String(version))) return res.status(400).json({ error: 'invalid version' });
|
|
1071
|
+
const spec = version ? ['--version', String(version)] : ['--' + channel];
|
|
1066
1072
|
try {
|
|
1067
1073
|
// downloading and unpacking a node build on a small host takes a while
|
|
1068
|
-
const { stdout } = await entry.driver.exec(['node', 'update',
|
|
1074
|
+
const { stdout } = await entry.driver.exec(['node', 'update', ...spec], { timeout: 900000 });
|
|
1069
1075
|
let result = {};
|
|
1070
1076
|
try {
|
|
1071
1077
|
result = JSON.parse(stdout);
|