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.
@@ -1,4 +1,4 @@
1
- import{m as O}from"./monaco-langs-DKJI_o9J.js";import{t as I}from"./index-D2S52Ody.js";/*!-----------------------------------------------------------------------------
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
@@ -175,10 +175,10 @@
175
175
  }
176
176
  })();
177
177
  </script>
178
- <script type="module" crossorigin src="/assets/index-D2S52Ody.js"></script>
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-BTq9iige.css">
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smart-home-engine",
3
- "version": "1.49.0",
3
+ "version": "1.50.0",
4
4
  "description": "Node.js based script runner for use in MQTT based Smart Home environments",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -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] install/refresh tj/n (downloaded, no npm), then n install <label> — JSON with the version before and after
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=14
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
- # the command behind <adapter> belongs to an npm package of that name — what tells an adapter
88
- # apart from any other daemon that happens to keep its binary in /usr/local/bin
89
- npm_adapter_pkg() {
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
- [ "$(basename "$dir")" = "$1" ] && return 0
92
- grep -Eq "\"name\"[[:space:]]*:[[:space:]]*\"(@[^\"/]*/)?$1\"" "$dir/package.json" 2>/dev/null
100
+ [ -f "$dir/package.json" ] || return 1
101
+ printf '%s' "$dir/package.json"
93
102
  }
94
103
 
95
- # a plain <adapter>.service of an npm-installed adapter (pre-core layout)
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
- # the unit naming the package directory is conclusive on its own
99
- if grep -Eq "/node_modules/$1/" "$UNIT_DIR/$1.service" 2>/dev/null; then
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
- printf '{"helper":%s,"hostname":"%s","node":%s,"brokerEnv":%s,"adapters":[' \
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
@@ -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
- /** @type {{lts: string|null, ltsName: string|null, latest: string|null, ts: number, ok: boolean}|null} */
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 = { lts: ltsEntry ? ltsEntry.version : null, ltsName: ltsEntry ? String(ltsEntry.lts) : null, latest, ts: now, ok: true };
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 { lts: _cache.lts, ltsName: _cache.ltsName, latest: _cache.latest, fetchedAt: _cache.ok ? _cache.ts : null, stale: !_cache.ok };
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 };
@@ -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 = 14; // must match VERSION in service/she-servicectl (test/unit/helper-version.test.js)
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 : [],
@@ -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', '--' + channel], { timeout: 900000 });
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);