thinkncollab-shell 2.0.0-beta.2 → 2.0.0-beta.4

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 (52) hide show
  1. package/Readme.md +43 -10
  2. package/dist/native/brand/favicon-64.png +0 -0
  3. package/dist/native/brand/tnc.ico +0 -0
  4. package/dist/native/tnc-tray/build-macos.sh +59 -0
  5. package/dist/native/tnc-tray/main.swift +204 -0
  6. package/dist/native/tnc-tray/tray.ps1 +137 -0
  7. package/dist/src/commands/dock/containers.js +1 -1
  8. package/dist/src/commands/dock/dock.js +1 -1
  9. package/dist/src/dock/api/routes.js +1 -1
  10. package/dist/src/dock/api/server.js +1 -1
  11. package/dist/src/dock/api/websocket.js +7 -0
  12. package/dist/src/dock/api/wire.js +1 -1
  13. package/dist/src/dock/build/builder.js +1 -1
  14. package/dist/src/dock/build/buildkit.js +1 -1
  15. package/dist/src/dock/build/cache.js +7 -0
  16. package/dist/src/dock/build/sshagent.js +7 -0
  17. package/dist/src/dock/cli/api.js +1 -1
  18. package/dist/src/dock/cli/commands.js +1 -1
  19. package/dist/src/dock/cli/compose.js +1 -1
  20. package/dist/src/dock/cli/trust.js +7 -0
  21. package/dist/src/dock/compat/cases.js +1 -1
  22. package/dist/src/dock/config.js +1 -1
  23. package/dist/src/dock/daemon-main.js +1 -1
  24. package/dist/src/dock/daemon.js +1 -1
  25. package/dist/src/dock/engine.js +1 -1
  26. package/dist/src/dock/image/auth.js +1 -1
  27. package/dist/src/dock/image/credhelpers.js +7 -0
  28. package/dist/src/dock/image/keychain.js +7 -0
  29. package/dist/src/dock/image/registry.js +1 -1
  30. package/dist/src/dock/image/scan.js +7 -0
  31. package/dist/src/dock/image/sign.js +7 -0
  32. package/dist/src/dock/linux/host.js +1 -1
  33. package/dist/src/dock/paths.js +1 -1
  34. package/dist/src/dock/runtime/cdi.js +7 -0
  35. package/dist/src/dock/runtime/fsops.js +1 -1
  36. package/dist/src/dock/runtime/network.js +1 -1
  37. package/dist/src/dock/runtime/overlaydns.js +7 -0
  38. package/dist/src/dock/runtime/ports.js +1 -1
  39. package/dist/src/dock/runtime/rootfs.js +1 -1
  40. package/dist/src/dock/runtime/spec.js +1 -1
  41. package/dist/src/dock/runtime/stats.js +1 -1
  42. package/dist/src/dock/runtime/userns.js +7 -0
  43. package/dist/src/dock/runtime/volumes.js +1 -1
  44. package/dist/src/dock/setup/autostart.js +1 -1
  45. package/dist/src/dock/setup/diagnose.js +7 -0
  46. package/dist/src/dock/setup/index.js +1 -1
  47. package/dist/src/dock/tray/agent.js +7 -0
  48. package/dist/src/dock/tray/install.js +7 -0
  49. package/dist/src/dock/ui/page.js +232 -16
  50. package/dist/src/dock/ui/server.js +1 -1
  51. package/package.json +1 -1
  52. package/dist/src/dock/setup/app.js +0 -7
@@ -4,4 +4,4 @@
4
4
  * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
5
  * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
6
  */
