@nodegx/mcp 0.2.5 → 0.2.7
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 +14 -7
- package/bin/noodl-mcp.js +14 -0
- package/dist/backend/LICENSE +21 -0
- package/dist/backend/cli.js +6534 -2836
- package/dist/backend/folder/start.cmd +4 -0
- package/dist/backend/folder/start.js +466 -0
- package/dist/backend/folder/start.sh +4 -0
- package/dist/backend/vm/provision-app.sh +235 -0
- package/dist/cloud-bundle.cjs +5051 -2381
- package/dist/kit-extract.cjs +5264 -2595
- package/dist/kit-extract.inputs.json +143 -113
- package/dist/node-catalog.json +44 -7
- package/dist/noodl-mcp.cjs +19819 -6657
- package/dist/package.json +1 -1
- package/dist/render-harness/data/node-catalog-enriched.json +1611 -257
- package/dist/render-harness/data/node-catalog.json +44 -7
- package/dist/render-harness/drive-steps.js +196 -21
- package/dist/render-harness/measure-from-disk.js +174 -22
- package/dist/render-harness/render-from-disk.js +136 -136
- package/dist/src/external/viewer/noodl.viewer.js +1 -1
- package/package.json +2 -3
- package/src/index.ts +0 -14
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# P112 OWN-016 / P26 DEP-002 — put a NodeGX app on a VM you own: the folder `nodegx-deploy --full-stack` writes (the
|
|
4
|
+
# app, its backend, its cloud functions, its access rules and `start.js`), run as a service, plus its function secrets.
|
|
5
|
+
# Modelled on P78's todo-digitalbricks/provision.sh, which put todo.digitalbricks.io on nexus-1.
|
|
6
|
+
#
|
|
7
|
+
# Run as root on the box, after the folder is staged in /tmp/<app>-provision — by @nodegx/mcp's deploy_to_vm, which
|
|
8
|
+
# ships this file in dist/backend/vm/, or by own-016/ship-to-vm.sh from a checkout:
|
|
9
|
+
#
|
|
10
|
+
# APP=lingo DOMAIN=lingo.nodegx.io PORT=8692 bash -s < provision-app.sh
|
|
11
|
+
# APP=lingo PORT=8692 CADDY=off bash -s < provision-app.sh # no public address yet (no DNS): the backend serves
|
|
12
|
+
# # the app on 127.0.0.1:<port> only (ssh -L to see it)
|
|
13
|
+
#
|
|
14
|
+
# SHARED HOST: other live sites may be served here, and Caddy loads the whole config or none of it. So this is
|
|
15
|
+
# strictly additive. It writes only:
|
|
16
|
+
#
|
|
17
|
+
# /opt/<app>/ the folder, replaced wholesale by every ship; read-only to the service
|
|
18
|
+
# /var/lib/<app>/data/ the app's data: records, uploads, the admin credential, and the cloud
|
|
19
|
+
# functions and rules start.js installs from the folder. A ship never
|
|
20
|
+
# writes here.
|
|
21
|
+
# /etc/<app>/secrets.env (written by ship-to-vm.sh over stdin, never here, never in the folder)
|
|
22
|
+
# /etc/systemd/system/<app>-backend.service
|
|
23
|
+
# /etc/caddy/conf.d/<app>.caddy (unless CADDY=off; deleted again if caddy validate fails)
|
|
24
|
+
# /opt/nodegx-native/sharp-<version>/ the image module that makes a photo's small versions, fetched from npm
|
|
25
|
+
# the first time any app ships here and shared by every app after
|
|
26
|
+
#
|
|
27
|
+
# and, once Caddy no longer points at it, removes /srv/<app>/ — the site the earlier layout (a site plus a loose
|
|
28
|
+
# cli.js) served from.
|
|
29
|
+
#
|
|
30
|
+
# The backend binds 127.0.0.1:<port> and serves the app AND its API on that one origin (`--serve-app`, set by
|
|
31
|
+
# start.js); Caddy is the only way in from outside. Behind a proxy every request arrives from loopback, so the admin
|
|
32
|
+
# surface is NOT proxied, and --no-admin is set. Secrets reach cloud functions through the backend's
|
|
33
|
+
# NODEGX_SECRET_<NAME> environment fallback (SecretsStore), from an EnvironmentFile only root and the app user read.
|
|
34
|
+
|
|
35
|
+
set -euo pipefail
|
|
36
|
+
|
|
37
|
+
APP="${APP:?APP=<name>}"
|
|
38
|
+
PORT="${PORT:?PORT=<loopback port>}"
|
|
39
|
+
CADDY="${CADDY:-on}"
|
|
40
|
+
DOMAIN="${DOMAIN:-}"
|
|
41
|
+
[ "$CADDY" = off ] || [ -n "$DOMAIN" ] || { echo "FATAL: DOMAIN=<host> is needed unless CADDY=off" >&2; exit 1; }
|
|
42
|
+
[[ "$APP" =~ ^[a-z][a-z0-9-]{1,30}$ ]] || { echo "FATAL: APP must be a short lowercase name" >&2; exit 1; }
|
|
43
|
+
[[ "$PORT" =~ ^[0-9]{2,5}$ ]] || { echo "FATAL: PORT must be a port number" >&2; exit 1; }
|
|
44
|
+
|
|
45
|
+
APP_USER="$APP"
|
|
46
|
+
FOLDER="/opt/$APP"
|
|
47
|
+
DATA_DIR="/var/lib/$APP/data"
|
|
48
|
+
OLD_SITE="/srv/$APP"
|
|
49
|
+
SECRETS="/etc/$APP/secrets.env"
|
|
50
|
+
UNIT="/etc/systemd/system/$APP-backend.service"
|
|
51
|
+
CADDY_MAIN=/etc/caddy/Caddyfile
|
|
52
|
+
CADDY_DROPIN="/etc/caddy/conf.d/$APP.caddy"
|
|
53
|
+
STAGE="/tmp/$APP-provision"
|
|
54
|
+
|
|
55
|
+
for f in start.js app/index.html backend/cli.js MANIFEST.json; do
|
|
56
|
+
[ -f "$STAGE/$f" ] || { echo "FATAL: $STAGE/$f missing — stage a full-stack folder (run ship-to-vm.sh)" >&2; exit 1; }
|
|
57
|
+
done
|
|
58
|
+
NODE="$(command -v node || true)"
|
|
59
|
+
[ -n "$NODE" ] || { echo "FATAL: this box has no node; the backend needs Node 22.13 or later" >&2; exit 1; }
|
|
60
|
+
"$NODE" -e 'const m=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")); process.exit(m.layout==="full-stack"?0:1)' \
|
|
61
|
+
"$STAGE/MANIFEST.json" || { echo "FATAL: $STAGE is not a full-stack deploy (MANIFEST.json layout)" >&2; exit 1; }
|
|
62
|
+
# start.js checks this too, but by then the service is installed; say it before anything is written.
|
|
63
|
+
"$NODE" -e 'require("node:sqlite")' 2>/dev/null \
|
|
64
|
+
|| { echo "FATAL: the backend needs Node 22.13 or later (node:sqlite); this box has $("$NODE" --version)" >&2; exit 1; }
|
|
65
|
+
if ss -tln | awk '{print $4}' | grep -qE ":${PORT}\$" && ! systemctl is-active --quiet "$APP-backend"; then
|
|
66
|
+
echo "FATAL: port $PORT is taken by something else" >&2; exit 1
|
|
67
|
+
fi
|
|
68
|
+
if [ "$CADDY" != off ]; then
|
|
69
|
+
[ -f "$CADDY_MAIN" ] || { echo "FATAL: Caddy is not set up on this server ($CADDY_MAIN is missing). Install Caddy, or ship with no domain. Nothing was changed." >&2; exit 1; }
|
|
70
|
+
# A freshly installed Caddy's file has no such line, so a stranger's first ship with a domain stops here. Adding it
|
|
71
|
+
# is theirs to do: this script never edits the main file, which other sites on a shared server depend on.
|
|
72
|
+
grep -qE '^\s*import\s+/etc/caddy/conf\.d/' "$CADDY_MAIN" || {
|
|
73
|
+
cat >&2 <<REFUSAL
|
|
74
|
+
FATAL: $CADDY_MAIN does not read /etc/caddy/conf.d/, where each app's site goes (a freshly installed Caddy's
|
|
75
|
+
file does not). Nothing was changed. ON THE SERVER, add the line once, then ship again:
|
|
76
|
+
|
|
77
|
+
mkdir -p /etc/caddy/conf.d && echo 'import /etc/caddy/conf.d/*' >> $CADDY_MAIN && systemctl reload caddy
|
|
78
|
+
REFUSAL
|
|
79
|
+
exit 1
|
|
80
|
+
}
|
|
81
|
+
fi
|
|
82
|
+
# 🔴 P26-VMOPEN: the earlier layout ran `cli.js serve` without --enforce-rules, so its first start minted
|
|
83
|
+
# security.json with "devOpen": true, and on a 127.0.0.1 bind the backend's interlock never refused it — the app was
|
|
84
|
+
# public behind Caddy with every access rule off. The staged start.js decides what a start will do with the rules
|
|
85
|
+
# (the project's replace the server's when this ship brings different ones — Richard, 2026-10-23) and refuses
|
|
86
|
+
# rules that are switched off. Ask it HERE, before anything is written, so a refusal leaves the running app as it is.
|
|
87
|
+
if ! CHECK="$("$NODE" "$STAGE/start.js" --check --data-dir "$DATA_DIR" 2>&1)"; then
|
|
88
|
+
echo "$CHECK" | sed 's/^/ /' >&2
|
|
89
|
+
cat >&2 <<REFUSAL
|
|
90
|
+
FATAL: refused before anything was changed: the app that is running now keeps running as it is.
|
|
91
|
+
If the file named is $DATA_DIR/security.json, either give the project its own rules (nodegx.security.json, which
|
|
92
|
+
a ship installs) or switch the server's on — ON THE SERVER (ssh in first; on a Mac this sed refuses the command):
|
|
93
|
+
|
|
94
|
+
sed -i 's/"devOpen": *true/"devOpen": false/' $DATA_DIR/security.json && systemctl restart $APP-backend
|
|
95
|
+
|
|
96
|
+
then ship again.
|
|
97
|
+
REFUSAL
|
|
98
|
+
exit 1
|
|
99
|
+
fi
|
|
100
|
+
echo "$CHECK" | sed 's/^/ /'
|
|
101
|
+
|
|
102
|
+
id -u "$APP_USER" >/dev/null 2>&1 || useradd --system --home-dir "/var/lib/$APP" --shell /usr/sbin/nologin "$APP_USER"
|
|
103
|
+
mkdir -p "$FOLDER" "$DATA_DIR" "/etc/$APP"
|
|
104
|
+
|
|
105
|
+
# The folder replaces what is there, wholesale (`app/` and `backend/` included). `data/` is excluded in case a folder
|
|
106
|
+
# was ever started by hand: the service's data is $DATA_DIR, never inside the folder.
|
|
107
|
+
rsync -a --delete --exclude=/data/ "$STAGE/" "$FOLDER/"
|
|
108
|
+
chown -R root:root "$FOLDER"; chmod -R u+rwX,go+rX,go-w "$FOLDER"
|
|
109
|
+
[ -f "$SECRETS" ] || install -o root -g "$APP_USER" -m 640 /dev/null "$SECRETS"
|
|
110
|
+
chown root:"$APP_USER" "$SECRETS"; chmod 640 "$SECRETS"
|
|
111
|
+
chown -R "$APP_USER":"$APP_USER" "/var/lib/$APP"; chmod 750 "/var/lib/$APP"
|
|
112
|
+
|
|
113
|
+
# 🔴 P112-OWN024-DEPLOYNOSHARP (Richard, 2026-10-25: the server fetches it once). A photo's small versions (?thumb=)
|
|
114
|
+
# are made by sharp, a native module the folder does not carry, so without it every thumbnail answers 501 and the
|
|
115
|
+
# marketplace draws its photos broken. It is fetched from npm ONCE per server into a place outside /opt/<app> (which
|
|
116
|
+
# every ship replaces), shared by every app here, and found through the unit's NODE_PATH. A server that cannot fetch
|
|
117
|
+
# it still ships: the app runs, and ?thumb= answers 501 until a later ship can. Pinned to the version the editor's
|
|
118
|
+
# backend ships (the lockfile's; tests/p112-own024-deploy-sharp.test.ts holds the two together).
|
|
119
|
+
SHARP_VERSION=0.33.5
|
|
120
|
+
NATIVE=/opt/nodegx-native
|
|
121
|
+
SHARP_DIR="$NATIVE/sharp-$SHARP_VERSION"
|
|
122
|
+
sharp_loads() { "$NODE" -e 'require(process.argv[1])' "$1/node_modules/sharp" >/dev/null 2>&1; }
|
|
123
|
+
if sharp_loads "$SHARP_DIR"; then
|
|
124
|
+
echo " photos: small versions (?thumb=) are made by sharp $SHARP_VERSION, already on this server"
|
|
125
|
+
else
|
|
126
|
+
mkdir -p "$NATIVE"
|
|
127
|
+
FETCH="$(mktemp -d "$NATIVE/.sharp-$SHARP_VERSION-XXXXXX")"
|
|
128
|
+
NPM="$(command -v npm || true)"
|
|
129
|
+
if [ -z "$NPM" ]; then
|
|
130
|
+
WHY="this server has no npm"
|
|
131
|
+
elif ! WHY="$(cd "$FETCH" && npm_config_cache="$FETCH/.cache" timeout 300 "$NPM" install --prefix "$FETCH" --omit=dev \
|
|
132
|
+
--no-audit --no-fund --loglevel=error --fetch-retries=1 "sharp@$SHARP_VERSION" 2>&1)"; then
|
|
133
|
+
# npm's last line points at a log in the cache this deletes; the line before it is the reason.
|
|
134
|
+
WHY="npm could not fetch it ($(echo "$WHY" | grep -v -e '^\s*$' -e 'complete log' | tail -1 | sed 's/^npm error //' || true))"
|
|
135
|
+
elif ! sharp_loads "$FETCH"; then
|
|
136
|
+
WHY="it was fetched but does not load on this server: $("$NODE" -e 'require(process.argv[1])' "$FETCH/node_modules/sharp" 2>&1 | grep -m1 -E '^[A-Za-z]*Error' || true)"
|
|
137
|
+
else
|
|
138
|
+
WHY=""
|
|
139
|
+
fi
|
|
140
|
+
rm -rf "$FETCH/.cache"
|
|
141
|
+
if [ -z "$WHY" ]; then
|
|
142
|
+
rm -rf "$SHARP_DIR"; mv "$FETCH" "$SHARP_DIR"; chmod -R go+rX,go-w "$SHARP_DIR"
|
|
143
|
+
echo " photos: fetched sharp $SHARP_VERSION from npm for this server's small photo versions (?thumb=), once"
|
|
144
|
+
else
|
|
145
|
+
rm -rf "$FETCH"
|
|
146
|
+
echo " photos: NOT AVAILABLE: small photo versions (?thumb=) answer 501 until sharp $SHARP_VERSION is on this server," \
|
|
147
|
+
"and the next ship fetches it again. $WHY. The app runs; uploads and full-size photos work."
|
|
148
|
+
fi
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
cat > "$UNIT" <<UNIT
|
|
152
|
+
[Unit]
|
|
153
|
+
Description=${APP} (NodeGX full-stack folder, P26 DEP-002)
|
|
154
|
+
After=network-online.target
|
|
155
|
+
Wants=network-online.target
|
|
156
|
+
|
|
157
|
+
[Service]
|
|
158
|
+
Type=simple
|
|
159
|
+
User=${APP_USER}
|
|
160
|
+
Group=${APP_USER}
|
|
161
|
+
WorkingDirectory=/var/lib/${APP}
|
|
162
|
+
EnvironmentFile=${SECRETS}
|
|
163
|
+
Environment=NODE_PATH=${SHARP_DIR}/node_modules
|
|
164
|
+
ExecStart=${NODE} ${FOLDER}/start.js --port ${PORT} --host 127.0.0.1 --data-dir ${DATA_DIR} --backend-id ${APP} --no-admin
|
|
165
|
+
Restart=always
|
|
166
|
+
RestartSec=3
|
|
167
|
+
|
|
168
|
+
NoNewPrivileges=true
|
|
169
|
+
PrivateTmp=true
|
|
170
|
+
ProtectSystem=strict
|
|
171
|
+
ProtectHome=true
|
|
172
|
+
ReadWritePaths=/var/lib/${APP}
|
|
173
|
+
ProtectKernelTunables=true
|
|
174
|
+
ProtectControlGroups=true
|
|
175
|
+
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
|
176
|
+
MemoryMax=384M
|
|
177
|
+
|
|
178
|
+
[Install]
|
|
179
|
+
WantedBy=multi-user.target
|
|
180
|
+
UNIT
|
|
181
|
+
|
|
182
|
+
if [ "$CADDY" != off ]; then
|
|
183
|
+
# Everything but the closed admin surface goes to the backend, which serves the app beside its API and answers
|
|
184
|
+
# /nodegx-config.json with this origin (Host + X-Forwarded-Proto, both of which reverse_proxy passes).
|
|
185
|
+
cat > "$CADDY_DROPIN" <<CADDYFILE
|
|
186
|
+
${DOMAIN} {
|
|
187
|
+
encode zstd gzip
|
|
188
|
+
|
|
189
|
+
header {
|
|
190
|
+
X-Content-Type-Options nosniff
|
|
191
|
+
Referrer-Policy strict-origin-when-cross-origin
|
|
192
|
+
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
|
193
|
+
-Server
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@closed path_regexp closed ^/(_admin|admin|executions|metrics)(/|$)
|
|
197
|
+
handle @closed {
|
|
198
|
+
respond 404
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
handle {
|
|
202
|
+
reverse_proxy 127.0.0.1:${PORT}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
CADDYFILE
|
|
206
|
+
if ! caddy validate --config "$CADDY_MAIN" --adapter caddyfile >/dev/null 2>&1; then
|
|
207
|
+
echo "FATAL: Caddy config invalid with the new drop-in — removing it, running config untouched." >&2
|
|
208
|
+
rm -f "$CADDY_DROPIN"
|
|
209
|
+
caddy validate --config "$CADDY_MAIN" --adapter caddyfile >&2 || true
|
|
210
|
+
exit 1
|
|
211
|
+
fi
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
systemctl daemon-reload
|
|
215
|
+
systemctl enable "$APP-backend" >/dev/null
|
|
216
|
+
# This start's lines only: the journal also holds every earlier start's, which said other things about the rules.
|
|
217
|
+
STARTED="$(date '+%Y-%m-%d %H:%M:%S')"
|
|
218
|
+
systemctl restart "$APP-backend"
|
|
219
|
+
for i in $(seq 1 20); do
|
|
220
|
+
curl -fsS "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1 && break
|
|
221
|
+
sleep 1
|
|
222
|
+
done
|
|
223
|
+
curl -fsS "http://127.0.0.1:${PORT}/health" >/dev/null || { echo "FATAL: backend not healthy" >&2; journalctl -u "$APP-backend" --since "$STARTED" -n 40 --no-pager >&2; exit 1; }
|
|
224
|
+
curl -fsS -o /dev/null "http://127.0.0.1:${PORT}/" || { echo "FATAL: the backend is up but does not serve the app at /" >&2; exit 1; }
|
|
225
|
+
# What start.js installed and which access rules are in force, in the backend's own words.
|
|
226
|
+
journalctl -u "$APP-backend" --since "$STARTED" --no-pager -o cat | grep -E '^\[start\]|polic|rules' | sed 's/^/ /' || true
|
|
227
|
+
|
|
228
|
+
[ "$CADDY" != off ] && systemctl reload caddy
|
|
229
|
+
# Only once no drop-in can still point at it: CADDY=off over an earlier domain ship leaves that drop-in in place.
|
|
230
|
+
if [ -d "$OLD_SITE" ] && { [ "$CADDY" != off ] || [ ! -f "$CADDY_DROPIN" ]; }; then
|
|
231
|
+
rm -rf "$OLD_SITE"
|
|
232
|
+
echo " removed $OLD_SITE (the earlier layout's site; the backend serves the app now)"
|
|
233
|
+
fi
|
|
234
|
+
rm -rf "$STAGE"
|
|
235
|
+
echo "==> provisioned ${APP} on 127.0.0.1:${PORT}${DOMAIN:+, https://$DOMAIN}"
|