@3d-print-shop/installer 1.0.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.
Files changed (2) hide show
  1. package/install.sh +592 -0
  2. package/package.json +32 -0
package/install.sh ADDED
@@ -0,0 +1,592 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # AIDEV-NOTE: this exists because the shop refuses to make its own data directory, and that refusal is
5
+ # deliberate - a service that creates whatever path it was pointed at puts a queue somewhere nobody
6
+ # is looking, and a typo is then work lost rather than a shop that will not start. See
7
+ # design/3d-print-shop.md, "The installer creates the root; the service never does". So the two
8
+ # directories, the user that owns them, and the supervisor that starts the process are this file's.
9
+ #
10
+ # Nothing here writes a credential. `init` does that, as the service user, and it is the one step
11
+ # that has to be a person's - the token it answers with exists nowhere else.
12
+
13
+ LABEL=com.dcorbin.3d-print-shop
14
+ # AIDEV-NOTE: three places rather than one, because the three things a shop keeps are three kinds and
15
+ # the system says where each goes: work awaiting processing, state that outlives a restart, and a
16
+ # claim that must not. The service works these out itself from `systemLayout` - what the installer
17
+ # does is MAKE the two that have to be there before it starts. The third is /var/run, which is
18
+ # emptied by a boot and so is the shop's own to create every time.
19
+ JOBS=/var/spool/3d-print-shop/jobs
20
+ STATE=/var/lib/3d-print-shop
21
+ ETC=/etc/3d-print-shop
22
+ OUT_LOG=/var/log/3d-print-shop.log
23
+ ERR_LOG=/var/log/3d-print-shop.err.log
24
+
25
+ # AIDEV-NOTE: the shop is COPIED here rather than run out of a checkout, because the service user is
26
+ # not the developer and a home directory is not theirs to walk into - `/Users/<somebody>` is 0750 on
27
+ # macOS, so a daemon pointed at a checkout inside one cannot read a byte of it. Copying also means a
28
+ # `git checkout` of another branch is not a live change to the running service.
29
+ INSTALL_DIR=/usr/local/lib/3d-print-shop
30
+
31
+ HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
32
+
33
+ # AIDEV-NOTE: two ways this file is reached, and the difference is who put the code somewhere the
34
+ # service can read. Exploded by npm, the server is a sibling in the same scope and npm has already
35
+ # resolved everything it needs - so there is nothing to copy and copying would be a second, staler
36
+ # tree. In a checkout there is a build, sitting where a daemon cannot get at it.
37
+ case "$HERE" in
38
+ */node_modules/@3d-print-shop/installer) MODE=package ;;
39
+ *) MODE=checkout ;;
40
+ esac
41
+
42
+ if [ "$MODE" = package ]; then
43
+ BUILT="$HERE/../server/dist/main.js"
44
+ SERVER=$BUILT
45
+ MANIFEST="$HERE/package.json"
46
+ BUILT_PAGE="$HERE/../ui/dist"
47
+ PAGE=$BUILT_PAGE
48
+ else
49
+ REPO=$(cd "$HERE/../.." && pwd)
50
+ BUILT="$REPO/packages/server/dist/main.js"
51
+ SERVER="$INSTALL_DIR/packages/server/dist/main.js"
52
+ MANIFEST="$REPO/package.json"
53
+ BUILT_PAGE="$REPO/packages/ui/dist"
54
+ PAGE="$INSTALL_DIR/packages/ui/dist"
55
+ fi
56
+
57
+ # AIDEV-NOTE: always said, because the page is not optional - `requirePage` has already refused an
58
+ # install without one. It used to be left out when the directory was missing, which is how a shop
59
+ # that served nobody anything came up looking like a successful install.
60
+ pageArguments() {
61
+ printf ' <string>--page</string>\n <string>%s</string>\n' "$PAGE"
62
+ }
63
+
64
+ pageOption() {
65
+ printf ' --page %s' "$PAGE"
66
+ }
67
+
68
+ PLIST="/Library/LaunchDaemons/$LABEL.plist"
69
+ UNIT=/etc/systemd/system/3d-print-shop.service
70
+
71
+ case "$(uname -s)" in
72
+ Darwin) PLATFORM=macos; SHOP_USER=_printshop; ROOT_GROUP=wheel ;;
73
+ Linux) PLATFORM=linux; SHOP_USER=printshop; ROOT_GROUP=root ;;
74
+ *) echo "3d-print-shop installs on macOS and Linux, not $(uname -s)" >&2; exit 1 ;;
75
+ esac
76
+ SHOP_GROUP=$SHOP_USER
77
+
78
+ say() { printf '%s\n' "$*"; }
79
+ refuse() { printf '%s\n' "$*" >&2; exit 1; }
80
+
81
+ usage() {
82
+ cat <<USAGE
83
+ usage: sudo $0 [install|update|uninstall]
84
+
85
+ install make the data and credentials directories, the user that owns them, the copy of the
86
+ shop the service runs, and the service itself
87
+ update copy a fresh build over the installed one and restart - what to run after 'yarn build',
88
+ and only from a checkout: an installed package is updated by installing it again
89
+ uninstall stop the service and remove it - the data, the credentials and the user are left,
90
+ because they hold work and secrets this script did not create
91
+ USAGE
92
+ }
93
+
94
+ # Named as the person would type it, which is the bin npm put on their PATH once there is one.
95
+ asTyped() {
96
+ if [ "$MODE" = package ]; then printf '3d-print-shop-install %s' "$1"; else printf '%s %s' "$0" "$1"; fi
97
+ }
98
+
99
+ requireRoot() {
100
+ [ "$(id -u)" = 0 ] || refuse "this writes under /var, /etc and /usr/local, so it needs sudo: sudo $(asTyped "$1")"
101
+ }
102
+
103
+ # AIDEV-NOTE: the page is REQUIRED, and this is where that is enforced. The shop is installed for a
104
+ # person to use and the page is how a person uses it - it is where somebody logs in, watches a
105
+ # machine and gives a verdict on what came off the bed. A shop answering only its API is a shop
106
+ # nobody in the workshop can do any of that with, so it is refused rather than installed and
107
+ # mentioned. Shaped like `requireBuild` below, and for the same reason: each mode is told the remedy
108
+ # it actually has.
109
+ requirePage() {
110
+ [ -d "$BUILT_PAGE" ] && return 0
111
+
112
+ if [ "$MODE" = package ]; then
113
+ refuse "$BUILT_PAGE is not there - @3d-print-shop/ui is not installed beside this"
114
+ fi
115
+
116
+ refuse "$BUILT_PAGE is not there - run 'yarn install && yarn build' in $REPO first, which builds it"
117
+ }
118
+
119
+ requireBuild() {
120
+ if [ ! -f "$BUILT" ]; then
121
+ if [ "$MODE" = package ]; then
122
+ refuse "$BUILT is not there - @3d-print-shop/server is not installed beside this"
123
+ fi
124
+
125
+ refuse "$BUILT is not there - run 'yarn install && yarn build' in $REPO first"
126
+ fi
127
+
128
+ # AIDEV-NOTE: the same rule the node has to pass, and for the same reason - an npm run under a
129
+ # version manager explodes its global packages inside the home directory, which is the one place
130
+ # the service user cannot walk into. Checked here rather than left to fail at boot with a daemon
131
+ # that starts, finds nothing, and is restarted for ever.
132
+ if [ "$MODE" = package ]; then
133
+ case "$HERE" in
134
+ /Users/* | /home/* | "$HOME"/*)
135
+ refuse "this is installed under $HERE, and the service runs as $SHOP_USER, which cannot read
136
+ into a home directory. Install it with a node that lives outside one (brew install node@24, or your
137
+ distribution's package), so that npm puts its global packages outside one too"
138
+ ;;
139
+ esac
140
+ fi
141
+ }
142
+
143
+ # The floor whichever manifest is in play already declares, so this script says it nowhere: the
144
+ # repo's when run from a checkout, the installed package's own when npm exploded one. The ceiling in
145
+ # `engines` is the package manager's business; what matters here is not running the service on
146
+ # something older than the code was written for.
147
+ requiredNode() {
148
+ local floor
149
+ floor=$(sed -n 's/.*"node" *: *">=\([0-9][0-9.]*\).*/\1/p' "$MANIFEST" | head -1)
150
+
151
+ # Said rather than shrugged at: a floor that came back empty would let every node past the check,
152
+ # which is the version guard silently not being one.
153
+ [ -n "$floor" ] || refuse "$MANIFEST does not say which node this runs on, as engines.node"
154
+
155
+ printf '%s\n' "$floor"
156
+ }
157
+
158
+ isAtLeast() {
159
+ [ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -1)" = "$2" ]
160
+ }
161
+
162
+ # AIDEV-NOTE: a node under somebody's home directory is the one that cannot work here, and it is
163
+ # also the one a developer has - a version manager installs per user, and the daemon runs as
164
+ # somebody else. Found rather than assumed, and refused with the fix rather than installed into a
165
+ # service that would never start.
166
+ findNode() {
167
+ local wanted=$1 candidate version found=''
168
+ local major=${wanted%%.*}
169
+
170
+ for candidate in \
171
+ ${PRINT_SHOP_NODE:-} \
172
+ "/opt/homebrew/opt/node@$major/bin/node" \
173
+ /opt/homebrew/bin/node \
174
+ "/usr/local/opt/node@$major/bin/node" \
175
+ /usr/local/bin/node \
176
+ /usr/bin/node \
177
+ "$(command -v node || true)"; do
178
+ if [ -z "$candidate" ] || [ ! -x "$candidate" ]; then continue; fi
179
+ case "$candidate" in /Users/* | /home/* | "$HOME"/*) continue ;; esac
180
+
181
+ version=$("$candidate" -v 2>/dev/null | tr -d v)
182
+ if isAtLeast "$version" "$wanted"; then
183
+ printf '%s\n' "$candidate"
184
+ return 0
185
+ fi
186
+
187
+ found="$found
188
+ $candidate (v$version)"
189
+ done
190
+
191
+ refuse "no node of $wanted or newer outside a home directory.${found:+ What is here:$found}
192
+
193
+ The shop runs as $SHOP_USER, which cannot read a node a version manager installed under yours.
194
+ Install one system-wide (brew install node@$major, or your distribution's package), or say
195
+ PRINT_SHOP_NODE=/path/to/node"
196
+ }
197
+
198
+ madeDirectory() {
199
+ local directory=$1
200
+
201
+ mkdir -p "$directory"
202
+ chown "$SHOP_USER:$SHOP_GROUP" "$directory"
203
+
204
+ # 0700 and nothing looser: a job directory another user could rename away is a print that silently
205
+ # never happens, and the keys under /etc open the printers directly.
206
+ chmod 700 "$directory"
207
+ say " $directory ($SHOP_USER, 0700)"
208
+ }
209
+
210
+ # What the service reads, so it is root's and readable by everybody - the opposite of the two
211
+ # directories above, and for the opposite reason: there is nothing secret in it, and the one thing
212
+ # that must not happen is the user the service runs as being able to rewrite its own code.
213
+ copiedIn() {
214
+ local what=$1 into="$INSTALL_DIR/$2"
215
+
216
+ mkdir -p "$into"
217
+ rm -rf "${into:?}/$(basename "$what")"
218
+ cp -R "$what" "$into/"
219
+ }
220
+
221
+ copiedTheBuild() {
222
+ local package
223
+
224
+ copiedIn "$REPO/package.json" ''
225
+ for package in client server; do
226
+ copiedIn "$REPO/packages/$package/package.json" "packages/$package"
227
+ copiedIn "$REPO/packages/$package/dist" "packages/$package"
228
+ done
229
+
230
+ # The page. Files rather than a module: the shop is pointed at the directory and told nothing about
231
+ # what is in it. Not conditional - `requirePage` has already refused an install without one.
232
+ copiedIn "$BUILT_PAGE" 'packages/ui'
233
+ }
234
+
235
+ # AIDEV-NOTE: node_modules whole, symlinks and all. Yarn links a workspace as a RELATIVE symlink
236
+ # (`@3d-print-shop/client -> ../../packages/client`), so a copy that keeps the layout keeps the
237
+ # links working, and `cp -R` copies a symlink as a symlink rather than following it.
238
+ copiedEverything() {
239
+ copiedTheBuild
240
+ copiedIn "$REPO/node_modules" ''
241
+ droppedTheLinksThatLeadNowhere
242
+
243
+ chown -R "root:$ROOT_GROUP" "$INSTALL_DIR"
244
+ chmod -R a+rX,go-w "$INSTALL_DIR"
245
+ }
246
+
247
+ # AIDEV-NOTE: only the packages the service RUNS are copied, so a workspace link to one left behind -
248
+ # the octoprint-sim the tests print against - arrives pointing at nothing. Nothing resolves it at
249
+ # runtime, and it is removed anyway: a link that leads nowhere is not part of an install, and the
250
+ # next person to look in here should not have to work out whether it matters.
251
+ #
252
+ # Just this directory, because a workspace is the only thing yarn links rather than copies.
253
+ droppedTheLinksThatLeadNowhere() {
254
+ local link
255
+
256
+ for link in "$INSTALL_DIR/node_modules/@3d-print-shop"/*; do
257
+ if [ -L "$link" ] && [ ! -e "$link" ]; then rm -f "$link"; fi
258
+ done
259
+ }
260
+
261
+ # ---------------------------------------------------------------------------- macOS
262
+
263
+ freeSystemId() {
264
+ local used candidate
265
+ used=$( (dscl . -list /Users UniqueID; dscl . -list /Groups PrimaryGroupID) | awk '{print $2}')
266
+
267
+ # 200-400 is where macOS keeps its own daemon users, and a uid below 500 is hidden from the login
268
+ # window without anything else having to say so.
269
+ for candidate in $(seq 200 400); do
270
+ grep -qx "$candidate" <<<"$used" || { printf '%s\n' "$candidate"; return 0; }
271
+ done
272
+
273
+ refuse 'every system id between 200 and 400 is taken'
274
+ }
275
+
276
+ madeServiceUserOnMacos() {
277
+ if dscl . -read "/Users/$SHOP_USER" >/dev/null 2>&1; then
278
+ say " $SHOP_USER is already here"
279
+ return 0
280
+ fi
281
+
282
+ local id
283
+ id=$(freeSystemId)
284
+
285
+ dscl . -create "/Groups/$SHOP_GROUP"
286
+ dscl . -create "/Groups/$SHOP_GROUP" PrimaryGroupID "$id"
287
+ dscl . -create "/Groups/$SHOP_GROUP" RealName '3D Print Shop'
288
+ dscl . -create "/Groups/$SHOP_GROUP" Password '*'
289
+
290
+ dscl . -create "/Users/$SHOP_USER"
291
+ dscl . -create "/Users/$SHOP_USER" UniqueID "$id"
292
+ dscl . -create "/Users/$SHOP_USER" PrimaryGroupID "$id"
293
+ dscl . -create "/Users/$SHOP_USER" RealName '3D Print Shop'
294
+ dscl . -create "/Users/$SHOP_USER" NFSHomeDirectory /var/empty
295
+ dscl . -create "/Users/$SHOP_USER" UserShell /usr/bin/false
296
+ dscl . -create "/Users/$SHOP_USER" IsHidden 1
297
+ dscl . -create "/Users/$SHOP_USER" Password '*'
298
+
299
+ say " $SHOP_USER (uid $id), which owns the data directory and can be logged in as by nobody"
300
+ }
301
+
302
+ wroteLog() {
303
+ local file=$1
304
+
305
+ touch "$file"
306
+ chown "$SHOP_USER:$SHOP_GROUP" "$file"
307
+ chmod 640 "$file"
308
+ }
309
+
310
+ wrotePlist() {
311
+ local node=$1
312
+
313
+ # AIDEV-NOTE: KeepAlive only where it did NOT exit cleanly. `3d-print-shop shutdown` is an
314
+ # operator asking it to stop, and a plain KeepAlive would start it again a second later - which is
315
+ # a shop that cannot be stopped without unloading the daemon.
316
+ cat >"$PLIST" <<PLIST
317
+ <?xml version="1.0" encoding="UTF-8"?>
318
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
319
+ <plist version="1.0">
320
+ <dict>
321
+ <key>Label</key><string>$LABEL</string>
322
+ <key>ProgramArguments</key>
323
+ <array>
324
+ <string>$node</string>
325
+ <string>$SERVER</string>
326
+ <string>serve</string>
327
+ $(pageArguments) </array>
328
+ <key>UserName</key><string>$SHOP_USER</string>
329
+ <key>GroupName</key><string>$SHOP_GROUP</string>
330
+ <key>WorkingDirectory</key><string>$STATE</string>
331
+ <key>RunAtLoad</key><true/>
332
+ <key>KeepAlive</key>
333
+ <dict>
334
+ <key>SuccessfulExit</key><false/>
335
+ </dict>
336
+ <key>StandardOutPath</key><string>$OUT_LOG</string>
337
+ <key>StandardErrorPath</key><string>$ERR_LOG</string>
338
+ </dict>
339
+ </plist>
340
+ PLIST
341
+
342
+ # launchd refuses a plist anybody but root can write to.
343
+ chown "root:$ROOT_GROUP" "$PLIST"
344
+ chmod 644 "$PLIST"
345
+ say " $PLIST"
346
+ }
347
+
348
+ # ---------------------------------------------------------------------------- Linux
349
+
350
+ madeServiceUserOnLinux() {
351
+ if getent passwd "$SHOP_USER" >/dev/null; then
352
+ say " $SHOP_USER is already here"
353
+ return 0
354
+ fi
355
+
356
+ useradd --system --user-group --home-dir /nonexistent --shell /usr/sbin/nologin "$SHOP_USER"
357
+ say " $SHOP_USER, which owns the data directory and can be logged in as by nobody"
358
+ }
359
+
360
+ wroteUnit() {
361
+ local node=$1
362
+
363
+ # Restart=on-failure rather than always, for the reason the plist keeps KeepAlive conditional:
364
+ # `3d-print-shop shutdown` is somebody asking it to stop. stdout is the shop's log and journald is
365
+ # what captures it, so there is no file here to rotate.
366
+ cat >"$UNIT" <<UNIT
367
+ [Unit]
368
+ Description=3D Print Shop
369
+ After=network-online.target
370
+ Wants=network-online.target
371
+
372
+ [Service]
373
+ ExecStart=$node $SERVER serve$(pageOption)
374
+ ExecReload=/bin/kill -HUP \$MAINPID
375
+ User=$SHOP_USER
376
+ Group=$SHOP_GROUP
377
+ WorkingDirectory=$STATE
378
+ Restart=on-failure
379
+ RestartSec=5
380
+ KillSignal=SIGTERM
381
+ StandardOutput=journal
382
+ StandardError=journal
383
+
384
+ [Install]
385
+ WantedBy=multi-user.target
386
+ UNIT
387
+
388
+ chmod 644 "$UNIT"
389
+ say " $UNIT"
390
+ }
391
+
392
+ # ---------------------------------------------------------------------------- starting and stopping
393
+
394
+ startIt() {
395
+ if [ "$PLATFORM" = macos ]; then
396
+ launchctl bootout "system/$LABEL" 2>/dev/null || true
397
+ launchctl enable "system/$LABEL"
398
+ launchctl bootstrap system "$PLIST"
399
+ else
400
+ systemctl daemon-reload
401
+ systemctl enable --now 3d-print-shop
402
+ fi
403
+ }
404
+
405
+ stopIt() {
406
+ if [ "$PLATFORM" = macos ]; then
407
+ launchctl bootout "system/$LABEL" 2>/dev/null || true
408
+ else
409
+ systemctl stop 3d-print-shop 2>/dev/null || true
410
+ fi
411
+ }
412
+
413
+ isInstalled() {
414
+ if [ "$PLATFORM" = macos ]; then [ -f "$PLIST" ]; else [ -f "$UNIT" ]; fi
415
+ }
416
+
417
+ restarting() {
418
+ if [ "$PLATFORM" = macos ]; then
419
+ printf 'sudo launchctl kickstart -k system/%s' "$LABEL"
420
+ else
421
+ printf 'sudo systemctl restart 3d-print-shop'
422
+ fi
423
+ }
424
+
425
+ reloading() {
426
+ if [ "$PLATFORM" = macos ]; then
427
+ printf 'sudo launchctl kill HUP system/%s' "$LABEL"
428
+ else
429
+ printf 'sudo systemctl reload 3d-print-shop'
430
+ fi
431
+ }
432
+
433
+ reading() {
434
+ if [ "$PLATFORM" = macos ]; then
435
+ printf 'tail -f %s' "$OUT_LOG"
436
+ else
437
+ printf 'journalctl -u 3d-print-shop -f'
438
+ fi
439
+ }
440
+
441
+ # ---------------------------------------------------------------------------- what it does
442
+
443
+ install() {
444
+ requireRoot install
445
+ requireBuild
446
+ requirePage
447
+
448
+ local wanted node
449
+ wanted=$(requiredNode)
450
+ node=$(findNode "$wanted")
451
+
452
+ say 'the user it runs as:'
453
+ if [ "$PLATFORM" = macos ]; then madeServiceUserOnMacos; else madeServiceUserOnLinux; fi
454
+
455
+ say 'what it keeps its work and its credentials in:'
456
+ madeDirectory "$JOBS"
457
+ madeDirectory "$STATE"
458
+ madeDirectory "$ETC"
459
+
460
+ say 'the shop itself:'
461
+ stopIt
462
+ if [ "$MODE" = package ]; then
463
+ say " $(cd "$HERE/.." && pwd) (where npm put it)"
464
+ else
465
+ copiedEverything
466
+ say " $INSTALL_DIR (root, and readable by everybody - there is nothing secret in it)"
467
+ fi
468
+
469
+ say " $PAGE (the page it serves beside the API)"
470
+
471
+ say 'what supervises it:'
472
+ if [ "$PLATFORM" = macos ]; then
473
+ wroteLog "$OUT_LOG"
474
+ wroteLog "$ERR_LOG"
475
+ wrotePlist "$node"
476
+ else
477
+ wroteUnit "$node"
478
+ fi
479
+
480
+ # AIDEV-NOTE: NOT started where there are no callers yet. Every route names its caller, so a shop
481
+ # with none refuses to start - and a supervisor would then restart it every few seconds for as
482
+ # long as the machine was up, which is a log nobody can read and a fault that reads like a bug.
483
+ if [ ! -f "$ETC/callers.json" ]; then
484
+ cat <<NEXT
485
+
486
+ Installed, and not started: this shop has no callers yet, and one with none refuses to start.
487
+
488
+ Give it its first admin, as the user it runs as - the token is said once and kept nowhere else:
489
+
490
+ sudo -u $SHOP_USER $node $SERVER init <your-name>
491
+
492
+ Then keep that token where the client looks for it, as yourself:
493
+
494
+ mkdir -p ~/.config/3d-print-shop && chmod 700 ~/.config/3d-print-shop
495
+ printf %s '<the token>' > ~/.config/3d-print-shop/token
496
+ chmod 600 ~/.config/3d-print-shop/token
497
+
498
+ Each printer's API key goes in $ETC/printer-keys.json as {"mk4": "..."}, 0600 and owned by
499
+ $SHOP_USER. Then run this again to start it:
500
+
501
+ sudo $(asTyped install)
502
+ NEXT
503
+ return 0
504
+ fi
505
+
506
+ startIt
507
+
508
+ cat <<RUNNING
509
+
510
+ Installed and running, on http://localhost:7373 - loopback, which is where a token travelling in
511
+ the clear belongs.
512
+
513
+ its log $(reading)
514
+ a new build $(if [ "$MODE" = package ]; then printf 'sudo npm i -g @3d-print-shop/installer'; else printf 'sudo %s update' "$0"; fi)
515
+ a credential edit a file in $ETC, then $(reloading)
516
+ RUNNING
517
+ }
518
+
519
+ update() {
520
+ requireRoot update
521
+ requireBuild
522
+ requirePage
523
+ [ "$MODE" = checkout ] || refuse 'an installed package is updated by installing it again: sudo npm i -g @3d-print-shop/installer'
524
+ isInstalled || refuse "there is no service to update - run 'sudo $(asTyped install)' first"
525
+
526
+ stopIt
527
+ copiedTheBuild
528
+ chown -R "root:$ROOT_GROUP" "$INSTALL_DIR"
529
+ chmod -R a+rX,go-w "$INSTALL_DIR"
530
+ startIt
531
+
532
+ say "the build in $REPO is now the one running - $(reading) to see it come up"
533
+ }
534
+
535
+ uninstall() {
536
+ requireRoot uninstall
537
+
538
+ stopIt
539
+ if [ "$PLATFORM" = macos ]; then
540
+ rm -f "$PLIST"
541
+ say "stopped, and $PLIST is gone"
542
+ else
543
+ rm -f "$UNIT"
544
+ systemctl daemon-reload
545
+ say "stopped, and $UNIT is gone"
546
+ fi
547
+
548
+ if [ -d "$INSTALL_DIR" ]; then
549
+ rm -rf "$INSTALL_DIR"
550
+ say "$INSTALL_DIR is gone"
551
+ fi
552
+
553
+ # AIDEV-NOTE: the data holds work nobody has judged and /etc holds every token the shop knows.
554
+ # Neither is this script's to throw away on the way out - uninstalling a service is not the same
555
+ # act as discarding what it was holding, and one of them cannot be undone.
556
+ cat <<KEPT
557
+
558
+ Left alone, because they hold work and secrets rather than installation:
559
+
560
+ $JOBS the work it was holding
561
+ $STATE its printers and sessions
562
+ $ETC its callers and its printer keys
563
+ $SHOP_USER the user that owns both
564
+
565
+ Remove them yourself if that is what you mean.
566
+ KEPT
567
+ }
568
+
569
+ case "${1:-install}" in
570
+ # AIDEV-NOTE: what npm's postinstall calls, and it is a separate word from `install` because yarn
571
+ # runs it on every install in the CHECKOUT too, where nobody asked for a daemon and there is
572
+ # nothing to explode. It also must not fail: a postinstall that exits non-zero fails the whole npm
573
+ # install, so a person who has not used sudo is told what to type rather than shouted at.
574
+ #
575
+ # npm drops to the owner of its prefix when it is run as root, so being root here is not something
576
+ # to count on even under sudo.
577
+ from-npm)
578
+ [ "$MODE" = package ] || exit 0
579
+ if [ "$(id -u)" != 0 ]; then
580
+ say "3d-print-shop is unpacked. It makes a system user, a data directory and a daemon, so the install
581
+ itself is one more command: sudo 3d-print-shop-install"
582
+ exit 0
583
+ fi
584
+
585
+ install
586
+ ;;
587
+ install | --install) install ;;
588
+ update | --update) update ;;
589
+ uninstall | --uninstall) uninstall ;;
590
+ -h | --help | help) usage ;;
591
+ *) usage >&2; exit 1 ;;
592
+ esac
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@3d-print-shop/installer",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Installs 3d-print-shop as a service: the user that owns its work, the directories it keeps it in, and launchd or systemd to run it",
6
+ "author": "dcorbin",
7
+ "license": "PolyForm-Noncommercial-1.0.0",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/dcorbin-apps/3d-print-shop.git",
11
+ "directory": "packages/installer"
12
+ },
13
+ "bin": {
14
+ "3d-print-shop-install": "./install.sh"
15
+ },
16
+ "files": [
17
+ "install.sh"
18
+ ],
19
+ "scripts": {
20
+ "postinstall": "./install.sh from-npm",
21
+ "build": "echo \"nothing to build - this package is a shell script\" && exit 0",
22
+ "typecheck": "echo \"no typescript here\" && exit 0",
23
+ "test": "echo \"tests run from root\" && exit 0"
24
+ },
25
+ "dependencies": {
26
+ "@3d-print-shop/server": "1.0.0",
27
+ "@3d-print-shop/ui": "1.0.0"
28
+ },
29
+ "engines": {
30
+ "node": ">=24.16 <25"
31
+ }
32
+ }