7
- "use strict";const SAMPLE='\ncg="$1"; pid="$2"\n[ -d "$cg" ] || { echo missing 1; exit 0; }\necho "cpu_usage_usec $(awk \'$1=="usage_usec"{print $2}\' "$cg/cpu.stat")"\necho "cpu_user_usec $(awk \'$1=="user_usec"{print $2}\' "$cg/cpu.stat")"\necho "cpu_system_usec $(awk \'$1=="system_usec"{print $2}\' "$cg/cpu.stat")"\necho "cpu_throttled $(awk \'$1=="nr_throttled"{print $2}\' "$cg/cpu.stat")"\necho "cpu_periods $(awk \'$1=="nr_periods"{print $2}\' "$cg/cpu.stat")"\necho "cpu_throttled_usec $(awk \'$1=="throttled_usec"{print $2}\' "$cg/cpu.stat")"\necho "system_jiffies $(awk \'$1=="cpu"{s=0; for(i=2;i<=NF;i++) s+=$i; print s}\' /proc/stat)"\necho "online_cpus $(grep -c \'^processor\' /proc/cpuinfo)"\necho "mem_total_kb $(awk \'$1=="MemTotal:"{print $2}\' /proc/meminfo)"\necho "memory_current $(cat "$cg/memory.current" 2>/dev/null)"\necho "memory_max $(cat "$cg/memory.max" 2>/dev/null)"\necho "memory_peak $(cat "$cg/memory.peak" 2>/dev/null)"\nawk \'{print "memstat_" $1, $2}\' "$cg/memory.stat" 2>/dev/null\necho "pids_current $(cat "$cg/pids.current" 2>/dev/null)"\necho "pids_max $(cat "$cg/pids.max" 2>/dev/null)"\nawk \'{r=0; w=0; for(i=2;i<=NF;i++){split($i,kv,"="); if(kv[1]=="rbytes") r+=kv[2]; if(kv[1]=="wbytes") w+=kv[2]} print "io", $1, r, w}\' "$cg/io.stat" 2>/dev/null\nif [ "$pid" != 0 ] && [ -r "/proc/$pid/net/dev" ]; then\n tail -n +3 "/proc/$pid/net/dev" | tr \':\' \' \' | awk \'$1!="lo"{print "net", $1, $2, $3, $4, $5, $10, $11, $12, $13}\'\nfi\n',TOP='\ncg="$1"\nbtime=$(awk \'$1=="btime"{print $2}\' /proc/stat)\necho "btime $btime"\nfind "$cg" -name cgroup.procs -exec cat {} + 2>/dev/null | sort -n | uniq | while read -r p; do\n [ -r "/proc/$p/stat" ] || continue\n uid=$(awk \'$1=="Uid:"{print $2}\' "/proc/$p/status")\n set -- $(sed \'s/^.*) //\' "/proc/$p/stat")\n ppid=$2; tty=$5; utime=${12}; stime=${13}; start=${20}\n cmd=$(tr \'\\0\' \' \' < "/proc/$p/cmdline")\n [ -n "$cmd" ] || cmd="[$(cat "/proc/$p/comm")]"\n printf \'%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\n\' "$uid" "$p" "$ppid" "$start" "$((utime + stime))" "$tty" "$cmd"\ndone\n',CLK_TCK=100;function parseSample(t){const e={memstat:{},io:[],net:{}};for(const s of t.split("\n")){const t=s.trim().split(/\s+/);t[0]&&("io"===t[0]?e.io.push({dev:t[1],read:Number(t[2])||0,write:Number(t[3])||0}):"net"===t[0]?e.net[t[1]]={rx_bytes:+t[2],rx_packets:+t[3],rx_errors:+t[4],rx_dropped:+t[5],tx_bytes:+t[6],tx_packets:+t[7],tx_errors:+t[8],tx_dropped:+t[9]}:t[0].startsWith("memstat_")?e.memstat[t[0].slice(8)]=Number(t[1]):e[t[0]]=void 0===t[1]||""===t[1]?void 0:"max"===t[1]?"max":Number(t[1]))}return e}class Stats{constructor(t){this.engine=t}cgroupDir(t){return`/sys/fs/cgroup/tnc-dock/${t}`}async sample(t){if("running"!==t.state)return null;const e=parseSample((await this.engine.host.run(["/bin/sh","-c",SAMPLE,"dock",this.cgroupDir(t.id),String(t.runtime.pid||0)])).stdout);return e.missing?null:(e.read=(new Date).toISOString(),e)}toApi(t,e,s){const r=t=>t?{cpu_usage:{total_usage:1e3*(t.cpu_usage_usec||0),usage_in_kernelmode:1e3*(t.cpu_system_usec||0),usage_in_usermode:1e3*(t.cpu_user_usec||0)},system_cpu_usage:Math.round(1e7*(t.system_jiffies||0)),online_cpus:t.online_cpus||1,throttling_data:{periods:t.cpu_periods||0,throttled_periods:t.cpu_throttled||0,throttled_time:1e3*(t.cpu_throttled_usec||0)}}:{cpu_usage:{total_usage:0},throttling_data:{periods:0,throttled_periods:0,throttled_time:0}},n={read:"0001-01-01T00:00:00Z",preread:"0001-01-01T00:00:00Z",pids_stats:{},blkio_stats:{},num_procs:0,storage_stats:{},cpu_stats:r(null),precpu_stats:r(null),memory_stats:{},name:`/${t.name}`,id:t.id};if(!e)return n;const i=1024*(e.mem_total_kb||0),a="max"!==e.memory_max&&e.memory_max?Math.min(e.memory_max,i||e.memory_max):i,o={read:e.read,preread:s?.read||"0001-01-01T00:00:00Z",pids_stats:{current:e.pids_current||0,..."max"!==e.pids_max&&e.pids_max?{limit:e.pids_max}:{}},blkio_stats:{io_service_bytes_recursive:e.io.flatMap(t=>{const[e,s]=t.dev.split(":").map(Number);return[{major:e,minor:s,op:"read",value:t.read},{major:e,minor:s,op:"write",value:t.write}]})},num_procs:0,storage_stats:{},cpu_stats:r(e),precpu_stats:r(s),memory_stats:{usage:e.memory_current||0,...e.memory_peak?{max_usage:e.memory_peak}:{},limit:a,stats:e.memstat},name:`/${t.name}`,id:t.id};return Object.keys(e.net).length&&(o.networks=e.net),o}async run(t,{stream:e=!0,oneShot:s=!1},r,n=()=>!1){const i=this.engine;let a=i.findContainer(t);"running"===a.state&&await i.ready();let o=null;for(e||s||"running"!==a.state||(o=await this.sample(a),await new Promise(t=>setTimeout(t,1e3)),a=i.store.getContainer(a.id)||a);;){const t=await this.sample(a);if(r(this.toApi(a,t,o)),!e||n())return;if(o=t,await new Promise(t=>setTimeout(t,1e3)),a=i.store.getContainer(a.id),!a||n())return}}async top(t){const e=this.engine,s=e.findContainer(t);if("running"!==s.state)throw Object.assign(new Error(`Container ${s.id} is not running`),{statusCode:409});await e.ready();const r=await e.host.run(["/bin/sh","-c",TOP,"dock",this.cgroupDir(s.id)]);let n=0;const i=[];for(const t of r.stdout.split("\n")){if(t.startsWith("btime ")){n=Number(t.slice(6));continue}const e=t.split("\t");if(e.length<7)continue;const[s,r,a,o,c,p,u]=e,m=new Date(1e3*(n+Number(o)/100)),d=Math.floor(Number(c)/100),$=[Math.floor(d/3600),Math.floor(d/60)%60,d%60].map(t=>String(t).padStart(2,"0")).join(":"),_=`${String(m.getHours()).padStart(2,"0")}:${String(m.getMinutes()).padStart(2,"0")}`;i.push([s,r,a,"0",_,Number(p)?"pts":"?",$,u.trim()])}return{Titles:["UID","PID","PPID","C","STIME","TTY","TIME","CMD"],Processes:i}}}module.exports={Stats,parseSample};
7
+ "use strict";const SAMPLE='\ncg="$1"; pid="$2"\nif [ ! -f /sys/fs/cgroup/cgroup.controllers ] && [ -d /sys/fs/cgroup/memory ]; then\n rel="${cg#/sys/fs/cgroup}"\n m=/sys/fs/cgroup/memory$rel; c=/sys/fs/cgroup/cpuacct$rel; [ -d "$c" ] || c="/sys/fs/cgroup/cpu,cpuacct$rel"\n q=/sys/fs/cgroup/cpu$rel; [ -d "$q" ] || q="/sys/fs/cgroup/cpu,cpuacct$rel"\n p=/sys/fs/cgroup/pids$rel; b=/sys/fs/cgroup/blkio$rel\n [ -d "$m" ] || [ -d "$p" ] || { echo missing 1; exit 0; }\n echo "cpu_usage_usec $(( $(cat "$c/cpuacct.usage" 2>/dev/null || echo 0) / 1000 ))"\n echo "cpu_user_usec $(awk \'$1=="user"{print $2 * 10000}\' "$c/cpuacct.stat" 2>/dev/null)"\n echo "cpu_system_usec $(awk \'$1=="system"{print $2 * 10000}\' "$c/cpuacct.stat" 2>/dev/null)"\n echo "cpu_throttled $(awk \'$1=="nr_throttled"{print $2}\' "$q/cpu.stat" 2>/dev/null)"\n echo "cpu_periods $(awk \'$1=="nr_periods"{print $2}\' "$q/cpu.stat" 2>/dev/null)"\n echo "cpu_throttled_usec $(awk \'$1=="throttled_time"{print int($2 / 1000)}\' "$q/cpu.stat" 2>/dev/null)"\n echo "system_jiffies $(awk \'$1=="cpu"{s=0; for(i=2;i<=NF;i++) s+=$i; print s}\' /proc/stat)"\n echo "online_cpus $(grep -c \'^processor\' /proc/cpuinfo)"\n echo "mem_total_kb $(awk \'$1=="MemTotal:"{print $2}\' /proc/meminfo)"\n echo "memory_current $(cat "$m/memory.usage_in_bytes" 2>/dev/null)"\n # "no limit" is a huge number in v1\n echo "memory_max $(awk \'{print ($1 > 4611686018427387904) ? "max" : $1}\' "$m/memory.limit_in_bytes" 2>/dev/null)"\n echo "memory_peak $(cat "$m/memory.max_usage_in_bytes" 2>/dev/null)"\n awk \'$1 ~ /^total_/ {k=substr($1, 7); if (k=="rss") k="anon"; if (k=="cache") k="file"; print "memstat_" k, $2}\' "$m/memory.stat" 2>/dev/null\n echo "pids_current $(cat "$p/pids.current" 2>/dev/null)"\n echo "pids_max $(cat "$p/pids.max" 2>/dev/null)"\n awk \'$2=="Read"{r[$1]+=$3} $2=="Write"{w[$1]+=$3} END{for (d in r) print "io", d, r[d], w[d]}\' "$b/blkio.throttle.io_service_bytes" 2>/dev/null\n if [ "$pid" != 0 ] && [ -r "/proc/$pid/net/dev" ]; then\n tail -n +3 "/proc/$pid/net/dev" | tr \':\' \' \' | awk \'$1!="lo"{print "net", $1, $2, $3, $4, $5, $10, $11, $12, $13}\'\n fi\n exit 0\nfi\n[ -d "$cg" ] || { echo missing 1; exit 0; }\necho "cpu_usage_usec $(awk \'$1=="usage_usec"{print $2}\' "$cg/cpu.stat")"\necho "cpu_user_usec $(awk \'$1=="user_usec"{print $2}\' "$cg/cpu.stat")"\necho "cpu_system_usec $(awk \'$1=="system_usec"{print $2}\' "$cg/cpu.stat")"\necho "cpu_throttled $(awk \'$1=="nr_throttled"{print $2}\' "$cg/cpu.stat")"\necho "cpu_periods $(awk \'$1=="nr_periods"{print $2}\' "$cg/cpu.stat")"\necho "cpu_throttled_usec $(awk \'$1=="throttled_usec"{print $2}\' "$cg/cpu.stat")"\necho "system_jiffies $(awk \'$1=="cpu"{s=0; for(i=2;i<=NF;i++) s+=$i; print s}\' /proc/stat)"\necho "online_cpus $(grep -c \'^processor\' /proc/cpuinfo)"\necho "mem_total_kb $(awk \'$1=="MemTotal:"{print $2}\' /proc/meminfo)"\necho "memory_current $(cat "$cg/memory.current" 2>/dev/null)"\necho "memory_max $(cat "$cg/memory.max" 2>/dev/null)"\necho "memory_peak $(cat "$cg/memory.peak" 2>/dev/null)"\nawk \'{print "memstat_" $1, $2}\' "$cg/memory.stat" 2>/dev/null\necho "pids_current $(cat "$cg/pids.current" 2>/dev/null)"\necho "pids_max $(cat "$cg/pids.max" 2>/dev/null)"\nawk \'{r=0; w=0; for(i=2;i<=NF;i++){split($i,kv,"="); if(kv[1]=="rbytes") r+=kv[2]; if(kv[1]=="wbytes") w+=kv[2]} print "io", $1, r, w}\' "$cg/io.stat" 2>/dev/null\nif [ "$pid" != 0 ] && [ -r "/proc/$pid/net/dev" ]; then\n tail -n +3 "/proc/$pid/net/dev" | tr \':\' \' \' | awk \'$1!="lo"{print "net", $1, $2, $3, $4, $5, $10, $11, $12, $13}\'\nfi\n',TOP='\ncg="$1"\n# cgroup v1: the processes are listed in each controller\'s tree; the pids one has them all\nif [ ! -f /sys/fs/cgroup/cgroup.controllers ] && [ -d /sys/fs/cgroup/pids ]; then cg="/sys/fs/cgroup/pids${cg#/sys/fs/cgroup}"; fi\nbtime=$(awk \'$1=="btime"{print $2}\' /proc/stat)\necho "btime $btime"\nfind "$cg" -name cgroup.procs -exec cat {} + 2>/dev/null | sort -n | uniq | while read -r p; do\n [ -r "/proc/$p/stat" ] || continue\n uid=$(awk \'$1=="Uid:"{print $2}\' "/proc/$p/status")\n set -- $(sed \'s/^.*) //\' "/proc/$p/stat")\n ppid=$2; tty=$5; utime=${12}; stime=${13}; start=${20}\n cmd=$(tr \'\\0\' \' \' < "/proc/$p/cmdline")\n [ -n "$cmd" ] || cmd="[$(cat "/proc/$p/comm")]"\n printf \'%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\n\' "$uid" "$p" "$ppid" "$start" "$((utime + stime))" "$tty" "$cmd"\ndone\n',CLK_TCK=100;function parseSample(t){const e={memstat:{},io:[],net:{}};for(const s of t.split("\n")){const t=s.trim().split(/\s+/);t[0]&&("io"===t[0]?e.io.push({dev:t[1],read:Number(t[2])||0,write:Number(t[3])||0}):"net"===t[0]?e.net[t[1]]={rx_bytes:+t[2],rx_packets:+t[3],rx_errors:+t[4],rx_dropped:+t[5],tx_bytes:+t[6],tx_packets:+t[7],tx_errors:+t[8],tx_dropped:+t[9]}:t[0].startsWith("memstat_")?e.memstat[t[0].slice(8)]=Number(t[1]):e[t[0]]=void 0===t[1]||""===t[1]?void 0:"max"===t[1]?"max":Number(t[1]))}return e}class Stats{constructor(t){this.engine=t}cgroupDir(t,e=null){return`/sys/fs/cgroup${e||`/tnc-dock/${t}`}`}async sample(t){if("running"!==t.state)return null;const e=parseSample((await this.engine.host.run(["/bin/sh","-c",SAMPLE,"dock",this.cgroupDir(t.id,t.runtime.cgroupPath),String(t.runtime.pid||0)])).stdout);return e.missing?null:(e.read=(new Date).toISOString(),e)}toApi(t,e,s){const r=t=>t?{cpu_usage:{total_usage:1e3*(t.cpu_usage_usec||0),usage_in_kernelmode:1e3*(t.cpu_system_usec||0),usage_in_usermode:1e3*(t.cpu_user_usec||0)},system_cpu_usage:Math.round(1e7*(t.system_jiffies||0)),online_cpus:t.online_cpus||1,throttling_data:{periods:t.cpu_periods||0,throttled_periods:t.cpu_throttled||0,throttled_time:1e3*(t.cpu_throttled_usec||0)}}:{cpu_usage:{total_usage:0},throttling_data:{periods:0,throttled_periods:0,throttled_time:0}},n={read:"0001-01-01T00:00:00Z",preread:"0001-01-01T00:00:00Z",pids_stats:{},blkio_stats:{},num_procs:0,storage_stats:{},cpu_stats:r(null),precpu_stats:r(null),memory_stats:{},name:`/${t.name}`,id:t.id};if(!e)return n;const i=1024*(e.mem_total_kb||0),c="max"!==e.memory_max&&e.memory_max?Math.min(e.memory_max,i||e.memory_max):i,o={read:e.read,preread:s?.read||"0001-01-01T00:00:00Z",pids_stats:{current:e.pids_current||0,..."max"!==e.pids_max&&e.pids_max?{limit:e.pids_max}:{}},blkio_stats:{io_service_bytes_recursive:e.io.flatMap(t=>{const[e,s]=t.dev.split(":").map(Number);return[{major:e,minor:s,op:"read",value:t.read},{major:e,minor:s,op:"write",value:t.write}]})},num_procs:0,storage_stats:{},cpu_stats:r(e),precpu_stats:r(s),memory_stats:{usage:e.memory_current||0,...e.memory_peak?{max_usage:e.memory_peak}:{},limit:c,stats:e.memstat},name:`/${t.name}`,id:t.id};return Object.keys(e.net).length&&(o.networks=e.net),o}async run(t,{stream:e=!0,oneShot:s=!1},r,n=()=>!1){const i=this.engine;let c=i.findContainer(t);"running"===c.state&&await i.ready();let o=null;for(e||s||"running"!==c.state||(o=await this.sample(c),await new Promise(t=>setTimeout(t,1e3)),c=i.store.getContainer(c.id)||c);;){const t=await this.sample(c);if(r(this.toApi(c,t,o)),!e||n())return;if(o=t,await new Promise(t=>setTimeout(t,1e3)),c=i.store.getContainer(c.id),!c||n())return}}async top(t){const e=this.engine,s=e.findContainer(t);if("running"!==s.state)throw Object.assign(new Error(`Container ${s.id} is not running`),{statusCode:409});await e.ready();const r=await e.host.run(["/bin/sh","-c",TOP,"dock",this.cgroupDir(s.id,s.runtime.cgroupPath)]);let n=0;const i=[];for(const t of r.stdout.split("\n")){if(t.startsWith("btime ")){n=Number(t.slice(6));continue}const e=t.split("\t");if(e.length<7)continue;const[s,r,c,o,a,p,u]=e,m=new Date(1e3*(n+Number(o)/100)),$=Math.floor(Number(a)/100),d=[Math.floor($/3600),Math.floor($/60)%60,$%60].map(t=>String(t).padStart(2,"0")).join(":"),l=`${String(m.getHours()).padStart(2,"0")}:${String(m.getMinutes()).padStart(2,"0")}`;i.push([s,r,c,"0",l,Number(p)?"pts":"?",d,u.trim()])}return{Titles:["UID","PID","PPID","C","STIME","TTY","TIME","CMD"],Processes:i}}}module.exports={Stats,parseSample,SAMPLE};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const fs=require("fs"),DEFAULT_USER="tnc-remap",RANGE=65536;function remapSetting(t=require("../config").effective()){const e=process.env.TNC_DOCK_USERNS_REMAP??t.usernsRemap??"";return String(e).trim()}function parseSubIds(t){return String(t||"").split("\n").map(t=>t.trim()).filter(t=>t&&!t.startsWith("#")).map(t=>{const[e,r,n]=t.split(":");return{name:e,start:Number(r),count:Number(n)}}).filter(t=>t.name&&Number.isInteger(t.start)&&Number.isInteger(t.count)&&t.count>0)}function freeStart(t){return Math.max(1e5,...t.map(t=>t.start+t.count))}function resolveRemap(t,{subuid:e="/etc/subuid",subgid:r="/etc/subgid",write:n=!0}={}){if(!t||"off"===t||"false"===t)return null;const[i,s]=t.split(":"),a="default"===i?"tnc-remap":i,u=s||a,f=(t,e)=>{const r=fs.existsSync(t)?fs.readFileSync(t,"utf8"):"",s=parseSubIds(r),a=s.find(t=>t.name===e&&t.count>=65536);if(a)return a.start;if("default"!==i||!n)throw Object.assign(new Error(`userns-remap: ${e} has no range of 65536 ids in ${t}; add one (sudo usermod --add-subuids 200000-265535 --add-subgids 200000-265535 ${e}) or use --userns-remap default`),{statusCode:500});const u=freeStart(s);return fs.appendFileSync(t,`${r&&!r.endsWith("\n")?"\n":""}${e}:${u}:65536\n`),u};return{uid:f(e,a),gid:f(r,u),size:65536,user:a}}function idMappings(t){return{uidMappings:[{containerID:0,hostID:t.uid,size:t.size}],gidMappings:[{containerID:0,hostID:t.gid,size:t.size}]}}function shiftId(t,e,r,n){return n?e+(t<r?t:Math.min(65534,r-1)):t>=e&&t<e+r?t-e:65534}function tarShifter(t,e,{asRoot:r=!1}={}){const n=(n,i)=>[shiftId(r?0:n,t.uid,t.size,e),shiftId(r?0:i,t.gid,t.size,e)];let i=Buffer.alloc(0),s=0,a=null;const u=(t,e,r)=>parseInt(t.subarray(e,e+r).toString("latin1").replace(/\0.*$/s,"").trim()||"0",8),f=(t,e,r,n)=>t.write(n.toString(8).padStart(r-1,"0")+"\0",e,r,"latin1"),o=t=>{t.fill(32,148,156);let e=0;for(let r=0;r<512;r++)e+=t[r];t.write(e.toString(8).padStart(6,"0")+"\0 ",148,8,"latin1")},c=t=>{const e=[];let r=0;for(;r<t.length;){const i=t.indexOf(32,r);if(-1===i)break;const s=Number(t.subarray(r,i).toString("latin1"));if(!s)break;const a=t.subarray(i+1,r+s-1).toString("utf8");r+=s;const u=a.indexOf("="),f=a.slice(0,u);let o=a.slice(u+1);if("uname"===f||"gname"===f)continue;"uid"===f?o=String(n(Number(o),0)[0]):"gid"===f&&(o=String(n(0,Number(o))[1]));const c=Buffer.from(`${f}=${o}\n`);let l=c.length+2;for(;String(l).length+1+c.length!==l;)l=String(l).length+1+c.length;e.push(Buffer.from(`${l} `),c)}return Buffer.concat(e)},l=t=>512*Math.ceil(t/512);return{push:function d(t){i=i.length?Buffer.concat([i,t]):t;const e=[];for(;;){if(s>0){const t=Math.min(s,i.length);if(!t)break;e.push(i.subarray(0,t)),i=i.subarray(t),s-=t;continue}if(a){const t=l(a.size);if(i.length<t)break;const r=c(i.subarray(0,a.size));i=i.subarray(t),f(a.header,124,12,r.length),o(a.header),e.push(a.header,r,Buffer.alloc(l(r.length)-r.length)),a=null;continue}if(i.length<512)break;const t=Buffer.from(i.subarray(0,512));if(i=i.subarray(512),t.every(t=>0===t)){e.push(t);continue}const r=String.fromCharCode(t[156]||48),d=u(t,124,12),[h,g]=n(u(t,108,8),u(t,116,8));f(t,108,8,h),f(t,116,8,g),"ustar"===t.subarray(257,262).toString("latin1")&&(t.fill(0,265,297),t.fill(0,297,329)),o(t),"x"!==r?(e.push(t),s=l(d)):a={header:t,size:d}}return e.length?Buffer.concat(e):Buffer.alloc(0)},end:()=>{const t=i;return i=Buffer.alloc(0),t}}}async function*shiftTarStream(t,e,r,n){const i=tarShifter(e,r,n);for await(const e of t){const t=i.push(Buffer.isBuffer(e)?e:Buffer.from(e));t.length&&(yield t)}const s=i.end();s.length&&(yield s)}function shiftTarSink(t,e,r){const n=tarShifter(e,r);return{...t,write:e=>{const r=n.push(Buffer.isBuffer(e)?e:Buffer.from(e));return!r.length||t.write(r)},end:()=>{const e=n.end();return e.length&&t.write(e),t.end()}}}module.exports={remapSetting,resolveRemap,parseSubIds,freeStart,idMappings,shiftId,tarShifter,shiftTarStream,shiftTarSink,DEFAULT_USER:"tnc-remap",RANGE:65536};
@@ -4,4 +4,4 @@
4
4
  * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
