thinkncollab-shell 0.0.20 → 2.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +82 -0
- package/Readme.md +246 -502
- package/dist/bin/thinknsh.js +8 -0
- package/dist/src/commands/auth/login.js +7 -0
- package/dist/src/commands/auth/logout.js +7 -0
- package/dist/src/commands/auth/register.js +7 -0
- package/dist/src/commands/auth/whoami.js +7 -0
- package/dist/src/commands/dock/bench.js +7 -0
- package/dist/src/commands/dock/ci.js +7 -0
- package/dist/src/commands/dock/cluster.js +7 -0
- package/dist/src/commands/dock/containers.js +7 -0
- package/dist/src/commands/dock/dock.js +7 -0
- package/dist/src/commands/github/commits.js +7 -0
- package/dist/src/commands/github/issue-create.js +7 -0
- package/dist/src/commands/github/issues.js +7 -0
- package/dist/src/commands/github/pulls.js +7 -0
- package/dist/src/commands/github/statusgh.js +7 -0
- package/dist/src/commands/index.js +7 -0
- package/dist/src/commands/message/say.js +7 -0
- package/dist/src/commands/room/join.js +7 -0
- package/dist/src/commands/room/leave.js +7 -0
- package/dist/src/commands/room/rooms.js +7 -0
- package/dist/src/commands/room/sync.js +7 -0
- package/dist/src/commands/sandbox/console.js +7 -0
- package/dist/src/commands/sandbox/dep.js +7 -0
- package/dist/src/commands/sandbox/deploy.js +7 -0
- package/dist/src/commands/sandbox/dev.js +7 -0
- package/dist/src/commands/sandbox/exec.js +7 -0
- package/dist/src/commands/sandbox/freeze.js +7 -0
- package/dist/src/commands/sandbox/push.js +7 -0
- package/dist/src/commands/sandbox/resume.js +7 -0
- package/dist/src/commands/sandbox/sandbox.js +7 -0
- package/dist/src/commands/sandbox/share.js +7 -0
- package/dist/src/commands/sandbox/snapshot.js +7 -0
- package/dist/src/commands/sandbox/sync-deps.js +7 -0
- package/dist/src/commands/shell/doctor.js +7 -0
- package/dist/src/commands/shell/notifications.js +7 -0
- package/dist/src/commands/shell/notify.js +7 -0
- package/dist/src/commands/shell/status.js +7 -0
- package/dist/src/commands/shell/version.js +7 -0
- package/dist/src/commands/tasks/build.js +7 -0
- package/dist/src/commands/tasks/complete.js +7 -0
- package/dist/src/commands/tasks/create-task.js +7 -0
- package/dist/src/commands/tasks/loadtest.js +7 -0
- package/dist/src/commands/tasks/task-status.js +7 -0
- package/dist/src/commands/tasks/task.js +7 -0
- package/dist/src/commands/tasks/tasks.js +7 -0
- package/dist/src/commands/team/invite.js +7 -0
- package/dist/src/commands/team/myteam.js +7 -0
- package/dist/src/commands/team/teammates.js +7 -0
- package/dist/src/commands/terminal/share.js +7 -0
- package/dist/src/core/api-client.js +7 -0
- package/dist/src/core/crypto/e2ee-vault.js +7 -0
- package/dist/src/core/dep-store.js +7 -0
- package/dist/src/core/dependency-tracker.js +7 -0
- package/dist/src/core/geekload.js +7 -0
- package/dist/src/core/local-runner.js +7 -0
- package/dist/src/core/multi-lang-scanner.js +7 -0
- package/dist/src/core/notification-window.js +8 -0
- package/dist/src/core/notify-auth.js +7 -0
- package/dist/src/core/project-config.js +7 -0
- package/dist/src/core/sandbox/arch-shim.js +7 -0
- package/dist/src/core/sandbox/capsule-builder.js +7 -0
- package/dist/src/core/sandbox/cgroups.js +7 -0
- package/dist/src/core/sandbox/container-monitor.js +7 -0
- package/dist/src/core/sandbox/crun-manager.js +7 -0
- package/dist/src/core/sandbox/engine.js +7 -0
- package/dist/src/core/sandbox/image-manager.js +7 -0
- package/dist/src/core/sandbox/image-store.js +7 -0
- package/dist/src/core/sandbox/index.js +7 -0
- package/dist/src/core/sandbox/macos-sandbox.js +7 -0
- package/dist/src/core/sandbox/microvm-manager.js +7 -0
- package/dist/src/core/sandbox/native-service-manager.js +7 -0
- package/dist/src/core/sandbox/network.js +7 -0
- package/dist/src/core/sandbox/oci-exporter.js +7 -0
- package/dist/src/core/sandbox/oci-puller.js +7 -0
- package/dist/src/core/sandbox/oci-runtime-spec.js +7 -0
- package/dist/src/core/sandbox/policy-engine.js +7 -0
- package/dist/src/core/sandbox/port-proxy-daemon.js +7 -0
- package/dist/src/core/sandbox/room-sync-manager.js +7 -0
- package/dist/src/core/sandbox/runtime-manager.js +7 -0
- package/dist/src/core/sandbox/snapshot-manager.js +7 -0
- package/dist/src/core/sandbox/storage.js +7 -0
- package/dist/src/core/sandbox/supervisor.js +7 -0
- package/dist/src/core/sandbox/tunnel-daemon.js +7 -0
- package/dist/src/core/sandbox/tunnel.js +7 -0
- package/dist/src/core/sandbox/window-launcher.js +7 -0
- package/dist/src/core/sandbox/windows-wsl2.js +7 -0
- package/dist/src/core/sandbox-window.js +8 -0
- package/dist/src/core/shell.js +7 -0
- package/dist/src/core/websocket.js +7 -0
- package/dist/src/dock/api/routes.js +7 -0
- package/dist/src/dock/api/server.js +7 -0
- package/dist/src/dock/api/wire.js +7 -0
- package/dist/src/dock/bench/memory.js +7 -0
- package/dist/src/dock/bench/runner.js +7 -0
- package/dist/src/dock/build/builder.js +7 -0
- package/dist/src/dock/build/buildfile.js +7 -0
- package/dist/src/dock/build/buildkit.js +7 -0
- package/dist/src/dock/build/changes.js +7 -0
- package/dist/src/dock/ci/actions.js +7 -0
- package/dist/src/dock/ci/runner.js +7 -0
- package/dist/src/dock/ci/workflow.js +7 -0
- package/dist/src/dock/cli/api.js +7 -0
- package/dist/src/dock/cli/args.js +7 -0
- package/dist/src/dock/cli/commands.js +7 -0
- package/dist/src/dock/cli/compose.js +7 -0
- package/dist/src/dock/cli/context.js +7 -0
- package/dist/src/dock/cli/files.js +7 -0
- package/dist/src/dock/cli/format.js +7 -0
- package/dist/src/dock/cli/manage.js +7 -0
- package/dist/src/dock/cli/moved.js +7 -0
- package/dist/src/dock/cli/services.js +7 -0
- package/dist/src/dock/cli/session.js +7 -0
- package/dist/src/dock/cli/workspace.js +7 -0
- package/dist/src/dock/client.js +7 -0
- package/dist/src/dock/compat/cases.js +7 -0
- package/dist/src/dock/compat/runner.js +7 -0
- package/dist/src/dock/config.js +7 -0
- package/dist/src/dock/daemon-main.js +7 -0
- package/dist/src/dock/daemon.js +7 -0
- package/dist/src/dock/deploy/compose.js +7 -0
- package/dist/src/dock/engine.js +7 -0
- package/dist/src/dock/image/archive.js +7 -0
- package/dist/src/dock/image/auth.js +7 -0
- package/dist/src/dock/image/reference.js +7 -0
- package/dist/src/dock/image/registry.js +7 -0
- package/dist/src/dock/image/tar.js +7 -0
- package/dist/src/dock/index.js +7 -0
- package/dist/src/dock/linux/host.js +7 -0
- package/dist/src/dock/paths.js +7 -0
- package/dist/src/dock/runtime/emulation.js +7 -0
- package/dist/src/dock/runtime/fsevents.js +7 -0
- package/dist/src/dock/runtime/fsops.js +7 -0
- package/dist/src/dock/runtime/hostfwd.js +7 -0
- package/dist/src/dock/runtime/logs.js +7 -0
- package/dist/src/dock/runtime/network.js +7 -0
- package/dist/src/dock/runtime/ports.js +7 -0
- package/dist/src/dock/runtime/rootfs.js +7 -0
- package/dist/src/dock/runtime/seccomp-profile.LICENSE +202 -0
- package/dist/src/dock/runtime/seccomp-profile.json +1227 -0
- package/dist/src/dock/runtime/seccomp.js +7 -0
- package/dist/src/dock/runtime/spec.js +7 -0
- package/dist/src/dock/runtime/stats.js +7 -0
- package/dist/src/dock/runtime/volumes.js +7 -0
- package/dist/src/dock/selftest.js +7 -0
- package/dist/src/dock/setup/app.js +7 -0
- package/dist/src/dock/setup/autostart.js +7 -0
- package/dist/src/dock/setup/index.js +7 -0
- package/dist/src/dock/setup/integrate.js +7 -0
- package/dist/src/dock/setup/migrate.js +7 -0
- package/dist/src/dock/state/store.js +7 -0
- package/dist/src/dock/ui/page.js +385 -0
- package/dist/src/dock/ui/server.js +7 -0
- package/dist/src/dock/version.js +7 -0
- package/dist/src/dock/workspace/apps.js +7 -0
- package/dist/src/dock/workspace/workspace.js +7 -0
- package/dist/src/events/notifications.js +7 -0
- package/dist/src/utils/logger.js +7 -0
- package/dist/src/utils/terminalCrypto.js +7 -0
- package/package.json +41 -16
- package/bin/thinknsh.js +0 -71
- package/src/commands/auth/login.js +0 -84
- package/src/commands/auth/logout.js +0 -32
- package/src/commands/auth/register.js +0 -52
- package/src/commands/auth/whoami.js +0 -52
- package/src/commands/github/commits.js +0 -58
- package/src/commands/github/issue-create.js +0 -59
- package/src/commands/github/issues.js +0 -57
- package/src/commands/github/pulls.js +0 -58
- package/src/commands/github/statusgh.js +0 -50
- package/src/commands/index.js +0 -136
- package/src/commands/message/say.js +0 -49
- package/src/commands/room/join.js +0 -142
- package/src/commands/room/leave.js +0 -24
- package/src/commands/room/rooms.js +0 -66
- package/src/commands/shell/notify.js +0 -88
- package/src/commands/shell/status.js +0 -73
- package/src/commands/shell/version.js +0 -18
- package/src/commands/tasks/build.js +0 -190
- package/src/commands/tasks/complete.js +0 -940
- package/src/commands/tasks/create-task.js +0 -101
- package/src/commands/tasks/loadtest.js +0 -101
- package/src/commands/tasks/suggestTask.js +0 -0
- package/src/commands/tasks/task-status.js +0 -72
- package/src/commands/tasks/task.js +0 -182
- package/src/commands/tasks/tasks.js +0 -184
- package/src/commands/team/invite.js +0 -46
- package/src/commands/team/myteam.js +0 -78
- package/src/commands/team/teammates.js +0 -52
- package/src/commands/terminal/share.js +0 -261
- package/src/core/api-client.js +0 -246
- package/src/core/geekload.js +0 -524
- package/src/core/local-runner.js +0 -861
- package/src/core/notification-window.js +0 -165
- package/src/core/project-config.js +0 -100
- package/src/core/shell.js +0 -734
- package/src/core/websocket.js +0 -657
- package/src/events/notifications.js +0 -184
- package/src/utils/logger.js +0 -159
- package/src/utils/terminalCrypto.js +0 -47
|
@@ -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 os=require("os"),PROFILE=require("./seccomp-profile.json"),ARCH={arm64:{name:"arm64",seccomp:"SCMP_ARCH_AARCH64"},x64:{name:"amd64",seccomp:"SCMP_ARCH_X86_64"}},VM_KERNEL="6.6";function kernelVersion(){return"linux"===process.platform?os.release():"6.6"}function atLeast(e,r){const s=String(e).split(/[.-]/).map(Number),t=String(r).split(/[.-]/).map(Number);for(let e=0;e<t.length;e++)if((s[e]||0)!==t[e])return(s[e]||0)>t[e];return!0}function applies(e,r,s,t){const a=e.includes||{},c=e.excludes||{};return!(a.arches&&!a.arches.includes(r)||a.caps&&!a.caps.every(e=>s.has(e))||a.minKernel&&!atLeast(t,a.minKernel)||c.arches&&c.arches.includes(r)||c.caps&&c.caps.some(e=>s.has(e)))}function defaultSeccomp(e=process.arch,r=[]){const s=ARCH[e];if(!s)return{defaultAction:"SCMP_ACT_ALLOW",architectures:[],syscalls:[]};const t=new Set(r),a=(PROFILE.archMap||[]).find(e=>e.architecture===s.seccomp),c=kernelVersion(),n=PROFILE.syscalls.filter(e=>applies(e,s.name,t,c)).map(e=>{const r={names:e.names,action:e.action};return e.args&&e.args.length&&(r.args=e.args.map(e=>({index:e.index,value:e.value,...e.valueTwo?{valueTwo:e.valueTwo}:{},op:e.op}))),void 0!==e.errnoRet&&(r.errnoRet=e.errnoRet),r});return{defaultAction:PROFILE.defaultAction,...void 0!==PROFILE.defaultErrnoRet?{defaultErrnoRet:PROFILE.defaultErrnoRet}:{},architectures:[s.seccomp,...a&&a.subArchitectures||[]],syscalls:n}}module.exports={defaultSeccomp};
|
|
@@ -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{defaultSeccomp}=require("./seccomp"),ALL_CAPS=["CAP_CHOWN","CAP_DAC_OVERRIDE","CAP_DAC_READ_SEARCH","CAP_FOWNER","CAP_FSETID","CAP_KILL","CAP_SETGID","CAP_SETUID","CAP_SETPCAP","CAP_LINUX_IMMUTABLE","CAP_NET_BIND_SERVICE","CAP_NET_BROADCAST","CAP_NET_ADMIN","CAP_NET_RAW","CAP_IPC_LOCK","CAP_IPC_OWNER","CAP_SYS_MODULE","CAP_SYS_RAWIO","CAP_SYS_CHROOT","CAP_SYS_PTRACE","CAP_SYS_PACCT","CAP_SYS_ADMIN","CAP_SYS_BOOT","CAP_SYS_NICE","CAP_SYS_RESOURCE","CAP_SYS_TIME","CAP_SYS_TTY_CONFIG","CAP_MKNOD","CAP_LEASE","CAP_AUDIT_WRITE","CAP_AUDIT_CONTROL","CAP_SETFCAP","CAP_MAC_OVERRIDE","CAP_MAC_ADMIN","CAP_SYSLOG","CAP_WAKE_ALARM","CAP_BLOCK_SUSPEND","CAP_AUDIT_READ"],DEFAULT_CAPS=["CAP_CHOWN","CAP_DAC_OVERRIDE","CAP_FSETID","CAP_FOWNER","CAP_MKNOD","CAP_NET_RAW","CAP_SETGID","CAP_SETUID","CAP_SETFCAP","CAP_SETPCAP","CAP_NET_BIND_SERVICE","CAP_SYS_CHROOT","CAP_KILL","CAP_AUDIT_WRITE"],MASKED=["/proc/asound","/proc/acpi","/proc/kcore","/proc/keys","/proc/latency_stats","/proc/timer_list","/proc/timer_stats","/proc/sched_debug","/proc/scsi","/sys/firmware","/sys/devices/virtual/powercap"],READONLY=["/proc/bus","/proc/fs","/proc/irq","/proc/sys","/proc/sysrq-trigger"],DEFAULT_PATH="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";function mergeEnv(e=[],s=[]){const t=new Map;for(const o of[...e,...s]){const e=o.indexOf("=");t.set(-1===e?o:o.slice(0,e),o)}return t.has("PATH")||t.set("PATH",DEFAULT_PATH),[...t.values()]}function processArgs(e,s){const t=Array.isArray(e.Entrypoint);return[...(t?e.Entrypoint.filter(e=>""!==e):s.Entrypoint||[])||[],...(Array.isArray(e.Cmd)&&e.Cmd.length?e.Cmd:t?[]:s.Cmd||[])||[]]}function resolveUser(e,s="",t=""){if(!e)return{uid:0,gid:0};const[o,r]=e.split(":"),i=s.split("\n").map(e=>e.split(":")).filter(e=>e.length>=4),n=t.split("\n").map(e=>e.split(":")).filter(e=>e.length>=3);let c,p;if(/^\d+$/.test(o)){c=Number(o);const e=i.find(e=>Number(e[2])===c);p=e?Number(e[3]):0}else{const e=i.find(e=>e[0]===o);if(!e){const e=new Error(`unable to find user ${o}: no matching entries in passwd file`);throw e.statusCode=400,e}c=Number(e[2]),p=Number(e[3])}if(void 0!==r)if(/^\d+$/.test(r))p=Number(r);else{const e=n.find(e=>e[0]===r);if(!e){const e=new Error(`unable to find group ${r}: no matching entries in group file`);throw e.statusCode=400,e}p=Number(e[2])}return{uid:c,gid:p}}function additionalGids(e=[],s=""){const t=s.split("\n").map(e=>e.split(":")).filter(e=>e.length>=3);return(e||[]).map(e=>{if(/^\d+$/.test(String(e)))return Number(e);const s=t.find(s=>s[0]===e);if(!s){const s=new Error(`unable to find group ${e}: no matching entries in group file`);throw s.statusCode=400,s}return Number(s[2])})}function resources(e={}){const s={},t={};e.Memory>0&&(t.limit=e.Memory),e.MemorySwap&&t.limit&&(t.swap=e.MemorySwap),e.MemoryReservation>0&&(t.reservation=e.MemoryReservation),Object.keys(t).length&&(s.memory=t);const o={};e.NanoCpus>0&&Object.assign(o,{quota:Math.round(e.NanoCpus/1e9*1e5),period:1e5}),e.CpuQuota>0&&Object.assign(o,{quota:e.CpuQuota,period:e.CpuPeriod>0?e.CpuPeriod:1e5}),e.CpuShares>0&&(o.shares=e.CpuShares),e.CpusetCpus&&(o.cpus=e.CpusetCpus),e.CpusetMems&&(o.mems=e.CpusetMems),Object.keys(o).length&&(s.cpu=o),e.PidsLimit>0&&(s.pids={limit:e.PidsLimit}),e.BlkioWeight>0&&(s.blockIO={weight:e.BlkioWeight});const r=e.Devices||[];return s.devices=e.Privileged?[{allow:!0,access:"rwm"}]:[{allow:!1,access:"rwm"},...DEFAULT_DEVICE_RULES,...r.map(e=>({allow:!0,type:e.type||"c",major:e.major,minor:e.minor,access:e.CgroupPermissions||"rwm"}))],s}const DEFAULT_DEVICE_RULES=[{allow:!0,type:"c",access:"m"},{allow:!0,type:"b",access:"m"},{allow:!0,type:"c",major:10,minor:200,access:"rwm"},{allow:!0,type:"c",major:1,minor:3,access:"rwm"},{allow:!0,type:"c",major:1,minor:5,access:"rwm"},{allow:!0,type:"c",major:1,minor:7,access:"rwm"},{allow:!0,type:"c",major:1,minor:8,access:"rwm"},{allow:!0,type:"c",major:1,minor:9,access:"rwm"},{allow:!0,type:"c",major:5,minor:0,access:"rwm"},{allow:!0,type:"c",major:5,minor:1,access:"rwm"},{allow:!0,type:"c",major:5,minor:2,access:"rwm"},{allow:!0,type:"c",major:136,access:"rwm"}];function rlimits(e=[]){const s=new Map([["RLIMIT_NOFILE",{type:"RLIMIT_NOFILE",hard:1048576,soft:1048576}]]);for(const t of e||[]){const e=`RLIMIT_${String(t.Name).toUpperCase()}`;s.set(e,{type:e,soft:t.Soft,hard:t.Hard})}return[...s.values()]}const NAMESPACED_SYSCTL=/^(net\.|kernel\.(shm|msg|sem)|fs\.mqueue\.)/;function sysctls(e={}){for(const s of Object.keys(e||{}))if(!NAMESPACED_SYSCTL.test(s)){const e=new Error(`sysctl "${s}" is not allowed: only namespaced ones (net.*, kernel.shm*, kernel.msg*, kernel.sem, fs.mqueue.*)`);throw e.statusCode=400,e}return e||{}}function buildSpec({id:e,rootfs:s,args:t,env:o,cwd:r,user:i,hostname:n,nsPath:c,mounts:p,hostConfig:a,terminal:m=!1,devices:u=[]}){const A=[{type:"pid"},{type:"ipc"},{type:"uts"},{type:"mount"}];"host"!==a.CgroupnsMode&&A.push({type:"cgroup"}),c&&A.push({type:"network",path:c});const l=a.ShmSize>0?a.ShmSize:67108864,C=e=>"CAP_"+String(e).toUpperCase().replace(/^CAP_/,""),_=!!a.Privileged,d=(a.CapAdd||[]).map(C),P=(a.CapDrop||[]).map(C),S=_||d.includes("CAP_ALL")?ALL_CAPS:DEFAULT_CAPS,E=P.includes("CAP_ALL")?[]:S.filter(e=>!P.includes(e)),y=[...new Set([...E,...d.filter(e=>"CAP_ALL"!==e)])],f=a.SecurityOpt||[],g=_||f.some(e=>/^seccomp[=:]unconfined$/.test(e)),w=f.some(e=>/^no-new-privileges(:true|=true)?$/.test(e));return{ociVersion:"1.0.2",process:{terminal:m,user:i,args:t,env:o,cwd:r,capabilities:{bounding:y,effective:y,permitted:y},rlimits:rlimits(a.Ulimits),noNewPrivileges:w},root:{path:s,readonly:!!a.ReadonlyRootfs},hostname:n,mounts:[{destination:"/proc",type:"proc",source:"proc"},{destination:"/dev",type:"tmpfs",source:"tmpfs",options:["nosuid","strictatime","mode=755","size=65536k"]},{destination:"/dev/pts",type:"devpts",source:"devpts",options:["nosuid","noexec","newinstance","ptmxmode=0666","mode=0620","gid=5"]},{destination:"/dev/shm",type:"tmpfs",source:"shm",options:["nosuid","noexec","nodev","mode=1777",`size=${l}`]},{destination:"/dev/mqueue",type:"mqueue",source:"mqueue",options:["nosuid","noexec","nodev"]},{destination:"/sys",type:"sysfs",source:"sysfs",options:["nosuid","noexec","nodev",_?"rw":"ro"]},{destination:"/sys/fs/cgroup",type:"cgroup",source:"cgroup",options:["nosuid","noexec","nodev","relatime","ro"]},...p],linux:{namespaces:A,cgroupsPath:`/tnc-dock/${e}`,resources:resources({...a,Devices:u}),...u.length?{devices:u.map(({path:e,type:s,major:t,minor:o,fileMode:r,uid:i,gid:n})=>({path:e,type:s,major:t,minor:o,fileMode:r,uid:i,gid:n}))}:{},...Object.keys(a.Sysctls||{}).length?{sysctl:sysctls(a.Sysctls)}:{},maskedPaths:_?[]:MASKED,readonlyPaths:_?[]:READONLY,...g?{}:{seccomp:defaultSeccomp(process.arch,y)}}}}module.exports={buildSpec,mergeEnv,processArgs,resolveUser,additionalGids,rlimits,sysctls,DEFAULT_CAPS,ALL_CAPS};
|
|
@@ -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 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};
|
|
@@ -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"),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};
|
|
@@ -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"),http=require("http"),{spawnSync}=require("child_process"),{request}=require("./client"),{dockPaths}=require("./paths"),{DOCK_VERSION}=require("./version"),{BUILD_FILE}=require("./api/wire"),sleep=t=>new Promise(e=>setTimeout(e,t)),PREFIX="dock-selftest";function demux(t){const e={stdout:"",stderr:""};for(;t.length>=8;){const a=t.readUInt32BE(4);e[2===t[0]?"stderr":"stdout"]+=t.subarray(8,8+a).toString("utf8"),t=t.subarray(8+a)}return e}async function runSelftest({log:t=()=>{},keepGoing:e=!0}={}){const a=dockPaths().socket,n=[],s=async(t,e,n,s=3e5)=>{const o=await request(a,t,e,{body:n,timeout:s});if(o.status>=400)throw new Error(`${t} ${e} → ${o.status} ${"object"==typeof o.body?o.body.message:o.body}`);return o.body},o=async(t,e)=>{await request(a,"DELETE",`/containers/${t}?force=1&v=1`).catch(()=>{});const{Id:n}=await s("POST",`/containers/create?name=${t}`,e);await s("POST",`/containers/${n}/start`);const{StatusCode:o}=await s("POST",`/containers/${n}/wait`),r=await(i=n,new Promise((t,e)=>{http.get({socketPath:a,path:`/containers/${i}/logs?stdout=1&stderr=1`},e=>{const a=[];e.on("data",t=>a.push(t)),e.on("end",()=>t(demux(Buffer.concat(a))))}).on("error",e)}));var i;return await s("DELETE",`/containers/${n}?v=1`),{code:o,...r}},r=async(a,s)=>{const o=Date.now();try{const e=await s();n.push({name:a,ok:!0,ms:Date.now()-o,detail:e||""}),t(`✓ ${a} (${Date.now()-o} ms)`)}catch(s){if(n.push({name:a,ok:!1,ms:Date.now()-o,error:s.message}),t(`✗ ${a}: ${s.message}`),!e)throw s}},i=(t,e)=>{if(!t)throw new Error(e)};await r("daemon reachable",async()=>{const t=await s("GET","/version");return`dock ${t.Version}, API ${t.ApiVersion}`}),await r("pull alpine:3.20 and nginx:alpine",async()=>{await s("POST","/images/create?fromImage=alpine&tag=3.20"),await s("POST","/images/create?fromImage=nginx&tag=alpine")}),await r("run a container (Linux runtime boots on demand)",async()=>{const t=await o(`${PREFIX}-run`,{Image:"alpine:3.20",Cmd:["sh","-c","uname -srm; echo err >&2; exit 3"]});return i(3===t.code&&"err\n"===t.stderr&&/Linux/.test(t.stdout),JSON.stringify(t)),t.stdout.trim()}),await r("container network: own IP, DNS and internet",async()=>{const t=await o(`${PREFIX}-net`,{Image:"alpine:3.20",Cmd:["sh","-c","wget -q -O /dev/null http://dl-cdn.alpinelinux.org/alpine/ && echo NET_OK"]});i(t.stdout.includes("NET_OK"),`${t.stdout} ${t.stderr}`)});let c=null;await r("published port: nginx on localhost:18765",async()=>{await request(a,"DELETE",`/containers/${PREFIX}-web?force=1`).catch(()=>{}),({Id:c}=await s("POST",`/containers/create?name=${PREFIX}-web`,{Image:"nginx:alpine",HostConfig:{PortBindings:{"80/tcp":[{HostPort:"18765"}]}}})),await s("POST",`/containers/${c}/start`);let t="";for(let e=0;e<60&&!t.includes("nginx");e++)t=await new Promise(t=>{http.get("http://127.0.0.1:18765/",e=>{let a="";e.on("data",t=>{a+=t}),e.on("end",()=>t(a))}).on("error",()=>t(""))}),t.includes("nginx")||await sleep(250);i(t.includes("Welcome to nginx"),"nginx not reachable on 127.0.0.1:18765")}),await r("exec into a running container",async()=>{i(c,"no web container");const{Id:t}=await s("POST",`/containers/${c}/exec`,{Cmd:["nginx","-v"],AttachStdout:!0,AttachStderr:!0}),e=demux(await new Promise((e,n)=>{const s=http.request({socketPath:a,method:"POST",path:`/exec/${t}/start`,headers:{"Content-Type":"application/json"}},t=>{const a=[];t.on("data",t=>a.push(t)),t.on("end",()=>e(Buffer.concat(a)))});s.on("error",n),s.end(JSON.stringify({Detach:!1,Tty:!1}))}));i(/nginx version/.test(e.stdout+e.stderr),JSON.stringify(e))}),c&&await request(a,"DELETE",`/containers/${c}?force=1`).catch(()=>{}),await r("named volume persists across containers",async()=>{await request(a,"DELETE",`/volumes/${PREFIX}-vol?force=1`).catch(()=>{}),await o(`${PREFIX}-vol-a`,{Image:"alpine:3.20",Cmd:["sh","-c","echo kept > /data/f"],HostConfig:{Binds:[`${PREFIX}-vol:/data`]}});const t=await o(`${PREFIX}-vol-b`,{Image:"alpine:3.20",Cmd:["cat","/data/f"],HostConfig:{Binds:[`${PREFIX}-vol:/data:ro`]}});await request(a,"DELETE",`/volumes/${PREFIX}-vol`).catch(()=>{}),i("kept\n"===t.stdout,JSON.stringify(t))}),await r("bind mount from your home folder",async()=>{const t=fs.mkdtempSync(path.join(os.homedir(),".tnc","selftest-bind-"));try{await o(`${PREFIX}-bind`,{Image:"alpine:3.20",Cmd:["sh","-c","echo from-container > /mnt/out.txt"],HostConfig:{Binds:[`${t}:/mnt`]}});const e=fs.readFileSync(path.join(t,"out.txt"),"utf8");i("from-container\n"===e,JSON.stringify(e))}finally{fs.rmSync(t,{recursive:!0,force:!0})}}),await r("build an image (multi-stage)",async()=>{const t=fs.mkdtempSync(path.join(os.tmpdir(),"dock-selftest-build-"));try{fs.writeFileSync(path.join(t,BUILD_FILE),'FROM alpine:3.20 AS b\nRUN echo built > /x\nFROM alpine:3.20\nCOPY --from=b /x /x\nCMD ["cat", "/x"]\n');const e=path.join(t,"ctx.tar"),n=spawnSync("tar",["-cf",e,"-C",t,BUILD_FILE]);i(0===n.status,`tar failed: ${n.stderr}`);const s=await new Promise((t,n)=>{const s=http.request({socketPath:a,method:"POST",path:`/build?t=${PREFIX}-img:1&q=1`,headers:{"Content-Type":"application/x-tar"}},e=>{let a="";e.on("data",t=>{a+=t}),e.on("end",()=>t(a))});s.on("error",n),s.end(fs.readFileSync(e))});i(!/"error"/.test(s),s.slice(0,300));const r=await o(`${PREFIX}-built`,{Image:`${PREFIX}-img:1`});i("built\n"===r.stdout,JSON.stringify(r)),await request(a,"DELETE",`/images/${PREFIX}-img:1?force=1`).catch(()=>{})}finally{fs.rmSync(t,{recursive:!0,force:!0})}}),await r("custom network: containers find each other by name",async()=>{await request(a,"DELETE",`/containers/${PREFIX}-srv?force=1`).catch(()=>{}),await request(a,"DELETE",`/networks/${PREFIX}-net`).catch(()=>{}),await s("POST","/networks/create",{Name:`${PREFIX}-net`});const{Id:t}=await s("POST",`/containers/create?name=${PREFIX}-srv`,{Image:"alpine:3.20",Cmd:["sleep","60"],HostConfig:{NetworkMode:`${PREFIX}-net`}});await s("POST",`/containers/${t}/start`);try{const t=await o(`${PREFIX}-cli`,{Image:"alpine:3.20",Cmd:["sh","-c",`ping -c1 -W2 ${PREFIX}-srv >/dev/null && echo FOUND`],HostConfig:{NetworkMode:`${PREFIX}-net`}});i(t.stdout.includes("FOUND"),JSON.stringify(t))}finally{await request(a,"DELETE",`/containers/${t}?force=1`).catch(()=>{}),await request(a,"DELETE",`/networks/${PREFIX}-net`).catch(()=>{})}}),await r("stop sends SIGTERM, then SIGKILL after the timeout",async()=>{await request(a,"DELETE",`/containers/${PREFIX}-stop?force=1`).catch(()=>{});const{Id:t}=await s("POST",`/containers/create?name=${PREFIX}-stop`,{Image:"alpine:3.20",Cmd:["sleep","1000"]});await s("POST",`/containers/${t}/start`),await s("POST",`/containers/${t}/stop?t=1`);const e=await s("GET",`/containers/${t}/json`);await s("DELETE",`/containers/${t}`),i("exited"===e.State.Status,e.State.Status)}),await r("nothing left behind",async()=>{const t=(await s("GET","/containers/json?all=1")).filter(t=>t.Names.some(t=>t.includes(PREFIX)));i(0===t.length,t.map(t=>t.Names[0]).join(", "))});let d=null;try{d=JSON.parse(fs.readFileSync(path.join(os.homedir(),".tnc","dock","runtime.json"),"utf8"))}catch(t){}const l={tool:"thinknsh dock selftest",when:(new Date).toISOString(),dock:DOCK_VERSION,runtime:d,machine:{platform:process.platform,release:os.release(),arch:process.arch,cpus:os.cpus().length,cpuModel:os.cpus()[0]?.model,memoryMB:Math.round(os.totalmem()/1048576),node:process.version},passed:n.filter(t=>t.ok).length,total:n.length,results:n},u=path.join(os.homedir(),".tnc","dock",`selftest-${process.platform}-${process.arch}-${l.when.replace(/[:.]/g,"-")}.json`);return fs.mkdirSync(path.dirname(u),{recursive:!0}),fs.writeFileSync(u,JSON.stringify(l,null,2)),{report:l,file:u}}module.exports={runSelftest};
|
|
@@ -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"),APP=path.join(os.homedir(),"Applications","TNC Dock.app"),BUNDLE_ID="com.thinkncollab.dock.menubar",LOGIN_AGENT=path.join(os.homedir(),"Library","LaunchAgents",`${BUNDLE_ID}.plist`),EXE="tnc-menubar",SOURCE=path.join(__dirname,"..","..","..","native","tnc-menubar","main.swift");function infoPlist(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>CFBundleIdentifier</key><string>${BUNDLE_ID}</string>\n <key>CFBundleName</key><string>TNC Dock</string>\n <key>CFBundleDisplayName</key><string>TNC Dock</string>\n <key>CFBundleExecutable</key><string>${EXE}</string>\n <key>CFBundlePackageType</key><string>APPL</string>\n <key>CFBundleShortVersionString</key><string>${n}</string>\n <key>CFBundleVersion</key><string>${n}</string>\n <key>LSMinimumSystemVersion</key><string>13.0</string>\n <key>LSUIElement</key><true/>\n <key>NSHighResolutionCapable</key><true/>\n</dict>\n</plist>\n`}function binary(n,e){const i=path.join(path.dirname(n),"bin",EXE);if(fs.existsSync(i))return i;if(fs.existsSync(SOURCE)&&0===spawnSync("swiftc",["--version"],{stdio:"ignore"}).status){const i=path.join(n,"build",EXE);fs.mkdirSync(path.dirname(i),{recursive:!0}),e("compiling the menu bar app (swiftc)…");const s=spawnSync("swiftc",["-swift-version","5","-O","-framework","AppKit","-o",i,SOURCE],{encoding:"utf8"});if(0!==s.status)throw new Error(`swiftc failed:\n${(s.stderr||s.stdout).trim().slice(-2e3)}`);return i}throw new Error("the menu bar app is not installed; run thinknsh dock setup (a newer runtime bundle has it)")}function quitApp(){spawnSync("pkill",["-f",`${APP}/Contents/MacOS/${EXE}`],{stdio:"ignore"})}function app({off:n=!1,home:e,socket:i,cli:s,version:t="0",log:r=()=>{}}){if("darwin"!==process.platform)throw new Error("the menu bar app is for macOS; on other systems use thinknsh dock ui");if(n){quitApp();const n=fs.existsSync(APP);return fs.rmSync(APP,{recursive:!0,force:!0}),fs.rmSync(LOGIN_AGENT,{force:!0}),[n?`removed ${APP}`:"TNC Dock was not installed"]}const o=binary(e,r);quitApp();const c=path.join(APP,"Contents");fs.rmSync(APP,{recursive:!0,force:!0}),fs.mkdirSync(path.join(c,"MacOS"),{recursive:!0}),fs.mkdirSync(path.join(c,"Resources"),{recursive:!0}),fs.writeFileSync(path.join(c,"Info.plist"),infoPlist(t)),fs.copyFileSync(o,path.join(c,"MacOS",EXE)),fs.chmodSync(path.join(c,"MacOS",EXE),493),fs.writeFileSync(path.join(c,"Resources","config.json"),JSON.stringify({node:process.execPath,cli:fs.realpathSync(s),socket:i},null,2)+"\n");const a=spawnSync("codesign",["-s","-","--force","--deep",APP],{encoding:"utf8"});return 0!==a.status&&r(`codesign: ${(a.stderr||"").trim()}`),spawnSync("open",[APP]),[`TNC Dock is in the menu bar (${APP})`,'its menu: containers, ports, logs, dock start/stop, the dashboard, and "Open TNC Dock at login"',`uses node ${process.execPath}; run this again if you move or upgrade node`]}module.exports={app,APP};
|
|
@@ -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"),LABEL="com.thinkncollab.dock",PLIST=path.join(os.homedir(),"Library","LaunchAgents",`${LABEL}.plist`),xml=n=>String(n).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");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};
|
|
@@ -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"),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{const n=await fetch(`${t}/manifest.json`,{redirect:"follow"});if(!n.ok)throw setupError(`cannot fetch bundle manifest from ${t} (HTTP ${n.status})`);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 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`];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(s)});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 o=path.join(TNC_DIR,"wsl","tnc-dock");fs.mkdirSync(o,{recursive:!0});const c=run("wsl.exe",["--import","tnc-dock",o,s,"--version","2"]);if(0!==c.code)throw setupError(`wsl --import failed: ${c.out.trim()}`);t(`WSL2 distro tnc-dock created in ${o}`)}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"};
|
|
@@ -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"),{ENGINE_CLI,ENGINE_SOCKET,CONTEXT_ENDPOINT_FLAG,ENGINE_HOST_VAR,TESTCONTAINERS_HOST_KEY,LEGACY_PREVIOUS_CONTEXT_FILE}=require("../api/wire"),CONTEXT="thinknsh",TC_FILE=path.join(os.homedir(),".testcontainers.properties"),TC_MARK="# added by thinknsh dock use",TC_KEY=new RegExp(`^\\s*${TESTCONTAINERS_HOST_KEY.replace(/\./g,"\\.")}\\s*=`);function engineHost(n){return"win32"===process.platform?`npipe:${n.replace(/\\/g,"/")}`:`unix://${n}`}function hasEngineCli(){return 0===spawnSync(ENGINE_CLI,["--version"],{encoding:"utf8"}).status}function engineCli(n){const e=spawnSync(ENGINE_CLI,n,{encoding:"utf8"});return{ok:0===e.status,out:`${e.stdout||""}${e.stderr||""}`.trim()}}function setTestcontainers(n,e){let t=fs.existsSync(TC_FILE)?fs.readFileSync(TC_FILE,"utf8").split("\n"):[];const s=t.indexOf(TC_MARK);if(-1!==s&&t.splice(s,2),e){if(t.some(n=>TC_KEY.test(n)))return`kept (it already sets ${TESTCONTAINERS_HOST_KEY})`;for(;t.length&&!t[t.length-1].trim();)t.pop();t.push(TC_MARK,`${TESTCONTAINERS_HOST_KEY}=${n}`,"")}const o=t.join("\n").replace(/\n{3,}/g,"\n\n");return o.trim()?(fs.writeFileSync(TC_FILE,o),e?"set":"restored"):(fs.existsSync(TC_FILE)&&fs.rmSync(TC_FILE),"removed")}function contextAlive(n){const e=engineCli(["context","inspect",n]);if(!e.ok)return!1;try{const[n]=JSON.parse(e.out),t=Object.values(n.Endpoints||{}).map(n=>n&&n.Host).find(Boolean)||"";return!t.startsWith("unix://")||fs.existsSync(t.slice(7))}catch(n){return!0}}const HOME=process.env.TNC_DOCK_HOME||path.join(os.homedir(),".tnc","dock"),PREVIOUS=path.join(HOME,"previous-engine-context"),LEGACY_PREVIOUS=path.join(HOME,LEGACY_PREVIOUS_CONTEXT_FILE);function readPrevious(){for(const n of[PREVIOUS,LEGACY_PREVIOUS])try{const e=fs.readFileSync(n,"utf8").trim();if(e)return e}catch(n){}return"default"}const PLUGIN_DIR=path.join(os.homedir(),`.${ENGINE_CLI}`,"cli-plugins"),PLUGIN_HOMES=["/opt/homebrew","/usr/local"].flatMap(n=>[path.join(n,"lib",ENGINE_CLI,"cli-plugins"),...["compose","buildx"].map(e=>path.join(n,"opt",`${ENGINE_CLI}-${e}`,"bin"))]);function repairPlugins(){const n=[];for(const e of["compose","buildx"]){const t=`${ENGINE_CLI}-${e}`,s=path.join(PLUGIN_DIR,t);let o=!1;try{o=fs.lstatSync(s).isSymbolicLink()&&!fs.existsSync(s)}catch(n){}if(!o)continue;const i=PLUGIN_HOMES.map(n=>path.join(n,t)).find(n=>fs.existsSync(n));i?(fs.rmSync(s,{force:!0}),fs.symlinkSync(i,s),n.push(`${e} plugin: relinked to ${i} (the old one was removed)`)):n.push(`${e} plugin: its link points at a removed install; reinstall the plugin (for example with your package manager)`)}return n}function socketLinkTarget(){try{return fs.readlinkSync(ENGINE_SOCKET)}catch(n){return null}}function systemSocket(n,{off:e,system:t}){if("darwin"!==process.platform)return[];const s=socketLinkTarget(),o=s===n,i=n=>0===spawnSync("sudo",n,{stdio:"inherit"}).status;if(e)return o?t?[i(["rm","-f",ENGINE_SOCKET])?`standard socket: ${ENGINE_SOCKET} removed`:"standard socket: not removed (sudo failed)"]:[`standard socket ${ENGINE_SOCKET} still points at dock; remove it with: thinknsh dock use --off --system`]:[];if(o)return[`standard socket: ${ENGINE_SOCKET} → dock`];let r=!1;try{r=fs.statSync(ENGINE_SOCKET).isSocket()}catch(n){}return t?r&&!s?[`standard socket: ${ENGINE_SOCKET} is another engine's live socket; stop that engine first`]:(console.log(` linking ${ENGINE_SOCKET} → ${n} (sudo may ask for your password)`),[i(["ln","-sfn",n,ENGINE_SOCKET])?`standard socket: ${ENGINE_SOCKET} → dock`:"standard socket: not linked (sudo failed)"]):[r?`standard socket: ${ENGINE_SOCKET} belongs to another running engine (left as it is)`:"standard socket: not linked; for tools that ignore contexts run: thinknsh dock use --system"]}function use(n,{off:e=!1,system:t=!1}={}){const s=engineHost(n),o=[];if(hasEngineCli())if(e){let n=readPrevious(),e="";n===CONTEXT?n="default":"default"===n||contextAlive(n)||(e=` ("${n}" points at an engine that is no longer installed)`,n="default");const t=engineCli(["context","use",n]);if(o.push(t.ok?`engine CLI: back on the "${n}" context${e}`:`engine CLI: ${t.out}`),t.ok)for(const n of[PREVIOUS,LEGACY_PREVIOUS])fs.rmSync(n,{force:!0})}else{const n=engineCli(["context","show"]);n.ok&&n.out&&n.out!==CONTEXT&&(fs.mkdirSync(path.dirname(PREVIOUS),{recursive:!0}),fs.writeFileSync(PREVIOUS,n.out+"\n"));const e=engineCli(["context","inspect",CONTEXT]).ok,t=engineCli(e?["context","update",CONTEXT,CONTEXT_ENDPOINT_FLAG,`host=${s}`]:["context","create",CONTEXT,"--description","ThinkNCollab dock",CONTEXT_ENDPOINT_FLAG,`host=${s}`]);if(!t.ok)throw new Error(`engine CLI context: ${t.out}`);const i=engineCli(["context","use",CONTEXT]);if(!i.ok)throw new Error(`engine CLI context use: ${i.out}`);o.push(`engine CLI: context "${CONTEXT}" → ${s} (the CLI, its compose plugin and VS Code Dev Containers follow it)`)}else e||o.push(`engine CLI: not installed; tools that read ${ENGINE_HOST_VAR} need: eval "$(thinknsh dock env)"`);return o.push(`testcontainers: ${setTestcontainers(s,!e)} (${TC_FILE})`),!e&&hasEngineCli()&&o.push(...repairPlugins()),o.push(...systemSocket(n,{off:e,system:t})),o}function env(n){return"win32"===process.platform?`$env:${ENGINE_HOST_VAR} = "${engineHost(n)}"`:`export ${ENGINE_HOST_VAR}=${engineHost(n)}`}module.exports={use,env,engineHost,CONTEXT};
|
|
@@ -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"),http=require("http"),{spawnSync}=require("child_process"),{ENGINE_CLI,ENGINE_SOCKET}=require("../api/wire"),{CONTEXT}=require("./integrate"),HELPER_IMAGE="alpine:3.20";function endpoint(e){if(!e)return null;if(e.startsWith("unix://"))return{socketPath:e.slice(7)};if(e.startsWith("/"))return{socketPath:e};const t=/^(?:tcp|http):\/\/([^:/]+):(\d+)\/?$/.exec(e);if(t)return{host:t[1],port:Number(t[2])};throw new Error(`unsupported engine address ${e} (use unix:///path.sock or tcp://host:port)`)}function detectSource(){const e=spawnSync(ENGINE_CLI,["context","show"],{encoding:"utf8"});if(0===e.status){const t=e.stdout.trim(),n=t===CONTEXT?["default"]:[t,"default"];for(const e of n){const t=spawnSync(ENGINE_CLI,["context","inspect",e],{encoding:"utf8"});if(0===t.status)try{const e=JSON.parse(t.stdout)[0]?.Endpoints||{},n=Object.values(e).map(e=>e?.Host).find(Boolean);if(n&&!n.includes("/.tnc/dock/"))return n}catch(e){}}}return fs.existsSync(ENGINE_SOCKET)?`unix://${ENGINE_SOCKET}`:null}function call(e,t,n,{body:o,json:a=!0,stream:r=!1}={}){return new Promise((s,i)=>{const c=o&&"function"==typeof o.pipe,u=o&&!c?JSON.stringify(o):null,l=c?{"Content-Type":"application/x-tar"}:u?{"Content-Type":"application/json","Content-Length":Buffer.byteLength(u)}:{},m=http.request({...e,method:t,path:n,headers:l},e=>{if(r&&e.statusCode<300)return s(e);const o=[];e.on("data",e=>o.push(e)),e.on("end",()=>{const r=Buffer.concat(o).toString("utf8");let c=r;if(a)try{c=JSON.parse(r)}catch(e){}if(e.statusCode>=300){const o=c&&c.message?c.message:r.trim().slice(0,300);return i(Object.assign(new Error(`${t} ${n}: HTTP ${e.statusCode} ${o}`),{status:e.statusCode}))}s(c)}),e.on("error",i)});m.on("error",i),c?(o.on("error",e=>m.destroy(e)),o.pipe(m)):(u&&m.write(u),m.end())})}function drainProgress(e){return new Promise((t,n)=>{let o="",a=null;e.on("data",e=>{let t;for(o+=e;-1!==(t=o.indexOf("\n"));){const e=o.slice(0,t).trim();if(o=o.slice(t+1),e)try{const t=JSON.parse(e);t.error&&(a=t.error)}catch(e){}}}),e.on("end",()=>a?n(new Error(a)):t()),e.on("error",n)})}async function ensureImage(e,t){try{return void await call(e,"GET",`/images/${encodeURIComponent(t)}/json`)}catch(e){if(404!==e.status)throw e}const[n,o]=t.split(":");await drainProgress(await call(e,"POST",`/images/create?fromImage=${encodeURIComponent(n)}&tag=${encodeURIComponent(o)}`,{stream:!0}))}async function migrateImages(e,t,{dryRun:n,log:o}){const a=(await call(e,"GET","/images/json")).filter(e=>(e.RepoTags||[]).some(e=>e&&"<none>:<none>"!==e)),r=await call(t,"GET","/images/json"),s=new Set(r.flatMap(e=>e.RepoTags||[]));let i=0,c=0;for(const r of a){const a=r.RepoTags.filter(e=>e&&"<none>:<none>"!==e).filter(e=>!s.has(e));if(!a.length){c++;continue}if(o(`image ${a.join(", ")}${n?" (dry run)":""}`),n){i++;continue}const u=await call(e,"GET",`/images/get?${a.map(e=>`names=${encodeURIComponent(e)}`).join("&")}`,{stream:!0});await drainProgress(await call(t,"POST","/images/load?quiet=1",{body:u,stream:!0})),i++}return{moved:i,skipped:c}}async function helper(e,t,n,o){return await call(e,"DELETE",`/containers/${t}?force=1`).catch(()=>{}),(await call(e,"POST",`/containers/create?name=${t}`,{body:{Image:HELPER_IMAGE,Cmd:["true"],Labels:{"tnc.migrate":"1"},HostConfig:{Binds:[`${n}:${o}`]}}})).Id}async function migrateVolumes(e,t,{dryRun:n,overwrite:o,log:a}){const r=(await call(e,"GET","/volumes")).Volumes||[],s=new Set(((await call(t,"GET","/volumes")).Volumes||[]).map(e=>e.Name));let i=0,c=0;!n&&r.length&&(await ensureImage(e,HELPER_IMAGE),await ensureImage(t,HELPER_IMAGE));for(const u of r){if(s.has(u.Name)&&!o){c++;continue}if(a(`volume ${u.Name}${n?" (dry run)":""}`),n){i++;continue}s.has(u.Name)||await call(t,"POST","/volumes/create",{body:{Name:u.Name,Labels:u.Labels||{}}});const r=await helper(e,"tnc-migrate-from",u.Name,"/from"),l=await helper(t,"tnc-migrate-to",u.Name,"/to");try{const n=await call(e,"GET",`/containers/${r}/archive?path=${encodeURIComponent("/from/.")}`,{stream:!0});await call(t,"PUT",`/containers/${l}/archive?path=${encodeURIComponent("/to")}©UIDGID=1`,{body:n,json:!1})}finally{await call(e,"DELETE",`/containers/${r}?force=1`).catch(()=>{}),await call(t,"DELETE",`/containers/${l}?force=1`).catch(()=>{})}i++}return{moved:i,skipped:c}}async function migrate({from:e,to:t,images:n,volumes:o,dryRun:a=!1,overwrite:r=!1,log:s=()=>{}}){const i=e||detectSource();if(!i)throw new Error("no other engine found; pass --from unix:///path/to/engine.sock");const c=endpoint(i),u=endpoint(t);if(c.socketPath&&u.socketPath&&fs.realpathSync(c.socketPath)===fs.realpathSync(u.socketPath))throw new Error(`${i} is dock itself; pass the other engine with --from`);await call(c,"GET","/_ping",{json:!1}).catch(e=>{throw new Error(`cannot reach ${i}: ${e.message}`)});const l=!n&&!o;s(`from ${i}`);const m={source:i};return(l||n)&&(m.images=await migrateImages(c,u,{dryRun:a,log:s})),(l||o)&&(m.volumes=await migrateVolumes(c,u,{dryRun:a,overwrite:r,log:s})),m}module.exports={migrate,endpoint,detectSource};
|
|
@@ -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";let DatabaseSync=null;try{({DatabaseSync}=require("node:sqlite"))}catch(e){}const MIGRATIONS=[{version:1,sql:"\n CREATE TABLE containers (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n image TEXT NOT NULL,\n state TEXT NOT NULL, -- created | running | paused | exited | dead\n config TEXT NOT NULL, -- JSON: container create request\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n );\n CREATE TABLE images (\n id TEXT PRIMARY KEY, -- sha256 of image config\n repo_tags TEXT NOT NULL DEFAULT '[]',\n digest TEXT,\n size INTEGER NOT NULL DEFAULT 0,\n config TEXT NOT NULL,\n created_at INTEGER NOT NULL\n );\n CREATE TABLE networks (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n driver TEXT NOT NULL,\n subnet TEXT,\n config TEXT NOT NULL,\n created_at INTEGER NOT NULL\n );\n CREATE TABLE volumes (\n name TEXT PRIMARY KEY,\n driver TEXT NOT NULL DEFAULT 'local',\n mountpoint TEXT NOT NULL,\n labels TEXT NOT NULL DEFAULT '{}',\n created_at INTEGER NOT NULL\n );\n CREATE TABLE events (\n seq INTEGER PRIMARY KEY AUTOINCREMENT,\n time INTEGER NOT NULL,\n type TEXT NOT NULL,\n action TEXT NOT NULL,\n actor_id TEXT NOT NULL DEFAULT '',\n attributes TEXT NOT NULL DEFAULT '{}'\n );\n "},{version:2,sql:"\n ALTER TABLE containers ADD COLUMN runtime TEXT NOT NULL DEFAULT '{}'; -- JSON: pid, ip, exit code, timestamps\n ALTER TABLE volumes ADD COLUMN anonymous INTEGER NOT NULL DEFAULT 0;\n CREATE INDEX events_time ON events (time);\n "},{version:3,sql:"\n CREATE TABLE build_cache (\n key TEXT PRIMARY KEY, -- sha256 of parent chain + instruction + inputs\n digest TEXT NOT NULL, -- layer blob digest ('' for steps without a layer)\n diff_id TEXT NOT NULL,\n size INTEGER NOT NULL,\n created_at INTEGER NOT NULL\n );\n "}],parse=(e,t)=>{try{return JSON.parse(e)}catch(e){return t}},IMAGE_ID=/^(sha256:)?[0-9a-f]{4,64}$/;class Store{constructor(e){if(!DatabaseSync)throw new Error(`dock needs Node.js >= 22.5 (built-in node:sqlite). Current: ${process.version}`);this.db=new DatabaseSync(e),this.db.exec("\n PRAGMA journal_mode = WAL;\n PRAGMA synchronous = NORMAL;\n PRAGMA foreign_keys = ON;\n PRAGMA busy_timeout = 5000;\n "),this._migrate()}_migrate(){this.db.exec("CREATE TABLE IF NOT EXISTS schema_migrations (\n version INTEGER PRIMARY KEY,\n applied_at INTEGER NOT NULL\n )");const e=new Set(this.db.prepare("SELECT version FROM schema_migrations").all().map(e=>e.version));for(const t of MIGRATIONS)if(!e.has(t.version)){this.db.exec("BEGIN IMMEDIATE");try{this.db.exec(t.sql),this.db.prepare("INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)").run(t.version,Date.now()),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),new Error(`dock state migration ${t.version} failed: ${e.message}`)}}}schemaVersion(){return this.db.prepare("SELECT COALESCE(MAX(version), 0) AS v FROM schema_migrations").get().v}journalMode(){return this.db.prepare("PRAGMA journal_mode").get().journal_mode}counts(){const e={};for(const t of this.db.prepare("SELECT state, COUNT(*) AS n FROM containers GROUP BY state").all())e[t.state]=t.n;const t=Object.values(e).reduce((e,t)=>e+t,0),n=this.db.prepare("SELECT COUNT(*) AS n FROM containers WHERE state = 'running' AND json_extract(runtime, '$.paused') = 1").get().n;return{containers:t,running:(e.running||0)-n,paused:(e.paused||0)+n,stopped:t-(e.running||0)-(e.paused||0),images:this.db.prepare("SELECT COUNT(*) AS n FROM images").get().n}}recordEvent(e,t,n="",r={},i=Date.now()){const a=this.db.prepare("INSERT INTO events (time, type, action, actor_id, attributes) VALUES (?, ?, ?, ?, ?)").run(i,e,t,n,JSON.stringify(r));return{seq:Number(a.lastInsertRowid),time:i,type:e,action:t,actorId:n,attributes:r}}eventsBetween(e,t){return this.db.prepare("SELECT * FROM events WHERE time >= ? AND time <= ? ORDER BY seq").all(e,t).map(e=>({seq:e.seq,time:e.time,type:e.type,action:e.action,actorId:e.actor_id,attributes:parse(e.attributes,{})}))}_container(e){return e?{id:e.id,name:e.name,image:e.image,state:e.state,config:parse(e.config,{}),runtime:parse(e.runtime,{}),createdAt:e.created_at,updatedAt:e.updated_at}:null}insertContainer(e){const t=Date.now();return this.db.prepare("INSERT INTO containers (id, name, image, state, config, runtime, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(e.id,e.name,e.image,e.state,JSON.stringify(e.config),JSON.stringify(e.runtime||{}),t,t),this.getContainer(e.id)}updateContainer(e,{state:t,runtime:n,name:r}){const i=this.getContainer(e);return i?(this.db.prepare("UPDATE containers SET state = ?, runtime = ?, name = ?, updated_at = ? WHERE id = ?").run(t??i.state,JSON.stringify(n??i.runtime),r??i.name,Date.now(),e),this.getContainer(e)):null}getContainer(e){return this._container(this.db.prepare("SELECT * FROM containers WHERE id = ?").get(e))}findContainer(e){if(!e)return null;const t=e.replace(/^\//,""),n=this.getContainer(e);if(n)return n;const r=this._container(this.db.prepare("SELECT * FROM containers WHERE name = ?").get(t));if(r)return r;if(!/^[0-9a-f]+$/.test(e))return null;const i=this.db.prepare("SELECT * FROM containers WHERE id LIKE ? LIMIT 2").all(e+"%");if(i.length>1){const t=new Error(`multiple containers found with provided prefix: ${e}`);throw t.statusCode=400,t}return this._container(i[0])}listContainers(){return this.db.prepare("SELECT * FROM containers ORDER BY created_at DESC").all().map(e=>this._container(e))}deleteContainer(e){this.db.prepare("DELETE FROM containers WHERE id = ?").run(e)}_image(e){return e?{id:e.id,repoTags:parse(e.repo_tags,[]),digest:e.digest,size:e.size,...parse(e.config,{}),createdAt:e.created_at}:null}saveImage(e){this.db.exec("BEGIN IMMEDIATE");try{for(const t of e.repoTags)for(const n of this.listImages())n.id!==e.id&&n.repoTags.includes(t)&&this.db.prepare("UPDATE images SET repo_tags = ? WHERE id = ?").run(JSON.stringify(n.repoTags.filter(e=>e!==t)),n.id);const t=this.getImage(e.id),n=[...new Set([...t?t.repoTags:[],...e.repoTags])],{id:r,repoTags:i,digest:a,size:s,createdAt:E,...o}=e;this.db.prepare("INSERT INTO images (id, repo_tags, digest, size, config, created_at) VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET repo_tags = excluded.repo_tags, digest = excluded.digest,\n size = excluded.size, config = excluded.config").run(r,JSON.stringify(n),a||null,s||0,JSON.stringify(o),E||Date.now()),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}return this.getImage(e.id)}setImageTags(e,t){this.db.prepare("UPDATE images SET repo_tags = ? WHERE id = ?").run(JSON.stringify(t),e)}getImage(e){return this._image(this.db.prepare("SELECT * FROM images WHERE id = ?").get(e))}findImage(e,t){for(const n of this.listImages()){if(n.repoTags.includes(t||e))return n;if((n.repoDigests||[]).some(n=>n===t||n===e))return n}if(IMAGE_ID.test(e)){const t=e.replace(/^sha256:/,""),n=this.db.prepare("SELECT * FROM images WHERE id LIKE ? LIMIT 2").all("sha256:"+t+"%");if(1===n.length)return this._image(n[0])}return null}listImages(){return this.db.prepare("SELECT * FROM images ORDER BY created_at DESC").all().map(e=>this._image(e))}deleteImage(e){this.db.prepare("DELETE FROM images WHERE id = ?").run(e)}getBuildCache(e){const t=this.db.prepare("SELECT * FROM build_cache WHERE key = ?").get(e);return t?{digest:t.digest,diffId:t.diff_id,size:t.size}:null}putBuildCache(e,{digest:t,diffId:n,size:r}){this.db.prepare("INSERT OR REPLACE INTO build_cache (key, digest, diff_id, size, created_at) VALUES (?, ?, ?, ?, ?)").run(e,t,n,r,Date.now())}_network(e){return e?{id:e.id,name:e.name,driver:e.driver,subnet:e.subnet,...parse(e.config,{}),createdAt:e.created_at}:null}insertNetwork(e){const{id:t,name:n,driver:r,subnet:i,createdAt:a,...s}=e;return this.db.prepare("INSERT INTO networks (id, name, driver, subnet, config, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(t,n,r,i,JSON.stringify(s),Date.now()),this.getNetwork(t)}getNetwork(e){return this._network(this.db.prepare("SELECT * FROM networks WHERE id = ?").get(e))}findNetwork(e){if(!e)return null;const t=this.db.prepare("SELECT * FROM networks WHERE id = ? OR name = ?").get(e,e);if(t)return this._network(t);if(!/^[0-9a-f]+$/.test(e))return null;const n=this.db.prepare("SELECT * FROM networks WHERE id LIKE ? LIMIT 2").all(e+"%");return 1===n.length?this._network(n[0]):null}listNetworks(){return this.db.prepare("SELECT * FROM networks ORDER BY created_at").all().map(e=>this._network(e))}deleteNetwork(e){this.db.prepare("DELETE FROM networks WHERE id = ?").run(e)}_volume(e){return e?{name:e.name,driver:e.driver,mountpoint:e.mountpoint,labels:parse(e.labels,{}),anonymous:!!e.anonymous,createdAt:e.created_at}:null}insertVolume(e){return this.db.prepare("INSERT INTO volumes (name, driver, mountpoint, labels, anonymous, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(e.name,e.driver||"local",e.mountpoint,JSON.stringify(e.labels||{}),e.anonymous?1:0,Date.now()),this.getVolume(e.name)}getVolume(e){return this._volume(this.db.prepare("SELECT * FROM volumes WHERE name = ?").get(e))}listVolumes(){return this.db.prepare("SELECT * FROM volumes ORDER BY created_at DESC").all().map(e=>this._volume(e))}deleteVolume(e){this.db.prepare("DELETE FROM volumes WHERE name = ?").run(e)}close(){try{this.db.close()}catch(e){}}}Store.isSupported=()=>null!==DatabaseSync,module.exports=Store;
|