frugal-iot-server 2.0.0 → 2.0.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.
@@ -34,6 +34,26 @@ function dynsecConfigured(config) {
34
34
  return !!(s.dynsec_admin_user && s.dynsec_admin_password);
35
35
  }
36
36
 
37
+ /*
38
+ * Another server owns this broker: derive credentials, never try to create them.
39
+ *
40
+ * For working on the server or the client against live data - a laptop pointed at the production
41
+ * broker. It has no admin credential and must not have one, but the accounts it needs already
42
+ * exist, created by the server that does. Given the same user_secret it derives exactly the same
43
+ * passwords, so what it hands a browser works.
44
+ *
45
+ * Declared in config.d/secrets.yaml rather than config.d/mqtt.yaml, for two reasons: that file is
46
+ * never committed, so this cannot be switched on by accident in a checkout; and it is where the
47
+ * borrowed user_secret has to go anyway, which is the real access control here. Without that
48
+ * secret a checkout of this repository cannot reach live data at all, whatever it sets.
49
+ *
50
+ * What does NOT work on such a server, by design: enrolling a node, resetting one, and
51
+ * frugal-iot-rebuild-dynsec - all of which create or delete accounts.
52
+ */
53
+ function brokerManagedElsewhere(config) {
54
+ return !!(config && config.secrets && config.secrets.broker_managed_elsewhere);
55
+ }
56
+
37
57
  // A connection per operation rather than one held open. These happen at login and at permission
38
58
  // changes - rare, and never in a hot path - and a short-lived connection cannot go stale, which on
39
59
  // a Pi that sleeps or loses its network it otherwise would.
@@ -69,6 +89,15 @@ export function syncUser(db, config, user, cb) {
69
89
  reportUnavailable(`user ${user.username}`, err);
70
90
  return cb(err);
71
91
  }
92
+ // Borrowing another server's broker: the account is already there, so derive and hand it over
93
+ // without trying to reach a plugin this server has no credential for.
94
+ if (brokerManagedElsewhere(config)) {
95
+ return cb(null, {
96
+ username: names.userClient(user.username),
97
+ password: derivePassword(userSecret, user.username, user.hashed_password),
98
+ brokerUpdated: false,
99
+ });
100
+ }
72
101
  // Nothing to hand a browser on a server that has never driven the plugin: the account cannot
73
102
  // exist, so the dashboard says live data is unavailable rather than failing to connect with a
74
103
  // credential that was never real. See dynsecConfigured.