5
  * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
6
  */
7
- "use strict";const fs=require("fs"),path=require("path"),crypto=require("crypto"),{ENGINE_SOCKETS:ENGINE_SOCKET_PATHS}=require("../api/wire"),VOLUME_NAME=/^[a-zA-Z0-9][a-zA-Z0-9_.-]+$/,WIN_DRIVE=/^[A-Za-z]:[\\/]/;function splitBind(t){const e=/^([A-Za-z]:[\\/][^:]*):(.+)$/.exec(t);if(e){const[t,o=""]=e[2].split(":");return[e[1],t,o]}return t.split(":")}function httpError(t,e){const o=new Error(e);return o.statusCode=t,o}const ENGINE_SOCKETS=new Set(ENGINE_SOCKET_PATHS);class Volumes{constructor({store:t,host:e,dir:o,emit:i=()=>{},engineSocket:n=null,guestEngineSocket:s=null}){this.store=t,this.host=e,this.dir=o,this.emit=i,this.engineSocket=n,this.guestEngineSocket=s,this.linuxSide="wsl"===e.kind}linuxDir(t){return`/var/lib/tnc-dock/volumes/${t}/_data`}create(t,e={},o=!1){if(t||(t=crypto.randomBytes(32).toString("hex"),o=!0),!VOLUME_NAME.test(t))throw httpError(400,`"${t}" includes invalid characters for a local volume name, only "[a-zA-Z0-9][a-zA-Z0-9_.-]" are allowed`);const i=this.store.getVolume(t);if(i)return i;const n=this.linuxSide?this.linuxDir(t):path.join(this.dir,t,"_data");this.linuxSide||fs.mkdirSync(n,{recursive:!0});const s=this.store.insertVolume({name:t,mountpoint:n,labels:e,anonymous:o});return this.emit("volume","create",t,{driver:"local"}),s}usedBy(t){return this.store.listContainers().filter(e=>(e.runtime.volumes||[]).includes(t)).map(t=>t.id)}remove(t,e=!1){if(!this.store.getVolume(t))throw httpError(404,`get ${t}: no such volume`);const o=this.usedBy(t);if(o.length&&!e)throw httpError(409,`remove ${t}: volume is in use - [${o.join(", ")}]`);this.linuxSide?this.host.sh('rm -rf "$1"',[`/var/lib/tnc-dock/volumes/${t}`]).catch(()=>{}):fs.rmSync(path.join(this.dir,t),{recursive:!0,force:!0}),this.store.deleteVolume(t),this.emit("volume","destroy",t,{driver:"local"})}toApi(t){return{Name:t.name,Driver:t.driver,Mountpoint:t.mountpoint,Labels:t.labels,Scope:"local",Options:null,CreatedAt:new Date(t.createdAt).toISOString()}}resolve(t={},e={}){const o=[],i=[],n=[],s=[],r=new Set,u=(t,e,n)=>{const s=this._engineSocketSource(t);if(s)return o.push({destination:e,type:"bind",source:s,options:["rbind",n?"ro":"rw"]}),i.push({Type:"bind",Source:t,Destination:e,Mode:n?"ro":"",RW:!n,Propagation:"rprivate"}),void r.add(e);o.push({destination:e,type:"bind",source:this.host.guestPath(t),options:["rbind",n?"ro":"rw"]}),i.push({Type:"bind",Source:t,Destination:e,Mode:n?"ro":"",RW:!n,Propagation:"rprivate"}),r.add(e)},c=(t,e,u,c=!1)=>{const a=this.create(t,{},c);n.push(a.name),a.anonymous&&s.push(a.name),o.push({destination:e,type:"bind",source:this.linuxSide?a.mountpoint:this.host.guestPath(a.mountpoint),options:["rbind",u?"ro":"rw"]}),i.push({Type:"volume",Name:a.name,Source:a.mountpoint,Destination:e,Driver:"local",Mode:u?"ro":"z",RW:!u,Propagation:""}),r.add(e)};for(const e of t.Binds||[]){const t=splitBind(e);if(t.length<2)throw httpError(400,`invalid volume specification: '${e}'`);const[o,i,n=""]=t;if(!i.startsWith("/"))throw httpError(400,`invalid mount config: mount path must be absolute: '${i}'`);const s=n.split(",").includes("ro");o.startsWith("/")||WIN_DRIVE.test(o)?(this._engineSocketSource(o)||fs.existsSync(o)||fs.mkdirSync(o,{recursive:!0}),u(o,i,s)):c(o,i,s)}for(const e of t.Mounts||[]){const t=e.Target||e.Destination;if(!t)throw httpError(400,"invalid mount config: Target is required");if("bind"===e.Type){if(!this._engineSocketSource(e.Source)&&!fs.existsSync(e.Source))throw httpError(400,`invalid mount config for type "bind": bind source path does not exist: ${e.Source}`);u(e.Source,t,!!e.ReadOnly)}else if("volume"===e.Type)c(e.Source||null,t,!!e.ReadOnly,!e.Source);else{if("tmpfs"!==e.Type)throw httpError(400,`mount type "${e.Type}" is not supported`);{const n=e.TmpfsOptions?.SizeBytes;o.push({destination:t,type:"tmpfs",source:"tmpfs",options:["nosuid","nodev",...n?[`size=${n}`]:[]]}),i.push({Type:"tmpfs",Source:"",Destination:t,Mode:"",RW:!0,Propagation:""}),r.add(t)}}}for(const[e,i]of Object.entries(t.Tmpfs||{}))o.push({destination:e,type:"tmpfs",source:"tmpfs",options:["nosuid","nodev",...i?i.split(","):[]]}),r.add(e);for(const t of Object.keys(e.Volumes||{}))r.has(t)||c(null,t,!1,!0);return{ociMounts:o,apiMounts:i,volumes:n,anonymous:s}}}Volumes.prototype._engineSocketSource=function(t){const e=this.engineSocket&&path.resolve(t)===path.resolve(this.engineSocket);if(!e&&!ENGINE_SOCKETS.has(t))return null;if("vm"===this.host.kind)return this.guestEngineSocket;if("wsl"===this.host.kind)throw httpError(400,"the engine socket cannot be mounted into containers on Windows yet");return e?t:fs.existsSync(t)?null:this.engineSocket},module.exports={Volumes,splitBind,ENGINE_SOCKETS};
7
+ "use strict";const fs=require("fs"),path=require("path"),crypto=require("crypto"),{ENGINE_SOCKETS:ENGINE_SOCKET_PATHS}=require("../api/wire"),VOLUME_NAME=/^[a-zA-Z0-9][a-zA-Z0-9_.-]+$/,WIN_DRIVE=/^[A-Za-z]:[\\/]/;function splitBind(t){const e=/^([A-Za-z]:[\\/][^:]*):(.+)$/.exec(t);if(e){const[t,o=""]=e[2].split(":");return[e[1],t,o]}return t.split(":")}function httpError(t,e){const o=new Error(e);return o.statusCode=t,o}const ENGINE_SOCKETS=new Set(ENGINE_SOCKET_PATHS);class Volumes{constructor({store:t,host:e,dir:o,emit:i=()=>{},engineSocket:n=null,guestEngineSocket:s=null}){this.store=t,this.host=e,this.dir=o,this.emit=i,this.engineSocket=n,this.guestEngineSocket=s,this.linuxSide="wsl"===e.kind}linuxDir(t){return`/var/lib/tnc-dock/volumes/${t}/_data`}create(t,e={},o=!1){if(t||(t=crypto.randomBytes(32).toString("hex"),o=!0),!VOLUME_NAME.test(t))throw httpError(400,`"${t}" includes invalid characters for a local volume name, only "[a-zA-Z0-9][a-zA-Z0-9_.-]" are allowed`);const i=this.store.getVolume(t);if(i)return i;const n=this.linuxSide?this.linuxDir(t):path.join(this.dir,t,"_data");this.linuxSide||fs.mkdirSync(n,{recursive:!0});const s=this.store.insertVolume({name:t,mountpoint:n,labels:e,anonymous:o});return this.emit("volume","create",t,{driver:"local"}),s}usedBy(t){return this.store.listContainers().filter(e=>(e.runtime.volumes||[]).includes(t)).map(t=>t.id)}remove(t,e=!1){if(!this.store.getVolume(t))throw httpError(404,`get ${t}: no such volume`);const o=this.usedBy(t);if(o.length&&!e)throw httpError(409,`remove ${t}: volume is in use - [${o.join(", ")}]`);this.linuxSide?this.host.sh('rm -rf "$1"',[`/var/lib/tnc-dock/volumes/${t}`]).catch(()=>{}):fs.rmSync(path.join(this.dir,t),{recursive:!0,force:!0}),this.store.deleteVolume(t),this.emit("volume","destroy",t,{driver:"local"})}toApi(t){return{Name:t.name,Driver:t.driver,Mountpoint:t.mountpoint,Labels:t.labels,Scope:"local",Options:null,CreatedAt:new Date(t.createdAt).toISOString()}}resolve(t={},e={}){const o=[],i=[],n=[],s=[],r=new Set,u=(t,e,n)=>{const s=this._engineSocketSource(t);if(s)return o.push({destination:e,type:"bind",source:s,options:["rbind",n?"ro":"rw"]}),i.push({Type:"bind",Source:t,Destination:e,Mode:n?"ro":"",RW:!n,Propagation:"rprivate"}),void r.add(e);o.push({destination:e,type:"bind",source:this.host.guestPath(t),options:["rbind",n?"ro":"rw"]}),i.push({Type:"bind",Source:t,Destination:e,Mode:n?"ro":"",RW:!n,Propagation:"rprivate"}),r.add(e)},c=(t,e,u,c=!1)=>{const a=this.create(t,{},c);n.push(a.name),a.anonymous&&s.push(a.name),o.push({destination:e,type:"bind",source:this.linuxSide?a.mountpoint:this.host.guestPath(a.mountpoint),options:["rbind",u?"ro":"rw"]}),i.push({Type:"volume",Name:a.name,Source:a.mountpoint,Destination:e,Driver:"local",Mode:u?"ro":"z",RW:!u,Propagation:""}),r.add(e)};for(const e of t.Binds||[]){const t=splitBind(e);if(t.length<2)throw httpError(400,`invalid volume specification: '${e}'`);const[o,i,n=""]=t;if(!i.startsWith("/"))throw httpError(400,`invalid mount config: mount path must be absolute: '${i}'`);const s=n.split(",").includes("ro");o.startsWith("/")||WIN_DRIVE.test(o)?(this._engineSocketSource(o)||fs.existsSync(o)||fs.mkdirSync(o,{recursive:!0}),u(o,i,s)):c(o,i,s)}for(const e of t.Mounts||[]){const t=e.Target||e.Destination;if(!t)throw httpError(400,"invalid mount config: Target is required");if("bind"===e.Type){if(!this._engineSocketSource(e.Source)&&!fs.existsSync(e.Source))throw httpError(400,`invalid mount config for type "bind": bind source path does not exist: ${e.Source}`);u(e.Source,t,!!e.ReadOnly)}else if("volume"===e.Type)c(e.Source||null,t,!!e.ReadOnly,!e.Source);else{if("tmpfs"!==e.Type)throw httpError(400,`mount type "${e.Type}" is not supported`);{const n=e.TmpfsOptions?.SizeBytes;o.push({destination:t,type:"tmpfs",source:"tmpfs",options:["nosuid","nodev",...n?[`size=${n}`]:[]]}),i.push({Type:"tmpfs",Source:"",Destination:t,Mode:"",RW:!0,Propagation:""}),r.add(t)}}}for(const[e,i]of Object.entries(t.Tmpfs||{}))o.push({destination:e,type:"tmpfs",source:"tmpfs",options:["nosuid","nodev",...i?i.split(","):[]]}),r.add(e);for(const t of Object.keys(e.Volumes||{})){const e=path.posix.normalize(t);/^\/(proc|sys|dev)(\/|$)/.test(e)||r.has(t)||c(null,t,!1,!0)}return{ociMounts:o,apiMounts:i,volumes:n,anonymous:s}}}Volumes.prototype._engineSocketSource=function(t){const e=this.engineSocket&&path.resolve(t)===path.resolve(this.engineSocket);if(!e&&!ENGINE_SOCKETS.has(t))return null;if("vm"===this.host.kind)return this.guestEngineSocket;if("wsl"===this.host.kind)throw httpError(400,"the engine socket cannot be mounted into containers on Windows yet");return e?t:fs.existsSync(t)?null:this.engineSocket},module.exports={Volumes,splitBind,ENGINE_SOCKETS};
@@ -4,4 +4,4 @@
4
4
  * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
5
  * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
6
  */
7
- "use strict";const fs=require("fs"),os=require("os"),path=require("path"),{spawnSync}=require("child_process"),LABEL="com.thinkncollab.dock",PLIST=path.join(os.homedir(),"Library","LaunchAgents",`${LABEL}.plist`),xml=n=>String(n).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");function plist({node:n,cli:t,logFile:r,env:e}){const s=Object.entries(e).map(([n,t])=>` <key>${xml(n)}</key><string>${xml(t)}</string>`).join("\n");return`<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>Label</key><string>${LABEL}</string>\n <key>ProgramArguments</key>\n <array>\n <string>${xml(n)}</string>\n <string>${xml(t)}</string>\n <string>dock</string>\n <string>start</string>\n </array>\n <key>RunAtLoad</key><true/>\n \x3c!-- "dock start" leaves the daemon running in the background and exits --\x3e\n <key>AbandonProcessGroup</key><true/>\n <key>EnvironmentVariables</key>\n <dict>\n${s}\n </dict>\n <key>StandardOutPath</key><string>${xml(r)}</string>\n <key>StandardErrorPath</key><string>${xml(r)}</string>\n</dict>\n</plist>\n`}const launchctl=n=>spawnSync("launchctl",n,{encoding:"utf8"}),domain=()=>`gui/${process.getuid()}`;function installed(){return fs.existsSync(PLIST)}function autostart({off:n=!1,cli:t,logFile:r}){if("darwin"!==process.platform)throw new Error("autostart is for macOS; on Linux run dock as a system service, on Windows start it from your login script");if(n)return installed()?(launchctl(["bootout",`${domain()}/${LABEL}`]),fs.rmSync(PLIST,{force:!0}),[`autostart off (${PLIST} removed); dock keeps running until you stop it`]):["autostart was not on"];const e={PATH:process.env.PATH||"/usr/bin:/bin:/usr/sbin:/sbin",HOME:os.homedir()};process.env.TNC_DOCK_HOME&&(e.TNC_DOCK_HOME=process.env.TNC_DOCK_HOME),fs.mkdirSync(path.dirname(PLIST),{recursive:!0}),fs.writeFileSync(PLIST,plist({node:process.execPath,cli:fs.realpathSync(t),logFile:r,env:e})),launchctl(["bootout",`${domain()}/${LABEL}`]);const s=launchctl(["bootstrap",domain(),PLIST]);if(0!==s.status)throw new Error(`launchctl bootstrap failed: ${(s.stderr||s.stdout).trim()}`);return[`autostart on: dock starts when you log in (${PLIST})`,`uses node ${process.execPath}; run this again if you move or upgrade node`]}module.exports={autostart,installed,PLIST,LABEL};
7
+ "use strict";const fs=require("fs"),os=require("os"),path=require("path"),{spawnSync}=require("child_process"),{dockPaths}=require("../paths"),LABEL="com.thinkncollab.dock",APP_LABEL="com.thinkncollab.dock.app",UNIT="tnc-dock.service",DAEMON_MAIN=path.join(__dirname,"..","daemon-main.js"),launchAgentsDir=()=>process.env.TNC_LAUNCH_AGENTS_DIR||path.join(os.homedir(),"Library","LaunchAgents"),userService=()=>"linux"===process.platform&&require("../paths").rootlessMode(),systemdDir=()=>process.env.TNC_SYSTEMD_DIR||(userService()?path.join(process.env.XDG_CONFIG_HOME||path.join(os.homedir(),".config"),"systemd","user"):"/etc/systemd/system"),systemctl=n=>userService()?["--user",...n]:n,startupDir=()=>process.env.TNC_STARTUP_DIR||path.join(process.env.APPDATA||path.join(os.homedir(),"AppData","Roaming"),"Microsoft","Windows","Start Menu","Programs","Startup"),dryRun=()=>"1"===process.env.TNC_AUTOSTART_DRY_RUN,xml=n=>String(n).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");function daemonEnvironment(n=process.env){const e={PATH:n.PATH||"/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"};for(const t of["TNC_DOCK_HOME","TNC_DOCK_BUILDKIT","TNC_DOCK_INSECURE_REGISTRIES","TNC_SHIM","TNC_BUSYBOX","TNC_DOCK_SOCKET"])n[t]&&(e[t]=n[t]);return e}function plist({label:n,program:e,logFile:t,env:s,keepAlive:r=!0}){const o=e.map(n=>` <string>${xml(n)}</string>`).join("\n"),a=Object.entries(s||{}).map(([n,e])=>` <key>${xml(n)}</key><string>${xml(e)}</string>`).join("\n");return`<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>Label</key><string>${xml(n)}</string>\n <key>ProgramArguments</key>\n <array>\n${o}\n </array>\n <key>RunAtLoad</key><true/>\n${r?" <key>KeepAlive</key>\n <dict><key>SuccessfulExit</key><false/></dict>\n <key>ThrottleInterval</key><integer>10</integer>\n":""} <key>ProcessType</key><string>${r?"Background":"Interactive"}</string>\n${t?` <key>StandardOutPath</key><string>${xml(t)}</string>\n <key>StandardErrorPath</key><string>${xml(t)}</string>\n`:""}${a?` <key>EnvironmentVariables</key>\n <dict>\n${a}\n </dict>\n`:""}</dict>\n</plist>\n`}function systemdUnit({node:n,main:e,env:t,user:s=!1}){return`[Unit]\nDescription=ThinkNCollab dock container engine\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nExecStart="${n}" "${e}"\nRestart=on-failure\nRestartSec=5\nKillMode=process\nTimeoutStopSec=30\n${s?"Delegate=yes\n":""}${Object.entries(t).map(([n,e])=>`Environment="${n}=${String(e).replace(/"/g,'\\"')}"`).join("\n")}\n\n[Install]\nWantedBy=${s?"default.target":"multi-user.target"}\n`}function windowsLauncher({node:n,main:e,logFile:t}){const s=n=>n.replace(/"/g,'""');return`' ThinkNCollab dock: starts the engine at login (written by thinknsh dock autostart on)\r\nSet sh = CreateObject("WScript.Shell")\r\nsh.Run "cmd /c """"${s(n)}"" ""${s(e)}"" >> ""${s(t)}"" 2>&1""", 0, False\r\n`}function run(n,e){return dryRun()?{status:0,stdout:"",stderr:""}:spawnSync(n,e,{encoding:"utf8"})}function appBundle(){for(const n of[path.join(os.homedir(),"Applications"),"/Applications"]){const e=path.join(n,"Thinknsh.app");if(fs.existsSync(path.join(e,"Contents","MacOS")))return e}return null}function files(n=process.platform){return"darwin"===n?{daemon:path.join(launchAgentsDir(),`${LABEL}.plist`),app:path.join(launchAgentsDir(),`${APP_LABEL}.plist`)}:"linux"===n?{daemon:path.join(systemdDir(),UNIT)}:"win32"===n?{daemon:path.join(startupDir(),"tnc-dock.vbs"),app:path.join(startupDir(),"Thinknsh-tray.vbs")}:{}}function enable({platform:n=process.platform,node:e=process.execPath,app:t=!0}={}){const s=dockPaths(),r=daemonEnvironment(),o=files(n),a=[];if("darwin"===n){fs.mkdirSync(path.dirname(o.daemon),{recursive:!0}),fs.writeFileSync(o.daemon,plist({label:LABEL,program:[e,DAEMON_MAIN],logFile:s.logFile,env:r}));const n=process.getuid();run("launchctl",["bootout",`gui/${n}/${LABEL}`]);const i=run("launchctl",["bootstrap",`gui/${n}`,o.daemon]);if(0!==i.status&&!/already/i.test(i.stderr||""))throw new Error(`launchctl bootstrap: ${(i.stderr||"").trim()}`);a.push(`daemon: starts at login (${o.daemon})`);const c=appBundle();t&&c?(fs.writeFileSync(o.app,plist({label:APP_LABEL,program:["/usr/bin/open","-a",c],keepAlive:!1})),a.push(`menu-bar app: opens at login (${c})`)):fs.existsSync(o.app)&&fs.rmSync(o.app)}else if("linux"===n){const n=userService();if(process.getuid&&0!==process.getuid()&&!n&&!process.env.TNC_SYSTEMD_DIR)throw new Error("the system engine runs as root: sudo thinknsh dock autostart on");fs.mkdirSync(path.dirname(o.daemon),{recursive:!0}),fs.writeFileSync(o.daemon,systemdUnit({node:e,main:DAEMON_MAIN,env:r,user:n}));const t=run("systemctl",systemctl(["daemon-reload"]));if(0!==t.status)throw new Error(`systemctl daemon-reload: ${(t.stderr||"").trim()}`);const s=run("systemctl",systemctl(["enable",UNIT]));if(0!==s.status)throw new Error(`systemctl enable: ${(s.stderr||"").trim()}`);a.push(n?`daemon: systemd user service ${UNIT}, starts when you log in (${o.daemon}); to start at boot without a login: loginctl enable-linger ${os.userInfo().username}`:`daemon: systemd unit ${UNIT}, starts at boot (${o.daemon})`)}else{if("win32"!==n)throw new Error(`auto-start is not available on ${n}`);{fs.mkdirSync(path.dirname(o.daemon),{recursive:!0}),fs.writeFileSync(o.daemon,windowsLauncher({node:e,main:DAEMON_MAIN,logFile:s.logFile})),a.push(`daemon: starts at login (${o.daemon})`);const n=path.join(process.env.TNC_APP_DIR||path.join(process.env.LOCALAPPDATA||path.join(os.homedir(),"AppData","Local"),"Thinknsh"),"Thinknsh.vbs");t&&fs.existsSync(n)?(fs.copyFileSync(n,o.app),a.push(`tray app: opens at login (${o.app})`)):fs.existsSync(o.app)&&fs.rmSync(o.app)}}return a}function disable({platform:n=process.platform}={}){const e=files(n),t=[];if("darwin"===n)for(const n of[e.daemon,e.app])fs.existsSync(n)&&(fs.rmSync(n),t.push(`removed ${n}`));else if("linux"===n)fs.existsSync(e.daemon)&&(run("systemctl",systemctl(["disable",UNIT])),fs.rmSync(e.daemon),run("systemctl",systemctl(["daemon-reload"])),t.push(`removed ${e.daemon}`));else if("win32"===n)for(const n of[e.daemon,e.app])fs.existsSync(n)&&(fs.rmSync(n),t.push(`removed ${n}`));return t.length||t.push("auto-start was not on"),t}function status({platform:n=process.platform}={}){const e=files(n);return{enabled:!(!e.daemon||!fs.existsSync(e.daemon)),app:!(!e.app||!fs.existsSync(e.app)),file:e.daemon||null}}function kickstart({platform:n=process.platform}={}){const e=status({platform:n});if(!e.enabled||dryRun())return!1;try{if(!fs.readFileSync(e.file,"utf8").includes("daemon-main.js"))return!1}catch(n){return!1}return process.env.XPC_SERVICE_NAME!==LABEL&&!process.env.INVOCATION_ID&&("darwin"===n?0===run("launchctl",["kickstart",`gui/${process.getuid()}/${LABEL}`]).status:"linux"===n&&0===run("systemctl",systemctl(["start",UNIT])).status)}module.exports={enable,disable,status,kickstart,plist,systemdUnit,windowsLauncher,daemonEnvironment,appBundle,LABEL,APP_LABEL,UNIT};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const fs=require("fs"),os=require("os"),path=require("path"),{spawnSync}=require("child_process"),{dockPaths}=require("../paths"),{request,ping}=require("../client"),TAIL_LINES=2e3;function tail(e,t=2e3){try{return fs.readFileSync(e,"utf8").split("\n").slice(-t).join("\n")}catch(e){return""}}function redact(e){return String(e).replace(/(\/\/)[^/\s:@]+:[^/\s@]+@/g,"$1***:***@").replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi,"$1 ***").replace(/\b([A-Z0-9_]*(?:PASSWORD|PASSWD|SECRET|TOKEN|API_?KEY|PRIVATE_?KEY)[A-Z0-9_]*)=([^\s"',]+)/gi,"$1=***")}function settingsForReport(){const{effective:e}=require("../config"),t=e(),s=t.proxy||{};return redact(JSON.stringify({...t,proxy:{...s}},null,2))}async function engineFacts(e){if(!await ping(e))return{running:!1};const t=async t=>(await request(e,"GET",t,{timeout:1e4}).catch(e=>({body:{error:e.message}}))).body,[s,r,n,o]=await Promise.all([t("/version"),t("/info"),t("/containers/json?all=1"),t("/system/df")]);return{running:!0,version:s,info:r,containers:Array.isArray(n)?n.map(e=>({id:e.Id.slice(0,12),name:e.Names?.[0],image:e.Image,state:e.State,status:e.Status,ports:e.Ports})):n,disk:o&&!o.error?{images:(o.Images||[]).length,containers:(o.Containers||[]).length,volumes:(o.Volumes||[]).length}:o}}async function diagnose({paths:e=dockPaths(),out:t}={}){const s=path.join(e.home,"diagnostics"),r=(new Date).toISOString().replace(/[:.]/g,"-"),n=fs.mkdtempSync(path.join(os.tmpdir(),"tnc-diag-"));try{const o=`dock-diag-${r}`,i=path.join(n,o);fs.mkdirSync(i);const a=(e,t)=>fs.writeFileSync(path.join(i,e),"string"==typeof t?t:JSON.stringify(t,null,2)),{DOCK_VERSION:c,API_VERSION:l}=require("../version");a("system.json",{dock:c,api:l,node:process.version,platform:process.platform,arch:process.arch,os:`${os.type()} ${os.release()}`,cpus:os.cpus().length,memory:os.totalmem(),home:e.home,time:(new Date).toISOString()}),a("settings.json",settingsForReport()),a("autostart.json",require("./autostart").status()),a("engine.json",redact(JSON.stringify(await engineFacts(e.socket),null,2))),a("dock.log",redact(tail(e.logFile)));const u=path.join(os.homedir(),".tnc","tnc-vz.log");fs.existsSync(u)&&a("tnc-vz.log",redact(tail(u)));const g=fs.existsSync(e.home)?fs.readdirSync(e.home).filter(e=>/^selftest-.*\.json$/.test(e)).sort():[];g.length&&a(g[g.length-1],redact(fs.readFileSync(path.join(e.home,g[g.length-1]),"utf8"))),fs.mkdirSync(s,{recursive:!0,mode:448});const d=path.join(s,`${o}.tar.gz`),f=spawnSync("tar",["-czf",d,"-C",n,o],{encoding:"utf8"});if(0!==f.status)throw new Error(`tar: ${(f.stderr||"").trim()}`);return t?.(`diagnostics: ${d}`),d}finally{fs.rmSync(n,{recursive:!0,force:!0})}}const RESET_ENTRIES=["state.db","state.db-wal","state.db-shm","blobs","containers","volumes","build","vm-disk.img","buildkit"],KEPT_ENTRIES=["config.json","auth.json"];function resetTargets(e,{all:t=!1}={}){return fs.existsSync(e)?t?fs.readdirSync(e).map(t=>path.join(e,t)):RESET_ENTRIES.map(t=>path.join(e,t)).filter(e=>fs.existsSync(e)):[]}function reset({paths:e=dockPaths(),all:t=!1}={}){const s=resetTargets(e.home,{all:t});for(const e of s)fs.rmSync(e,{recursive:!0,force:!0});return s}module.exports={diagnose,reset,resetTargets,redact,RESET_ENTRIES,KEPT_ENTRIES};
@@ -4,4 +4,4 @@
4
4
  * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
5
  * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
6
  */
7
- "use strict";const fs=require("fs"),os=require("os"),path=require("path"),crypto=require("crypto"),{spawnSync}=require("child_process"),TNC_DIR=path.join(os.homedir(),".tnc"),BUNDLE_VERSION="0.2.2",DEFAULT_BASE_URL=process.env.TNC_DOCK_BUNDLE_URL||"https://thinkncollab.com/downloads/dock/0.2.2",WSL_DISTRO="tnc-dock",linuxArch=(n=process.arch)=>({arm64:"aarch64",x64:"x86_64"}[n]);function setupError(n){const t=new Error(n);return t.setup=!0,t}function sha256File(n){const t=crypto.createHash("sha256"),e=fs.openSync(n,"r"),r=Buffer.alloc(1<<20);let i;for(;(i=fs.readSync(e,r,0,r.length,null))>0;)t.update(r.subarray(0,i));return fs.closeSync(e),t.digest("hex")}const OPTIONAL_FILES=/^(tnc-busybox-linux-|tnc-menubar-)/;function wantedFiles(n=process.platform,t=process.arch){const e=linuxArch(t);if(!e)throw setupError(`unsupported CPU architecture ${t}`);if("darwin"===n)return[`tnc-dock-vm-${e}.tar.gz`,"tnc-vz-darwin-universal","tnc-menubar-darwin-universal"];if("win32"===n)return[`tnc-dock-wsl-${e}.tar.gz`];if("linux"===n)return[`tnc-shim-linux-${e}`,`crun-linux-${e}`,`tnc-busybox-linux-${e}`];throw setupError(`unsupported platform ${n}`)}async function download(n,t){const e=await fetch(n,{redirect:"follow"});if(!e.ok)throw setupError(`download failed: ${n} (HTTP ${e.status})`);fs.mkdirSync(path.dirname(t),{recursive:!0}),fs.writeFileSync(`${t}.part`,Buffer.from(await e.arrayBuffer())),fs.renameSync(`${t}.part`,t)}async function fetchBundle({from:n,baseUrl:t=DEFAULT_BASE_URL,log:e}){let r,i;if(n)i=path.resolve(n),r=JSON.parse(fs.readFileSync(path.join(i,"manifest.json"),"utf8"));else{let n=await fetch(`${t}/manifest.json`,{redirect:"follow"});if(!n.ok){const e="https://thinkncollab.com/downloads/dock",i=await fetch(`${e}/manifest.json`,{redirect:"follow"});i.ok&&(r=await i.json(),t=`${e}/${r.version}`,n=i)}if(!n.ok)throw setupError(`cannot fetch bundle manifest from ${t} (HTTP ${n.status})`);r||(r=await n.json()),i=path.join(TNC_DIR,"cache","bundles",r.version)}const s={};for(const o of wantedFiles()){const c=r.files[o];if(!c&&OPTIONAL_FILES.test(o)){e(`bundle ${r.version} has no ${o}; ${o.startsWith("tnc-menubar")?"the menu bar app":"cp"} needs a newer bundle`);continue}if(!c)throw setupError(`bundle ${r.version} has no ${o}`);const a=path.join(i,o);if(n||fs.existsSync(a)&&sha256File(a)===c.sha256||(e(`downloading ${o} (${(c.size/1024/1024).toFixed(1)} MB)`),await download(`${t}/${o}`,a)),!fs.existsSync(a))throw setupError(`missing ${a}`);if(sha256File(a)!==c.sha256)throw setupError(`sha256 mismatch for ${o}; refusing to install`);s[o]=a}return{manifest:r,files:s}}function run(n,t,e={}){const r=spawnSync(n,t,{encoding:"utf8",...e});return{code:r.status,out:`${r.stdout||""}${r.stderr||""}`.replace(/\0/g,"")}}function installDarwin(n,t){const e=linuxArch(),r=path.join(TNC_DIR,"kernel"),i=path.join(TNC_DIR,"bin");fs.mkdirSync(r,{recursive:!0}),fs.mkdirSync(i,{recursive:!0}),run(path.join(i,"tnc-vz"),["stop"]);for(const n of["vmlinuz","initramfs.img"]){const t=path.join(r,n);fs.existsSync(t)&&!fs.existsSync(`${t}.pre-bundle`)&&fs.copyFileSync(t,`${t}.pre-bundle`)}const s=run("tar",["-xzf",n[`tnc-dock-vm-${e}.tar.gz`],"-C",r]);if(0!==s.code)throw setupError(`cannot unpack VM image: ${s.out}`);const o=path.join(i,"tnc-vz");if(fs.existsSync(o)&&!fs.existsSync(`${o}.pre-bundle`)&&fs.copyFileSync(o,`${o}.pre-bundle`),fs.copyFileSync(n["tnc-vz-darwin-universal"],`${o}.new`),fs.chmodSync(`${o}.new`,493),fs.renameSync(`${o}.new`,o),t(`MicroVM image (${e}) and tnc-vz installed`),n["tnc-menubar-darwin-universal"]){const e=path.join(i,"tnc-menubar");fs.copyFileSync(n["tnc-menubar-darwin-universal"],`${e}.new`),fs.chmodSync(`${e}.new`,493),fs.renameSync(`${e}.new`,e),t("menu bar app installed (open it with: thinknsh dock app)")}}function wslDistros(){const n=run("wsl.exe",["--list","--quiet"]);return 0===n.code?n.out.split(/\r?\n/).map(n=>n.trim()).filter(Boolean):null}function sanitizeWslTar(n){const t=require("zlib");let e;try{e=t.gunzipSync(fs.readFileSync(n))}catch(t){return{path:n,cleanup:()=>{}}}let r=0;const i=[];let s=!1;for(;r<e.length;){const n=e.subarray(r,r+512);if(n.length<512||n.every(n=>0===n)){i.push(e.subarray(r));break}const t=n.subarray(124,136).toString("latin1").replace(/\0.*$/,"").trim(),o=parseInt(t,8)||0,c=String.fromCharCode(n[156]),a=512+512*Math.ceil(o/512);"x"===c||"g"===c?s=!0:i.push(e.subarray(r,r+a)),r+=a}if(!s)return{path:n,cleanup:()=>{}};const o=path.join(os.tmpdir(),`tnc-dock-wsl-clean-${Date.now()}.tar.gz`);return fs.writeFileSync(o,t.gzipSync(Buffer.concat(i))),{path:o,cleanup:()=>{try{fs.unlinkSync(o)}catch(n){}}}}function installWindows(n,t,{force:e=!1}={}){const r=linuxArch();if(0!==run("wsl.exe",["--status"]).code)throw setupError("WSL is not installed. Run in an admin PowerShell: wsl --install --no-distribution, reboot, then run thinknsh dock setup again");const i=wslDistros()||[],s=n[`tnc-dock-wsl-${r}.tar.gz`],{path:o,cleanup:c}=sanitizeWslTar(s);try{if(i.includes("tnc-dock")&&!e){const n=run("wsl.exe",["-d","tnc-dock","-u","root","--","tar","-xzf","-","-C","/","./usr/bin/tnc-shim","./usr/bin/crun","./usr/local/bin/tnc-wsl-init","./etc/wsl.conf","./bin/busybox.static"],{input:fs.readFileSync(o)});if(0!==n.code)throw setupError(`cannot upgrade WSL distro: ${n.out}`);return run("wsl.exe",["--terminate","tnc-dock"]),void t("WSL distro tnc-dock upgraded in place (layers and volumes kept)")}i.includes("tnc-dock")&&run("wsl.exe",["--unregister","tnc-dock"]);const n=path.join(TNC_DIR,"wsl","tnc-dock");fs.mkdirSync(n,{recursive:!0});const r=run("wsl.exe",["--import","tnc-dock",n,o,"--version","2"]);if(0!==r.code)throw setupError(`wsl --import failed: ${r.out.trim()}`);t(`WSL2 distro tnc-dock created in ${n}`)}finally{c()}}function installLinux(n,t){const e=linuxArch(),r=path.join(TNC_DIR,"bin");fs.mkdirSync(r,{recursive:!0});for(const t of[`tnc-shim-linux-${e}`,`crun-linux-${e}`,`tnc-busybox-linux-${e}`]){const e=t;n[t]&&(fs.copyFileSync(n[t],path.join(r,`${e}.new`)),fs.chmodSync(path.join(r,`${e}.new`),493),fs.renameSync(path.join(r,`${e}.new`),path.join(r,e)))}t(`tnc-shim, crun and busybox installed in ${r}${0===process.getuid?.()?"":" (dock must run as root on Linux)"}`)}async function prefetchBuildKit(n){if("0"===process.env.TNC_DOCK_BUILDKIT)return;const{installBuildKit:t,buildKitDir:e,buildKitInstalled:r,VERSION:i}=require("../build/buildkit"),s={arm64:"arm64",x64:"amd64"}[process.arch];if(!s)return;const o=e(process.env.TNC_DOCK_HOME||path.join(TNC_DIR,"dock"),s);if(r(o))return n(`BuildKit ${i} already installed`);try{n(`downloading BuildKit ${i} (linux/${s})`),await t(o,s),n(`BuildKit ${i} installed in ${o}`)}catch(t){n(`warning: ${t.message}; it will be downloaded on the first BuildKit build`)}}async function setup({from:n,baseUrl:t,force:e=!1,log:r=()=>{}}={}){const{manifest:i,files:s}=await fetchBundle({from:n,baseUrl:t,log:r});return"darwin"===process.platform?installDarwin(s,r):"win32"===process.platform?installWindows(s,r,{force:e}):installLinux(s,r),await prefetchBuildKit(r),fs.mkdirSync(path.join(TNC_DIR,"dock"),{recursive:!0}),fs.writeFileSync(path.join(TNC_DIR,"dock","runtime.json"),JSON.stringify({version:i.version,platform:process.platform,arch:process.arch,installedAt:(new Date).toISOString()},null,2)),i.version}module.exports={setup,prefetchBuildKit,wantedFiles,fetchBundle,linuxArch,WSL_DISTRO:"tnc-dock",sha256File,BUNDLE_VERSION:"0.2.2",sanitizeWslTar};
7
+ "use strict";const fs=require("fs"),os=require("os"),path=require("path"),crypto=require("crypto"),{spawnSync}=require("child_process"),TNC_DIR=path.join(os.homedir(),".tnc"),BUNDLE_VERSION="0.2.3",DEFAULT_BASE_URL=process.env.TNC_DOCK_BUNDLE_URL||"https://thinkncollab.com/downloads/dock/0.2.3",WSL_DISTRO="tnc-dock",linuxArch=(n=process.arch)=>({arm64:"aarch64",x64:"x86_64"}[n]);function setupError(n){const t=new Error(n);return t.setup=!0,t}function sha256File(n){const t=crypto.createHash("sha256"),r=fs.openSync(n,"r"),e=Buffer.alloc(1<<20);let s;for(;(s=fs.readSync(r,e,0,e.length,null))>0;)t.update(e.subarray(0,s));return fs.closeSync(r),t.digest("hex")}const OPTIONAL_FILES=/^tnc-busybox-linux-|^tnc-tray-/;function wantedFiles(n=process.platform,t=process.arch){const r=linuxArch(t);if(!r)throw setupError(`unsupported CPU architecture ${t}`);if("darwin"===n)return[`tnc-dock-vm-${r}.tar.gz`,"tnc-vz-darwin-universal","tnc-tray-darwin-universal.tar.gz"];if("win32"===n)return[`tnc-dock-wsl-${r}.tar.gz`];if("linux"===n)return[`tnc-shim-linux-${r}`,`crun-linux-${r}`,`tnc-busybox-linux-${r}`];throw setupError(`unsupported platform ${n}`)}async function download(n,t){const r=await fetch(n,{redirect:"follow"});if(!r.ok)throw setupError(`download failed: ${n} (HTTP ${r.status})`);fs.mkdirSync(path.dirname(t),{recursive:!0}),fs.writeFileSync(`${t}.part`,Buffer.from(await r.arrayBuffer())),fs.renameSync(`${t}.part`,t)}async function fetchBundle({from:n,baseUrl:t=DEFAULT_BASE_URL,log:r}){let e,s;if(n)s=path.resolve(n),e=JSON.parse(fs.readFileSync(path.join(s,"manifest.json"),"utf8"));else{let n=await fetch(`${t}/manifest.json`,{redirect:"follow"});if(!n.ok){const r="https://thinkncollab.com/downloads/dock",s=await fetch(`${r}/manifest.json`,{redirect:"follow"});s.ok&&(e=await s.json(),t=`${r}/${e.version}`,n=s)}if(!n.ok)throw setupError(`cannot fetch bundle manifest from ${t} (HTTP ${n.status})`);e||(e=await n.json()),s=path.join(TNC_DIR,"cache","bundles",e.version)}const i={};for(const o of wantedFiles()){const c=e.files[o];if(!c&&OPTIONAL_FILES.test(o)){r(`bundle ${e.version} has no ${o}; ${/tray/.test(o)?"the menu-bar app":"cp"} needs a newer bundle`);continue}if(!c)throw setupError(`bundle ${e.version} has no ${o}`);const a=path.join(s,o);if(n||fs.existsSync(a)&&sha256File(a)===c.sha256||(r(`downloading ${o} (${(c.size/1024/1024).toFixed(1)} MB)`),await download(`${t}/${o}`,a)),!fs.existsSync(a))throw setupError(`missing ${a}`);if(sha256File(a)!==c.sha256)throw setupError(`sha256 mismatch for ${o}; refusing to install`);i[o]=a}return{manifest:e,files:i}}function run(n,t,r={}){const e=spawnSync(n,t,{encoding:"utf8",...r});return{code:e.status,out:`${e.stdout||""}${e.stderr||""}`.replace(/\0/g,"")}}function installDarwin(n,t){const r=linuxArch(),e=path.join(TNC_DIR,"kernel"),s=path.join(TNC_DIR,"bin");fs.mkdirSync(e,{recursive:!0}),fs.mkdirSync(s,{recursive:!0}),run(path.join(s,"tnc-vz"),["stop"]);for(const n of["vmlinuz","initramfs.img"]){const t=path.join(e,n);fs.existsSync(t)&&!fs.existsSync(`${t}.pre-bundle`)&&fs.copyFileSync(t,`${t}.pre-bundle`)}const i=run("tar",["-xzf",n[`tnc-dock-vm-${r}.tar.gz`],"-C",e]);if(0!==i.code)throw setupError(`cannot unpack VM image: ${i.out}`);const o=path.join(s,"tnc-vz");fs.existsSync(o)&&!fs.existsSync(`${o}.pre-bundle`)&&fs.copyFileSync(o,`${o}.pre-bundle`),fs.copyFileSync(n["tnc-vz-darwin-universal"],`${o}.new`),fs.chmodSync(`${o}.new`,493),fs.renameSync(`${o}.new`,o),t(`MicroVM image (${r}) and tnc-vz installed`);const c=n["tnc-tray-darwin-universal.tar.gz"];if(c){fs.rmSync(path.join(s,"Thinknsh.app"),{recursive:!0,force:!0});const n=run("tar",["-xzf",c,"-C",s]);if(0!==n.code)throw setupError(`cannot unpack the menu-bar app: ${n.out}`);t("menu-bar app unpacked (install it: thinknsh dock app install)")}}function wslDistros(){const n=run("wsl.exe",["--list","--quiet"]);return 0===n.code?n.out.split(/\r?\n/).map(n=>n.trim()).filter(Boolean):null}function sanitizeWslTar(n){const t=require("zlib");let r;try{r=t.gunzipSync(fs.readFileSync(n))}catch(t){return{path:n,cleanup:()=>{}}}let e=0;const s=[];let i=!1;for(;e<r.length;){const n=r.subarray(e,e+512);if(n.length<512||n.every(n=>0===n)){s.push(r.subarray(e));break}const t=n.subarray(124,136).toString("latin1").replace(/\0.*$/,"").trim(),o=parseInt(t,8)||0,c=String.fromCharCode(n[156]),a=512+512*Math.ceil(o/512);"x"===c||"g"===c?i=!0:s.push(r.subarray(e,e+a)),e+=a}if(!i)return{path:n,cleanup:()=>{}};const o=path.join(os.tmpdir(),`tnc-dock-wsl-clean-${Date.now()}.tar.gz`);return fs.writeFileSync(o,t.gzipSync(Buffer.concat(s))),{path:o,cleanup:()=>{try{fs.unlinkSync(o)}catch(n){}}}}function installWindows(n,t,{force:r=!1}={}){const e=linuxArch();if(0!==run("wsl.exe",["--status"]).code)throw setupError("WSL is not installed. Run in an admin PowerShell: wsl --install --no-distribution, reboot, then run thinknsh dock setup again");const s=wslDistros()||[],i=n[`tnc-dock-wsl-${e}.tar.gz`],{path:o,cleanup:c}=sanitizeWslTar(i);try{if(s.includes("tnc-dock")&&!r){const n=run("wsl.exe",["-d","tnc-dock","-u","root","--","tar","-xzf","-","-C","/","./usr/bin/tnc-shim","./usr/bin/crun","./usr/local/bin/tnc-wsl-init","./etc/wsl.conf","./bin/busybox.static"],{input:fs.readFileSync(o)});if(0!==n.code)throw setupError(`cannot upgrade WSL distro: ${n.out}`);return run("wsl.exe",["--terminate","tnc-dock"]),void t("WSL distro tnc-dock upgraded in place (layers and volumes kept)")}s.includes("tnc-dock")&&run("wsl.exe",["--unregister","tnc-dock"]);const n=path.join(TNC_DIR,"wsl","tnc-dock");fs.mkdirSync(n,{recursive:!0});const e=run("wsl.exe",["--import","tnc-dock",n,o,"--version","2"]);if(0!==e.code)throw setupError(`wsl --import failed: ${e.out.trim()}`);t(`WSL2 distro tnc-dock created in ${n}`)}finally{c()}}function installLinux(n,t){const r=linuxArch(),e=path.join(TNC_DIR,"bin");fs.mkdirSync(e,{recursive:!0});for(const t of[`tnc-shim-linux-${r}`,`crun-linux-${r}`,`tnc-busybox-linux-${r}`]){const r=t;n[t]&&(fs.copyFileSync(n[t],path.join(e,`${r}.new`)),fs.chmodSync(path.join(e,`${r}.new`),493),fs.renameSync(path.join(e,`${r}.new`),path.join(e,r)))}if(t(`tnc-shim, crun and busybox installed in ${e}`),0!==process.getuid?.()){const n=require("../linux/host").rootlessProblems();t(n.length?`rootless mode needs:\n - ${n.join("\n - ")}`:"rootless mode is ready (dock runs as you, no sudo)")}}async function prefetchBuildKit(n){if("0"===process.env.TNC_DOCK_BUILDKIT)return;const{installBuildKit:t,buildKitDir:r,buildKitInstalled:e,VERSION:s}=require("../build/buildkit"),i={arm64:"arm64",x64:"amd64"}[process.arch];if(!i)return;const o=r(process.env.TNC_DOCK_HOME||path.join(TNC_DIR,"dock"),i);if(e(o))return n(`BuildKit ${s} already installed`);try{n(`downloading BuildKit ${s} (linux/${i})`),await t(o,i),n(`BuildKit ${s} installed in ${o}`)}catch(t){n(`warning: ${t.message}; it will be downloaded on the first BuildKit build`)}}async function setup({from:n,baseUrl:t,force:r=!1,log:e=()=>{}}={}){const{manifest:s,files:i}=await fetchBundle({from:n,baseUrl:t,log:e});return"darwin"===process.platform?installDarwin(i,e):"win32"===process.platform?installWindows(i,e,{force:r}):installLinux(i,e),await prefetchBuildKit(e),fs.mkdirSync(path.join(TNC_DIR,"dock"),{recursive:!0}),fs.writeFileSync(path.join(TNC_DIR,"dock","runtime.json"),JSON.stringify({version:s.version,platform:process.platform,arch:process.arch,installedAt:(new Date).toISOString()},null,2)),s.version}module.exports={setup,prefetchBuildKit,wantedFiles,fetchBundle,linuxArch,WSL_DISTRO:"tnc-dock",sha256File,BUNDLE_VERSION:"0.2.3",sanitizeWslTar};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const fs=require("fs"),path=require("path"),readline=require("readline"),{spawn}=require("child_process"),{dockPaths}=require("../paths"),{request,ping}=require("../client"),CLI=path.join(__dirname,"..","..","..","bin","thinknsh.js"),PROJECT_LABEL="tnc.compose.project",POLL_MS=3e3;function openCommand(t,e=process.platform){return"darwin"===e?["open",[t]]:"win32"===e?/^[a-z]+:\/\//i.test(t)?["rundll32",["url.dll,FileProtocolHandler",t]]:["explorer.exe",[t]]:["xdg-open",[t]]}function openTarget(t,e=process.platform){const[s,i]=openCommand(t,e);try{spawn(s,i,{stdio:"ignore",detached:!0,windowsHide:!0}).on("error",()=>{}).unref()}catch(t){}return[s,i]}function windowsBatchLine(t){return t.map(t=>`"${String(t).replace(/%/g,"%%").replace(/"/g,'""')}"`).join(" ")}const shortName=t=>(t.Names?.[0]||t.Id.slice(0,12)).replace(/^\//,""),quoteSh=t=>`'${String(t).replace(/'/g,"'\\''")}'`;function publishedPorts(t){const e=(t.Ports||[]).filter(t=>t.PublicPort&&"tcp"===(t.Type||"tcp"));return[...new Set(e.map(t=>t.PublicPort))].sort((t,e)=>t-e)}function buildMenu(t){const e=[],s=t.containers||[],i=s.filter(t=>"running"===t.State),n=t.starting?"starting":t.running?"running":"stopped";if(e.push({id:"status",enabled:!1,label:"running"===n?`dock ${t.version||""} is running · ${i.length} container${1===i.length?"":"s"} up`.replace(" "," "):"starting"===n?"dock is starting…":"dock is stopped"}),e.push({separator:!0}),t.running){e.push({id:"dashboard",label:"Open Dashboard"});const t=new Map;for(const e of s){const s=e.Labels?.[PROJECT_LABEL],i=s?`app:${s}`:"single";t.has(i)||t.set(i,{project:s,list:[]}),t.get(i).list.push(e)}const n=t=>{const e=shortName(t),s="running"===t.State,i=[];if(s){for(const e of publishedPorts(t).slice(0,5))i.push({id:`open:${e}`,label:`Open localhost:${e}`});i.push({id:`logs:${e}`,label:"Logs in Terminal"}),i.push({id:`shell:${e}`,label:"Shell in Terminal"}),i.push({separator:!0}),i.push({id:`restart:${t.Id}`,label:"Restart"}),i.push({id:`stop:${t.Id}`,label:"Stop"})}else i.push({id:`start:${t.Id}`,label:"Start"}),i.push({id:`rm:${t.Id}`,label:"Remove",confirm:`Remove container ${e}?`});return{id:`c:${t.Id}`,label:`${s?"●":"○"} ${e} ${t.Image}`,items:i}},a=[...t.values()].filter(t=>t.project).sort((t,e)=>t.project.localeCompare(e.project)),r=t.get("single")?.list||[];if(a.length||r.length){const t=[];for(const e of a){const s=e.list.filter(t=>"running"===t.State).length;t.push({id:`app:${e.project}`,label:`${s?"●":"○"} ${e.project} (${s}/${e.list.length})`,items:[...e.list.map(n),{separator:!0},{id:`appstop:${e.project}`,label:"Stop all",enabled:s>0}]})}a.length&&r.length&&t.push({separator:!0}),t.push(...r.slice(0,30).map(n)),e.push({id:"containers",label:`Containers (${i.length} running)`,items:t})}else e.push({id:"containers",label:"No containers yet",enabled:!1});e.push({separator:!0}),e.push({id:"settings",label:"Settings…"}),e.push({id:"restart-vm",label:"Restart Linux VM",confirm:i.length?`Restart the Linux VM? ${i.length} running container${1===i.length?"":"s"} will stop.`:void 0}),e.push({id:"stop",label:"Stop dock",confirm:i.length?`Stop dock? ${i.length} running container${1===i.length?"":"s"} will stop.`:void 0})}else t.starting||e.push({id:"start",label:"Start dock"});return e.push({separator:!0}),e.push({id:"autostart",label:"Start at Login",checked:!!t.autostart?.enabled}),e.push({id:"troubleshoot",label:"Troubleshoot",items:[{id:"selftest",label:"Run Self-test in Terminal"},{id:"diagnose",label:"Create Diagnostics Bundle"},{id:"logfile",label:"Show Engine Log"},{separator:!0},{id:"clean",label:"Clean Up (stopped containers, unused images, build cache)…",enabled:!!t.running,confirm:"Remove stopped containers, unused networks, unused images and the build cache? Volumes are kept."},{id:"reset",label:"Reset to Factory Defaults…",confirm:"Delete ALL containers, images, volumes and the Linux VM disk? Settings and registry logins are kept. This cannot be undone."}]}),e.push({separator:!0}),e.push({id:"quit",label:"Quit Tray"}),e.push({id:"quit-stop",label:"Quit and Stop dock",enabled:!!t.running,confirm:i.length?`Stop dock? ${i.length} running container${1===i.length?"":"s"} will stop.`:void 0}),{status:n,title:t.running?`${i.length}`:"",items:e}}class TrayAgent{constructor({out:t=process.stdout,paths:e=dockPaths(),cli:s=CLI,node:i=process.execPath,platform:n=process.platform}={}){this.out=t,this.paths=e,this.cli=s,this.node=i,this.platform=n,this.ui=null,this.starting=!1,this.lastMenu=""}send(t){this.out.write(JSON.stringify(t)+"\n")}async snapshot(){const t=require("../setup/autostart").status();if(!await ping(this.paths.socket,800))return{running:!1,starting:this.starting,containers:[],autostart:t};this.starting=!1;const[e,s]=await Promise.all([request(this.paths.socket,"GET","/containers/json?all=1").catch(()=>({body:[]})),request(this.paths.socket,"GET","/version").catch(()=>({body:{}}))]);return{running:!0,containers:Array.isArray(e.body)?e.body:[],autostart:t,version:s.body?.Version}}async refresh(t=!1){const e=buildMenu(await this.snapshot()),s=JSON.stringify(e);(t||s!==this.lastMenu)&&(this.lastMenu=s,this.send({menu:e}))}runCli(t){return new Promise(e=>{const s=spawn(this.node,[this.cli,...t],{stdio:"ignore",windowsHide:!0,env:{...process.env,FORCE_COLOR:"0"}});s.on("exit",t=>e(t??1)),s.on("error",()=>e(1))})}open(t){openTarget(t,this.platform)}terminal(t){if("darwin"===this.platform){const e=[this.node,this.cli,...t].map(quoteSh).join(" "),s=`tell application "Terminal"\n activate\n do script ${JSON.stringify(e)}\nend tell`;spawn("osascript",["-e",s],{stdio:"ignore",detached:!0}).on("error",()=>{}).unref()}else if("win32"===this.platform){const e=path.join(require("os").tmpdir(),`thinknsh-${process.pid}-${Date.now()}.cmd`);fs.writeFileSync(e,`@echo off\r\n${windowsBatchLine([this.node,this.cli,...t])}\r\n`),spawn("cmd.exe",[`/d /c start "thinknsh" "${e}"`],{stdio:"ignore",detached:!0,windowsHide:!0,windowsVerbatimArguments:!0}).on("error",()=>{}).unref(),setTimeout(()=>fs.rm(e,{force:!0},()=>{}),6e4).unref()}else spawn("x-terminal-emulator",["-e",this.node,this.cli,...t],{stdio:"ignore",detached:!0}).on("error",()=>{}).unref()}async dashboard(t){if(!this.ui){const{startUi:t}=require("../ui/server");this.ui=await t({socketPath:this.paths.socket})}this.open(t?`${this.ui.url}&tab=${t}`:this.ui.url)}async click(t){const[e,...s]=String(t).split(":"),i=s.join(":"),n=(t,e)=>request(this.paths.socket,t,e,{timeout:6e4});switch(e){case"start":if(i){await n("POST",`/containers/${i}/start`);break}this.starting=!0,await this.refresh(),await this.runCli(["dock","start"]),this.starting=!1;break;case"stop":i?await n("POST",`/containers/${i}/stop`):await this.runCli(["dock","stop"]);break;case"restart":await n("POST",`/containers/${i}/restart`);break;case"rm":await n("DELETE",`/containers/${i}?force=1`);break;case"appstop":{const t=await n("GET","/containers/json?filters="+encodeURIComponent(JSON.stringify({label:[`${PROJECT_LABEL}=${i}`]})));await Promise.all((t.body||[]).map(t=>n("POST",`/containers/${t.Id}/stop`)));break}case"open":this.open(`http://localhost:${i}`);break;case"logs":this.terminal(["logs","-f","-n","200",i]);break;case"shell":this.terminal(["exec","-it",i,"sh","-c","command -v bash >/dev/null && exec bash || exec sh"]);break;case"dashboard":await this.dashboard();break;case"settings":await this.dashboard("settings");break;case"restart-vm":await this.runCli(["dock","vm-restart","--force"]);break;case"autostart":{const t=require("../setup/autostart");try{t.status().enabled?t.disable():t.enable()}catch(t){this.send({notify:t.message})}break}case"selftest":this.terminal(["dock","selftest"]);break;case"diagnose":{const{diagnose:t}=require("../setup/diagnose"),e=await t({paths:this.paths});this.send({notify:`Diagnostics saved: ${e}`}),this.open(path.dirname(e));break}case"logfile":fs.existsSync(this.paths.logFile)&&this.open(this.paths.logFile);break;case"clean":await this.runCli(["system","prune","-f"]);break;case"reset":await this.runCli(["dock","reset","--yes"]);break;case"quit-stop":await this.runCli(["dock","stop"]);case"quit":return this.ui&&await this.ui.close(),this.send({exit:!0}),void(this.stopped=!0)}await this.refresh()}async run(t=process.stdin){await this.refresh(!0);const e=setInterval(()=>this.refresh().catch(()=>{}),3e3),s=readline.createInterface({input:t});s.on("line",t=>{let i;try{i=JSON.parse(t)}catch(t){return}const n=i.click?this.click(i.click):i.refresh?this.refresh(!0):null;n&&n.catch(t=>this.send({notify:t.message})).then(()=>{this.stopped&&(clearInterval(e),s.close())})}),await new Promise(t=>s.on("close",t)),clearInterval(e),this.ui&&await this.ui.close()}}module.exports={TrayAgent,buildMenu,publishedPorts,openTarget,openCommand,windowsBatchLine};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const fs=require("fs"),os=require("os"),path=require("path"),{spawnSync,spawn}=require("child_process"),ROOT=path.join(__dirname,"..","..",".."),CLI=path.join(ROOT,"bin","thinknsh.js"),SOURCE=path.join(ROOT,"native","tnc-tray"),macTarget=()=>process.env.TNC_APP_DIR||path.join(os.homedir(),"Applications","Thinknsh.app"),winDir=()=>process.env.TNC_APP_DIR||path.join(process.env.LOCALAPPDATA||path.join(os.homedir(),"AppData","Local"),"Thinknsh"),winMenuEntry=()=>path.join(process.env.APPDATA||path.join(os.homedir(),"AppData","Roaming"),"Microsoft","Windows","Start Menu","Programs","Thinknsh.vbs");function agentConfig(){return JSON.stringify({node:process.execPath,cli:CLI},null,2)+"\n"}function findMacApp(n){const t=path.join(os.homedir(),".tnc","bin","Thinknsh.app");if(fs.existsSync(path.join(t,"Contents","MacOS","thinknsh-tray")))return t;const s=path.join(SOURCE,"build-macos.sh");if(fs.existsSync(s)&&0===spawnSync("xcrun",["--find","swiftc"],{stdio:"ignore"}).status){const t=fs.mkdtempSync(path.join(os.tmpdir(),"tnc-tray-"));n?.("building the menu-bar app from source (swiftc)");const e=spawnSync("bash",[s,t],{encoding:"utf8"});if(0!==e.status)throw new Error(`building the app failed: ${(e.stderr||e.stdout).trim().split("\n").slice(-3).join(" ")}`);return path.join(t,"Thinknsh.app")}throw new Error("the menu-bar app is not in this runtime: run thinknsh dock setup again, or install Xcode command line tools to build it")}function winLauncher(n){const t=n=>n.replace(/"/g,'""');return`' thinknsh dock tray (written by thinknsh dock app install)\r\nSet sh = CreateObject("WScript.Shell")\r\nsh.Run "powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File ""${t(path.join(n,"tray.ps1"))}"" -Node ""${t(process.execPath)}"" -Cli ""${t(CLI)}""", 0, False\r\n`}function removeEarlierApp(n){const t=path.join(os.homedir(),"Applications","TNC Dock.app"),s=path.join(os.homedir(),"Library","LaunchAgents","com.thinkncollab.dock.menubar.plist");fs.existsSync(s)&&(spawnSync("launchctl",["bootout",`gui/${process.getuid()}/com.thinkncollab.dock.menubar`],{stdio:"ignore"}),fs.rmSync(s,{force:!0})),fs.existsSync(t)&&(spawnSync("pkill",["-f",`${t}/Contents/MacOS/`],{stdio:"ignore"}),fs.rmSync(t,{recursive:!0,force:!0}),n.push(`removed the earlier menu bar app (${t})`))}function install({log:n}={}){const t=[];if("darwin"===process.platform){removeEarlierApp(t);const s=findMacApp(n),e=macTarget();fs.mkdirSync(path.dirname(e),{recursive:!0}),spawnSync("pkill",["-f",`${e}/Contents/MacOS/thinknsh-tray`],{stdio:"ignore"}),fs.rmSync(e,{recursive:!0,force:!0});const i=spawnSync("ditto",[s,e],{encoding:"utf8"});if(0!==i.status)throw new Error(`copying the app: ${i.stderr.trim()}`);fs.writeFileSync(path.join(e,"Contents","Resources","agent.json"),agentConfig()),t.push(`menu-bar app: ${e}`)}else{if("win32"!==process.platform)throw new Error("there is no tray app on Linux; the dashboard is thinknsh dock ui");{const n=winDir();fs.mkdirSync(n,{recursive:!0}),fs.copyFileSync(path.join(SOURCE,"tray.ps1"),path.join(n,"tray.ps1"));const s=path.join(ROOT,"native","brand","tnc.ico");fs.existsSync(s)&&fs.copyFileSync(s,path.join(n,"tnc.ico")),fs.writeFileSync(path.join(n,"Thinknsh.vbs"),winLauncher(n)),fs.mkdirSync(path.dirname(winMenuEntry()),{recursive:!0}),fs.copyFileSync(path.join(n,"Thinknsh.vbs"),winMenuEntry()),t.push(`tray app: ${n} (Start menu: Thinknsh)`)}}const s=require("../setup/autostart");return s.status().enabled&&t.push(...s.enable().filter(n=>/app/.test(n))),open(),t}function installedPath(){if("darwin"===process.platform)return fs.existsSync(macTarget())?macTarget():require("../setup/autostart").appBundle();if("win32"===process.platform){const n=path.join(winDir(),"Thinknsh.vbs");return fs.existsSync(n)?n:null}return null}function open(){const n=installedPath();if(!n)throw new Error("the tray app is not installed: thinknsh dock app install");"darwin"===process.platform?spawn("open",[n],{stdio:"ignore",detached:!0}).unref():spawn("wscript.exe",[n],{stdio:"ignore",detached:!0}).unref()}function uninstall(){const n=[];if("darwin"===process.platform){const t=macTarget();spawnSync("pkill",["-f",`${t}/Contents/MacOS/thinknsh-tray`],{stdio:"ignore"}),fs.existsSync(t)&&(fs.rmSync(t,{recursive:!0,force:!0}),n.push(`removed ${t}`))}else if("win32"===process.platform)for(const t of[winDir(),winMenuEntry()])fs.existsSync(t)&&(fs.rmSync(t,{recursive:!0,force:!0}),n.push(`removed ${t}`));const t=require("../setup/autostart");return t.status().app&&(fs.rmSync(path.join(path.dirname(t.status().file),`${t.APP_LABEL}.plist`),{force:!0}),n.push("removed the login item of the app")),n.length||n.push("the tray app was not installed"),n}module.exports={install,uninstall,open,installedPath,winLauncher};