@@ -136,7 +165,7 @@ export function syncLoggers(config, orgs, cb) {
136
165
  const creds = {};
137
166
  // No credentials at all means the logger falls back to the organization's shared password, which
138
167
  // works until S8. That is the right answer whenever an account of its own could not exist.
139
- if (!userSecret || !dynsecConfigured(config)) {
168
+ if (!userSecret || (!dynsecConfigured(config) && !brokerManagedElsewhere(config))) {
140
169
  reportUnavailable('logger accounts',
141
170
  new Error(userSecret ? 'No dynsec_admin_user in config.d/secrets.yaml'
142
171
  : 'No user_secret in config.d/secrets.yaml'));
@@ -145,6 +174,10 @@ export function syncLoggers(config, orgs, cb) {
145
174
  for (const org of orgs) {
146
175
  creds[org] = { username: names.loggerClient(org), password: deriveLoggerPassword(userSecret, org) };
147
176
  }
177
+ // Same as for a user: the accounts exist, this server just did not make them. Two loggers on one
178
+ // account is fine - mqtt.js generates a client id per connection, and MQTT evicts by client id,
179
+ // not by user - so a laptop recording alongside production does not disturb it.
180
+ if (brokerManagedElsewhere(config)) return cb(null, creds);
148
181
  withDynsec(config, (cerr, dynsec, done) => {
149
182
  // Configured but not answering: the accounts are almost certainly there from an earlier run,
150
183
  // so the derived credentials are handed over and the logger reconnects when the broker returns.
package/package.json CHANGED
@@ -12,8 +12,8 @@
12
12
  "cookie-parser": "^1.4.7",
13
13
  "express": "^5.2.1",
14
14
  "express-session": "^1.19.0",
15
- "frugal-iot-client": "^2.0.0",
16
- "frugal-iot-logger": "^2.0.0",
15
+ "frugal-iot-client": "^2.0.1",
16
+ "frugal-iot-logger": "^2.0.1",
17
17
  "hash-wasm": "^4.12.0",
18
18
  "js-yaml": "^4.2.0",
19
19
  "morgan": "^1.11.0",
@@ -83,5 +83,5 @@
83
83
  "check-files": "node scripts/check-published-files.js"
84
84
  },
85
85
  "type": "module",
86
- "version": "2.0.0"
86
+ "version": "2.0.1"
87
87
  }
@@ -6,7 +6,7 @@
6
6
  // it, an installed browser keeps serving the files it already cached - and since the list now covers
7
7
  // the dashboard's own code, that means old JS behind freshly fetched HTML. Any edit to the list
8
8
  // needs this bumped, or it does not reach anyone who already has the old one.
9
- const CACHE_NAME = 'frugal-iot-cache-2.0.0';
9
+ const CACHE_NAME = 'frugal-iot-cache-2.0.1';
10
10
  /*
11
11
  * Everything the app needs, so that a phone on a poor or expensive link fetches it once.
12
12
  *
@@ -25,7 +25,8 @@
25
25
  * is what decides how many decimals a reading is shown to; without it the UI guesses.
26
26
  *
27
27
  * - A numeric topic with no "units". Often right - plenty of readings are dimensionless - but
28
- * worth a look, because the UI has nothing to put after the number.
28
+ * worth a look, because the UI has nothing to put after the number. The topics that are
29
+ * deliberately dimensionless are listed in UNITS_EXEMPT below, and are not mentioned.
29
30
  *
30
31
  * And, separately, things that are never deliberate and so count as errors:
31
32
  *
@@ -35,8 +36,13 @@
35
36
  * Usage:
36
37
  * node scripts/check-schema.js # checks ./config.d/schema
37
38
  * node scripts/check-schema.js <dir> [<dir> ...] # each dir holding topics.yaml and modules.yaml
39
+ * node scripts/check-schema.js -q <dir> # say nothing unless there is something to say
38
40
  * node scripts/check-schema.js --resolve <otakey> # which devices.yaml entry that OTA key uses
39
41
  *
42
+ * -q (--quiet) prints nothing at all when a schema is clean, so it can run from a release script
43
+ * without burying the one line that matters. Warnings and errors print exactly as they otherwise
44
+ * would, and the exit code is the same either way.
45
+ *
40
46
  * Exits 0 for warnings, so it can run from a prerelease script without blocking a release for
41
47
  * something that may well be deliberate. Exits 1 for an error, or if it could not read a schema.
42
48
  * Both callers guard it with "|| true", so neither blocks on either.
@@ -51,6 +57,17 @@ import path from 'path';
51
57
  const TYPES_LOGGED_BY_DEFAULT = ['float', 'int', 'bool'];
52
58
  // The types with decimals to decide, and so the ones that need a width
53
59
  const NUMERIC_TYPES = ['float', 'exponential'];
60
+ /*
61
+ * Topics that are dimensionless on purpose, so "no units" is the right answer rather than
62
+ * something to look at. Without this the same three warnings appear at every release, which is
63
+ * exactly how a real one comes to be skimmed past. Add a name here only once you are sure the
64
+ * reading genuinely has no unit - not because you have not decided yet.
65
+ */
66
+ const UNITS_EXEMPT = [
67
+ 'controlfloat', // a control's value, in whatever the thing it controls is measured in
68
+ 'hdop', // dilution of precision - a ratio
69
+ 'loadcell', // raw ADC counts until a calibration turns them into grams
70
+ ];
54
71
 
55
72
  function loadYaml(file) {
56
73
  if (!existsSync(file)) return null;
@@ -111,7 +128,7 @@ function checkSchemaDir(dir) {
111
128
  + ` characters before any decimal point, so values will overflow it`);
112
129
  }
113
130
  }
114
- if (hasDecimals && (topic.units === undefined)) {
131
+ if (hasDecimals && (topic.units === undefined) && !UNITS_EXEMPT.includes(name)) {
115
132
  warnings.push(`topic "${name}" has no "units" - fine if it is dimensionless, but the UI has`
116
133
  + ` nothing to put after the number`);
117
134
  }
@@ -187,6 +204,7 @@ function resolveDeviceEntry(devices, otakey) {
187
204
  }
188
205
 
189
206
  const argv = process.argv.slice(2);
207
+ const quiet = argv.includes('-q') || argv.includes('--quiet');
190
208
  const resolveAt = argv.indexOf('--resolve');
191
209
  if (resolveAt !== -1) {
192
210
  const otakey = argv[resolveAt + 1];
@@ -201,7 +219,8 @@ if (resolveAt !== -1) {
201
219
  process.exit(0);
202
220
  }
203
221
 
204
- const dirs = argv.length ? argv : ['config.d/schema'];
222
+ const named = argv.filter((a) => !a.startsWith('-'));
223
+ const dirs = named.length ? named : ['config.d/schema'];
205
224
  let unreadable = 0;
206
225
  let totalWarnings = 0;
207
226
  let totalErrors = 0;
@@ -214,7 +233,8 @@ for (const dir of dirs) {
214
233
  continue;
215
234
  }
216
235
  if (!warnings.length && !errors.length) {
217
- console.log(`${dir}: nothing to report`);
236
+ // Under -q a clean schema says nothing, so whatever a release script does print is a finding
237
+ if (!quiet) console.log(`${dir}: nothing to report`);
218
238
  continue;
219
239
  }
220
240
  if (warnings.length) {
@@ -8,16 +8,31 @@
8
8
  # schema is maintained.
9
9
  #
10
10
  # Usage:
11
- # scripts/copy-schema-to-examples.zsh [<frugal-iot-logger directory>]
11
+ # scripts/copy-schema-to-examples.zsh [-q|--quiet] [<frugal-iot-logger directory>]
12
12
  #
13
13
  # With no argument it looks for frugal-iot-logger beside this checkout, which is how these are
14
14
  # developed. Says what it did and changes nothing else; committing is up to you.
15
+ #
16
+ # -q (--quiet) names only the files it changed. A run that copies nothing says nothing, so from a
17
+ # release script the output is the list of files you now have to commit.
15
18
 
16
19
  set -euo pipefail
17
20
 
21
+ QUIET=0
22
+ ARGS=()
23
+ for arg in "$@"; do
24
+ case "$arg" in
25
+ -q|--quiet) QUIET=1 ;;
26
+ *) ARGS+=("$arg") ;;
27
+ esac
28
+ done
29
+
30
+ # Progress, as opposed to a change or a problem: silent under -q
31
+ say() { (( QUIET )) || print -r -- "$@" }
32
+
18
33
  HERE="${0:A:h:h}" # The frugal-iot-server checkout this script is in
19
34
  SCHEMA="${HERE}/config.d/schema"
20
- LOGGER="${1:-${HERE:h}/frugal-iot-logger}"
35
+ LOGGER="${ARGS[1]:-${HERE:h}/frugal-iot-logger}"
21
36
 
22
37
  if [[ ! -d "$LOGGER" ]]; then
23
38
  echo " No frugal-iot-logger checkout at ${LOGGER}, so nothing copied."
@@ -44,7 +59,7 @@ COPIED=0
44
59
  for d in $DIRS; do
45
60
  for f in $FILES; do
46
61
  if cmp -s "${SCHEMA}/${f}" "${d}/${f}"; then
47
- echo " same ${d#${LOGGER}/}/${f}"
62
+ say " same ${d#${LOGGER}/}/${f}"
48
63
  else
49
64
  cp "${SCHEMA}/${f}" "${d}/${f}"
50
65
  echo " updated ${d#${LOGGER}/}/${f}"
@@ -4,6 +4,18 @@
4
4
  # afterwards. Run it, read what it says, then publish:
5
5
  #
6
6
  # npm run prerelease
7
+ # zsh scripts/prerelease.zsh --release-version 2.1.0
8
+ # zsh scripts/prerelease.zsh -q
9
+ #
10
+ # -q (--quiet) prints only findings and changes, so a clean run prints nothing at all. That is what
11
+ # the release script uses; run it without -q to see each check confirm itself.
12
+ #
13
+ # --release-version says which version this release is about to publish everywhere, and is what the
14
+ # release script in frugal-iot-demo passes. Without it, "requires a version nobody has published"
15
+ # is a blocker, because that is what it means on an ordinary day. During a release it is the normal
16
+ # state of affairs - the logger and client are about to be published at that version and have not
17
+ # been yet - so with it, a range that only the version being released satisfies is reported and
18
+ # allowed, and CACHE_NAME is set to that version rather than to whatever is on npm today.
7
19
  #
8
20
  # It checks the sensor schema and copies it into the examples that ship with frugal-iot-logger, sets
9
21
  # the service worker's cache version, and looks for the package being wired to a checkout on this
@@ -21,27 +33,52 @@
21
33
 
22
34
  set -euo pipefail
23
35
 
36
+ RELEASE_VERSION=""
37
+ QUIET=0
38
+ while (( $# )); do
39
+ case "$1" in
40
+ --release-version) RELEASE_VERSION="${2:-}"; shift 2 || true ;;
41
+ --release-version=*) RELEASE_VERSION="${1#*=}"; shift ;;
42
+ -q|--quiet) QUIET=1; shift ;;
43
+ -h|--help) sed -n '2,34p' "$0" | sed 's/^# \?//'; exit 0 ;;
44
+ *) print -u2 "Unknown option: $1 (try --release-version <version>, -q, or -h)"; exit 2 ;;
45
+ esac
46
+ done
47
+
48
+ # A check confirming itself, as opposed to a finding or a change: silent under -q
49
+ say() { (( QUIET )) || print -r -- "$@" }
50
+
51
+ # Passed on to the scripts that take the same flag
52
+ QARG=()
53
+ (( QUIET )) && QARG=(-q)
54
+
24
55
  HERE="${0:A:h:h}" # The frugal-iot-server checkout this script is in
25
56
  SCHEMA="${HERE}/config.d/schema"
26
57
 
58
+ # Does the version being released satisfy this range? Only asked when --release-version was given.
59
+ satisfied_by_release() {
60
+ [[ -n "$RELEASE_VERSION" ]] || return 1
61
+ node -e "process.exit(require('semver').satisfies('${RELEASE_VERSION}', process.argv[1]) ? 0 : 1)" "$1" 2>/dev/null
62
+ }
63
+
27
64
  cd "$HERE"
28
65
 
29
66
  BLOCKERS=() # never right in a published package - this script exits non-zero for these
30
67
  WARNINGS=() # probably worth doing something about, but yours to judge
31
68
 
32
- echo "=== Checking ${SCHEMA} ==="
33
- node scripts/check-schema.js config.d/schema || true
69
+ say "=== Checking ${SCHEMA} ==="
70
+ node scripts/check-schema.js "${QARG[@]}" config.d/schema || true
34
71
 
35
- echo
36
- echo "=== Copying the schema into the frugal-iot-logger examples ==="
37
- zsh scripts/copy-schema-to-examples.zsh
72
+ say
73
+ say "=== Copying the schema into the frugal-iot-logger examples ==="
74
+ zsh scripts/copy-schema-to-examples.zsh "${QARG[@]}"
38
75
 
39
76
  # ---- Is this package wired to a checkout on this machine? ----
40
77
  # Developing the server, logger and client together means pointing the server at sibling checkouts,
41
78
  # and every way of doing that is invisible once you have got used to it. Published, each one either
42
79
  # breaks every install outright or ships code nobody else has.
43
- echo
44
- echo "=== Checking nothing is wired to a local checkout ==="
80
+ say
81
+ say "=== Checking nothing is wired to a local checkout ==="
45
82
 
46
83
  # 1. "file:" or "link:" dependencies - these would make "npm install frugal-iot-server" fail for
47
84
  # everybody, because the path they name does not exist on anyone else's machine
@@ -59,7 +96,7 @@ if [[ -n "$LOCALDEPS" ]]; then
59
96
  print -r -- "$LOCALDEPS" | sed 's/^/ /'
60
97
  BLOCKERS+=("package.json names a dependency by path - publishing that breaks every install. Put the version range back.")
61
98
  else
62
- echo " package.json names no dependencies by path"
99
+ say " package.json names no dependencies by path"
63
100
  fi
64
101
 
65
102
  # 2. npm link - node_modules/<pkg> being a symlink means what you have been testing is your
@@ -74,8 +111,9 @@ if (( ${#LINKED} )); then
74
111
  for l in $LINKED; do echo " linked: $l"; done
75
112
  echo " What you have been running is that checkout, not the published package."
76
113
  echo " Publish the linked package first, then require the new version here."
114
+ WARNINGS+=("node_modules links to a checkout (${(j:, :)LINKED}) - what the tests exercised is that checkout, not the published package.")
77
115
  else
78
- echo " node_modules holds published packages, not links to checkouts"
116
+ say " node_modules holds published packages, not links to checkouts"
79
117
  fi
80
118
 
81
119
  # 3. The development alternatives inside the source, which are switched by editing rather than by
@@ -83,12 +121,12 @@ fi
83
121
  if grep -qE '^\s*import .*from\s+"\.\./frugal-iot-logger' frugal-iot-server.js 2>/dev/null; then
84
122
  BLOCKERS+=("frugal-iot-server.js is importing the logger from ../frugal-iot-logger - comment that line out and uncomment the \"frugal-iot-logger\" one.")
85
123
  else
86
- echo " frugal-iot-server.js imports the logger as a package, not by path"
124
+ say " frugal-iot-server.js imports the logger as a package, not by path"
87
125
  fi
88
126
  if grep -qE '^\s*(htmldir|nodemodulesdir):\s*\.\./' config.d/server.yaml 2>/dev/null; then
89
127
  BLOCKERS+=("config.d/server.yaml points htmldir or nodemodulesdir at ../frugal-iot-client - that is the development setting and ships to everyone who installs.")
90
128
  else
91
- echo " config.d/server.yaml points at ./node_modules, not at a sibling checkout"
129
+ say " config.d/server.yaml points at ./node_modules, not at a sibling checkout"
92
130
  fi
93
131
 
94
132
  # ---- Are the versions this requires actually available? ----
@@ -96,8 +134,8 @@ fi
96
134
  # on a Pi as well as here, and nothing in the repo shows it. Asking for one that is published but
97
135
  # not the newest is a lesser problem - a fresh install resolves to the newest anyway, but an
98
136
  # existing one is free to stay where it is, so your changes reach nobody who already has it.
99
- echo
100
- echo "=== Checking the versions this requires have been published ==="
137
+ say
138
+ say "=== Checking the versions this requires have been published ==="
101
139
 
102
140
  # Highest published version of $1 satisfying range $2, or empty if none - and it prints nothing and
103
141
  # exits 1 when nothing matches, which is an answer rather than a failure, hence the "|| true"
@@ -110,15 +148,19 @@ for p in frugal-iot-logger frugal-iot-client; do
110
148
  RANGE=$(node -e "console.log(require('./package.json').dependencies['$p'] || '')")
111
149
  if [[ -z "$RANGE" ]]; then continue; fi
112
150
  if ! LATEST=$(npm view "$p" version 2>/dev/null); then
113
- echo " $p: could not ask npm (offline?) - requires ${RANGE}, check it yourself"
151
+ say " $p: could not ask npm (offline?) - requires ${RANGE}, check it yourself"
114
152
  continue
115
153
  fi
116
154
  MATCH=$(published_match "$p" "$RANGE")
117
- if [[ -z "$MATCH" ]]; then
118
- echo " $p: requires ${RANGE}, but the newest published is ${LATEST}"
155
+ if [[ -z "$MATCH" ]] && satisfied_by_release "$RANGE"; then
156
+ # Mid-release: this range was raised to the version being published, and $p has not been
157
+ # published at it yet. Expected, and the publishing step is about to deal with it.
158
+ say " $p: requires ${RANGE}, awaiting the ${RELEASE_VERSION} publish (newest so far is ${LATEST})"
159
+ elif [[ -z "$MATCH" ]]; then
160
+ say " $p: requires ${RANGE}, but the newest published is ${LATEST}"
119
161
  BLOCKERS+=("Nothing published satisfies ${p}@${RANGE} (newest is ${LATEST}) - publish ${p} first, or every \"npm install frugal-iot-server\" fails.")
120
162
  elif [[ "$MATCH" != "$LATEST" ]]; then
121
- echo " $p: requires ${RANGE} -> ${MATCH}, but ${LATEST} is published"
163
+ say " $p: requires ${RANGE} -> ${MATCH}, but ${LATEST} is published"
122
164
  WARNINGS+=("${p}@${RANGE} resolves to ${MATCH}, not the newest published ${LATEST} - bump the range if you meant installs to pick up ${LATEST}.")
123
165
  else
124
166
  # The range allows the newest, but if its floor is older then an existing install can sit on
@@ -126,10 +168,10 @@ for p in frugal-iot-logger frugal-iot-client; do
126
168
  FLOOR=${RANGE#[\^~>=]}
127
169
  FLOOR=${FLOOR#=}
128
170
  if [[ -n "$FLOOR" && "$FLOOR" != "$LATEST" ]]; then
129
- echo " $p: requires ${RANGE}, newest published ${LATEST} (an existing install may stay on ${FLOOR})"
171
+ say " $p: requires ${RANGE}, newest published ${LATEST} (an existing install may stay on ${FLOOR})"
130
172
  WARNINGS+=("${p} requires ${RANGE}, so an install that already has ${FLOOR} is not obliged to move to ${LATEST}. Raise the range to ^${LATEST} if this release needs it.")
131
173
  else
132
- echo " $p: requires ${RANGE}, newest published ${LATEST} - in step"
174
+ say " $p: requires ${RANGE}, newest published ${LATEST} - in step"
133
175
  fi
134
176
  fi
135
177
  done
@@ -139,27 +181,33 @@ done
139
181
  # installed PWA fetch it again. Left behind, people keep running the client they already had, which
140
182
  # looks exactly like a release that did nothing. It tracks the client version, so set it from
141
183
  # whichever published client this release will actually resolve to.
142
- echo
143
- echo "=== Checking the service worker's cache version ==="
184
+ say
185
+ say "=== Checking the service worker's cache version ==="
144
186
  SW=public/service-worker.js
145
187
  CLIENT_RANGE=$(node -e "console.log(require('./package.json').dependencies['frugal-iot-client'] || '')")
146
188
  if [[ ! -f "$SW" ]]; then
147
- echo " no ${SW} here"
189
+ say " no ${SW} here"
148
190
  elif [[ -z "$CLIENT_RANGE" ]]; then
149
- echo " package.json has no frugal-iot-client dependency to match against"
191
+ say " package.json has no frugal-iot-client dependency to match against"
150
192
  else
151
193
  SW_VERSION=$(sed -n "s/^const CACHE_NAME = 'frugal-iot-cache-\([^']*\)'.*/\1/p" "$SW" | head -1)
152
194
  CLIENT_LATEST=$(npm view frugal-iot-client version 2>/dev/null || true)
153
195
  CLIENT_WANTED=$(published_match frugal-iot-client "$CLIENT_RANGE")
196
+ # Mid-release the client this ships with is the one about to be published, not the newest on npm
197
+ # today, so follow that instead - otherwise CACHE_NAME would be set back to last month's version.
198
+ if satisfied_by_release "$CLIENT_RANGE"; then
199
+ CLIENT_WANTED="$RELEASE_VERSION"
200
+ CLIENT_LATEST="$RELEASE_VERSION"
201
+ fi
154
202
  if [[ -z "$SW_VERSION" ]]; then
155
- echo " could not find a CACHE_NAME line of the form frugal-iot-cache-<version> in ${SW}"
203
+ say " could not find a CACHE_NAME line of the form frugal-iot-cache-<version> in ${SW}"
156
204
  WARNINGS+=("Could not read CACHE_NAME from ${SW} - check by hand that it changes when the client does.")
157
205
  elif [[ -z "$CLIENT_WANTED" ]]; then
158
- echo " CACHE_NAME says ${SW_VERSION}, and nothing published satisfies ${CLIENT_RANGE}"
206
+ say " CACHE_NAME says ${SW_VERSION}, and nothing published satisfies ${CLIENT_RANGE}"
159
207
  BLOCKERS+=("Cannot set CACHE_NAME: no published frugal-iot-client satisfies ${CLIENT_RANGE}.")
160
208
  else
161
209
  if [[ "$SW_VERSION" == "$CLIENT_WANTED" ]]; then
162
- echo " CACHE_NAME is ${SW_VERSION}, matching the client this release installs"
210
+ say " CACHE_NAME is ${SW_VERSION}, matching the client this release installs"
163
211
  else
164
212
  # An in-place edit rather than a warning: there is only one right answer, and a stale cache
165
213
  # name is invisible until somebody's browser keeps serving them last month's dashboard
@@ -174,16 +222,19 @@ else
174
222
  fi
175
223
 
176
224
  # ---- What would and would not be published ----
177
- echo
178
- echo "=== Checking what the package would contain ==="
179
- if ! node scripts/check-published-files.js; then
225
+ say
226
+ say "=== Checking what the package would contain ==="
227
+ if ! FILESOUT=$(node scripts/check-published-files.js 2>&1); then
228
+ print -r -- "$FILESOUT"
180
229
  BLOCKERS+=("Something git ignores would be published - see above.")
230
+ else
231
+ say "$FILESOUT"
181
232
  fi
182
233
 
183
- echo
184
- echo "=== Reminders ==="
185
- echo " - Is the version in package.json the one you mean to publish? $(node -e 'console.log(require("./package.json").version)')"
186
- echo " - Nothing here is committed or published - do that yourself."
234
+ say
235
+ say "=== Reminders ==="
236
+ say " - Is the version in package.json the one you mean to publish? $(node -e 'console.log(require("./package.json").version)')"
237
+ say " - Nothing here is committed or published - do that yourself."
187
238
 
188
239
  if (( ${#WARNINGS} )); then
189
240
  echo