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.
Files changed (201) hide show
  1. package/LICENSE +82 -0
  2. package/Readme.md +246 -502
  3. package/dist/bin/thinknsh.js +8 -0
  4. package/dist/src/commands/auth/login.js +7 -0
  5. package/dist/src/commands/auth/logout.js +7 -0
  6. package/dist/src/commands/auth/register.js +7 -0
  7. package/dist/src/commands/auth/whoami.js +7 -0
  8. package/dist/src/commands/dock/bench.js +7 -0
  9. package/dist/src/commands/dock/ci.js +7 -0
  10. package/dist/src/commands/dock/cluster.js +7 -0
  11. package/dist/src/commands/dock/containers.js +7 -0
  12. package/dist/src/commands/dock/dock.js +7 -0
  13. package/dist/src/commands/github/commits.js +7 -0
  14. package/dist/src/commands/github/issue-create.js +7 -0
  15. package/dist/src/commands/github/issues.js +7 -0
  16. package/dist/src/commands/github/pulls.js +7 -0
  17. package/dist/src/commands/github/statusgh.js +7 -0
  18. package/dist/src/commands/index.js +7 -0
  19. package/dist/src/commands/message/say.js +7 -0
  20. package/dist/src/commands/room/join.js +7 -0
  21. package/dist/src/commands/room/leave.js +7 -0
  22. package/dist/src/commands/room/rooms.js +7 -0
  23. package/dist/src/commands/room/sync.js +7 -0
  24. package/dist/src/commands/sandbox/console.js +7 -0
  25. package/dist/src/commands/sandbox/dep.js +7 -0
  26. package/dist/src/commands/sandbox/deploy.js +7 -0
  27. package/dist/src/commands/sandbox/dev.js +7 -0
  28. package/dist/src/commands/sandbox/exec.js +7 -0
  29. package/dist/src/commands/sandbox/freeze.js +7 -0
  30. package/dist/src/commands/sandbox/push.js +7 -0
  31. package/dist/src/commands/sandbox/resume.js +7 -0
  32. package/dist/src/commands/sandbox/sandbox.js +7 -0
  33. package/dist/src/commands/sandbox/share.js +7 -0
  34. package/dist/src/commands/sandbox/snapshot.js +7 -0
  35. package/dist/src/commands/sandbox/sync-deps.js +7 -0
  36. package/dist/src/commands/shell/doctor.js +7 -0
  37. package/dist/src/commands/shell/notifications.js +7 -0
  38. package/dist/src/commands/shell/notify.js +7 -0
  39. package/dist/src/commands/shell/status.js +7 -0
  40. package/dist/src/commands/shell/version.js +7 -0
  41. package/dist/src/commands/tasks/build.js +7 -0
  42. package/dist/src/commands/tasks/complete.js +7 -0
  43. package/dist/src/commands/tasks/create-task.js +7 -0
  44. package/dist/src/commands/tasks/loadtest.js +7 -0
  45. package/dist/src/commands/tasks/task-status.js +7 -0
  46. package/dist/src/commands/tasks/task.js +7 -0
  47. package/dist/src/commands/tasks/tasks.js +7 -0
  48. package/dist/src/commands/team/invite.js +7 -0
  49. package/dist/src/commands/team/myteam.js +7 -0
  50. package/dist/src/commands/team/teammates.js +7 -0
  51. package/dist/src/commands/terminal/share.js +7 -0
  52. package/dist/src/core/api-client.js +7 -0
  53. package/dist/src/core/crypto/e2ee-vault.js +7 -0
  54. package/dist/src/core/dep-store.js +7 -0
  55. package/dist/src/core/dependency-tracker.js +7 -0
  56. package/dist/src/core/geekload.js +7 -0
  57. package/dist/src/core/local-runner.js +7 -0
  58. package/dist/src/core/multi-lang-scanner.js +7 -0
  59. package/dist/src/core/notification-window.js +8 -0
  60. package/dist/src/core/notify-auth.js +7 -0
  61. package/dist/src/core/project-config.js +7 -0
  62. package/dist/src/core/sandbox/arch-shim.js +7 -0
  63. package/dist/src/core/sandbox/capsule-builder.js +7 -0
  64. package/dist/src/core/sandbox/cgroups.js +7 -0
  65. package/dist/src/core/sandbox/container-monitor.js +7 -0
  66. package/dist/src/core/sandbox/crun-manager.js +7 -0
  67. package/dist/src/core/sandbox/engine.js +7 -0
  68. package/dist/src/core/sandbox/image-manager.js +7 -0
  69. package/dist/src/core/sandbox/image-store.js +7 -0
  70. package/dist/src/core/sandbox/index.js +7 -0
  71. package/dist/src/core/sandbox/macos-sandbox.js +7 -0
  72. package/dist/src/core/sandbox/microvm-manager.js +7 -0
  73. package/dist/src/core/sandbox/native-service-manager.js +7 -0
  74. package/dist/src/core/sandbox/network.js +7 -0
  75. package/dist/src/core/sandbox/oci-exporter.js +7 -0
  76. package/dist/src/core/sandbox/oci-puller.js +7 -0
  77. package/dist/src/core/sandbox/oci-runtime-spec.js +7 -0
  78. package/dist/src/core/sandbox/policy-engine.js +7 -0
  79. package/dist/src/core/sandbox/port-proxy-daemon.js +7 -0
  80. package/dist/src/core/sandbox/room-sync-manager.js +7 -0
  81. package/dist/src/core/sandbox/runtime-manager.js +7 -0
  82. package/dist/src/core/sandbox/snapshot-manager.js +7 -0
  83. package/dist/src/core/sandbox/storage.js +7 -0
  84. package/dist/src/core/sandbox/supervisor.js +7 -0
  85. package/dist/src/core/sandbox/tunnel-daemon.js +7 -0
  86. package/dist/src/core/sandbox/tunnel.js +7 -0
  87. package/dist/src/core/sandbox/window-launcher.js +7 -0
  88. package/dist/src/core/sandbox/windows-wsl2.js +7 -0
  89. package/dist/src/core/sandbox-window.js +8 -0
  90. package/dist/src/core/shell.js +7 -0
  91. package/dist/src/core/websocket.js +7 -0
  92. package/dist/src/dock/api/routes.js +7 -0
  93. package/dist/src/dock/api/server.js +7 -0
  94. package/dist/src/dock/api/wire.js +7 -0
  95. package/dist/src/dock/bench/memory.js +7 -0
  96. package/dist/src/dock/bench/runner.js +7 -0
  97. package/dist/src/dock/build/builder.js +7 -0
  98. package/dist/src/dock/build/buildfile.js +7 -0
  99. package/dist/src/dock/build/buildkit.js +7 -0
  100. package/dist/src/dock/build/changes.js +7 -0
  101. package/dist/src/dock/ci/actions.js +7 -0
  102. package/dist/src/dock/ci/runner.js +7 -0
  103. package/dist/src/dock/ci/workflow.js +7 -0
  104. package/dist/src/dock/cli/api.js +7 -0
  105. package/dist/src/dock/cli/args.js +7 -0
  106. package/dist/src/dock/cli/commands.js +7 -0
  107. package/dist/src/dock/cli/compose.js +7 -0
  108. package/dist/src/dock/cli/context.js +7 -0
  109. package/dist/src/dock/cli/files.js +7 -0
  110. package/dist/src/dock/cli/format.js +7 -0
  111. package/dist/src/dock/cli/manage.js +7 -0
  112. package/dist/src/dock/cli/moved.js +7 -0
  113. package/dist/src/dock/cli/services.js +7 -0
  114. package/dist/src/dock/cli/session.js +7 -0
  115. package/dist/src/dock/cli/workspace.js +7 -0
  116. package/dist/src/dock/client.js +7 -0
  117. package/dist/src/dock/compat/cases.js +7 -0
  118. package/dist/src/dock/compat/runner.js +7 -0
  119. package/dist/src/dock/config.js +7 -0
  120. package/dist/src/dock/daemon-main.js +7 -0
  121. package/dist/src/dock/daemon.js +7 -0
  122. package/dist/src/dock/deploy/compose.js +7 -0
  123. package/dist/src/dock/engine.js +7 -0
  124. package/dist/src/dock/image/archive.js +7 -0
  125. package/dist/src/dock/image/auth.js +7 -0
  126. package/dist/src/dock/image/reference.js +7 -0
  127. package/dist/src/dock/image/registry.js +7 -0
  128. package/dist/src/dock/image/tar.js +7 -0
  129. package/dist/src/dock/index.js +7 -0
  130. package/dist/src/dock/linux/host.js +7 -0
  131. package/dist/src/dock/paths.js +7 -0
  132. package/dist/src/dock/runtime/emulation.js +7 -0
  133. package/dist/src/dock/runtime/fsevents.js +7 -0
  134. package/dist/src/dock/runtime/fsops.js +7 -0
  135. package/dist/src/dock/runtime/hostfwd.js +7 -0
  136. package/dist/src/dock/runtime/logs.js +7 -0
  137. package/dist/src/dock/runtime/network.js +7 -0
  138. package/dist/src/dock/runtime/ports.js +7 -0
  139. package/dist/src/dock/runtime/rootfs.js +7 -0
  140. package/dist/src/dock/runtime/seccomp-profile.LICENSE +202 -0
  141. package/dist/src/dock/runtime/seccomp-profile.json +1227 -0
  142. package/dist/src/dock/runtime/seccomp.js +7 -0
  143. package/dist/src/dock/runtime/spec.js +7 -0
  144. package/dist/src/dock/runtime/stats.js +7 -0
  145. package/dist/src/dock/runtime/volumes.js +7 -0
  146. package/dist/src/dock/selftest.js +7 -0
  147. package/dist/src/dock/setup/app.js +7 -0
  148. package/dist/src/dock/setup/autostart.js +7 -0
  149. package/dist/src/dock/setup/index.js +7 -0
  150. package/dist/src/dock/setup/integrate.js +7 -0
  151. package/dist/src/dock/setup/migrate.js +7 -0
  152. package/dist/src/dock/state/store.js +7 -0
  153. package/dist/src/dock/ui/page.js +385 -0
  154. package/dist/src/dock/ui/server.js +7 -0
  155. package/dist/src/dock/version.js +7 -0
  156. package/dist/src/dock/workspace/apps.js +7 -0
  157. package/dist/src/dock/workspace/workspace.js +7 -0
  158. package/dist/src/events/notifications.js +7 -0
  159. package/dist/src/utils/logger.js +7 -0
  160. package/dist/src/utils/terminalCrypto.js +7 -0
  161. package/package.json +41 -16
  162. package/bin/thinknsh.js +0 -71
  163. package/src/commands/auth/login.js +0 -84
  164. package/src/commands/auth/logout.js +0 -32
  165. package/src/commands/auth/register.js +0 -52
  166. package/src/commands/auth/whoami.js +0 -52
  167. package/src/commands/github/commits.js +0 -58
  168. package/src/commands/github/issue-create.js +0 -59
  169. package/src/commands/github/issues.js +0 -57
  170. package/src/commands/github/pulls.js +0 -58
  171. package/src/commands/github/statusgh.js +0 -50
  172. package/src/commands/index.js +0 -136
  173. package/src/commands/message/say.js +0 -49
  174. package/src/commands/room/join.js +0 -142
  175. package/src/commands/room/leave.js +0 -24
  176. package/src/commands/room/rooms.js +0 -66
  177. package/src/commands/shell/notify.js +0 -88
  178. package/src/commands/shell/status.js +0 -73
  179. package/src/commands/shell/version.js +0 -18
  180. package/src/commands/tasks/build.js +0 -190
  181. package/src/commands/tasks/complete.js +0 -940
  182. package/src/commands/tasks/create-task.js +0 -101
  183. package/src/commands/tasks/loadtest.js +0 -101
  184. package/src/commands/tasks/suggestTask.js +0 -0
  185. package/src/commands/tasks/task-status.js +0 -72
  186. package/src/commands/tasks/task.js +0 -182
  187. package/src/commands/tasks/tasks.js +0 -184
  188. package/src/commands/team/invite.js +0 -46
  189. package/src/commands/team/myteam.js +0 -78
  190. package/src/commands/team/teammates.js +0 -52
  191. package/src/commands/terminal/share.js +0 -261
  192. package/src/core/api-client.js +0 -246
  193. package/src/core/geekload.js +0 -524
  194. package/src/core/local-runner.js +0 -861
  195. package/src/core/notification-window.js +0 -165
  196. package/src/core/project-config.js +0 -100
  197. package/src/core/shell.js +0 -734
  198. package/src/core/websocket.js +0 -657
  199. package/src/events/notifications.js +0 -184
  200. package/src/utils/logger.js +0 -159
  201. 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
+ const{spawn}=require("child_process"),path=require("path"),chalk=require("chalk");class SandboxWindowLauncher{static spawn(e){const n=path.resolve(__dirname,"../sandbox-window.js"),o=process.execPath;if("darwin"===process.platform){"vscode"===process.env.TERM_PROGRAM||process.env.VSCODE_INJECTION;const a=`\n tell application "Terminal"\n launch\n do script "'${o}' '${n}' '${e}'"\n activate\n end tell\n `;try{return spawn("osascript",["-e",a],{detached:!0,stdio:"ignore"}).unref(),console.log(chalk.green("[TNC] Dedicated Sandbox Console opened in separate Terminal window!")),!0}catch(n){return console.log(chalk.yellow(`[Notice] Could not auto-launch Terminal window: ${n.message}`)),console.log(chalk.dim(`You can manually view it with: thinknsh logs -f ${e}`)),!1}}if("win32"===process.platform)try{return spawn(`start cmd.exe /k "title TNC Sandbox: ${e} && "${o}" "${n}" ${e}""`,[],{detached:!0,stdio:"ignore",shell:!0}).unref(),console.log(chalk.green("[TNC] Sandbox Console opened in separate Command Prompt window!")),!0}catch(e){return!1}const a=["x-terminal-emulator","gnome-terminal","konsole","xterm"];for(const t of a)try{return spawn(t,["-e",`${o} ${n} ${e}`],{detached:!0,stdio:"ignore"}).unref(),console.log(chalk.green(`[TNC] Sandbox Console opened in ${t}!`)),!0}catch(e){}return console.log(chalk.dim(`Tip: Run "thinknsh logs -f ${e}" to view live logs.`)),!1}}module.exports=SandboxWindowLauncher;
@@ -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
+ const{execSync,spawn}=require("child_process");let fs;try{fs=require("fs-extra")}catch(e){try{fs=require("/Users/ramansingh/ThinkNCollab-Shell-V2/node_modules/fs-extra")}catch(e){const t=require("fs");fs={...t,ensureDirSync:e=>t.mkdirSync(e,{recursive:!0}),readJsonSync:e=>JSON.parse(t.readFileSync(e,"utf8")),writeJsonSync:(e,r)=>t.writeFileSync(e,JSON.stringify(r,null,2),"utf8"),removeSync:e=>t.rmSync(e,{recursive:!0,force:!0})}}}const path=require("path"),os=require("os");class WindowsWSL2Sandbox{constructor(){this.isWindows="win32"===process.platform,this._wsl2Available=null,this._defaultDistro=null}detect(){if(!this.isWindows)return{available:!1,reason:"not-windows"};if(null!==this._wsl2Available)return{available:this._wsl2Available,distro:this._defaultDistro};try{const e=execSync("wsl.exe --list --quiet 2>nul",{encoding:"utf16le",timeout:5e3}).split("\n").map(e=>e.replace(/\0/g,"").trim()).filter(Boolean);return 0===e.length?(this._wsl2Available=!1,{available:!1,reason:"no-distros"}):(this._defaultDistro=e.find(e=>e.toLowerCase().includes("ubuntu"))||e[0],this._defaultDistro=this._defaultDistro.replace(/\s*\(default\)/i,"").trim(),this._wsl2Available=!0,{available:!0,distro:this._defaultDistro,allDistros:e})}catch{return this._wsl2Available=!1,{available:!1,reason:"wsl-not-installed"}}}toWslPath(e){if(!e)return"/home";try{return execSync(`wsl.exe wslpath -u "${e.replace(/\\/g,"\\\\")}"`,{encoding:"utf8",timeout:3e3}).trim()||this._manualConvert(e)}catch{return this._manualConvert(e)}}_manualConvert(e){return e.replace(/^([A-Za-z]):\\/,(e,t)=>`/mnt/${t.toLowerCase()}/`).replace(/\\/g,"/")}setupPortProxy(e,t){if(!this.isWindows)return{success:!1};try{const r=execSync("wsl.exe -- bash -c \"ip -4 addr show eth0 | grep -oP '(?<=inet )\\d+\\.\\d+\\.\\d+\\.\\d+'\"",{encoding:"utf8",timeout:5e3}).trim();if(!r)return{success:!1,reason:"no-wsl-ip"};execSync(`netsh interface portproxy add v4tov4 listenport=${e} listenaddress=0.0.0.0 connectport=${t} connectaddress=${r}`,{stdio:"ignore",timeout:5e3});try{execSync(`netsh advfirewall firewall add rule name="TNC-${e}" dir=in action=allow protocol=TCP localport=${e}`,{stdio:"ignore",timeout:5e3})}catch(e){}return{success:!0,wslIp:r,hostPort:e,containerPort:t}}catch(e){return{success:!1,reason:e.message}}}removePortProxy(e){if(!this.isWindows)return!1;try{execSync(`netsh interface portproxy delete v4tov4 listenport=${e} listenaddress=0.0.0.0`,{stdio:"ignore",timeout:3e3});try{execSync(`netsh advfirewall firewall delete rule name="TNC-${e}"`,{stdio:"ignore",timeout:3e3})}catch(e){}return!0}catch{return!1}}spawnInWSL2(e,t,r={}){const{cwd:s=process.cwd(),hostPort:n=null,containerPort:i=null,memoryLimit:o="2g",distro:l=this._defaultDistro,outFd:a=null,runtimeBinDir:c=null}=r,u=this.detect();if(!u.available)return{success:!1,reason:u.reason,fallback:"host-process"};const d=this.toWslPath(s),h=c?this.toWslPath(c):null;let f=null;n&&i&&(f=this.setupPortProxy(n,i));const w=this._parseMemoryMB(o),p=1024*w,m=[];h&&m.push(`export PATH="${h}:$PATH"`),m.push(`cd "${d}" 2>/dev/null || cd ~`,`ulimit -v ${p} 2>/dev/null || true`,"ulimit -n 4096 2>/dev/null || true",'eval "$TNC_CMD"');const y=m.join("; "),S=spawn("wsl.exe",[...l?["-d",l]:[],"--","bash","-c",y],{detached:!0,stdio:["ignore",a||"ignore",a||"ignore"],env:{...process.env,TNC_CMD:t,TNC_SANDBOX_ID:e,TNC_PLATFORM:"wsl2-kernel",PORT:String(i||n||3e3),WSL_SANDBOX:"1",PATH:c?`${c};${process.env.PATH||""}`:process.env.PATH}});return S.unref(),{success:!0,child:S,isolationLevel:"wsl2-kernel",memoryLimitMB:w,distro:l||"default",wslCwd:d,portProxy:f}}killSandbox(e,t){if(!this.isWindows||!e)return!1;try{execSync(`taskkill /F /T /PID ${e} 2>nul`,{stdio:"ignore"})}catch(e){}try{execSync(`wsl.exe -- bash -c "pkill -f TNC_SANDBOX_ID=${t} 2>/dev/null || true"`,{stdio:"ignore",timeout:3e3})}catch(e){}return!0}freezeCleanup(e){this.isWindows&&e.hostPort&&this.removePortProxy(e.hostPort)}generateWSLConfig(e=4,t=2){const r=path.join(os.homedir(),".wslconfig"),s=`\n[wsl2]\nmemory=${e}GB\nprocessors=${t}\nswap=0\nlocalhostForwarding=true\nnestedVirtualization=false\n`.trim();return fs.writeFileSync(r,s),{path:r,config:s}}getMemoryUsage(e){if(!this.isWindows||!e)return 0;try{const t=execSync(`wmic process where ProcessId=${e} get WorkingSetSize /value 2>nul`,{encoding:"utf8",timeout:3e3}).match(/WorkingSetSize=(\d+)/);if(t)return Math.round(parseInt(t[1])/1024/1024)}catch(e){}return 0}_parseMemoryMB(e){const t=String(e||"2g").toLowerCase().trim().match(/^(\d+(?:\.\d+)?)([kmg]?)b?$/);if(!t)return 2048;const r=parseFloat(t[1]);switch(t[2]){case"k":return Math.floor(r/1024);case"m":default:return Math.floor(r);case"g":return Math.floor(1024*r)}}}module.exports=WindowsWSL2Sandbox;
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
4
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
5
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
6
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
7
+ */
8
+ const fs=require("fs"),path=require("path"),os=require("os"),chalk=require("chalk"),{spawn}=require("child_process"),SandboxTunnel=require("./sandbox/tunnel"),sandboxId=process.argv[2];sandboxId||(console.log(chalk.red("Error: Sandbox ID required.")),console.log(chalk.dim("Usage: node sandbox-window.js <sandbox-id>")),process.exit(1));const sandboxDir=path.join(os.homedir(),".tnc","sandboxes",sandboxId),metaPath=path.join(sandboxDir,"meta.json"),logPath=path.join(sandboxDir,"output.log");function clearScreen(){process.stdout.write("c")}function getMeta(){if(fs.existsSync(metaPath))try{return JSON.parse(fs.readFileSync(metaPath,"utf8"))}catch(o){}return null}function drawHeader(o){clearScreen();const t=process.stdout.columns||80,n="━".repeat(t),e=` [Sandbox] ThinkNCollab - Console [${sandboxId}] `,s=e.padStart(Math.floor((t+e.length)/2)).padEnd(t);console.log(chalk.cyan.bold(n)),console.log(chalk.bgCyan.black.bold(s)),console.log(chalk.cyan.bold(n));const a=o?o.state:"UNKNOWN",l="RUNNING"===a?chalk.green.bold:"FROZEN"===a?chalk.blue.bold:chalk.red.bold,r=o&&o.pid?o.pid:chalk.dim("none"),c=o&&o.hostPort?`${o.hostPort} -> ${o.containerPort||o.hostPort}`:o&&o.containerPort?o.containerPort:"none",i=o&&o.cmd?o.cmd:"dev";console.log(` State: ${l(a)} | PID: ${chalk.yellow(r)} | Port: ${chalk.magenta(c)} | Command: ${chalk.white(i)}`),console.log(chalk.dim(` Logs: ${logPath}`)),console.log(chalk.dim(" Press Ctrl+C to close this console window (sandbox will keep running).\n")),console.log(chalk.dim("─".repeat(t)))}function streamLogs(){if(drawHeader(getMeta()),!fs.existsSync(logPath)){console.log(chalk.dim("Waiting for sandbox log stream to start..."));const o=setInterval(()=>{fs.existsSync(logPath)&&(clearInterval(o),startTailing())},500);return}startTailing()}function startTailing(){let o=0;if(fs.existsSync(logPath))try{const t=fs.readFileSync(logPath,"utf8");process.stdout.write(t),o=Buffer.byteLength(t)}catch(o){}"win32"!==process.platform?spawn("tail",["-n","50","-F",logPath],{stdio:["ignore","inherit","inherit"]}).on("error",()=>{watchWithPolling()}):watchWithPolling()}function watchWithPolling(){let o=fs.existsSync(logPath)?fs.statSync(logPath).size:0;setInterval(()=>{if(!fs.existsSync(logPath))return;const t=fs.statSync(logPath);t.size>o?(fs.createReadStream(logPath,{start:o,end:t.size}).pipe(process.stdout),o=t.size):t.size<o&&(o=t.size)},300)}process.stdout.isTTY&&process.stdout.write(`]2;TNC Sandbox: ${sandboxId}`);const meta=getMeta();let tunnel=null;meta&&meta.hostPort&&(tunnel=new SandboxTunnel,tunnel.connect(sandboxId,meta.hostPort)),process.on("SIGINT",()=>{tunnel&&tunnel.stop(),console.log(chalk.dim(`\n\n[Console disconnected. Sandbox "${sandboxId}" continues running in background.]`)),process.exit(0)}),streamLogs();
@@ -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
+ const path=require("path"),fs=require("fs-extra"),os=require("os"),chalk=require("chalk"),net=require("net"),{spawn}=require("child_process"),EventEmitter=require("events"),WebSocketManager=require("./websocket"),CommandRegistry=require("../commands"),ThinkNCollabAPI=require("./api-client"),{loadProjectConfig,hasProjectConfig}=require("./project-config"),{SandboxEngine}=require("./sandbox");function getAvailablePort(e){return new Promise(t=>{const o=net.createServer();o.listen(e,"127.0.0.1",()=>{const e=o.address().port;o.close(()=>t(e))}),o.on("error",()=>t(getAvailablePort(e+1)))})}class ThinkNCollabShell extends EventEmitter{constructor(e={}){super(),this.config={historyFile:path.join(os.homedir(),".thinknsh_history"),configDir:path.join(os.homedir(),".thinkncollab"),maxHistory:1e3,websocket:{serverUrl:e.serverUrl||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com",autoConnect:e.autoConnect||!1},...e},fs.ensureDirSync(this.config.configDir),this.ws=new WebSocketManager({...this.config.websocket,_shellRef:this}),this.currentDir=process.cwd(),this.username=os.userInfo().username,this.history=[],this.running=!1,this.notifyClients=new Set,this.tcpServer=null,this.api=new ThinkNCollabAPI({apiUrl:e.apiUrl||process.env.THINKNCOLLAB_API_URL||e.serverUrl||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com",wsUrl:e.wsUrl||process.env.THINKNCOLLAB_WS_URL||e.serverUrl||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com"}),this.api.getCurrentRoom=()=>{const e=this.ws.getCurrentRoom();return e?{id:e,name:e}:null},this.commands=new Map,this.aliases=new Map,this.variables=new Map,this.sandboxEngine=new SandboxEngine,this.activeSandbox=null,this.api.isAuthenticated()&&this.ws.setUser(this.api.getUser(),this.api.getToken()),this.loadHistory(),this.registerBuiltinCommands(),this.commandRegistry=new CommandRegistry(this),this.setupWebSocketHandlers()}async startTCPServer(){this.notifyPort=await getAvailablePort(7379);const e=require("./notify-auth");try{this.notifyToken=e.writeToken(this.notifyPort)}catch(e){this.notifyToken=null}return new Promise(t=>{this.tcpServer=net.createServer(t=>{t.on("error",()=>this.notifyClients.delete(t)),this.notifyToken?e.guard(t,this.notifyToken,()=>{this.notifyClients.add(t),t.on("close",()=>this.notifyClients.delete(t))}):t.destroy()}),this.tcpServer.listen(this.notifyPort,"127.0.0.1",()=>t()),this.tcpServer.on("error",e=>{console.log(chalk.yellow(`[Notice] Notification server: ${e.message}`)),t()})})}pushNotification(e){if(0===this.notifyClients.size)return;const t=JSON.stringify(e)+"\n";for(const e of this.notifyClients)try{e.write(t)}catch{}if("darwin"===process.platform)try{let t="";if("message"===e.type?t=`${e.from}: ${e.text}`:"task"===e.type?t=e.message||e.title||"Task update":"task_assigned"===e.type?t=`Task assigned: ${e.taskTitle||""}`:"verdict"===e.type?t=`Verdict [${e.passed?"PASSED":"FAILED"}]: ${e.title||""}`:"notification"===e.type&&(t=`${e.title||""}: ${e.message||""}`),t){const e=t.replace(/["\\]/g," ").slice(0,150);spawn("osascript",["-e",`display notification "${e}" with title "ThinkNCollab"`],{detached:!0,stdio:"ignore"}).unref()}}catch(e){}(this.isVSCode||0===this.notifyClients.size)&&this.printInlineNotification(e)}printInlineNotification(e){const t=chalk.dim(`[${(new Date).toLocaleTimeString()}]`);switch(e.type){case"message":console.log(`\n${t} ${chalk.cyan("[Chat]")} ${chalk.bold(e.from)}: ${e.text}`);break;case"task_assigned":console.log(`\n${t} ${chalk.yellow("[Task]")} Assigned: ${chalk.white(e.taskTitle)} by ${chalk.cyan(e.assignedBy||"Admin")}`);break;case"task_completed":console.log(`\n${t} ${chalk.green("[Task Done]")} ${chalk.white(e.message||e.taskTitle)}`);break;case"verdict":break;case"notification":{const o="error"===e.level?chalk.red:"warn"===e.level?chalk.yellow:chalk.blue;console.log(`\n${t} ${o(`[Alert: ${e.title||"Info"}]`)} ${e.message}`);break}case"userJoined":console.log(`\n${t} ${chalk.dim(`[User+] ${e.username} joined`)}`);break;case"userLeft":console.log(`\n${t} ${chalk.dim(`[User-] ${e.username} left`)}`)}}spawnNotificationWindow(){const e=path.join(__dirname,"notification-window.js"),t=this.notifyPort,o={...process.env,THINKNSH_NOTIFY_PORT:String(t)},s="vscode"===process.env.TERM_PROGRAM||"1"===process.env.VSCODE_INJECTION||Boolean(process.env.VSCODE_PID)||Boolean(process.env.VSCODE_GIT_IPC_HANDLE)||Boolean(process.env.VSCODE_IPC_HOOK_CLI);if(this.isVSCode=s,s)return console.log(chalk.cyan(` [VS Code Integrated Mode] Live notification server active on port ${t}`)),console.log(chalk.dim(" - Inline notifications: ENABLED (alerts & chat display directly in VS Code)")),void console.log(chalk.dim(' - Split panel view: Run "thinknsh notifications" in a split terminal tab (Cmd+\\)\n'));const n="linux"===process.platform&&(()=>{try{return fs.readFileSync("/proc/version","utf8").toLowerCase().includes("microsoft")}catch{return!1}})(),i=()=>{console.log(chalk.yellow("\n [Notice] Could not open notification window.")),console.log(chalk.dim(" Open a new terminal and run:")),console.log(chalk.cyan(` THINKNSH_NOTIFY_PORT=${t} node "${e}"\n`))};try{if(n){const s=e.replace(/^\/mnt\/([a-z])/,(e,t)=>`${t.toUpperCase()}:`).replace(/\//g,"\\"),n=e=>{const t=spawn(`start cmd.exe /k ""${e}" "${s}""`,[],{detached:!0,stdio:"ignore",env:o,shell:!0});t.on("error",i),t.unref()},a=["/mnt/c/Program Files/nodejs/node.exe","/mnt/c/Program Files (x86)/nodejs/node.exe"].find(e=>{try{return fs.existsSync(e)}catch{return!1}});if(a){const e=a.replace(/^\/mnt\/([a-z])/,(e,t)=>`${t.toUpperCase()}:`).replace(/\//g,"\\");n(e)}else{const o=spawn("cmd.exe",["/c","where","node"],{stdio:["ignore","pipe","ignore"]});let s="";o.stdout.on("data",e=>s+=e),o.on("close",()=>{const o=s.trim().split("\n")[0]?.trim();if(o?.endsWith(".exe"))n(o);else{const o=spawn(`start cmd.exe /k "wsl.exe -- bash -c 'THINKNSH_NOTIFY_PORT=${t} node \\"${e}\\"; read'"`,[],{detached:!0,stdio:"ignore",shell:!0});o.on("error",i),o.unref()}})}}else if("win32"===process.platform){const s=process.execPath;if("vscode"===process.env.TERM_PROGRAM)console.log(chalk.yellow("\n VS Code detected — open a new terminal tab and run:")),console.log(chalk.cyan(` $env:THINKNSH_NOTIFY_PORT=${t}; node "${e}"\n`));else{const t=()=>{const t=spawn(`start cmd.exe /k ""${s}" "${e}""`,[],{detached:!0,stdio:"ignore",env:o,shell:!0});t.on("error",i),t.unref()},n=spawn("wt.exe",["cmd.exe","/k",`"${s}" "${e}"`],{detached:!0,stdio:"ignore",env:o});n.on("error",t),n.unref()}}else if("darwin"===process.platform){const o=process.execPath,s=`\n tell application "Terminal"\n launch\n do script "THINKNSH_NOTIFY_PORT=${t} '${o}' '${e}'"\n activate\n end tell\n `;try{spawn("osascript",["-e",s],{detached:!0,stdio:"ignore"}).unref()}catch{const s=`\n tell application "iTerm2"\n tell current window\n create tab with default profile\n tell current session of current tab\n write text "THINKNSH_NOTIFY_PORT=${t} '${o}' '${e}'"\n end tell\n end tell\n end tell\n `;try{spawn("osascript",["-e",s],{detached:!0,stdio:"ignore"}).unref()}catch{console.log(chalk.yellow(" [Notice] Could not auto-open notification window.")),console.log(chalk.dim(` Open Terminal.app and run: THINKNSH_NOTIFY_PORT=${t} node "${e}"`))}}}else{const s=process.execPath,n=`THINKNSH_NOTIFY_PORT=${t} '${s}' '${e}'; read`,a=[["gnome-terminal",["--","bash","-c",n]],["konsole",["--noclose","-e","bash","-c",n]],["xfce4-terminal",["--hold","-e",`bash -c "${n}"`]],["tilix",["-e",`bash -c "${n}"`]],["alacritty",["-e","bash","-c",n]],["kitty",["bash","-c",n]],["xterm",["-hold","-e",`bash -c "${n}"`]]];let c=!1;for(const[e,t]of a)try{spawn(e,t,{detached:!0,stdio:"ignore",env:o}).unref(),c=!0;break}catch{}c||i()}}catch(e){i()}}setupWebSocketHandlers(){this.ws.on("connected",e=>{this.api.logEvent("connected",null,{socketId:e?.socketId}),this.api.startHeartbeat(),this.pushNotification({type:"connected",socketId:e?.socketId})}),this.ws.on("disconnected",e=>{this.api.logEvent("disconnected",null,{reason:e?.reason}),this.api.stopHeartbeat(),this.pushNotification({type:"disconnected",reason:e?.reason})}),this.ws.on("reconnected",()=>{this.api.logEvent("reconnected"),this.api.startHeartbeat(),this.pushNotification({type:"connected",socketId:"reconnected"})}),this.ws.on("task:verdict",e=>{const t=e.passed,o=t?chalk.green("─".repeat(52)):chalk.red("─".repeat(52)),s=t?chalk.bgGreen.black(" PASS "):chalk.bgRed.white(" FAIL ");console.log("\n"+o),console.log(` ${s} ${chalk.bold(e.title||"Task")} ${t?chalk.green("PASSED"):chalk.red("FAILED")}`),console.log(` ${chalk.dim("Reason :")} ${t?chalk.green(e.reason):chalk.red(e.reason)}`),e.diff&&(console.log(` ${chalk.dim("Expected:")} ${chalk.cyan(JSON.stringify(e.diff.expected??e.diff.conditions))}`),console.log(` ${chalk.dim("Actual :")} ${chalk.yellow(JSON.stringify(e.diff.actual))}`)),console.log(` ${chalk.dim("At :")} ${chalk.dim(new Date(e.at||Date.now()).toLocaleTimeString())}`),console.log(o+"\n"),this.pushNotification({type:"verdict",passed:t,title:e.title,reason:e.reason,taskId:e.taskId,at:e.at})}),this.ws.on("message",e=>{const t=this.api.getUser()?._id;if(t&&e.userId===t)return;const o=e.name||e.username||e.sender?.name||"Someone",s=e.message||e.content||e.text||"",n=chalk.dim(`[${(new Date).toLocaleTimeString()}]`),i=e.isE2EE?chalk.green(" [🔒 E2EE]"):"";console.log(`\n${n}${i} ${chalk.cyan(o+":")} ${chalk.white(s)}`),this.pushNotification({type:"message",from:o,text:s})}),this.ws.on("messageHistory",e=>{e?.length&&(console.log(chalk.dim("\n ─── Room History ───────────────────────────────")),e.forEach(e=>{const t=e.name||e.username||e.sender?.name||"Unknown",o=e.message||e.content||e.text||"",s=e.timestamp||e.createdAt,n=s?chalk.dim(`[${new Date(s).toLocaleTimeString()}]`):"",i=e.isE2EE?chalk.green(" [🔒 E2EE]"):"";console.log(` ${n}${i} ${chalk.cyan(t+":")} ${chalk.white(o)}`)}),console.log(chalk.dim(" ────────────────────────────────────────────────\n")))}),this.ws.on("userJoined",e=>{console.log(chalk.dim(`\n [User+] ${e.username} joined the room`)),this.pushNotification({type:"userJoined",username:e.username})}),this.ws.on("userLeft",e=>{console.log(chalk.dim(`\n [User-] ${e.username} left the room`)),this.pushNotification({type:"userLeft",username:e.username})}),this.ws.on("joined",e=>this.api.logEvent("room_join",e)),this.ws.on("left",e=>this.api.logEvent("room_leave",e)),this.ws.on("notification",e=>{const t=chalk.dim(`[${(new Date).toLocaleTimeString()}]`),o={success:chalk.green,warning:chalk.yellow,error:chalk.red,info:chalk.blue}[e.level]||chalk.blue;console.log(`\n${t} [ALERT] ${o(`${e.title||"Notification"}: ${e.message||""}`)}`),this.pushNotification({type:e.type||"notification",level:e.level,title:e.title,message:e.message,taskTitle:e.taskTitle,assignedBy:e.assignedBy,meta:e.meta})});let e=0;this.ws.on("error",t=>{const o=Date.now();o-e>3e4&&(e=o,console.log(chalk.yellow(`[Notice] WebSocket reconnecting: ${t.message}`))),this.pushNotification({type:"notification",level:"error",title:"WebSocket Error",message:t.message})}),this.ws.onSandboxAction(this.sandboxEngine,(e,t)=>{t?.success?console.log(chalk.cyan(`\n[Web Dashboard Action] ${e.toUpperCase()} completed: ${t.message}`)):console.log(chalk.yellow(`\n[Web Dashboard Action] ${e.toUpperCase()} failed: ${t?.message}`))})}async start(){this.showWelcome(),await this.startTCPServer(),console.log(chalk.cyan(` Notification server active on port ${chalk.bold(this.notifyPort)}`)),console.log(chalk.dim(" To open live notification panel in another tab/terminal:")),console.log(chalk.white(` thinknsh notify ${this.notifyPort}\n`)),this.running=!0,process.on("SIGINT",()=>console.log(chalk.yellow('\nUse "exit" to quit'))),!await this._bootFromProjectConfig()&&this.config.websocket.autoConnect&&await this.ws.connect().catch(()=>{});const{input:e}=require("@inquirer/prompts");for(;this.running;)try{const t=await e({message:this.getPromptText(),required:!1}).catch(()=>null);if(null===t){console.log(chalk.yellow('Use "exit" to quit'));continue}const o=t.trim();if(!o)continue;this.history.push(o),this.saveHistory(),await this.execute(o)}catch(e){"ExitPromptError"===e.name||e.message?.includes("force closed")?console.log(chalk.yellow('Use "exit" to quit')):console.log(chalk.red(`Shell error: ${e.message}`))}}async _bootFromProjectConfig(){if(!hasProjectConfig())return!1;let e,t;try{e=loadProjectConfig()}catch(e){return console.log(chalk.yellow(`[Notice] tncproject: ${e.message}\n`)),!1}console.log(chalk.dim(" [Project] tncproject found")),console.log(chalk.dim(` Room : ${e.roomId}`)),console.log(chalk.dim(` File : ${e.filePath}\n`));try{t=await this.api._request("POST","/config/verify-config",{fileBase64:e.fileBase64,roomId:e.roomId,machineId:os.hostname()})}catch(e){return console.log(chalk.red(`[Error] tncproject auth failed: ${e.message}`)),console.log(chalk.dim(" Falling back to manual login.\n")),!1}this.api._saveSession({token:t.shellToken,shellSessionId:t.shellSessionId||null,user:{_id:t._id.toString(),userId:t._id.toString(),email:t.email,name:t.name,userType:"User"},timestamp:(new Date).toISOString()}),this.ws.setUser(this.api.getUser(),t.shellToken);try{await this.ws.connect(t.shellToken)}catch(e){return console.log(chalk.yellow(`[Notice] WebSocket: ${e.message}`)),console.log(chalk.green(`[Auth] ${t.name} logged in (offline mode)\n`)),!0}try{await this.ws.joinRoom(t.roomId.toString()),console.log(chalk.green(`[Room] ${t.name} -> room "${t.roomName||t.roomId}"\n`))}catch(e){console.log(chalk.yellow(`[Notice] Could not auto-join room: ${e.message}`)),console.log(chalk.dim(' Use "join <room-id>" manually.\n'))}return!0}async execute(e,t=null){if(!(e.trim()||t&&0!==t.length))return;const o=t&&t.length>0?t:this.parseCommand(e),s=o[0].toLowerCase(),n=o.slice(1);if(["login","logout","set","unset"].includes(s)||this.api.logEvent("command",null,{cmd:s}).catch(()=>{}),this.commands.has(s)){try{await this.commands.get(s).handler(n,this)}catch(e){console.log(chalk.red(`Error: ${e.message}`))}process.stdout.write("\r\n")}else{if(!this.commandRegistry||!await this.commandRegistry.execute(e,this,t))return"win32"===process.platform&&["open","which","grep","touch","cat"].includes(s)?(console.log(chalk.red(`'${s}' is not a thinknsh command.`)),console.log(chalk.dim(" Type 'help' to see available commands.")),void process.stdout.write("\r\n")):void await this.executeSystemCommand(e);this.commandRegistry.getCommand(s)?.selfExiting||process.stdout.write("\r\n")}}getPromptText(){let e="";const t=this.ws.getCurrentRoom?.();t&&(e+=`[${t}] `),this.ws.isConnected?.()&&(e+="● "),this.activeSandbox&&(e+=`[Sandbox: ${this.activeSandbox}] `);let o=this.currentDir;const s=os.homedir();return o="win32"===process.platform?o.toLowerCase()===s.toLowerCase()?"~":o.toLowerCase().startsWith(s.toLowerCase())?"~"+o.slice(s.length).replace(/\\/g,"/"):o.replace(/\\/g,"/"):o.replace(s,"~"),`${e}${this.username}@thinknsh:${o} $`}getPrompt(){return this.getPromptText()}printMessage(e){console.log(e)}registerBuiltinCommands(){const e=(e,t,o)=>this.commands.set(e,{handler:t,description:o});e("help",this.helpCommand.bind(this),"Show available commands"),e("exit",()=>{this.cleanup(),process.exit(0)},"Exit the shell"),e("quit",()=>{this.cleanup(),process.exit(0)},"Exit the shell"),e("clear",()=>console.clear(),"Clear screen"),e("history",this.historyCommand.bind(this),"Show command history"),e("cd",this.cdCommand.bind(this),"Change directory"),e("pwd",()=>console.log(process.cwd()),"Print working directory"),e("ls",this.lsCommand.bind(this),"List files"),e("use",e=>{const t=e[0];t?(this.activeSandbox=t,console.log(chalk.green(`\n[Sandbox] Switched active execution context to: [${t}]`)),console.log(chalk.dim(" All system commands will now execute within this isolated sandbox.\n"))):console.log(chalk.red("Usage: use <sandbox-id>"))},"Switch active shell context to a TNC sandbox"),e("leave-sandbox",()=>{this.activeSandbox?(console.log(chalk.cyan(`\nExited sandbox [${this.activeSandbox}]. Back to host shell.\n`)),this.activeSandbox=null):console.log(chalk.yellow("Not currently inside any sandbox context."))},"Exit active sandbox context"),e("connect",this.connectCommand.bind(this),"Connect to WebSocket"),e("disconnect",()=>this.ws.disconnect(),"Disconnect WebSocket"),e("status",this.statusCommand.bind(this),"Show connection status"),e("join",this.joinCommand.bind(this),"Join a room"),e("leave",()=>this.ws.leaveRoom?.(),"Leave current room"),e("rooms",this.roomsCommand.bind(this),"List rooms"),e("say",this.sayCommand.bind(this),"Send a message"),e("msg",this.sayCommand.bind(this),"Send a message"),e("set",this.setCommand.bind(this),"Set a variable"),e("unset",this.unsetCommand.bind(this),"Unset a variable"),this.addAlias("ll","ls -la"),this.addAlias("..","cd .."),this.addAlias("~","cd ~"),this.addAlias("unuse","leave-sandbox")}registerCommand(e,t,o=""){this.commands.set(e,{handler:t,description:o})}addAlias(e,t){this.aliases.set(e,t)}async helpCommand(){this.commandRegistry&&console.log(this.commandRegistry.getHelpText()),console.log(chalk.dim(" Any unrecognised command runs in your system shell\n"))}historyCommand(){this.history.forEach((e,t)=>console.log(`${(t+1).toString().padStart(4)} ${e}`))}async cdCommand(e){const t=e.join(" ").trim()||os.homedir();try{let e;if(e="~"===t?os.homedir():".."===t?path.dirname(this.currentDir):path.resolve(this.currentDir,t),e=path.normalize(e),!(await fs.stat(e)).isDirectory())return void console.log(chalk.red(`cd: not a directory: ${t}`));this.previousDir=this.currentDir,process.chdir(e),this.currentDir=process.cwd()}catch{console.log(chalk.red(`cd: no such directory: ${t}`))}}async lsCommand(e){try{const t=await fs.readdir(this.currentDir),o=e.includes("-a")?t:t.filter(e=>!e.startsWith("."));(await Promise.all(o.map(async e=>({name:e,isDir:(await fs.stat(path.join(this.currentDir,e))).isDirectory()})))).forEach(e=>e.isDir?console.log(chalk.blue(e.name+"/")):console.log(e.name))}catch(e){console.log(chalk.red(`ls: ${e.message}`))}}async connectCommand(){console.log(chalk.cyan("Connecting..."));try{await this.ws.connect()}catch(e){console.log(chalk.red(`Error: ${e.message}`))}}async statusCommand(){const e=this.ws.isConnected();console.log(chalk.cyan("\nStatus:")),console.log(` WebSocket : ${e?chalk.green("Connected"):chalk.red("Disconnected")}`),e&&console.log(` Socket ID : ${chalk.dim(this.ws.getSocketId())}`);const t=this.ws.getCurrentRoom();console.log(` Room : ${t?chalk.green(t):chalk.yellow("None")}`),console.log(` Auth : ${this.api.isAuthenticated()?chalk.green("Logged in"):chalk.yellow("Not logged in")}`);const o=this.api.getUser();o&&console.log(` User : ${chalk.dim(o.name||o.email)}`),console.log("")}async joinCommand(e){if(e[0]){this.ws.isConnected()||(console.log(chalk.yellow("Connecting...")),await this.ws.connect());try{const t=await this.ws.joinRoom(e[0]);console.log(chalk.green(`Joined: ${t.roomName||e[0]}`))}catch(e){console.log(chalk.red(`Error: ${e.message}`))}}else console.log(chalk.red("Usage: join <room-id>"))}async roomsCommand(){this.ws.isConnected()?(this.ws.socket.emit("rooms:list"),this.ws.socket.once("rooms:list",e=>{e.rooms?.length?e.rooms.forEach(e=>console.log(` ${chalk.cyan(e.name||e.id)} - Members: ${e.participantCount||0}`)):console.log(" No rooms")})):console.log(chalk.yellow("Not connected"))}async sayCommand(e){const t=e.join(" ");if(t)try{await this.ws.sendMessage(t);const e=this.api.getUser()?.name||this.username;console.log(chalk.green(`You: ${t}`)),this.pushNotification({type:"message",from:`You (${e})`,text:t})}catch(e){console.log(chalk.red(`Failed: ${e.message}`))}else console.log(chalk.red("Usage: say <message>"))}async setCommand(e){if(e.length)this.variables.set(e[0],e.slice(1).join(" ")),console.log(chalk.green(`${e[0]} = ${e.slice(1).join(" ")}`));else for(const[e,t]of this.variables)console.log(` ${chalk.green(e.padEnd(12))} = ${t}`)}async unsetCommand(e){e[0]&&this.variables.delete(e[0])}getSessionPath(){return path.join(this.config.configDir,"session.json")}parseCommand(e){const t=[];let o="",s=!1,n="",i=!1;for(const a of e)"\\"!==a||i||s?i?(o+=a,i=!1):'"'!==a&&"'"!==a||s?a===n&&s?(s=!1,n=""):" "!==a||s?o+=a:o&&(t.push(o),o=""):(s=!0,n=a):i=!0;return o&&t.push(o),t}executeSystemCommand(e){return new Promise(t=>{let o=e,s={...process.env},n=this.currentDir;if(this.activeSandbox){const t=this.sandboxEngine?.loadMeta(this.activeSandbox);t&&(s.TNC_SANDBOX_ID=this.activeSandbox,t.containerPort&&(s.PORT=String(t.containerPort))),console.log(chalk.cyan(`[TNC Sandbox: ${this.activeSandbox}] ${e}`))}const i=spawn(o,[],{cwd:n,shell:!0,stdio:"inherit",env:s});i.on("close",async o=>{try{this.currentDir=process.cwd()}catch{}if(0===o)try{const t=new(require("./dependency-tracker"))(n);await t.interceptCommand(e,this)}catch(e){}process.stdout.write("\r\n"),t()}),i.on("error",()=>{console.log(chalk.red(`Command not found: ${e}`)),t()})})}showWelcome(){try{const e=require("figlet");console.log(chalk.cyan(e.textSync("ThinkNCollab",{font:"Standard"})))}catch{}console.log(chalk.cyan("\n╔══════════════════════════════════════════════════════╗\n║ ThinkNCollab Shell v0.0.20 ║\n║ Type 'help' for commands ║\n╚══════════════════════════════════════════════════════╝"))}loadHistory(){try{fs.existsSync(this.config.historyFile)&&(this.history=fs.readFileSync(this.config.historyFile,"utf8").split("\n").filter(e=>e.trim()))}catch{}}saveHistory(){try{fs.writeFileSync(this.config.historyFile,this.history.slice(-this.config.maxHistory).join("\n"))}catch{}}cleanup(){if(this.activeSandbox)try{this.sandboxEngine.freeze(this.activeSandbox),console.log(chalk.cyan(`\n[TNC Smart Freeze] Auto-froze active sandbox [${this.activeSandbox}]. 0 RAM used!`))}catch(e){}if(this.api.logout(),this.tcpServer)try{this.tcpServer.close()}catch{}this.notifyPort&&require("./notify-auth").removeToken(this.notifyPort),this.ws&&this.ws.disconnect(),this.saveHistory()}}module.exports=ThinkNCollabShell;
@@ -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
+ const{io}=require("socket.io-client"),EventEmitter=require("events"),e2eeVault=require("./crypto/e2ee-vault");class WebSocketManager extends EventEmitter{constructor(e={}){super(),this.config={serverUrl:e.serverUrl||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com",reconnectAttempts:e.reconnectAttempts||5,reconnectDelay:e.reconnectDelay||1e3,...e},this.socket=null,this.connected=!1,this.currentUser=null,this.roomId=null,this.reconnectCount=0,this.messageQueue=[],this._authToken=null,this._handlersSetup=!1,this._serverReady=!1,this._shellRef=e._shellRef||null}connect(e=null){return new Promise((t,s)=>{try{e&&(this._authToken=e),this.socket=io(this.config.serverUrl+"/thinknsh",{transports:["websocket","polling"],reconnection:!0,reconnectionAttempts:this.config.reconnectAttempts,reconnectionDelay:this.config.reconnectDelay,auth:this._authToken?{token:this._authToken}:{}});const o=setTimeout(()=>{s(new Error("Connection timeout"))},1e4);this.socket.on("connect",()=>{clearTimeout(o),this.connected=!0,this.reconnectCount=0,this.currentUser&&this._announceUser(),this.processPendingMessages(),this.emit("connected",{socketId:this.socket.id}),t({success:!0,socketId:this.socket.id})}),this.socket.on("connect_error",e=>{clearTimeout(o),0===this.reconnectCount&&s(new Error(`Cannot connect to ${this.config.serverUrl}`)),this.reconnectCount++,this.emit("error",{message:e.message})}),this.socket.on("disconnect",e=>{this.connected=!1,this.emit("disconnected",{reason:e})}),this.socket.on("task:verdict",e=>{this.emit("task:verdict",e)}),this.socket.on("task:run-local",async e=>{try{const t=require("chalk"),{runLocal:s}=require("./local-runner"),o=e.taskId,r=this._shellRef,n=r?r.api:null;if(!n)return void console.log(t.red("\n[Error] CLI not authenticated. Run: login\n"));let i;try{i=await n._request("GET",`/thinknsh/${o}/cli`)}catch(e){return void console.log(t.red(`\n[Error] Could not fetch task info: ${e.message}\n`))}if(!i||!i.webhookSecret)return void console.log(t.red("\n[Error] Task has no webhookSecret - cannot submit result.\n"));await s({taskId:o,webhookSecret:i.webhookSecret,apiUrl:this.config.serverUrl||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com",repoUrl:i.repoUrl||i.githubUrl||i.repo_url||null,repoBranch:i.branch||i.repoBranch||"main",workflowFile:i.workflowFile||i.workflow||null,chalk:t,shell:r,onLog:e=>{this.socket.emit("task:log",{taskId:o,boardId:i.boardId,...e})}})}catch(e){console.error("task:run-local error:",e.message)}}),this.socket.on("reconnect",()=>{this.connected=!0,this.emit("reconnected",{}),this.currentUser&&this._announceUser(),this.roomId&&this.currentUser&&this._emitJoin(this.roomId)}),this._handlersSetup||(this.setupEventHandlers(),this._handlersSetup=!0)}catch(e){s(e)}})}_announceUser(){if(this.socket&&this.currentUser&&(this.socket.emit("thinknsh:connected",{userId:this.currentUser.userId,userName:this.currentUser.name,shellToken:this._authToken}),this._shellRef?.sandboxEngine))try{this._shellRef.sandboxEngine.list().forEach(e=>{this.emitSandboxSync(e,"sync")})}catch(e){}}setUser(e,t=null){if(this._user=e,!e)return;t&&(this._authToken=t);const s=(e.userId||e._id||"").toString();this.currentUser={userId:s,name:e.name||e.email||"Unknown",userType:e.userType||e.model||e.role||"User"},this.connected&&this.socket&&this._announceUser()}setupEventHandlers(){this.socket.on("thinknsh:ready",e=>{this._serverReady=!0,this.emit("ready",e)}),this.socket.on("thinknsh:joined",e=>{if(this.roomId=e.roomId,e.history?.length){const t=e.history.map(e=>{let t=e.message||e.content||e.text||"",s=!1;if("string"==typeof t&&t.startsWith("e2ee:")){const e=e2eeVault.decrypt(t,this.roomId);e!==t&&(t=e,s=!0)}return{...e,message:t,isE2EE:s}});this.emit("messageHistory",t)}this.emit("roomJoined",e)}),this.socket.on("thinknsh:message",e=>{let t=e.message||"",s=e.isE2EE||!1;if("string"==typeof t&&t.startsWith("e2ee:")){const e=e2eeVault.decrypt(t,this.roomId);e!==t&&(t=e,s=!0)}this.emit("message",{username:e.name,userId:e.userId,message:t,isE2EE:s,source:"shell",timestamp:e.timestamp})}),this.socket.on("chat-message",e=>{let t=e.message||"",s=e.isE2EE||!1;if("string"==typeof t&&t.startsWith("e2ee:")){const e=e2eeVault.decrypt(t,this.roomId);e!==t&&(t=e,s=!0)}this.emit("message",{username:e.name,userId:e.userId,message:t,isE2EE:s,source:e.source||"web",timestamp:e.timestamp})}),this.socket.on("e2ee:key-sync",e=>{!e?.roomId||this.roomId&&e.roomId!==this.roomId||this.emit("e2ee:key-sync",e)}),this.socket.on("e2ee:key-delivered",e=>{e?.packet?.rawKey&&(e2eeVault.setRawRoomKey(e.roomId,e.packet.rawKey),this.emit("e2ee:key-updated",e))}),this.socket.on("e2ee:packet",e=>{this.emit("e2ee:packet",e)}),this.socket.on("thinknsh:userJoined",e=>{this.emit("userJoined",{username:e.name,source:"shell"})}),this.socket.on("thinknsh:userLeft",e=>{this.emit("userLeft",{username:e.name,source:"shell"})}),this.socket.on("user-joined",e=>{this.emit("userJoined",{username:e,source:"web"})}),this.socket.on("user-left",e=>{this.emit("userLeft",{username:e,source:"web"})}),this.socket.on("user-list",e=>{this.emit("userList",{users:e})}),this.socket.on("thinknsh:history",e=>{this.emit("messageHistory",e.messages?.length?e.messages:[])}),this.socket.on("new-notification",e=>{this.emit("notification",{type:e.type||"info",level:e.type||"info",title:e.title||"Notification",message:e.message||"",meta:e.meta||{},taskTitle:e.taskTitle||e.meta?.taskTitle||"",assignedBy:e.assignedBy||e.meta?.assignedBy||""})}),this.socket.on("room:activity",e=>{this.emit("roomActivity",e)}),this.socket.on("thinknsh:status",e=>{this.emit("shellStatus",e)}),this.socket.on("thinknsh:notifications",e=>{this.emit("notificationList",e.notifications)}),this.socket.on("thinknsh:error",e=>{this.emit("serverError",e)}),this.socket.on("error",e=>{this.emit("serverError",e)});const e=new Set;this.socket.on("cicd:run-dispatch",async t=>{if(!t||!t.runId)return;if(e.has(t.runId))return;const s=this.roomId||this._joiningRoomId;if(!s||t.roomId&&String(t.roomId)!==String(s)){const e=require("chalk");return void console.log(e.yellow(`\n[CI/CD] Ignored run ${t.runId}: not for the room this shell joined.`))}e.add(t.runId);try{const e=require("chalk"),{runCicdLocal:s}=require("./local-runner"),o=this._shellRef,r=o?o.api:null;console.log(e.cyan(`\n[CI/CD] Execution request received for room: ${t.roomId||this.roomId}`)),await s({runId:t.runId,roomId:t.roomId||this.roomId,workflowFile:t.workflowFile||"universal-polyglot-ci.yml",workflowYaml:t.workflowYaml||"",localPath:t.localPath||"",apiUrl:r?r.baseURL:this.config.serverUrl,chalk:e,api:r,socket:this.socket})}catch(e){console.error("\n[Error] Error in CI/CD local execution:",e)}finally{setTimeout(()=>e.delete(t.runId),3e4)}});const t=new Set;this.socket.on("thinknsh:test-dispatch",async e=>{if(e&&e.runId&&e.taskId&&!t.has(e.runId)){t.add(e.runId);try{const t=require("chalk"),{spawn:s}=require("child_process"),o=require("http"),r=require("https"),n=require("crypto"),i=(e,t)=>{if(!t)return null;const s=n.createHash("sha256").update(t).digest(),o=n.randomBytes(12),r=n.createCipheriv("aes-256-gcm",s,o),i=JSON.stringify(e||{}),c=Buffer.concat([r.update(i,"utf8"),r.final()]),a=r.getAuthTag();return{iv:o.toString("hex"),tag:a.toString("hex"),cipherText:c.toString("hex")}},c=(e,t)=>{if(!(e&&e.cipherText&&e.iv&&e.tag))return null;try{const s=n.createHash("sha256").update(t).digest(),o=Buffer.from(e.iv,"hex"),r=Buffer.from(e.tag,"hex"),i=Buffer.from(e.cipherText,"hex"),c=n.createDecipheriv("aes-256-gcm",s,o);c.setAuthTag(r);const a=Buffer.concat([c.update(i),c.final()]);return JSON.parse(a.toString("utf8"))}catch(e){return null}},{runId:a,taskId:h,runResultToken:l,encryptedPayload:u}=e;let d=e.testConfig,m=e.commit,k=e.taskTitle;if(u&&l){const e=c(u,l);e&&(d=e.testConfig||d,m=e.commit||m,k=e.taskTitle||k)}console.log(t.cyan(`\n[TNC TestRunner - AES-256-GCM Secure] Task: "${k}"`)),console.log(t.gray(` Run ID : ${a}`)),console.log(t.gray(` Type : ${d?.type}`)),console.log(t.gray(` Commit : ${m?.sha?.slice(0,7)||"N/A"} — ${m?.message||""}`));let g={exitCode:1,stdout:"",stderr:"",status:null,body:null,url:null};if("exitCode"===d.type||"stdout"===d.type||"regex"===d.type){const e=d.command||d.script;e?(console.log(t.yellow(` Running : ${e}`)),g=await new Promise(o=>{const r=s(e,[],{shell:!0,cwd:process.cwd(),timeout:6e4});let n="",i="";r.stdout.on("data",e=>{n+=e.toString(),process.stdout.write(t.gray(" │ ")+e.toString())}),r.stderr.on("data",e=>{i+=e.toString(),process.stderr.write(t.red(" │ ")+e.toString())}),r.on("close",e=>{o({exitCode:e??1,stdout:n.trim(),stderr:i.trim(),status:null,body:null,url:null})}),r.on("error",e=>{o({exitCode:1,stdout:"",stderr:e.message,status:null,body:null,url:null})})}),console.log(0===g.exitCode?t.green(" [OK] Exit 0"):t.red(` [FAIL] Exit ${g.exitCode} - ${g.stderr.slice(0,100)}`))):g.stderr="No command specified in testConfig"}else if("http"===d.type){const e=d.endpoint||d.url,s=(d.method||"GET").toUpperCase();console.log(t.yellow(` Hitting : ${s} ${e}`)),g=await new Promise(n=>{const i=e?.startsWith("https")?r:o,c=new URL(e),a={hostname:c.hostname,port:c.port||("https:"===c.protocol?443:80),path:c.pathname+c.search,method:s},h=i.request(a,s=>{let o="";s.on("data",e=>o+=e.toString()),s.on("end",()=>{let r=o;try{r=JSON.parse(o)}catch(e){}console.log(t.green(` [OK] HTTP ${s.statusCode}`)),n({exitCode:0,stdout:o.slice(0,500),stderr:"",status:s.statusCode,body:r,url:e})})});h.on("error",s=>{console.log(t.red(` [Error] HTTP error: ${s.message}`)),n({exitCode:1,stdout:"",stderr:s.message,status:null,body:null,url:e})}),h.setTimeout(15e3,()=>{h.destroy(),n({exitCode:1,stdout:"",stderr:"Request timeout (15s)",status:null,body:null,url:e})})})}else if("browser"===d.type){console.log(t.yellow(` Browser Flow : ${d.url}`));try{const{runBrowserTest:e}=require("../commands/tasks/complete"),s=await e(d,null,{});g={exitCode:s.elementExists?0:1,stdout:s.bodyText?s.bodyText.slice(0,500):"",stderr:s.elementExists?"":`Element "${d.expect?.element}" not found`,elementExists:s.elementExists,elementText:s.elementText,url:s.url,status:200,body:null},console.log(t.green(` [OK] Browser check completed: elementExists=${s.elementExists}`))}catch(e){console.log(t.red(` [Error] Browser check failed: ${e.message}`)),g={exitCode:1,stdout:"",stderr:e.message,elementExists:!1,elementText:"",url:d.url,status:null,body:null}}}else{const e=d.command||d.script;e?g=await new Promise(t=>{const o=s(e,[],{shell:!0,cwd:process.cwd(),timeout:6e4});let r="",n="";o.stdout.on("data",e=>r+=e.toString()),o.stderr.on("data",e=>n+=e.toString()),o.on("close",e=>t({exitCode:e??1,stdout:r.trim(),stderr:n.trim(),status:null,body:null,url:null})),o.on("error",e=>t({exitCode:1,stdout:"",stderr:e.message,status:null,body:null,url:null}))}):g.stderr=`Unsupported testConfig type: ${d.type}`}const p={runId:a,taskId:h,runResultToken:l,encryptedResult:l?i(g,l):null,...g};this.socket.emit("thinknsh:test-result",p),console.log(t.cyan(" [AES-256-GCM] Encrypted result dispatched to TNC server.\n"))}catch(e){console.error("thinknsh:test-dispatch error:",e.message)}finally{setTimeout(()=>t.delete(e.runId),6e4)}}})}joinRoom(e){return new Promise((t,s)=>{if(!this.connected||!this.socket)return s(new Error("Not connected to server"));if(!this.currentUser)return s(new Error("Not logged in — run: login"));const o=setTimeout(()=>{this.socket.off("thinknsh:joined",r),this.socket.off("thinknsh:error",i),this.socket.off("thinknsh:ready",n),s(new Error("Join timeout"))},8e3),r=e=>{clearTimeout(o),this.socket.off("thinknsh:error",i),this.roomId=e.roomId,t(e)};let n=null;const i=e=>{clearTimeout(o),this.socket.off("thinknsh:joined",r),n&&this.socket.off("thinknsh:ready",n),s(new Error(e.message||"Join error"))},c=()=>{this.socket.once("thinknsh:joined",r),this.socket.once("thinknsh:error",i),this._emitJoin(e)};this._serverReady?c():(n=()=>{this._serverReady=!0,c()},this.socket.once("thinknsh:ready",n))})}_emitJoin(e){this.socket&&this.currentUser&&(this._joiningRoomId=e,this.socket.emit("thinknsh:join",{roomId:e,userId:this.currentUser.userId,name:this.currentUser.name,userType:this.currentUser.userType}))}leaveRoom(){return this.socket&&this.roomId&&(this.socket.emit("leave-room",{roomId:this.roomId}),this.roomId=null,this._joiningRoomId=null),Promise.resolve({success:!0})}sendMessage(e,t=null){return new Promise((s,o)=>{if(!this.socket?.connected)return o(new Error("Not connected"));const r=this.roomId;if(!r)return o(new Error("Not in any room. Use: join <room-id>"));if(!this.currentUser)return o(new Error("Not authenticated. Run: login"));const n=e?e.trim():"",i=e2eeVault.encrypt(n,r);this.socket.emit("thinknsh:message",{roomId:r,userId:this.currentUser.userId,name:this.currentUser.name,userType:this.currentUser.userType||"User",message:i,isE2EE:!0,replyTo:t||null}),s({success:!0,message:i,plaintext:n})})}getHistory(e=30,t=0){this.connected&&this.socket&&this.roomId&&this.socket.emit("thinknsh:getHistory",{roomId:this.roomId,limit:e,skip:t})}getNotifications(){this.connected&&this.currentUser&&this.socket.emit("thinknsh:getNotifications",{userId:this.currentUser.userId})}sendNotification(e,t="room"){return this.connected&&this.socket?(this.socket.emit("notification:send",{type:e.type||"info",title:e.title||"Notification",message:e.message,data:e.data||{},target:"room"===t?this.roomId:t,timestamp:(new Date).toISOString()}),Promise.resolve({success:!0})):Promise.reject(new Error("Not connected"))}markNotificationsRead(e=[]){this.connected&&this.currentUser&&this.socket.emit("thinknsh:markRead",{userId:this.currentUser.userId,notificationIds:e})}processPendingMessages(){this.messageQueue.length&&this.roomId&&this.currentUser&&(this.messageQueue.forEach(e=>this.sendMessage(e)),this.messageQueue=[])}disconnect(){this.socket&&(this.socket.removeAllListeners(),this.socket.disconnect(),this.socket=null,this.connected=!1,this.currentUser=null,this.roomId=null,this._handlersSetup=!1,this._serverReady=!1,this._serverReady=!1)}emitSandboxSync(e,t){this.connected&&this.socket&&e&&this.socket.emit("sandbox:sync",{userId:this.currentUser?.userId,roomId:this.roomId,event:t,sandbox:{id:e.id,state:e.state,pid:e.pid||null,cmd:e.cmd,hostPort:e.hostPort,containerPort:e.containerPort,isolationLevel:e.isolationLevel,memoryLimitMB:e.memoryLimitMB,startedAt:e.startedAt,frozenAt:e.frozenAt||null,platform:process.platform},timestamp:(new Date).toISOString()})}onSandboxAction(e,t){this.socket&&this.socket.on("sandbox:action",async s=>{const{sandboxId:o,action:r,requestedBy:n}=s||{};if(!o||!r)return;let i=null;try{switch(r){case"freeze":i=await e.freeze(o),i.success&&this.emitSandboxSync(i.meta,"frozen");break;case"resume":i=await e.resume(o),i.success&&this.emitSandboxSync(i.meta,"resumed");break;case"destroy":i=await e.destroy(o),this.emitSandboxSync({id:o,state:"DESTROYED"},"destroyed");break;default:i={success:!1,message:`Unknown action: ${r}`}}}catch(e){i={success:!1,message:e.message}}this.socket.emit("sandbox:action-ack",{sandboxId:o,action:r,requestedBy:n,success:i?.success,message:i?.message,timestamp:(new Date).toISOString()}),t&&t(r,i)})}isConnected(){return this.connected&&!!this.socket?.connected}getCurrentRoom(){return this.roomId}getSocketId(){return this.socket?.id||null}getParticipants(){return Promise.resolve([])}sendTyping(){}ping(){this.connected&&this.socket&&this.socket.emit("ping")}}module.exports=WebSocketManager;
@@ -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"),{DOCK_VERSION,API_VERSION,MIN_API_VERSION}=require("../version"),{DEFAULT_REGISTRY,RAW_STREAM,MUX_STREAM,ROOT_DIR_FIELD,REGISTRY_AUTH_HEADER,PATH_STAT_HEADER,BUILD_FILE,BUILD_FILE_PARAM}=require("./wire"),{muxFrame,readEntries,follow,formatEntry}=require("../runtime/logs"),{labelMatch}=require("../engine"),GO_ARCH={x64:"amd64",arm64:"arm64",arm:"arm",ia32:"386"},UNAME_ARCH={x64:"x86_64",arm64:"aarch64",arm:"armv7l",ia32:"i386"};function httpError(e,t){const r=new Error(t);return r.statusCode=e,r}function parseFilters(e){const t=e.query.get("filters");if(!t)return{};try{const e=JSON.parse(t);return Object.fromEntries(Object.entries(e).map(([e,t])=>[e,Array.isArray(t)?t:Object.keys(t)]))}catch(e){throw httpError(400,"invalid filters")}}function parseTime(e,t){if(null==e||""===e)return t;if(/^\d+(\.\d+)?$/.test(e))return Math.floor(1e3*parseFloat(e));const r=Date.parse(e);if(Number.isNaN(r))throw httpError(400,`invalid timestamp: ${e}`);return r}function requestCredentials(e){const t=e.req.headers[REGISTRY_AUTH_HEADER];if(!t)return null;try{const e=JSON.parse(Buffer.from(String(t).replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8"));return e&&e.username&&"string"==typeof e.password?{username:e.username,password:e.password}:null}catch(e){return null}}async function progressStream(e,t){let r=null;const a=t=>{r||(r=e.stream("application/json")),r.write(JSON.stringify(t)+"\r\n")};try{await t(a),r||(r=e.stream("application/json")),r.end()}catch(e){if(!r)throw e;a({errorDetail:{message:e.message},error:e.message}),r.end()}}function toApiEvent(e,t){const r={...e.attributes},a={Type:e.type,Action:e.action,Actor:{ID:e.actorId,Attributes:r},scope:"local",time:Math.floor(e.time/1e3),timeNano:1e6*e.time};return"container"!==e.type&&"image"!==e.type||(a.status=e.action,a.id=e.actorId,r.image&&(a.from=r.image)),a}function eventMatches(e,t){const r=(e,r)=>!t[e]?.length||t[e].some(r);return r("type",t=>t===e.type)&&r("event",t=>e.action===t||e.action.startsWith(t+":"))&&r("container",t=>"container"===e.type&&(e.actorId.startsWith(t)||e.attributes.name===t))&&r("image",t=>e.attributes.image===t||e.actorId===t)&&r("volume",t=>"volume"===e.type&&e.actorId===t)&&(t.label||[]).every(t=>labelMatch(e.attributes,t))}function buildRoutes({store:e,engine:t,home:r}){const a=[],n=(e,t,r,n,s={})=>{const o=t instanceof RegExp?t:new RegExp("^"+t.replace(/:\w+/g,"([^/]+)")+"$");a.push({method:e,re:o,name:r,handler:n,...s})},s=e=>e.text(200,"OK","text/plain; charset=utf-8",{"Cache-Control":"no-cache, no-store, must-revalidate",Pragma:"no-cache","Builder-Version":"0"===process.env.TNC_DOCK_BUILDKIT?"1":"2"});n("GET","/_ping","ping",s),n("HEAD","/_ping","ping",s),n("GET","/version","version",e=>{const r={ApiVersion:API_VERSION,MinAPIVersion:MIN_API_VERSION,Os:"linux",Arch:GO_ARCH[process.arch]||process.arch};e.json(200,{Platform:{Name:"ThinkNCollab dock"},Components:[{Name:"Engine",Version:DOCK_VERSION,Details:r}],Version:DOCK_VERSION,ApiVersion:API_VERSION,MinAPIVersion:MIN_API_VERSION,GitCommit:"",GoVersion:`node ${process.version}`,Os:"linux",Arch:r.Arch,KernelVersion:t.kernelVersion||"",BuildTime:""})}),n("GET","/info","info",a=>{const n=e.counts();a.json(200,{ID:"",Name:os.hostname(),ServerVersion:DOCK_VERSION,Containers:n.containers,ContainersRunning:n.running,ContainersPaused:n.paused,ContainersStopped:n.stopped,Images:n.images,Driver:"overlayfs",DriverStatus:[],OperatingSystem:"ThinkNCollab dock",OSType:"linux",OSVersion:"",KernelVersion:t.kernelVersion||"",Architecture:UNAME_ARCH[process.arch]||process.arch,NCPU:os.cpus().length,MemTotal:os.totalmem(),[ROOT_DIR_FIELD]:r,IndexServerAddress:DEFAULT_REGISTRY,RegistryConfig:{IndexConfigs:{},InsecureRegistryCIDRs:[],Mirrors:[]},Plugins:{Volume:["local"],Network:["bridge","host","null"],Authorization:null,Log:["json-file"]},LoggingDriver:"json-file",CgroupDriver:"cgroupfs",CgroupVersion:"2",Runtimes:{crun:{path:"crun"}},DefaultRuntime:"crun",SecurityOptions:[],Labels:[],ExperimentalBuild:!1,Swarm:{LocalNodeState:"inactive",ControlAvailable:!1,NodeID:"",NodeAddr:"",RemoteManagers:null},Warnings:[]})}),n("GET","/events","events",async r=>{const a=parseFilters(r),n=Date.now(),s=parseTime(r.query.get("since"),null),o=parseTime(r.query.get("until"),null),i=r.stream("application/json"),c=t=>{eventMatches(t,a)&&i.write(JSON.stringify(toApiEvent(t,e))+"\n")};if(null!==s||null!==o)for(const t of e.eventsBetween(s??0,Math.min(o??n,n)))c(t);if(null!==o&&o<=n)return i.end();let l=null;const m=e=>{null!==o&&e.time>o||c(e)};t.on("event",m);const u=()=>{t.off("event",m),l&&clearTimeout(l)};i.onClose(u),null!==o&&(l=setTimeout(()=>{u(),i.end()},o-n))}),n("POST","/auth","auth",async e=>{const{username:r,password:a,serveraddress:n}=e.body||{},s=await t.login({username:r,password:a,serveraddress:n});e.json(200,{Status:"Login Succeeded",IdentityToken:"",Registry:s})}),n("DELETE","/auth","auth.remove",e=>{const{host:r,removed:a}=t.logout(e.query.get("serveraddress")||"");e.json(200,{Registry:r,Removed:a})});const o=e=>{let t=0;const r=e=>{let a;try{a=require("fs").lstatSync(e)}catch(e){return}if(!a.isDirectory())return void(t+=a.size);let n=[];try{n=require("fs").readdirSync(e)}catch(e){}for(const t of n)r(require("path").join(e,t))};return r(e),t};n("GET","/system/df","system.df",r=>{const a=e.listImages().map(e=>t.imageSummary(e)),n=new Map;for(const t of e.listImages())for(const e of t.layers||[])n.set(e.digest,e.size);r.json(200,{LayersSize:[...n.values()].reduce((e,t)=>e+t,0),Images:a,Containers:e.listContainers().map(e=>({...t.containerSummary(e),SizeRw:0,SizeRootFs:0})),Volumes:e.listVolumes().map(e=>({...t.volumes.toApi(e),UsageData:{Size:o(e.mountpoint),RefCount:t.volumes.usedBy(e.name).length}})),BuildCache:[]})});const i=(e,t,r)=>!(r.label||[]).some(t=>!labelMatch(e,t))&&!(r["label!"]||[]).some(t=>labelMatch(e,t))&&(!r.until?.length||t<parseTime(r.until[0],1/0));n("POST","/containers/prune","containers.prune",async r=>{const a=parseFilters(r),n=[];for(const r of e.listContainers())"running"!==r.state&&"removing"!==r.state&&i(r.config.Config.Labels||{},r.createdAt,a)&&(await t.remove(r.id,{}),n.push(r.id));r.json(200,{ContainersDeleted:n,SpaceReclaimed:0})}),n("POST","/images/prune","images.prune",async r=>{const a=parseFilters(r),n="false"===a.dangling?.[0]||"0"===a.dangling?.[0],s=new Set(e.listContainers().map(e=>e.image)),o=[];let i=0;for(const r of e.listImages())s.has(r.id)||!n&&r.repoTags.length||(o.push(...await t.removeImage(r.id,{force:!0})),i+=r.size);r.json(200,{ImagesDeleted:o,SpaceReclaimed:i})}),n("POST","/volumes/prune","volumes.prune",r=>{const a=parseFilters(r),n="true"===a.all?.[0]||"1"===a.all?.[0],s=[];let c=0;for(const r of e.listVolumes())!n&&!r.anonymous||t.volumes.usedBy(r.name).length||i(r.labels||{},r.createdAt,a)&&(c+=o(r.mountpoint),t.volumes.remove(r.name),s.push(r.name));r.json(200,{VolumesDeleted:s,SpaceReclaimed:c})}),n("POST","/build/prune","build.prune",t=>{const r=e.db.prepare("SELECT COUNT(*) AS n FROM build_cache").get().n;e.db.exec("DELETE FROM build_cache"),t.json(200,{CachesDeleted:Array.from({length:r},(e,t)=>String(t)),SpaceReclaimed:0})}),n("GET","/images/json","images.list",r=>{const a=parseFilters(r);let n=e.listImages();a.reference?.length&&(n=n.filter(e=>e.repoTags.some(e=>a.reference.some(t=>e===t||e.startsWith(t+":"))))),a.dangling?.length&&(n=n.filter(e=>0===e.repoTags.length==("true"===a.dangling[0]))),r.json(200,n.map(e=>t.imageSummary(e)))}),n("POST","/images/create","images.pull",async e=>{const r=e.query.get("fromSrc");if(r){if("-"!==r)throw httpError(501,"importing from a URL is not supported; send the tar as the request body (fromSrc=-)");return void await progressStream(e,async r=>{r({status:(await t.importImage(e.req,{repo:e.query.get("repo")||"",tag:e.query.get("tag")||"",changes:e.query.getAll("changes"),message:e.query.get("message")||"",platform:e.query.get("platform")||""})).id})})}e.req.resume();const a=e.query.get("fromImage");if(!a)throw httpError(400,"fromImage is required");const n=e.query.get("tag"),s=n?n.startsWith("sha256:")?`${a}@${n}`:`${a}:${n}`:a;await progressStream(e,r=>t.pull(s,r,{credentials:requestCredentials(e),platform:e.query.get("platform")||void 0}))},{rawBody:!0});const c=async(e,r)=>{const a=r.flatMap(e=>e.split(",")).map(e=>e.trim()).filter(Boolean);t.saveSelection(a);const n=e.stream("application/x-tar");try{await t.saveImages(a,n)}finally{n.end()}};n("GET","/images/get","images.save",e=>c(e,e.query.getAll("names"))),n("GET",/^\/images\/(.+)\/get$/,"images.save",e=>c(e,[e.params[0]])),n("POST","/images/load","images.load",async e=>{const r=e.flag("quiet");await progressStream(e,a=>t.loadImages(e.req,e=>{r&&!e.stream?.startsWith("Loaded")||a(e)}))},{rawBody:!0}),n("GET",/^\/images\/(.+)\/history$/,"images.history",e=>{e.json(200,t.imageHistory(e.params[0]))}),n("POST","/commit","commit",async e=>{const r=e.query.get("container");if(!r)throw httpError(400,"container is required");e.json(201,await t.commit(r,{repo:e.query.get("repo")||"",tag:e.query.get("tag")||"",comment:e.query.get("comment")||"",author:e.query.get("author")||"",pause:e.flag("pause",!0),changes:e.query.getAll("changes"),config:e.body||{}}))}),n("GET",/^\/images\/(.+)\/json$/,"images.inspect",e=>{e.json(200,t.imageInspect(t.requireImage(e.params[0])))}),n("POST",/^\/images\/(.+)\/tag$/,"images.tag",e=>{const r=e.query.get("repo");if(!r)throw httpError(400,"repo is required");t.tagImage(e.params[0],r,e.query.get("tag")||"latest"),e.empty(201)}),n("POST",/^\/images\/(.+)\/push$/,"images.push",async e=>{const r=e.params[0],a=e.query.get("tag")||"";await progressStream(e,n=>t.push(r,a,n,{credentials:requestCredentials(e)}))}),n("DELETE",/^\/images\/(.+)$/,"images.remove",async e=>{e.json(200,await t.removeImage(e.params[0],{force:e.flag("force")}))}),n("POST","/build","build",async e=>{if(e.query.get("remote"))throw httpError(501,"building from a remote URL is not supported yet; send a build context");const a=(t,r)=>{try{return JSON.parse(e.query.get(t)||"null")??r}catch(e){throw httpError(400,`invalid ${t}`)}},n=Object.fromEntries(Object.entries(a("buildargs",{})).filter(([,e])=>null!==e)),s=require("fs"),o=require("path").join(r,`build-context-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.tar`);await new Promise((t,r)=>{const a=s.createWriteStream(o);e.req.pipe(a),a.on("finish",t),a.on("error",r),e.req.on("error",r)});const i=e.stream("application/json"),c=e=>i.write(JSON.stringify(e)+"\r\n");try{await t.build(o,{buildFile:e.query.get(BUILD_FILE_PARAM)||BUILD_FILE,tags:e.query.getAll("t"),buildArgs:n,labels:a("labels",{}),target:e.query.get("target")||"",noCache:e.flag("nocache"),pull:e.flag("pull"),quiet:e.flag("q"),networkMode:["","default",null].includes(e.query.get("networkmode"))?"bridge":e.query.get("networkmode"),platform:e.query.get("platform")||""},c)}catch(e){c({errorDetail:{code:e.code||1,message:e.message},error:e.message})}finally{s.rmSync(o,{force:!0}),i.end()}},{rawBody:!0}),n("POST","/grpc","buildkit.grpc",async e=>{if("0"===process.env.TNC_DOCK_BUILDKIT)throw httpError(501,"BuildKit is turned off (TNC_DOCK_BUILDKIT=0)");await t.buildkit.ready();const r=e.upgrade("h2c");r&&await t.track(()=>t.buildkit.grpc(r))}),n("POST","/session","buildkit.session",async e=>{if("0"===process.env.TNC_DOCK_BUILDKIT)throw httpError(501,"BuildKit is turned off (TNC_DOCK_BUILDKIT=0)");await t.buildkit.ready();const r=e.upgradeStream(e.req.headers.upgrade||"h2c");r&&await t.track(()=>t.buildkit.session(r,e.req.rawHeaders))}),n("GET","/containers/json","containers.list",e=>{let r=t.listContainers({all:e.flag("all"),filters:parseFilters(e)});const a=parseInt(e.query.get("limit")||"-1",10);a>0&&(r=r.slice(0,a)),e.json(200,r)}),n("POST","/containers/create","containers.create",e=>{e.json(201,t.create(e.query.get("name"),e.body,{platform:e.query.get("platform")||e.body.Platform||""}))});const l=e=>Buffer.from(JSON.stringify(e)).toString("base64");return n("HEAD","/containers/:id/archive","containers.archive.stat",async e=>{const r=await t.fs.stat(e.params[0],e.query.get("path"));e.head(200,{[PATH_STAT_HEADER]:l(r),"Content-Type":"application/json"})}),n("GET","/containers/:id/archive","containers.archive.get",async e=>{let r=null;await t.fs.getArchive(e.params[0],e.query.get("path"),t=>{r=e.stream("application/x-tar",{[PATH_STAT_HEADER]:l(t)})},{write:e=>r.write(e)}),r?.end()}),n("PUT","/containers/:id/archive","containers.archive.put",async e=>{await t.fs.putArchive(e.params[0],e.query.get("path"),e.req,{copyUIDGID:e.flag("copyUIDGID"),noOverwriteDirNonDir:e.flag("noOverwriteDirNonDir")}),e.empty(200)},{rawBody:!0}),n("GET","/containers/:id/export","containers.export",async e=>{t.findContainer(e.params[0]);const r=e.stream("application/x-tar");try{await t.fs.exportTar(e.params[0],r)}finally{r.end()}}),n("GET","/containers/:id/changes","containers.changes",async e=>{const r=await t.fs.changes(e.params[0]);e.json(200,r.length?r:null)}),n("GET","/containers/:id/stats","containers.stats",async e=>{t.findContainer(e.params[0]);const r=e.flag("stream",!0),a=e.stream("application/json");let n=!1;a.onClose(()=>{n=!0});try{await t.stats.run(e.params[0],{stream:r,oneShot:e.flag("one-shot")},e=>a.write(JSON.stringify(e)+"\n"),()=>n)}finally{a.end()}}),n("GET","/containers/:id/top","containers.top",async e=>e.json(200,await t.stats.top(e.params[0]))),n("POST","/containers/:id/pause","containers.pause",async e=>{await t.pause(e.params[0]),e.empty(204)}),n("POST","/containers/:id/unpause","containers.unpause",async e=>{await t.unpause(e.params[0]),e.empty(204)}),n("POST","/containers/:id/rename","containers.rename",e=>{t.rename(e.params[0],e.query.get("name")),e.empty(204)}),n("POST","/containers/:id/update","containers.update",async e=>{e.json(200,await t.update(e.params[0],e.body||{}))}),n("GET","/containers/:id/json","containers.inspect",e=>e.json(200,t.inspect(e.params[0]))),n("POST","/containers/:id/start","containers.start",async e=>{const r=await t.start(e.params[0]);e.empty("notModified"===r?304:204)}),n("POST","/containers/:id/stop","containers.stop",async e=>{const r=e.query.get("t")??e.query.get("timeout"),a=await t.stop(e.params[0],null===r?void 0:parseInt(r,10));e.empty("notModified"===a?304:204)}),n("POST","/containers/:id/restart","containers.restart",async e=>{const r=e.query.get("t");await t.restart(e.params[0],null===r?void 0:parseInt(r,10)),e.empty(204)}),n("POST","/containers/:id/kill","containers.kill",async e=>{await t.kill(e.params[0],e.query.get("signal")||"SIGKILL"),e.empty(204)}),n("POST","/containers/:id/wait","containers.wait",async e=>{const r=t.wait(e.params[0],e.query.get("condition")||"not-running"),a=e.stream("application/json"),n=await r;a.write(JSON.stringify(n)+"\n"),a.end()}),n("GET","/containers/:id/probe","containers.probe",async e=>{e.json(200,{ok:await t.probe(e.params[0],parseInt(e.query.get("port"),10))})}),n("POST","/containers/:id/resize","containers.resize",async e=>{await t.resize(e.params[0],parseInt(e.query.get("h"),10),parseInt(e.query.get("w"),10)),e.empty(200)}),n("DELETE","/containers/:id","containers.remove",async e=>{await t.remove(e.params[0],{force:e.flag("force"),volumes:e.flag("v")}),e.empty(204)}),n("GET","/containers/:id/logs","containers.logs",async r=>{const a=t.findContainer(r.params[0]),n={stdout:r.flag("stdout"),stderr:r.flag("stderr")};if(!n.stdout&&!n.stderr)throw httpError(400,"Bad parameters: you must choose at least one stream");const s={since:parseTime(r.query.get("since"),0),until:parseTime(r.query.get("until"),1/0),tail:r.query.get("tail")||"all"},o=r.flag("timestamps"),i=!!a.config.Config.Tty,c=r.stream(i?RAW_STREAM:MUX_STREAM),l=e=>{if(!n[e.stream])return;const t=formatEntry(e,{timestamps:o});c.write(i?t:muxFrame(e.stream,t))},m=t.logFile(a.id),{entries:u,offset:p}=readEntries(m,s);if(u.forEach(l),!r.flag("follow")||"running"!==a.state)return c.end();const d=follow(m,p,l,()=>{const t=e.getContainer(a.id);return!t||"running"!==t.state}),g=e=>{e.actorId!==a.id||"die"!==e.action&&"destroy"!==e.action||d.pin()};t.on("event",g),c.onClose(d.cancel);try{await d.done}finally{t.off("event",g)}c.end()}),n("POST","/containers/:id/attach","containers.attach",async r=>{const a=t.findContainer(r.params[0]);if("exited"===a.state)throw httpError(409,"You cannot attach to a stopped container, start it first");const n={stdout:r.flag("stdout"),stderr:r.flag("stderr")},s=!!a.config.Config.Tty,o=r.hijack(s?RAW_STREAM:MUX_STREAM),i=t.logFile(a.id),c=e=>{n[e.stream]&&o.write(s?formatEntry(e):muxFrame(e.stream,formatEntry(e)))};let l=0;if(r.flag("logs")){const e=readEntries(i);e.entries.forEach(c),l=e.offset}else try{l=require("fs").statSync(i).size}catch(e){l=0}if(!r.flag("stream"))return o.end();let m=!1,u=null;const p=e=>{e.actorId!==a.id||"die"!==e.action&&"destroy"!==e.action||(m=!0,u?.pin())};if(t.on("event",p),r.flag("stdin")&&a.config.Config.OpenStdin){let r=null;const n=[],s=(async()=>{for(let t=0;t<200&&!m;t++){const t=e.getContainer(a.id);if("running"===t?.state)break;await new Promise(e=>setTimeout(e,50))}r=t.openStdin(a);for(const e of n.splice(0))r.write(e)})();o.onData(e=>{r?r.write(e):n.push(e)}),o.onEnd(async()=>{await s,await r.end()})}else o.onData(()=>{});u=follow(i,l,c,()=>m,s?25:100),o.onClose(()=>u.cancel());try{await u.done}finally{t.off("event",p)}o.end()}),n("POST","/containers/:id/exec","exec.create",e=>{e.json(201,t.createExec(e.params[0],e.body))}),n("POST","/exec/:id/start","exec.start",async e=>{const r=t.getExec(e.params[0]),a=await t.startExec(e.params[0]);if(e.body.Detach)return a.end(),a.stdout.resume(),a.stderr.resume(),e.empty(200);const n=!!r.config.Tty,s=e.hijack(n?RAW_STREAM:MUX_STREAM);a.stdout.on("data",e=>{!1!==r.config.AttachStdout&&s.write(n?e:muxFrame("stdout",e))}),a.stderr.on("data",e=>{!1!==r.config.AttachStderr&&s.write(n?e:muxFrame("stderr",e))}),r.config.AttachStdin?(s.onData(e=>a.write(e)),s.onEnd(()=>a.end())):a.end(),s.onClose(()=>{r.Running&&a.kill("SIGKILL")}),await a.exited,await new Promise(e=>setImmediate(e)),s.end()}),n("GET","/exec/:id/json","exec.inspect",e=>e.json(200,t.execInspect(e.params[0]))),n("POST","/exec/:id/resize","exec.resize",async e=>{await t.resizeExec(e.params[0],parseInt(e.query.get("h"),10),parseInt(e.query.get("w"),10)),e.empty(200)}),n("GET","/volumes","volumes.list",r=>{const a=parseFilters(r);let n=e.listVolumes();if(a.name?.length&&(n=n.filter(e=>a.name.some(t=>e.name.includes(t)))),a.label?.length&&(n=n.filter(e=>a.label.every(t=>labelMatch(e.labels,t)))),a.dangling?.length){const e="true"===a.dangling[0]||"1"===a.dangling[0];n=n.filter(r=>0===t.volumes.usedBy(r.name).length===e)}r.json(200,{Volumes:n.map(e=>t.volumes.toApi(e)),Warnings:[]})}),n("POST","/volumes/create","volumes.create",e=>{if(e.body.Driver&&"local"!==e.body.Driver)throw httpError(400,`volume driver "${e.body.Driver}" is not supported; only local`);e.json(201,t.volumes.toApi(t.volumes.create(e.body.Name||null,e.body.Labels||{})))}),n("GET","/volumes/:name","volumes.inspect",r=>{const a=e.getVolume(r.params[0]);if(!a)throw httpError(404,`get ${r.params[0]}: no such volume`);r.json(200,t.volumes.toApi(a))}),n("DELETE","/volumes/:name","volumes.remove",e=>{t.volumes.remove(e.params[0],e.flag("force")),e.empty(204)}),n("GET","/networks","networks.list",e=>{t.host;const r=parseFilters(e);e.json(200,t.network.list().filter(e=>((e,t)=>{const r=(e,r)=>!t[e]?.length||t[e].some(r);return r("name",t=>e.name.includes(t))&&r("id",t=>e.id.startsWith(t))&&r("driver",t=>e.driver===t)&&r("type",t=>"builtin"===t==!!e.builtin)&&(t.label||[]).every(t=>labelMatch(e.labels,t))})(e,r)).map(e=>t.networkInspect(e)))}),n("POST","/networks/create","networks.create",e=>{t.host;const r=t.network.create(e.body);t.emitEvent("network","create",r.id,{name:r.name,type:r.driver}),e.json(201,{Id:r.id,Warning:""})}),n("POST","/networks/prune","networks.prune",async r=>{t.host;const a=[];for(const r of e.listNetworks())e.listContainers().some(e=>(e.config.networks||[]).some(e=>e.name===r.name))||(await t.removeNetwork(r.id),a.push(r.name));r.json(200,{NetworksDeleted:a})}),n("GET","/networks/:id","networks.inspect",e=>{t.host,e.json(200,t.networkInspect(t.network.resolve(e.params[0])))}),n("DELETE","/networks/:id","networks.remove",async e=>{await t.removeNetwork(e.params[0]),e.empty(204)}),n("POST","/networks/:id/connect","networks.connect",async e=>{await t.connectNetwork(e.params[0],e.body.Container,e.body.EndpointConfig||{}),e.empty(200)}),n("POST","/networks/:id/disconnect","networks.disconnect",async e=>{await t.disconnectNetwork(e.params[0],e.body.Container),e.empty(200)}),a}module.exports={buildRoutes,toApiEvent,parseTime};
@@ -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 http=require("http"),{DOCK_VERSION,API_VERSION,MIN_API_VERSION}=require("../version"),{buildRoutes}=require("./routes"),{MUX_STREAM}=require("./wire"),VERSION_PREFIX=/^\/v(\d+\.\d+)(\/.*)$/,MAX_BODY=16777216,SECRET_PARAMS=new Set(["buildargs"]);function redactSearch(e){if(![...e.keys()].length)return"";const t=new URLSearchParams;for(const[s,r]of e)t.append(s,SECRET_PARAMS.has(s)?"***":r);return`?${t}`}function compareVersions(e,t){const[s,r]=e.split(".").map(Number),[n,o]=t.split(".").map(Number);return s!==n?s-n:r-o}function baseHeaders(e={}){return{"Api-Version":API_VERSION,Ostype:"linux",Server:`tnc-dock/${DOCK_VERSION} (${process.platform})`,...e}}function readBody(e,t=Buffer.alloc(0)){return new Promise((s,r)=>{const n=[t];let o=t.length;e.on("data",t=>{if(o+=t.length,o>16777216)return r(Object.assign(new Error("request body too large"),{statusCode:413})),void e.destroy();n.push(t)}),e.on("end",()=>s(Buffer.concat(n))),e.on("error",r)})}function parseJson(e){if(!e||0===e.length)return{};try{return JSON.parse(e.toString("utf8"))}catch(e){throw Object.assign(new Error("invalid JSON in request body"),{statusCode:400})}}class Context{constructor({req:e,res:t,socket:s,url:r,params:n,body:o,stdinPrefix:i}){this.req=e,this.res=t,this.socket=s,this.url=r,this.query=r.searchParams,this.params=n,this.body=o,this.stdinPrefix=i||Buffer.alloc(0),this.done=!1}flag(e,t=!1){const s=this.query.get(e);return null===s?t:"1"===s||"true"===s||"True"===s}_rawHead(e,t){const s=[`HTTP/1.1 ${e} ${101===e?"UPGRADED":http.STATUS_CODES[e]}`];for(const[e,r]of Object.entries(baseHeaders(t)))s.push(`${e}: ${r}`);this.socket.write(s.join("\r\n")+"\r\n\r\n")}json(e,t){if(this.done)return;this.done=!0;const s=Buffer.from(JSON.stringify(t));if(this.socket)return this._rawHead(e,{"Content-Type":"application/json","Content-Length":s.length,Connection:"close"}),void this.socket.end(s);this.res.writeHead(e,baseHeaders({"Content-Type":"application/json","Content-Length":s.length})),this.res.end(s)}empty(e=204){if(!this.done){if(this.done=!0,this.socket)return this._rawHead(e,{"Content-Length":0,Connection:"close"}),void this.socket.end();this.res.writeHead(e,baseHeaders({"Content-Length":0})),this.res.end()}}text(e,t,s="text/plain; charset=utf-8",r={}){this.done=!0,this.res.writeHead(e,baseHeaders({"Content-Type":s,...r})),this.res.end("HEAD"===this.req.method?void 0:t)}_guard(e){let t=!1;e.on("error",()=>{t=!0}),e.on("close",()=>{t=!0});const s=()=>!t&&!e.destroyed&&!e.writableEnded;return{write:t=>!!s()&&e.write(t),drained:()=>new Promise(t=>{if(!s()||!e.writableNeedDrain)return t();const r=()=>{e.off("drain",r),e.off("close",r),t()};e.once("drain",r),e.once("close",r)}),closed:()=>!s(),end:()=>{s()&&e.end()},onClose:s=>{t||e.destroyed?setImmediate(s):e.on("close",s)}}}stream(e,t={}){return this.done=!0,this.socket?(this._rawHead(200,{"Content-Type":e,Connection:"close",...t}),this._guard(this.socket)):(this.res.writeHead(200,baseHeaders({"Content-Type":e,...t})),this.res.flushHeaders(),this._guard(this.res))}head(e,t={}){this.done=!0,this.res.writeHead(e,baseHeaders(t)),this.res.end()}upgrade(e){return this.socket?(this.done=!0,this._rawHead(101,{Connection:"Upgrade",Upgrade:e}),this.stdinPrefix.length&&this.socket.unshift(this.stdinPrefix),this.socket):(this.json(400,{message:`this endpoint needs a connection upgrade to ${e}`}),null)}upgradeStream(e){const t=this.upgrade(e);return t?{...this._guard(t),onData:e=>{t.on("data",e),t.resume()},onEnd:e=>t.on("end",e)}:null}hijack(e=MUX_STREAM){if(this.done=!0,this.socket){this._rawHead(101,{"Content-Type":e,Connection:"Upgrade",Upgrade:"tcp"});const t=this.socket,s=this.stdinPrefix;return{...this._guard(t),onData:e=>{s.length&&e(s),t.on("data",e),t.resume()},onEnd:e=>t.on("end",e)}}return this.res.writeHead(200,baseHeaders({"Content-Type":e})),this.res.flushHeaders(),{...this._guard(this.res),onData:()=>{},onEnd:e=>e()}}}class ApiServer{constructor({store:e,engine:t,socketPath:s,home:r,log:n=()=>{}}){this.store=e,this.engine=t,this.socketPath=s,this.home=r,this.log=n,this.server=null,this.upgraded=new Set,this.routes=buildRoutes({store:e,engine:t,home:r})}start(){return new Promise((e,t)=>{this.server=http.createServer((e,t)=>this._handle(e,t,null,null)),this.server.on("upgrade",(e,t,s)=>this._handle(e,null,t,s)),this.server.once("error",t),this.server.listen(this.socketPath,()=>{this.server.removeListener("error",t),e()})})}stop(){return new Promise(e=>{if(!this.server)return e();this.server.close(()=>e()),this.server.closeAllConnections?.();for(const e of this.upgraded)e.destroy()})}_match(e,t){for(const s of this.routes){if(s.method!==e&&("GET"!==s.method||"HEAD"!==e||!s.allowHead))continue;const r=s.re.exec(t);if(r)return{route:s,params:r.slice(1).map(decodeURIComponent)}}return null}async _handle(e,t,s,r){s&&(s.on("error",t=>this.log(`client socket ${e.method} ${e.url}: ${t.code||t.message}`)),this.upgraded.add(s),s.on("close",()=>this.upgraded.delete(s)));const n=new URL(e.url,"http://dock");let o=n.pathname;const i=(r,o)=>new Context({req:e,res:t,socket:s,url:n,params:[]}).json(r,{message:o}),a=VERSION_PREFIX.exec(o);if(a){if(compareVersions(a[1],API_VERSION)>0)return i(400,`client version ${a[1]} is too new. Maximum supported API version is ${API_VERSION}`);if(compareVersions(a[1],MIN_API_VERSION)<0)return i(400,`client version ${a[1]} is too old. Minimum supported API version is ${MIN_API_VERSION}, please upgrade your client to a newer version`);o=a[2]}"/_ping"!==o&&this.engine?.touch?.();const d=this._match(e.method,o);if(this.log(`${e.method} ${n.pathname}${redactSearch(n.searchParams)} -> ${d?d.route.name:"501"}`),!d)return e.resume(),i(501,`dock does not implement ${e.method} ${o} yet`);let h;try{let o={},i=Buffer.alloc(0);if(s){const t=parseInt(e.headers["content-length"]||"0",10);let n=r||Buffer.alloc(0);for(;n.length<t;){const e=await new Promise(e=>s.once("data",e));n=Buffer.concat([n,e])}s.pause(),o=parseJson(n.subarray(0,t)),i=n.subarray(t)}else d.route.rawBody||(o=parseJson(await readBody(e)));h=new Context({req:e,res:t,socket:s,url:n,params:d.params,body:o,stdinPrefix:i}),await d.route.handler(h)}catch(r){const o=r.statusCode||500;o>=500&&this.log(`error ${e.method} ${n.pathname}: ${r.stack||r.message}`),h=h||new Context({req:e,res:t,socket:s,url:n,params:[]}),h.done||h.json(o,{message:r.message})}}}module.exports=ApiServer;
@@ -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";module.exports={DEFAULT_REGISTRY:"https://index.docker.io/v1/",DEFAULT_REGISTRY_HOST:"docker.io",DEFAULT_REGISTRY_API:"registry-1.docker.io",MANIFEST_LIST_V2:"application/vnd.docker.distribution.manifest.list.v2+json",MANIFEST_V2:"application/vnd.docker.distribution.manifest.v2+json",LAYER_V2_GZIP:"application/vnd.docker.image.rootfs.diff.tar.gzip",RAW_STREAM:"application/vnd.docker.raw-stream",MUX_STREAM:"application/vnd.docker.multiplexed-stream",ROOT_DIR_FIELD:"DockerRootDir",REGISTRY_AUTH_HEADER:"x-registry-auth",PATH_STAT_HEADER:"X-Docker-Container-Path-Stat",SESSION_HEADER_PREFIX:"x-docker-expose-session-",BUILDKIT_GRPC_PACKAGE:"moby.buildkit.v1",ENGINE_EXPORTER:"moby",BUILDKIT_RELEASES:"https://github.com/moby/buildkit/releases/download",BUILD_FILE:"Dockerfile",BUILD_IGNORE_FILE:".dockerignore",BUILD_FILE_PARAM:"dockerfile",COMPOSE_LEGACY_FILES:["docker-compose.yaml","docker-compose.yml"],HOST_ALIAS:"host.docker.internal",ENGINE_SOCKET:"/var/run/docker.sock",ENGINE_SOCKETS:["/var/run/docker.sock","/run/docker.sock"],ENV_MARKER:"/.dockerenv",INDEX_HOST:"index.docker.io",ENGINE_CLI:"docker",CONTEXT_ENDPOINT_FLAG:"--docker",ENGINE_HOST_VAR:"DOCKER_HOST",TESTCONTAINERS_HOST_KEY:"docker.host",ENGINE_CLI_IMAGE:"docker:27-cli",LEGACY_PREVIOUS_CONTEXT_FILE:"previous-docker-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"),{spawnSync}=require("child_process"),VZ_XPC="com.apple.Virtualization.VirtualMachine",VZ_ATTRIBUTION_WINDOW_MS=15e3,UNIT={B:1,KB:1024,MB:1048576,GB:1024**3},DAEMON_SCRIPTS=/[/\\](dock[/\\]daemon-main|port-proxy-daemon|tunnel-daemon)\.js\b/;function parseFootprint(s){const e=/phys_footprint:\s*([\d.]+)\s*(B|KB|MB|GB)\b/.exec(s)||/Footprint:\s*([\d.]+)\s*(B|KB|MB|GB)\b/.exec(s);return e?Math.round(parseFloat(e[1])*UNIT[e[2]]):null}function parseKbField(s,e){const t=new RegExp(`^${e}:\\s*(\\d+)\\s*kB`,"m").exec(s);return t?1024*parseInt(t[1],10):null}function parsePsLine(s){const e=s.trim().split(/\s+/);if(e.length<8)return null;const t=parseInt(e[0],10),r=parseInt(e[1],10),n=Date.parse(e.slice(2,7).join(" "));return!Number.isInteger(t)||Number.isNaN(n)?null:{pid:t,ppid:r,started:n,args:e.slice(7).join(" ")}}function listProcesses(){const s=spawnSync("ps",["-axo","pid=,ppid=,lstart=,args="],{encoding:"utf8",maxBuffer:33554432});if(0!==s.status)throw new Error(`ps failed: ${s.stderr}`);return s.stdout.split("\n").map(parsePsLine).filter(Boolean)}function rssBytes(s){const e=spawnSync("ps",["-o","rss=","-p",String(s)],{encoding:"utf8"}),t=parseInt((e.stdout||"").trim(),10);return Number.isInteger(t)?1024*t:null}function memoryOf(s){if("darwin"===process.platform){const e=spawnSync("footprint",[String(s)],{encoding:"utf8"}),t=0===e.status?parseFootprint(e.stdout):null;if(null!==t)return{bytes:t,method:"phys_footprint"}}else if("linux"===process.platform){try{const e=parseKbField(fs.readFileSync(`/proc/${s}/smaps_rollup`,"utf8"),"Pss");if(null!==e)return{bytes:e,method:"pss"}}catch(s){}try{const e=parseKbField(fs.readFileSync(`/proc/${s}/status`,"utf8"),"VmRSS");if(null!==e)return{bytes:e,method:"rss"}}catch(s){}}const e=rssBytes(s);return null===e?null:{bytes:e,method:"rss"}}function ancestorsOf(s,e){const t=new Set;let r=e.get(s);for(;r&&!t.has(r.pid);)t.add(r.pid),r=e.get(r.ppid);return t}function findOurProcesses(s=listProcesses()){const e=new Map(s.map(s=>[s.pid,s])),t=ancestorsOf(process.pid,e),r=[],n=s.filter(s=>/[/\\]\.tnc[/\\]bin[/\\]tnc-vz\s+start\b/.test(s.args));for(const s of n)r.push({...s,role:"vm-launcher"});const o=s.filter(s=>s.args.includes(VZ_XPC));for(const s of n){const e=o.filter(e=>e.started>=s.started-1e3&&e.started-s.started<=15e3).sort((e,t)=>Math.abs(e.started-s.started)-Math.abs(t.started-s.started))[0];e&&!r.some(s=>s.pid===e.pid)&&r.push({...e,role:"vm"})}for(const e of s)t.has(e.pid)||r.some(s=>s.pid===e.pid)||DAEMON_SCRIPTS.test(e.args)&&r.push({...e,role:"daemon"});return r}function sample(){const s=findOurProcesses().map(s=>({pid:s.pid,role:s.role,args:s.args,...memoryOf(s.pid)||{bytes:0,method:"gone"}}));return{time:Date.now(),totalBytes:s.reduce((s,e)=>s+e.bytes,0),processes:s}}module.exports={parseFootprint,parseKbField,parsePsLine,findOurProcesses,memoryOf,sample,listProcesses};
@@ -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"),net=require("net"),path=require("path"),crypto=require("crypto"),{spawn,spawnSync}=require("child_process"),{sample,findOurProcesses}=require("./memory"),TNC_DIR=path.join(os.homedir(),".tnc"),TNC_VZ_BIN=path.join(TNC_DIR,"bin","tnc-vz"),VM_SOCK=path.join(TNC_DIR,"vmctl.sock"),BENCH_DIR=path.join(TNC_DIR,"bench"),sleep=e=>new Promise(t=>setTimeout(t,e));function sha256Prefix(e){try{return crypto.createHash("sha256").update(fs.readFileSync(e)).digest("hex").slice(0,16)}catch(e){return null}}function cmdOut(e,t){const n=spawnSync(e,t,{encoding:"utf8"});return 0===n.status?n.stdout.trim():null}function machineInfo(){const e={platform:process.platform,arch:process.arch,osRelease:os.release(),cpuModel:os.cpus()[0]?.model||null,ncpu:os.cpus().length,totalMem:os.totalmem(),node:process.version};return"darwin"===process.platform&&(e.macos=cmdOut("sw_vers",["-productVersion"]),e.model=cmdOut("sysctl",["-n","hw.model"]),e.vmArtifacts={tncVz:sha256Prefix(TNC_VZ_BIN),kernel:sha256Prefix(path.join(TNC_DIR,"kernel","vmlinuz")),initramfs:sha256Prefix(path.join(TNC_DIR,"kernel","initramfs.img"))}),e}function stats(e){if(0===e.length)return{min:0,median:0,p95:0,max:0};const t=[...e].sort((e,t)=>e-t),n=e=>t[Math.min(t.length-1,Math.floor(e*(t.length-1)+.5))];return{min:t[0],median:n(.5),p95:n(.95),max:t[t.length-1]}}function pingVM(e=1500){return new Promise(t=>{const n=net.createConnection(VM_SOCK,()=>n.write(JSON.stringify({cmd:"ping"})+"\n")),s=setTimeout(()=>{n.destroy(),t(!1)},e);n.on("data",()=>{clearTimeout(s),n.destroy(),t(!0)}),n.on("error",()=>{clearTimeout(s),t(!1)})})}async function collect({duration:e,interval:t,onSample:n}){const s=[],r=Date.now()+1e3*e;do{const e=sample();if(s.push(e),n?.(e,s.length),Date.now()+1e3*t>r)break;await sleep(1e3*t)}while(Date.now()<r);return s}function summarize(e){const t={};for(const n of e){const e={};for(const t of n.processes)e[t.role]=(e[t.role]||0)+t.bytes;for(const[n,s]of Object.entries(e))(t[n]||=[]).push(s)}const n=new Set(e.flatMap(e=>e.processes.map(e=>e.method)));return{samples:e.length,total:stats(e.map(e=>e.totalBytes)),byRole:Object.fromEntries(Object.entries(t).map(([e,t])=>[e,stats(t)])),methods:[...n],processes:e.at(-1)?.processes.map(({pid:e,role:t,args:n,method:s})=>({pid:e,role:t,method:s,args:n}))||[]}}async function bootVM({cpus:e,mem:t,log:n}){if(!fs.existsSync(TNC_VZ_BIN))throw new Error("tnc-vz is not installed. Run: thinknsh vm setup");const s=path.join(BENCH_DIR,"workspace");fs.mkdirSync(s,{recursive:!0});const r=path.join(BENCH_DIR,"vm-boot.log"),o=fs.openSync(r,"w"),a=Date.now(),i=spawn(TNC_VZ_BIN,["start","--cpus",String(e),"--mem",String(t),"--workspace",s],{detached:!0,stdio:["ignore",o,o]});i.unref(),fs.closeSync(o);const c=a+3e4;for(;Date.now()<c;){if(await pingVM())return{pid:i.pid,bootMs:Date.now()-a,logFile:r};if(null!==i.exitCode)break;await sleep(200)}throw new Error(`MicroVM did not become ready within 30s (see ${r})`)}async function stopVM(e){spawnSync(TNC_VZ_BIN,["stop"],{stdio:"ignore",timeout:15e3});for(let t=0;t<50;t++){try{process.kill(e,0)}catch(e){return}await sleep(100)}try{process.kill(e,"SIGKILL")}catch(e){}}async function runBench({scenario:e="idle",duration:t=30,interval:n=2,settle:s=10,mem:r=512,cpus:o=2,log:a=()=>{},onSample:i}={}){if("win32"===process.platform)throw new Error("thinknsh bench does not support Windows yet (WSL2 measurement lands with the Windows VM backend)");const c={tool:"thinknsh bench",version:1,scenario:e,startedAt:(new Date).toISOString(),machine:machineInfo(),params:{duration:t,interval:n}};if("stopped"===e){const e=findOurProcesses();return c.params={},c.result={processes:e.map(({pid:e,role:t,args:n})=>({pid:e,role:t,args:n})),totalBytes:0===e.length?0:null,satisfied:0===e.length},c}if("current"===e)return c.result=summarize(await collect({duration:t,interval:n,onSample:i})),c;if("containers"===e)return c.result=await benchContainers({duration:t,interval:n,settle:s,onSample:i,log:a}),c.params={...c.params,settle:s,containers:STACK.map(e=>e.Image)},c;if("idle"!==e)throw new Error(`unknown scenario "${e}" (use idle, containers, current or stopped)`);if("darwin"!==process.platform)return c.result=summarize(await collect({duration:t,interval:n,onSample:i})),c.note="Linux runs containers natively; no VM is involved.",c;if(findOurProcesses().some(e=>"vm"===e.role||"vm-launcher"===e.role))throw new Error('A MicroVM is already running, so "idle" cannot be measured cleanly. Stop it (thinknsh vm stop) or use: thinknsh bench current');c.params={...c.params,settle:s,vmMemMB:r,vmCpus:o},a(`booting MicroVM (${o} CPUs, ${r} MB)...`);const l=await bootVM({cpus:o,mem:r,log:a});c.bootMs=l.bootMs;try{a(`ready in ${l.bootMs} ms; settling ${s}s before sampling...`),await sleep(1e3*s),c.result=summarize(await collect({duration:t,interval:n,onSample:i}))}finally{a("stopping MicroVM..."),await stopVM(l.pid)}return c}const STACK=[{name:"bench-postgres",Image:"postgres:16-alpine",Env:["POSTGRES_PASSWORD=bench"]},{name:"bench-redis",Image:"redis:7-alpine"}];async function benchContainers({duration:e,interval:t,settle:n,onSample:s,log:r}){const{dockPaths:o}=require("../paths"),{request:a,ping:i}=require("../client"),c=o().socket;if(!await i(c))throw new Error("dock is not running. Start it first: thinknsh dock start");const l=async(e,t,n)=>{const s=await a(c,e,t,{body:n,timeout:3e5});if(s.status>=400)throw new Error(`${e} ${t}: ${s.status} ${JSON.stringify(s.body)}`);return s.body};try{for(const e of STACK){await a(c,"DELETE",`/containers/${e.name}?force=1`).catch(()=>{});const[t,n]=e.Image.split(":");r(`pulling ${e.Image} (if needed)`),await l("POST",`/images/create?fromImage=${t}&tag=${n}`);const{Id:s}=await l("POST",`/containers/create?name=${e.name}`,{Image:e.Image,Env:e.Env});await l("POST",`/containers/${s}/start`)}return r(`containers up; settling ${n}s before sampling...`),await sleep(1e3*n),summarize(await collect({duration:e,interval:t,onSample:s}))}finally{for(const e of STACK)await a(c,"DELETE",`/containers/${e.name}?force=1`,{timeout:3e4}).catch(()=>{})}}function saveReport(e){fs.mkdirSync(BENCH_DIR,{recursive:!0});const t=path.join(BENCH_DIR,`${e.scenario}-${e.startedAt.replace(/[:.]/g,"-")}.json`);return fs.writeFileSync(t,JSON.stringify(e,null,2)),t}module.exports={runBench,saveReport,stats,summarize,machineInfo};
@@ -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"),{parseBuildFile,expand,splitWords,parseKeyValues,parseDuration,buildError,HEREDOC}=require("./buildfile"),{parsePlatform,platformString}=require("../runtime/emulation"),{buildSpec,resolveUser}=require("../runtime/spec"),{parseReference}=require("../image/reference"),{IO_CGROUP}=require("../runtime/rootfs"),LAYER_MEDIA="application/vnd.oci.image.layer.v1.tar+gzip",DEFAULT_SHELL=["/bin/sh","-c"],GO_ARCH={x64:"amd64",arm64:"arm64"},sha256=t=>crypto.createHash("sha256").update(t).digest("hex"),short=t=>t.replace(/^sha256:/,"").slice(0,12),COMMIT=`\nset -e\nupper="$1"; out="$2"; shim="$3"; DOCK_IO_CGROUP="$4"\n${IO_CGROUP}\n"$shim" oci-whiteouts "$upper"\ncd "$upper"\nif [ -z "$(ls -A)" ]; then echo EMPTY; exit 0; fi\ntar -cf - . | gzip -n > "$out"\nd=$(sha256sum "$out" | cut -d' ' -f1)\ni=$(gzip -dc "$out" | sha256sum | cut -d' ' -f1)\ns=$(stat -c %s "$out")\necho "$d $i $s"\n`,COPY=`\nset -e\nbase="$1"; tmp="$2"; merged="$3"; dest="$4"; forcedir="$5"; owner="$6"; mode="$7"; extract="$8"; DOCK_IO_CGROUP="$9"; shift 9\n${IO_CGROUP}\nrm -rf "$tmp"; mkdir -p "$tmp"\nlist="$tmp.list"; : > "$list"\nfor pat; do\n found=0\n for src in "$base"/$pat; do\n if [ -e "$src" ] || [ -L "$src" ]; then printf '%s\\n' "$src" >> "$list"; found=1; fi\n done\n if [ $found = 0 ]; then echo "file not found in build context or excluded by the ignore file: $pat" >&2; exit 2; fi\ndone\ncount=$(wc -l < "$list")\ndirmode=$forcedir\n[ "$count" -gt 1 ] && dirmode=1\n[ -d "$merged$dest" ] && dirmode=1\nx="$tmp/x"\nwhile IFS= read -r src; do\n if [ -d "$src" ] && [ ! -L "$src" ]; then\n mkdir -p "$x"; cp -a "$src/." "$x/"\n elif [ "$extract" = 1 ] && case "$src" in *.tar|*.tar.gz|*.tgz|*.tar.bz2|*.tar.xz) true;; *) false;; esac; then\n mkdir -p "$x"; tar -xf "$src" -C "$x"\n elif [ "$dirmode" = 1 ]; then\n mkdir -p "$x"; cp -a "$src" "$x/"\n else\n cp -a "$src" "$x"\n fi\ndone < "$list"\n[ -n "$owner" ] && chown -R "$owner" "$x"\n[ -n "$mode" ] && chmod -R "$mode" "$x"\nmkdir -p "$merged$(dirname "$dest")"\nif [ -d "$x" ] && [ -d "$merged$dest" ]; then cp -a "$x/." "$merged$dest/"; else rm -rf "$merged$dest"; cp -a "$x" "$merged$dest"; fi\nrm -rf "$tmp" "$list"\n`;function globToRegex(t){let e="";for(const s of t)e+="*"===s?"[^/]*":"?"===s?"[^/]":s.replace(/[.+^${}()|[\]\\]/g,"\\$&");return new RegExp(`^${e}$`)}function matchContext(t,e){const s=e.split("/").filter(t=>t&&"."!==t);let i=[t];for(const t of s){const e=[],s=/[*?]/.test(t)?globToRegex(t):null;for(const r of i){if(!s){const s=path.join(r,t);(fs.existsSync(s)||isLink(s))&&e.push(s);continue}let i=[];try{i=fs.readdirSync(r)}catch(t){}for(const t of i)s.test(t)&&e.push(path.join(r,t))}i=e}return s.length?i:[t]}function isLink(t){try{return fs.lstatSync(t).isSymbolicLink()}catch(t){return!1}}function hashPaths(t,e){const s=crypto.createHash("sha256"),i=e=>{const r=fs.lstatSync(e);if(s.update(`${path.relative(t,e)}\0${r.mode}\0`),r.isSymbolicLink())s.update(fs.readlinkSync(e));else if(r.isDirectory())for(const t of fs.readdirSync(e).sort())i(path.join(e,t));else s.update(fs.readFileSync(e))};for(const t of e.sort())i(t);return s.digest("hex")}function parseMount(t,e){const s={type:"bind"};for(const e of String(t).split(",")){if(!e)continue;const t=e.indexOf("="),i=(-1===t?e:e.slice(0,t)).toLowerCase(),r=-1===t?"true":e.slice(t+1);s[{dst:"target",destination:"target",src:"source",ro:"readonly","read-only":"readonly",readwrite:"rw"}[i]||i]=r}if(!["cache","secret","bind","tmpfs","ssh"].includes(s.type))throw buildError(`line ${e}: RUN --mount type "${s.type}" is not supported (cache, secret, bind, tmpfs)`);if("ssh"===s.type)throw buildError(`line ${e}: RUN --mount=type=ssh is not supported yet; use --mount=type=secret for keys or tokens`);if("secret"===s.type){if(!s.id&&!s.target)throw buildError(`line ${e}: RUN --mount=type=secret needs id=…`);s.id=s.id||path.posix.basename(s.target),s.target||s.env||(s.target=`/run/secrets/${s.id}`)}else if(!s.target)throw buildError(`line ${e}: RUN --mount=type=${s.type} needs target=…`);if(s.target&&!s.target.startsWith("/"))throw buildError(`line ${e}: mount target must be absolute: ${s.target}`);return s}const GO_ARCH_NAME=t=>GO_ARCH[t]||t;class Builder{constructor(t,{contextDir:e,buildDir:s,out:i,quiet:r=!1,noCache:n=!1,pull:o=!1,buildArgs:a={},target:c="",labels:h={},networkMode:l="bridge",platform:d=null,secretsDir:u=null}){this.engine=t,this.store=t.store,this.host=t.host,this.contextDir=e,this.buildDir=s,this.out=i,this.quiet=r,this.noCache=n,this.pull=o,this.buildArgs=a,this.usedArgs=new Set,this.target=c,this.labels=h,this.networkMode=l,this.bid=crypto.randomBytes(6).toString("hex"),this.mounted=new Set,this.step=0,this.secretsDir=u;const f={os:"linux",arch:GO_ARCH_NAME(process.arch)};this.buildPlatform=f,this.targetPlatform=d||f;const p=(t,e)=>({[`${t}PLATFORM`]:platformString(e),[`${t}OS`]:e.os||"linux",[`${t}ARCH`]:e.arch,[`${t}VARIANT`]:e.variant||""});this.autoArgs={...p("TARGET",this.targetPlatform),...p("BUILD",f)}}say(t){this.quiet||this.out({stream:t.endsWith("\n")?t:t+"\n"})}async run(t){const e=fs.readFileSync(t,"utf8"),{instructions:s}=parseBuildFile(e);if(!s.length)throw buildError("the build file cannot be empty");const i={},r=[];for(const t of s)if("FROM"===t.cmd){const e=splitWords(t.args),s=e.findIndex(t=>"as"===t.toLowerCase());r.push({index:r.length,name:-1!==s?e[s+1].toLowerCase():null,baseRaw:e[0],from:t,instructions:[]})}else if(r.length)r[r.length-1].instructions.push(t);else{if("ARG"!==t.cmd)throw buildError(`line ${t.line}: ${t.cmd} must come after FROM (only ARG may come before)`);for(const[e,s]of parseKeyValues(t.args,"ARG"))i[e]=this.buildArgs[e]??s??this.autoArgs[e]??"",e in this.buildArgs&&this.usedArgs.add(e)}this.globalArgs=i;const n={...this.autoArgs,...i};for(const t of r)t.platform=t.from.flags.platform?parsePlatform(expand(t.from.flags.platform,n)):this.targetPlatform;const o=this.target?r.find(t=>t.name===this.target.toLowerCase()):r[r.length-1];if(!o)throw buildError(`failed to reach build target ${this.target} in the build file`);const a=new Set,c=t=>{if(a.has(t.index))return;a.add(t.index);const e=this._stageRef(r,expand(t.baseRaw,{...this.autoArgs,...i}),t.index);e&&c(e);for(const e of t.instructions)if(("COPY"===e.cmd||"ADD"===e.cmd)&&e.flags.from){const s=this._stageRef(r,e.flags.from,t.index);s&&c(s)}};c(o);const h=r.filter(t=>a.has(t.index)&&t.index<=o.index);this.total=h.reduce((t,e)=>t+1+e.instructions.length,0);try{for(const t of h)t.state=await this._buildStage(t,r,i);const t=Object.keys(this.buildArgs).filter(t=>!this.usedArgs.has(t));return t.length&&this.say(`[Warning] One or more build-args [${t.join(" ")}] were not consumed`),this._finish(o.state,o.platform)}finally{for(const t of this.mounted)await this.engine.rootfs.remove(t).catch(()=>{})}}_stageRef(t,e,s){return/^\d+$/.test(e)?t[Number(e)]:t.find(t=>t.name&&t.name===e.toLowerCase()&&t.index<s)||null}async _image(t,e){let s=this.pull?null:this.engine.findImage(t);return s&&s.architecture&&e&&s.architecture!==e&&(s=null),s||(this.say(`Pulling ${t}${e!==this.buildPlatform.arch?` (linux/${e})`:""}`),s=await this.engine.pull(t,()=>{},{platform:{os:"linux",arch:e}})),s}async _baseState(t,e,s){const i=expand(t.baseRaw,{...this.autoArgs,...s}),r=this._stageRef(e,i,t.index);if(r)return{...JSON.parse(JSON.stringify(r.state)),arch:r.platform.arch};if("scratch"===i)return{layers:[],diffIds:[],config:{Env:["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]},history:[],baseId:"scratch",arch:t.platform.arch};const n=await this._image(i,t.platform.arch);return{arch:n.architecture||t.platform.arch,layers:(n.layers||[]).map(t=>({...t})),diffIds:[...n.config?.rootfs?.diff_ids||[]],config:JSON.parse(JSON.stringify(n.config?.config||{})),history:[...n.config?.history||[]],baseId:n.id}}async _buildStage(t,e,s){this.say(`Step ${++this.step}/${this.total} : ${t.from.original}`);const i=await this._baseState(t,e,s);i.args={},i.cmdSet=!1,this.say(` ---\x3e ${short("scratch"===i.baseId?sha256("scratch"):i.baseId)}`);for(const s of t.instructions)this.say(`Step ${++this.step}/${this.total} : ${s.original}`),await this._instruction(i,s,e,t);return i}_env(t){const e={...t.args};for(const s of t.config.Env||[]){const t=s.indexOf("=");e[s.slice(0,t)]=s.slice(t+1)}return e}_setEnv(t,e,s){const i=(t.config.Env||[]).filter(t=>t.split("=")[0]!==e);i.push(`${e}=${s}`),t.config.Env=i}_shellForm(t,e){return[...t.config.Shell||DEFAULT_SHELL,e]}_chainKey(t){return sha256(JSON.stringify(t.diffIds))}_stepId(t){return short(sha256(JSON.stringify([t.diffIds,t.config])))}async _instruction(t,e,s,i){const r=this._env(t),n=t=>expand(t,r);switch(e.cmd){case"ENV":for(const[s,i]of parseKeyValues(e.args,"ENV"))this._setEnv(t,s,expand(i,this._env(t)));return this._meta(t,e);case"ARG":for(const[s,i]of parseKeyValues(e.args,"ARG"))s in this.buildArgs?(t.args[s]=this.buildArgs[s],this.usedArgs.add(s)):t.args[s]=void 0!==i?n(i):this.globalArgs[s]??this.autoArgs[s]??"";return this._meta(t,e);case"LABEL":t.config.Labels={...t.config.Labels||{}};for(const[s,i]of parseKeyValues(e.args,"LABEL"))t.config.Labels[n(s)]=n(i);return this._meta(t,e);case"MAINTAINER":return t.author=e.args,this._meta(t,e);case"EXPOSE":t.config.ExposedPorts={...t.config.ExposedPorts||{}};for(const s of splitWords(n(e.args)))t.config.ExposedPorts[s.includes("/")?s:`${s}/tcp`]={};return this._meta(t,e);case"VOLUME":t.config.Volumes={...t.config.Volumes||{}};for(const s of(e.json||splitWords(e.args)).map(n))t.config.Volumes[s]={};return this._meta(t,e);case"USER":return t.config.User=n(e.args),this._meta(t,e);case"STOPSIGNAL":return t.config.StopSignal=n(e.args),this._meta(t,e);case"SHELL":if(!e.json)throw buildError(`line ${e.line}: SHELL requires the arguments to be in JSON form`);return t.config.Shell=e.json,this._meta(t,e);case"CMD":return t.config.Cmd=e.json||this._shellForm(t,e.args),t.cmdSet=!0,this._meta(t,e);case"ENTRYPOINT":return t.config.Entrypoint=e.json||this._shellForm(t,e.args),t.cmdSet||(t.config.Cmd=null),this._meta(t,e);case"HEALTHCHECK":return t.config.Healthcheck=this._healthcheck(e),this._meta(t,e);case"ONBUILD":return t.config.OnBuild=[...t.config.OnBuild||[],e.args],this.say(" ---\x3e ONBUILD triggers are stored but not executed by dock yet"),this._meta(t,e);case"WORKDIR":{const s=path.posix.resolve(t.config.WorkingDir||"/",n(e.args));return t.config.WorkingDir=s,this._fsStep(t,e,`WORKDIR ${s}`,async()=>{})}case"RUN":return this._run(t,e,r,s,i);case"COPY":case"ADD":return this._copy(t,e,r,s,i);default:throw buildError(`line ${e.line}: unknown instruction: ${e.cmd}`)}}_healthcheck(t){const e=t.args.trim();if(/^NONE$/i.test(e))return{Test:["NONE"]};const s=/^CMD\s+(.*)$/is.exec(e);if(!s)throw buildError(`line ${t.line}: HEALTHCHECK must be NONE or CMD ...`);let i;try{const t=JSON.parse(s[1]);i=Array.isArray(t)?["CMD",...t]:null}catch(t){}const r={Test:i||["CMD-SHELL",s[1]]},n=t.flags;return n.interval&&(r.Interval=parseDuration(n.interval)),n.timeout&&(r.Timeout=parseDuration(n.timeout)),n["start-period"]&&(r.StartPeriod=parseDuration(n["start-period"])),n["start-interval"]&&(r.StartInterval=parseDuration(n["start-interval"])),n.retries&&(r.Retries=parseInt(n.retries,10)),r}_meta(t,e){t.history.push({created:(new Date).toISOString(),created_by:`/bin/sh -c #(nop) ${e.original}`,empty_layer:!0}),this.say(` ---\x3e ${this._stepId(t)}`)}async _fsStep(t,e,s,i,{cacheable:r=!0,createdBy:n}={}){const o=sha256(`${this._chainKey(t)}\n${s}`),a=r&&!this.noCache?this.store.getBuildCache(o):null;if(a&&(""===a.digest||fs.existsSync(this._blobPath(a.digest))))return this.say(" ---\x3e Using cache"),this._addLayer(t,a,n||e.original),void this.say(` ---\x3e ${this._stepId(t)}`);const c=`build-${this.bid}-${this.step}`,h=await this.engine.rootfs.prepare(c,{layers:t.layers},t.config.WorkingDir||"/");this.mounted.add(c);try{await i(h,c);const s=await this._commit(c);this.store.putBuildCache(o,s),this._addLayer(t,s,n||e.original)}finally{await this.engine.rootfs.remove(c).catch(()=>{}),this.mounted.delete(c)}this.say(` ---\x3e ${this._stepId(t)}`)}_blobPath(t){return path.join(this.engine.paths.blobs,t.replace(/^sha256:/,""))}async _commit(t){const e=path.join(this.engine.paths.blobs,`build-${t}.tmp`),{shim:s}=this.host.binaries(),i=`${this.engine.rootfs.root}/containers/${t}/upper`,r=(await this.host.sh(COMMIT,[i,this.host.guestPath(e),s,"vm"===this.host.kind?1:0])).stdout.trim().split("\n").pop();if("EMPTY"===r)return{digest:"",diffId:"",size:0};const[n,o,a]=r.split(" ");return fs.renameSync(e,this._blobPath(n)),{digest:`sha256:${n}`,diffId:`sha256:${o}`,size:Number(a)}}_addLayer(t,e,s){const i={created:(new Date).toISOString(),created_by:s};e.digest?(t.layers.push({digest:e.digest,size:e.size,mediaType:LAYER_MEDIA}),t.diffIds.push(e.diffId)):i.empty_layer=!0,t.history.push(i)}_runArgv(t,e){if(e.json)return{argv:e.json,stdin:null};if(e.heredocs?.length){if(!e.args.replace(new RegExp(HEREDOC.source,"g"),"").trim()&&1===e.heredocs.length){const s=e.heredocs[0].content,i=/^#!(.*)\n/.exec(s);return i?{argv:splitWords(i[1].trim()),stdin:s}:{argv:[...t.config.Shell||DEFAULT_SHELL,s],stdin:null}}return{argv:this._shellForm(t,e.heredocText),stdin:null}}return{argv:this._shellForm(t,e.args),stdin:null}}async _runMounts(t,e,s,i,r,n){const o=(e.mounts||[]).map(t=>parseMount(expand(t,s),e.line)),a=t=>this.host.guestPath(t),c=this.engine.rootfs.root,h=[],l=[],d=[],u=[];for(const f of o)if("cache"===f.type){const t=f.id||f.target,e=`${c}/cache-mounts/${sha256(t).slice(0,32)}`,s=`${f.uid||0}:${f.gid||0}`;await this.host.sh('[ -d "$1" ] || { mkdir -p "$1"; chown "$2" "$1"; chmod "$3" "$1"; }',[e,s,f.mode||"0755"]),h.push({destination:f.target,type:"bind",source:e,options:["rbind","rw"]}),l.push(["cache",f.target,t])}else if("tmpfs"===f.type)h.push({destination:f.target,type:"tmpfs",source:"tmpfs",options:["nosuid","nodev",...f.size?[`size=${f.size}`]:[]]}),l.push(["tmpfs",f.target]);else if("secret"===f.type){const t=this.secretsDir&&path.join(this.secretsDir,f.id);if(!t||!fs.existsSync(t)){if("true"===f.required)throw buildError(`line ${e.line}: secret ${f.id} is required; pass it with: thinknsh build --secret id=${f.id},src=FILE`);continue}if(f.env&&d.push(`${f.env}=${fs.readFileSync(t,"utf8")}`),f.target){const e=`/run/tnc-secrets/${n}`;await this.host.sh('umask 077; mkdir -p "$1"; cat > "$1/$2"; chown "$3" "$1/$2"; chmod "$4" "$1/$2"',[e,f.id,`${f.uid||0}:${f.gid||0}`,f.mode||"0400"],{input:fs.readFileSync(t)}),h.push({destination:f.target,type:"bind",source:`${e}/${f.id}`,options:["rbind","ro"]}),u.push(()=>this.host.sh('rm -rf "$1"',[e]))}l.push(["secret",f.id,f.target||"",f.env||""])}else if("bind"===f.type){const o=path.posix.normalize("/"+(f.source||"/")).replace(/\/+$/,"")||"/",d="true"===f.rw||"true"===f.readwrite;let p;if(f.from){const e=expand(f.from,s),a=this._stageRef(i,e,r.index);let c;if(a)c=a.state.layers,l.push(["bind",f.target,"stage",a.state.diffIds,o]);else{const s=await this._image(e,t.arch);c=s.layers,l.push(["bind",f.target,"image",s.id,o])}const d=`${n}-bind-${h.length}`,g=await this.engine.rootfs.prepare(d,{layers:c},"/");this.mounted.add(d),u.push(async()=>{await this.engine.rootfs.remove(d).catch(()=>{}),this.mounted.delete(d)}),p=`${g}${"/"===o?"":o}`}else{const t=o.replace(/^\//,"");if(t.startsWith(".."))throw buildError(`line ${e.line}: bind source outside the build context: ${f.source}`);l.push(["bind",f.target,"context",hashPaths(this.contextDir,matchContext(this.contextDir,t||"."))]),p=a(path.join(this.contextDir,t))}if(d){const t=`${c}/containers/${n}/bind-rw-${h.length}`;await this.host.sh('mkdir -p "$2" && cp -a "$1/." "$2/" 2>/dev/null || cp -a "$1" "$2"',[p,t]),p=t}h.push({destination:f.target,type:"bind",source:p,options:["rbind",d?"rw":"ro"]})}return{mounts:h,cacheText:l,env:d,cleanup:u}}async _run(t,e,s,i,r){const n=Object.keys(e.flags).filter(t=>!["network","mount","security"].includes(t));if(n.length)throw buildError(`line ${e.line}: RUN --${n[0]} is not supported yet`);if(e.flags.security&&!["insecure","sandbox"].includes(e.flags.security))throw buildError(`line ${e.line}: RUN --security must be insecure or sandbox`);const{argv:o,stdin:a}=this._runArgv(t,e),c=Object.entries(s).map(([t,e])=>`${t}=${e}`),h=e.flags.network||this.networkMode,l="insecure"===e.flags.security,d=(e.mounts||[]).length?(e.mounts||[]).map(t=>parseMount(expand(t,s),e.line)).map(t=>[t.type,t.target,t.id||"",t.source||"",t.from||""]):[],u=a||d.length||l||t.arch!==this.buildPlatform.arch?JSON.stringify(["RUN",o,a,c.sort(),t.config.User||"",t.config.WorkingDir||"/",d,l,t.arch]):JSON.stringify(["RUN",o,c.sort(),t.config.User||"",t.config.WorkingDir||"/"]);await this.engine.emulation.ensure(t.arch),await this._fsStep(t,e,u,async(n,d)=>{this.say(` ---\x3e Running in ${short(sha256(d))}`);const u=await this._runMounts(t,e,s,i,r,d),f=t.config.User||"",p=f&&f.split(":").some(t=>!/^\d+$/.test(t))?resolveUser(f,await this.engine.rootfs.readFile(d,"/etc/passwd"),await this.engine.rootfs.readFile(d,"/etc/group")):resolveUser(f),g=path.join(this.buildDir,`step-${this.step}`);fs.mkdirSync(g,{recursive:!0});const m=t=>this.host.guestPath(t),$="none"===h?await this.engine.network.attachAll(d,[{name:"none"}]):"host"===h?{nsPath:null,endpoints:{host:{}}}:await this.engine.network.attachAll(d,[{name:"bridge"}]);try{fs.writeFileSync(path.join(g,"hosts"),"127.0.0.1\tlocalhost\n::1\tlocalhost\n"),fs.writeFileSync(path.join(g,"resolv.conf"),await this.engine._resolvConf());const e=buildSpec({id:d,rootfs:n,args:o,env:[...c,...u.env],cwd:t.config.WorkingDir||"/",user:p,hostname:short(sha256(d)),nsPath:$.nsPath,hostConfig:l?{Privileged:!0}:{},mounts:[{destination:"/etc/hosts",type:"bind",source:m(path.join(g,"hosts")),options:["rbind","rw"]},{destination:"/etc/resolv.conf",type:"bind",source:m(path.join(g,"resolv.conf")),options:["rbind","rw"]},...u.mounts]});fs.writeFileSync(path.join(g,"config.json"),JSON.stringify(e));const{crun:s}=this.host.binaries(),i=this.host.spawn([s,"--cgroup-manager=cgroupfs","run","--bundle",m(g),d]);a&&i.write(a),i.end();const r=t=>{this.quiet||this.out({stream:t.toString("utf8")})};i.stdout.on("data",r),i.stderr.on("data",r);const h=await i.exited;if(this.say(` ---\x3e Removed intermediate container ${short(sha256(d))}`),0!==h){const t=buildError(`The command '${o.join(" ")}' returned a non-zero code: ${h}`);throw t.code=h,t}}finally{$.nsPath&&await this.engine.network.detachAll(d,$.endpoints);for(const t of u.cleanup)await t().catch(()=>{});await this._dropMountTargets(d,u.mounts)}},{createdBy:e.heredocs?.length?`RUN ${e.original.replace(/^RUN\s+/i,"")}`:`${o.join(" ")}`})}async _dropMountTargets(t,e){if(!e.length)return;const s=`${this.engine.rootfs.root}/containers/${t}/upper`;await this.host.sh('u="$1"; shift; for t; do p="$u$t"; while [ "$p" != "$u" ]; do if [ -d "$p" ] && [ ! -L "$p" ]; then rmdir "$p" 2>/dev/null || break; elif [ -f "$p" ] && [ ! -s "$p" ]; then rm -f "$p"; else break; fi; p=$(dirname "$p"); done; done; true',[s,...e.map(t=>t.destination)]).catch(()=>{})}async _copy(t,e,s,i,r){const n=t=>expand(t,s);let o;if(e.heredocs?.length&&!e.flags.from){const t=`.tnc-heredoc-${this.step}`;fs.mkdirSync(path.join(this.contextDir,t),{recursive:!0});const i=new Map(e.heredocs.map(t=>[t.name,t]));o=splitWords(e.args).map(e=>{const r=/^<<-?["']?([A-Za-z_][A-Za-z0-9_]*)["']?$/.exec(e);if(!r||!i.has(r[1]))return n(e);const o=i.get(r[1]);return fs.writeFileSync(path.join(this.contextDir,t,o.name),o.expand?expand(o.content,s):o.content),`${t}/${o.name}`})}else o=(e.json||splitWords(e.args)).map(n);if(o.length<2)throw buildError(`line ${e.line}: ${e.cmd} requires at least two arguments`);let a=o.pop();const c=a.endsWith("/")||o.length>1;a=path.posix.resolve(t.config.WorkingDir||"/",a);let h,l,d=o,u=null;if(e.flags.from){const s=n(e.flags.from),o=this._stageRef(i,s,r.index);let a;if(o)a=o.state.layers,l=sha256(JSON.stringify(o.state.diffIds)+JSON.stringify(d));else{const e=await this._image(s,t.arch);a=e.layers,l=sha256(e.id+JSON.stringify(d))}u=`build-${this.bid}-from-${this.step}`,h=await this.engine.rootfs.prepare(u,{layers:a},"/"),this.mounted.add(u),d=d.map(t=>t.replace(/^\/+/,""))}else{"ADD"===e.cmd&&(d=await Promise.all(d.map(t=>/^https?:\/\//.test(t)?this._download(t):t)));for(const t of d)if(path.posix.normalize(t.replace(/^\/+/,"")).startsWith(".."))throw buildError(`line ${e.line}: forbidden path outside the build context: ${t}`);d=d.map(t=>path.posix.normalize(t.replace(/^\/+/,"")));const t=d.flatMap(t=>matchContext(this.contextDir,t));l=hashPaths(this.contextDir,t),h=this.host.guestPath(this.contextDir)}const f="ADD"===e.cmd&&!e.flags.from,p=JSON.stringify([e.cmd,e.flags,d,a,l]);try{await this._fsStep(t,e,p,async(t,s)=>{let i=e.flags.from?"":"0:0";if(e.flags.chown){const t=resolveUser(n(e.flags.chown),await this.engine.rootfs.readFile(s,"/etc/passwd"),await this.engine.rootfs.readFile(s,"/etc/group"));i=`${t.uid}:${t.gid}`}const r=`${this.engine.rootfs.root}/containers/${s}/copy`;await this.host.sh(COPY,[h,r,t,a,c?1:0,i,e.flags.chmod||"",f?1:0,"vm"===this.host.kind?1:0,...d])})}finally{u&&(await this.engine.rootfs.remove(u).catch(()=>{}),this.mounted.delete(u))}}async _download(t){const e=await fetch(t);if(!e.ok)throw buildError(`failed to download ${t}: HTTP ${e.status}`);const s=path.posix.basename(new URL(t).pathname)||"download",i=`.dock-add-${this.step}/${s}`;return fs.mkdirSync(path.join(this.contextDir,path.dirname(i)),{recursive:!0}),fs.writeFileSync(path.join(this.contextDir,i),Buffer.from(await e.arrayBuffer())),i}_finish(t,e){const s={architecture:e?.arch||t.arch||GO_ARCH[process.arch]||process.arch,...e?.variant?{variant:e.variant}:{},os:"linux",created:(new Date).toISOString(),...t.author?{author:t.author}:{},config:{...t.config,Labels:{...t.config.Labels||{},...this.labels}},rootfs:{type:"layers",diff_ids:t.diffIds},history:t.history},i=JSON.stringify(s),r=`sha256:${sha256(i)}`;return fs.writeFileSync(this._blobPath(r),i),{id:r,repoTags:[],repoDigests:[],digest:null,size:t.layers.reduce((t,e)=>t+e.size,0),layers:t.layers,config:s,architecture:s.architecture,os:"linux",createdAt:Date.now()}}}function normalizeTag(t){return parseReference(t).familiar}module.exports={Builder,normalizeTag,matchContext,hashPaths,parseMount,COMMIT};
@@ -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";function buildError(e){const t=new Error(e);return t.statusCode=400,t}const JSON_FORMS=new Set(["RUN","CMD","ENTRYPOINT","COPY","ADD","SHELL","VOLUME"]);function parseDirectives(e){const t={escape:"\\"};for(const r of e){const e=/^#\s*([a-zA-Z]+)\s*=\s*(.+?)\s*$/.exec(r);if(!e)break;t[e[1].toLowerCase()]=e[2]}if("\\"!==t.escape&&"`"!==t.escape)throw buildError(`invalid escape token '${t.escape}'`);return t}function takeFlags(e){const t={},r=[];let n=e;for(;;){const e=/^--([a-zA-Z-]+)(?:=(\S*))?\s+/.exec(n);if(!e)break;const s=e[1].toLowerCase();t[s]=e[2]??"true","mount"===s&&r.push(e[2]??""),n=n.slice(e[0].length)}return{flags:t,mounts:r,rest:n}}const HEREDOC=/<<(-?)(["']?)([A-Za-z_][A-Za-z0-9_]*)\2/g;function readHeredocs(e,t,r,n){const s=[];let o=r;for(const r of e.matchAll(HEREDOC)){const e="-"===r[1],i=r[3],c=[];for(;;){if(o>=t.length)throw buildError(`line ${n}: heredoc <<${i} has no terminating "${i}" line`);const r=t[o++];if((e?r.replace(/^\t+/,""):r)===i)break;c.push(e?r.replace(/^\t+/,""):r)}s.push({name:i,content:c.length?c.join("\n")+"\n":"",expand:!r[2],chomp:e})}return{heredocs:s,next:o}}function parseJsonArray(e){if(!e.startsWith("["))return null;try{const t=JSON.parse(e);return Array.isArray(t)&&t.every(e=>"string"==typeof e)?t:null}catch(e){return null}}function parseBuildFile(e){const t=e.replace(/\r\n/g,"\n").split("\n"),r=parseDirectives(t),n=r.escape,s=[];let o="",i=0;for(let e=0;e<t.length;e++){let r=t[e];if(!o&&/^\s*$/.test(r))continue;if(/^\s*#/.test(r))continue;o||(i=e+1);const c=r.replace(/\s+$/,"");if(c.endsWith(n)){o+=c.slice(0,-1);continue}o+=r;const l=/^\s*(\S+)\s*(.*)$/s.exec(o);if(o="",!l)continue;const a=l[1].toUpperCase();let u=l[2].trim();const{flags:d,mounts:p=[],rest:f}=["COPY","ADD","RUN","FROM","HEALTHCHECK"].includes(a)?takeFlags(u+" "):{flags:{},rest:u};u=f.trim();let h=[],E=null;if(["RUN","COPY","ADD"].includes(a)&&!u.startsWith("[")&&new RegExp(HEREDOC.source).test(u)){const r=readHeredocs(u,t,e+1,i);h=r.heredocs,E=[u,...t.slice(e+1,r.next)].join("\n"),e=r.next-1}const g=JSON_FORMS.has(a)?parseJsonArray(u):null;s.push({cmd:a,flags:d,mounts:p,args:u,json:g,heredocs:h,heredocText:E,line:i,original:l[0].trim().replace(/\s+/g," ")})}return{directives:r,instructions:s}}function expand(e,t){let r="";for(let n=0;n<e.length;n++){const s=e[n];if("\\"===s&&"$"===e[n+1]){r+="$",n++;continue}if("$"!==s){r+=s;continue}if("{"===e[n+1]){const s=e.indexOf("}",n+2);if(-1===s)throw buildError(`missing '}' in "${e}"`);const o=e.slice(n+2,s),i=/^([A-Za-z_][A-Za-z0-9_]*)(?::([-+])(.*))?$/s.exec(o);if(!i)throw buildError(`unsupported variable expression \${${o}}`);const c=t[i[1]];"-"===i[2]?r+=c||expand(i[3],t):"+"===i[2]?r+=c?expand(i[3],t):"":r+=c??"",n=s;continue}const o=/^[A-Za-z_][A-Za-z0-9_]*/.exec(e.slice(n+1));o?(r+=t[o[0]]??"",n+=o[0].length):r+="$"}return r}function splitWords(e){const t=[];let r="",n=null,s=!1;for(let o=0;o<e.length;o++){const i=e[o];n?i===n?n=null:"\\"===i&&'"'===n&&o+1<e.length?r+=e[++o]:r+=i:'"'!==i&&"'"!==i?"\\"===i&&o+1<e.length?(r+=e[++o],s=!0):/\s/.test(i)?((s||r)&&t.push(r),r="",s=!1):(r+=i,s=!0):(n=i,s=!0)}if(n)throw buildError(`unterminated quote in "${e}"`);return(s||r)&&t.push(r),t}function parseKeyValues(e,t){const r=splitWords(e);if(r.length&&!r[0].includes("=")){if("ARG"===t)return[[r[0],void 0]];const n=e.trim().search(/\s/);if(-1===n)throw buildError(`${t} requires a value: ${e}`);return[[e.trim().slice(0,n),splitWords(e.trim().slice(n+1)).join(" ")]]}return r.map(e=>{const r=e.indexOf("=");if(-1===r){if("ARG"===t)return[e,void 0];throw buildError(`${t} names can not be blank: ${e}`)}return[e.slice(0,r),e.slice(r+1)]})}function parseDuration(e){const t=/(\d+(?:\.\d+)?)(ns|us|µs|ms|s|m|h)/g,r={ns:1,us:1e3,µs:1e3,ms:1e6,s:1e9,m:6e10,h:36e11};let n=0,s="";for(const o of e.matchAll(t))n+=parseFloat(o[1])*r[o[2]],s+=o[0];if(s!==e)throw buildError(`invalid duration "${e}"`);return Math.round(n)}module.exports={parseBuildFile,expand,splitWords,parseKeyValues,parseDuration,buildError,HEREDOC};
@@ -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"),http2=require("http2"),crypto=require("crypto"),{Duplex}=require("stream"),{spawnSync}=require("child_process"),{parseReference}=require("../image/reference"),{SESSION_HEADER_PREFIX,BUILDKIT_GRPC_PACKAGE,ENGINE_EXPORTER,BUILDKIT_RELEASES}=require("../api/wire"),VERSION="v0.33.1",RELEASES={arm64:"4e1ba91f139761f249a1fa6c71e632dfdbdafeda416176ecadefbb95225e56c4",amd64:"4e044bcd62a0c0bbe6a8c94d73989de2bfe4c04dbc0f9d6021cf96b72cd1d965"},releaseUrl=t=>`${BUILDKIT_RELEASES}/${VERSION}/buildkit-${VERSION}.linux-${t}.tar.gz`,ROOT="/var/lib/tnc-dock/buildkit",RUN_DIR="/run/tnc-buildkit",ADDR=`unix://${RUN_DIR}/buildkitd.sock`,CONTENT=`${ROOT}/runc-overlayfs/content/blobs/sha256`,BUILDKITD_ARGV0="^[^ ]*/buildkitd( |$)",SOLVE=`/${BUILDKIT_GRPC_PACKAGE}.Control/Solve`,SESSION=`/${BUILDKIT_GRPC_PACKAGE}.Control/Session`;function httpError(t,e){const i=new Error(e);return i.statusCode=t,i}function varint(t){const e=[];for(;t>127;)e.push(127&t|128),t=Math.floor(t/128);return e.push(t),Buffer.from(e)}function readVarint(t,e){let i,r=0,s=1;do{if(e>=t.length)throw new Error("truncated protobuf");i=t[e++],r+=(127&i)*s,s*=128}while(128&i);return[r,e]}function eachField(t,e){let i=0;for(;i<t.length;){let r;[r,i]=readVarint(t,i);const s=Math.floor(r/8),n=7&r;if(0===n){let r;[r,i]=readVarint(t,i),e(s,n,r)}else if(2===n){let r;[r,i]=readVarint(t,i),e(s,n,t.subarray(i,i+r)),i+=r}else if(1===n)i+=8;else{if(5!==n)throw new Error(`unsupported protobuf wire type ${n}`);i+=4}}}function grpcFrame(t){const e=Buffer.alloc(5);return e.writeUInt32BE(t.length,1),Buffer.concat([e,t])}const bytesMessage=t=>Buffer.concat([Buffer.from([10]),varint(t.length),t]);function bytesMessageData(t){let e=Buffer.alloc(0);return eachField(t,(t,i,r)=>{1===t&&2===i&&(e=r)}),e}class FrameReader{constructor(){this.buf=Buffer.alloc(0)}push(t){this.buf=Buffer.concat([this.buf,t]);const e=[];for(;this.buf.length>=5;){const t=this.buf.readUInt32BE(1);if(this.buf.length<5+t)break;if(0!==this.buf[0])throw new Error("compressed gRPC messages are not supported");e.push(this.buf.subarray(5,5+t)),this.buf=this.buf.subarray(5+t)}return e}}function solveExporterResponse(t){const e={};return eachField(t,(t,i,r)=>{if(1!==t||2!==i)return;let s="",n="";eachField(r,(t,e,i)=>{2===e&&(1===t&&(s=i.toString("utf8")),2===t&&(n=i.toString("utf8")))}),e[s]=n}),e}function rewriteFields(t,e){const i=[];let r=0;for(;r<t.length;){const s=r;let n;[n,r]=readVarint(t,r);const o=Math.floor(n/8),a=7&n;if(2===a){let l;[l,r]=readVarint(t,r);const c=t.subarray(r,r+l);r+=l;const u=e(o,a,c);i.push(void 0===u?t.subarray(s,r):Buffer.concat([varint(n),varint(u.length),u]));continue}if(0===a)[,r]=readVarint(t,r);else if(1===a)r+=8;else{if(5!==a)throw new Error(`unsupported protobuf wire type ${a}`);r+=4}i.push(t.subarray(s,r))}return Buffer.concat(i)}const ENGINE=Buffer.from(ENGINE_EXPORTER),IMAGE=Buffer.from("image");function rewriteSolveRequest(t){return rewriteFields(t,(t,e,i)=>3===t&&i.equals(ENGINE)?IMAGE:13===t?rewriteFields(i,(t,e,i)=>1===t&&i.equals(ENGINE)?IMAGE:void 0):void 0)}function setExporterResponse(t,e){const i=(t,e)=>{const i=Buffer.from(e);return Buffer.concat([varint(t<<3|2),varint(i.length),i])};return rewriteFields(t,(t,r,s)=>{if(1!==t)return;let n=null;return eachField(s,(t,e,i)=>{1===t&&2===e&&(n=i.toString("utf8"))}),n in e?Buffer.concat([i(1,n),i(2,e[n])]):void 0})}function sessionHeaders(t){const e={};for(let i=0;i+1<t.length;i+=2){const r=t[i].toLowerCase();r.startsWith(SESSION_HEADER_PREFIX)&&(e[r]||=[]).push(t[i+1])}return e}const buildKitDir=(t,e)=>path.join(t,"buildkit",`${VERSION}-${e}`),buildKitInstalled=t=>["buildkitd","buildctl","runc"].every(e=>fs.existsSync(path.join(t,e)));async function installBuildKit(t,e){const i=RELEASES[e];if(!i)throw httpError(501,`BuildKit is not available for linux/${e}`);const r=path.dirname(t);fs.mkdirSync(r,{recursive:!0});const s=fs.mkdtempSync(path.join(r,".download-"));try{const r=path.join(s,"buildkit.tar.gz"),n=await fetch(releaseUrl(e));if(!n.ok)throw new Error(`HTTP ${n.status}`);fs.writeFileSync(r,Buffer.from(await n.arrayBuffer()));const o=crypto.createHash("sha256").update(fs.readFileSync(r)).digest("hex");if(o!==i)throw new Error(`sha256 mismatch for ${path.basename(releaseUrl(e))}: ${o}`);const a=path.join(s,"bin");fs.mkdirSync(a);const l=spawnSync("tar",["-xzf",r,"-C",a,"--strip-components=1","bin/buildkitd","bin/buildctl","bin/buildkit-runc"],{encoding:"utf8"});if(0!==l.status)throw new Error(`unpacking: ${(l.stderr||"").trim()}`);fs.renameSync(path.join(a,"buildkit-runc"),path.join(a,"runc")),fs.rmSync(t,{recursive:!0,force:!0}),fs.renameSync(a,t)}catch(t){throw httpError(t.statusCode||500,`could not install BuildKit ${VERSION}: ${t.message}`)}finally{fs.rmSync(s,{recursive:!0,force:!0})}}class BuildKit{constructor(t){this.engine=t,this.arch=t.linuxArch(),this.binDir=buildKitDir(t.paths.home,this.arch),this._starting=null,this._emulated=null}get host(){return this.engine.host}async _download(){await installBuildKit(this.binDir,this.arch)}_installed(){return buildKitInstalled(this.binDir)}_bin(t){return`${this.host.guestPath(this.binDir)}/${t}`}async _running(){if(0!==(await this.host.run(["sh","-c",'test -S "$1/buildkitd.sock" && ! test -e "$1/exit" && test "$(cat "$1/bin" 2>/dev/null)" = "$2"',"dock",RUN_DIR,this.host.guestPath(this.binDir)])).code)return!1;const t=await this.host.run([this._bin("buildctl"),"--addr",ADDR,"--timeout","5","debug","workers"]);return 0===t.code&&(!this._emulated||t.stdout.includes(`linux/${this._emulated}`))}async _emulate(){if(this._emulated=null,"vm"===this.host.kind&&"arm64"===this.engine.emulation.hostArch())try{await this.engine.emulation.ensure("amd64"),this._emulated="amd64"}catch(t){this.engine.log(`buildkit: linux/amd64 builds unavailable: ${t.message}`)}}async ready(){if(await this.engine.ready(),await this._emulate(),!this._installed()||!await this._running())return this._starting||(this._starting=this._start().finally(()=>{this._starting=null})),this._starting}async _start(){if(this._installed()||(this.engine.log(`installing BuildKit ${VERSION} (linux/${this.arch})`),await this._download()),await this._running())return;this.engine.log(`starting buildkitd ${VERSION}`);const{shim:t}=this.host.binaries();await this.host.run(["sh","-c",'p="$3"; pkill -f "$p"; for i in 1 2 3 4 5 6 7 8 9 10; do pgrep -f "$p" >/dev/null || break; sleep 0.5; done; pkill -9 -f "$p"; rm -rf "$1"; mkdir -p "$1" "$2"',"dock",RUN_DIR,ROOT,BUILDKITD_ARGV0]),await this.host.run(["sh","-c",'printf "%s\n" "$2" > "$1/bin"',"dock",RUN_DIR,this.host.guestPath(this.binDir)]),await this.host.check([t,"--log",`${ROOT}/buildkitd.log`,"--exit",`${RUN_DIR}/exit`,"--","/bin/env",`PATH=${this.host.guestPath(this.binDir)}:/usr/sbin:/usr/bin:/sbin:/bin`,this._bin("buildkitd"),"--root",ROOT,"--addr",ADDR,"--oci-worker-binary","runc","--oci-worker-net","host","--containerd-worker","false",...this._emulated?["--oci-worker-platform",`linux/${this.arch}`,"--oci-worker-platform",`linux/${this._emulated}`]:[]]);for(let t=0;t<100;t++){if(await this._running())return void this.engine.log(`buildkitd ${VERSION} running`);await new Promise(t=>setTimeout(t,200))}const e=(await this.host.run(["tail","-c","20000",`${ROOT}/buildkitd.log`])).stdout.split("\n").map(t=>{try{return JSON.parse(t).log.trim()}catch(t){return""}});throw httpError(500,`buildkitd did not start: ${(e.filter(t=>/^(buildkitd:|time=.*level=(error|fatal))/.test(t)).pop()||e.filter(Boolean).pop()||"no log output").slice(0,500)}`)}connect(){const t=this.host.spawn([this._bin("buildctl"),"--addr",ADDR,"dial-stdio"]),e=new Duplex({read(){},write(e,i,r){t.write(e),r()},final(e){t.end(),e()},destroy(e,i){t.kill("SIGTERM"),i(e)}});return t.stdout.on("data",t=>e.push(t)),t.stdout.on("end",()=>e.push(null)),t.stderr.on("data",t=>this.engine.log(`buildctl dial-stdio: ${String(t).trim()}`)),t.exited.then(()=>e.push(null)),e}_client(){const t=this.connect(),e=http2.connect("http://buildkitd",{createConnection:()=>t});return e.on("error",t=>this.engine.log(`buildkitd connection: ${t.message}`)),e.on("close",()=>t.destroy()),e}async session(t,e){await this.ready();const i=this._client(),r=i.request({":method":"POST",":path":SESSION,"content-type":"application/grpc",te:"trailers",...sessionHeaders(e)}),s=new FrameReader;r.on("data",e=>{for(const i of s.push(e))t.write(bytesMessageData(i))}),t.onData(t=>r.write(grpcFrame(bytesMessage(Buffer.from(t))))),t.onEnd(()=>r.end()),await new Promise(e=>{const s=()=>{i.close(),t.end(),e()};r.on("close",s),r.on("error",s),t.onClose(()=>{r.close(),s()})})}async grpc(t){await this.ready();const e=this._client(),i=http2.createServer();i.on("stream",(t,i)=>this._proxyStream(e,t,i)),i.on("sessionError",t=>this.engine.log(`buildkit proxy: ${t.message}`)),await new Promise(e=>{t.on("close",e),i.emit("connection",t),t.resume()}),e.close()}_proxyStream(t,e,i){const r={};for(const[t,e]of Object.entries(i))":authority"!==t&&":scheme"!==t&&"connection"!==t&&(r[t]=e);const s=t.request(r),n=i[":path"]===SOLVE;let o={};if(n){const t=[];e.on("data",e=>t.push(e)),e.on("end",()=>{try{const e=(new FrameReader).push(Buffer.concat(t));s.end(Buffer.concat(e.map(t=>grpcFrame(rewriteSolveRequest(t)))))}catch(t){this.engine.log(`buildkit solve request: ${t.message}`),s.close(http2.constants.NGHTTP2_INTERNAL_ERROR)}})}else e.pipe(s);s.on("trailers",t=>{o=t}),s.on("response",(t,i)=>{const r={...t};if(i&http2.constants.NGHTTP2_FLAG_END_STREAM||void 0!==t["grpc-status"])return void e.respond(r,{endStream:!0});if(e.respond(r,{waitForTrailers:!0}),e.on("wantTrailers",()=>e.sendTrailers(o)),!n)return void s.pipe(e);const a=[];s.on("data",t=>a.push(t)),s.on("end",async()=>{let t=Buffer.concat(a);try{if("0"===String(o["grpc-status"])){const[e]=(new FrameReader).push(t),i=e&&await this.importResult(solveExporterResponse(e));i&&(t=grpcFrame(setExporterResponse(e,{"containerimage.digest":i.id})))}}catch(t){return this.engine.log(`buildkit import: ${t.stack||t.message}`),o={"grpc-status":"13","grpc-message":encodeURIComponent(`dock: ${t.message}`)},e.end()}e.end(t)})}),s.on("error",()=>e.close(http2.constants.NGHTTP2_INTERNAL_ERROR)),e.on("error",()=>s.close()),e.on("close",()=>{s.closed||s.close()})}async _blobText(t){const e=t.replace(/^sha256:/,"");if(!/^[0-9a-f]{64}$/.test(e))throw new Error(`bad digest ${t}`);const i=await this.host.run(["cat",`${CONTENT}/${e}`]);if(0!==i.code)throw new Error(`BuildKit has no blob ${t}`);return i.stdout}async importResult(t){const e=t["containerimage.digest"];if(!e)return null;let i=JSON.parse(await this._blobText(e)),r=e;if(i.manifests){const t=this.arch,e=i.manifests.find(e=>"linux"===e.platform?.os&&e.platform.architecture===t)||i.manifests.find(t=>"unknown"!==t.platform?.architecture);if(!e)throw new Error("the built index has no image manifest");r=e.digest,i=JSON.parse(await this._blobText(e.digest))}const s=await this._blobText(i.config.digest),n=JSON.parse(s),o=(i.layers||[]).map(t=>({digest:t.digest,size:t.size,mediaType:t.mediaType}));if(o.some(t=>/zstd/.test(t.mediaType||"")))throw new Error("zstd layers are not supported; build with compression=gzip");const a=this.engine.paths.blobs,l=[i.config.digest,...o.map(t=>t.digest)].map(t=>t.replace(/^sha256:/,"")).filter(t=>!fs.existsSync(path.join(a,t)));l.length&&await this.host.check(["sh","-c",'src="$1"; dst="$2"; shift 2; for h; do cp "$src/$h" "$dst/.$h.tmp" && mv "$dst/.$h.tmp" "$dst/$h"; done',"dock",CONTENT,this.host.guestPath(a),...l]);const c=[];for(const e of(t["image.name"]||"").split(",").map(t=>t.trim()).filter(Boolean))try{const t=parseReference(e);t.tagged&&c.push(t.tagged)}catch(t){}const u=i.config.digest,d=this.engine.store.saveImage({id:u,repoTags:c,repoDigests:[],digest:null,size:o.reduce((t,e)=>t+(e.size||0),0),layers:o,config:n,architecture:n.architecture,os:n.os,createdAt:Date.parse(n.created)||Date.now()});for(const t of c)this.engine.emitEvent("image","tag",d.id,{name:t});return this.engine.log(`buildkit: imported ${u.slice(7,19)} (${r.slice(7,19)}) ${c.join(" ")}`),d}}module.exports={BuildKit,VERSION,RELEASES,installBuildKit,buildKitDir,buildKitInstalled,grpcFrame,bytesMessage,bytesMessageData,FrameReader,solveExporterResponse,sessionHeaders,varint,rewriteSolveRequest,setExporterResponse};
@@ -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 path=require("path"),{parseBuildFile,parseKeyValues,splitWords,parseDuration,buildError}=require("./buildfile"),DEFAULT_SHELL=["/bin/sh","-c"];function applyChanges(s={},r=[]){const e=JSON.parse(JSON.stringify(s||{})),a=(Array.isArray(r)?r:[r]).map(String).filter(s=>s.trim());if(!a.length)return e;const{instructions:t}=parseBuildFile(a.join("\n")),o=s=>[...e.Shell||DEFAULT_SHELL,s];for(const s of t)switch(s.cmd){case"CMD":e.Cmd=s.json||o(s.args);break;case"ENTRYPOINT":e.Entrypoint=s.json||o(s.args);break;case"ENV":{const r=(e.Env||[]).slice();for(const[e,a]of parseKeyValues(s.args,"ENV")){const s=r.findIndex(s=>s.split("=")[0]===e);-1===s?r.push(`${e}=${a}`):r[s]=`${e}=${a}`}e.Env=r;break}case"EXPOSE":e.ExposedPorts={...e.ExposedPorts||{}};for(const r of splitWords(s.args))e.ExposedPorts[r.includes("/")?r:`${r}/tcp`]={};break;case"LABEL":e.Labels={...e.Labels||{}};for(const[r,a]of parseKeyValues(s.args,"LABEL"))e.Labels[r]=a;break;case"ONBUILD":e.OnBuild=[...e.OnBuild||[],s.args];break;case"STOPSIGNAL":e.StopSignal=s.args;break;case"USER":e.User=s.args;break;case"VOLUME":e.Volumes={...e.Volumes||{}};for(const r of s.json||splitWords(s.args))e.Volumes[r]={};break;case"WORKDIR":e.WorkingDir=path.posix.resolve(e.WorkingDir||"/",s.args);break;case"HEALTHCHECK":{if(/^NONE$/i.test(s.args.trim())){e.Healthcheck={Test:["NONE"]};break}const r=/^CMD\s+(.*)$/is.exec(s.args.trim());if(!r)throw buildError("HEALTHCHECK must be NONE or CMD ...");let a;try{const s=JSON.parse(r[1]);Array.isArray(s)&&(a=["CMD",...s])}catch(s){}e.Healthcheck={Test:a||["CMD-SHELL",r[1]]};for(const[r,a]of[["interval","Interval"],["timeout","Timeout"],["start-period","StartPeriod"]])s.flags[r]&&(e.Healthcheck[a]=parseDuration(s.flags[r]));s.flags.retries&&(e.Healthcheck.Retries=parseInt(s.flags.retries,10));break}default:throw Object.assign(new Error(`${s.cmd} is not a valid change command`),{statusCode:400})}return e}module.exports={applyChanges};
@@ -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"),https=require("https"),crypto=require("crypto"),{execFile,execFileSync}=require("child_process"),{promisify}=require("util"),yaml=require("js-yaml"),{globToRegExp}=require("../cli/context"),MOUNT="/__tnc",REFRESH_MS=864e5,NODE={version:"v24.22.0",sha256:{arm64:"b7e7991d5407815adfa74a2a9cd4db683552c8ce32ff4a295c22e972849c3f5d",amd64:"28ea0a438de9a13bd7891fd758eee6c7819ee8af5442dfd8d45bd65e56d4228c"}},NODE_ARCH={arm64:"arm64",amd64:"x64"},cacheDir=e=>path.join(e,"ci","cache"),toPosix=e=>e.split(path.sep).join("/"),NAME=/^[A-Za-z0-9_.-]+$/,safeSegment=e=>NAME.test(e)&&"."!==e&&".."!==e;function parseUses(e){const t=String(e||"").trim();if(t.startsWith("docker://"))return{kind:"docker",image:t.slice(9)};if("."===t||t.startsWith("./")){const r=path.posix.normalize(t);if(r.startsWith(".."))throw new Error(`uses: ${e} leaves the project`);return{kind:"local",rel:r}}const r=t.lastIndexOf("@");if(-1===r)throw new Error(`uses: ${e} needs a version (owner/repo@ref)`);const[n,s,...o]=t.slice(0,r).split("/"),i=t.slice(r+1);if(!n||!s||![n,s,...o].every(safeSegment)||!/^[A-Za-z0-9_.\/-]+$/.test(i)||i.split("/").some(e=>!safeSegment(e)))throw new Error(`cannot read uses: ${e}`);return{kind:"remote",owner:n,repo:s,sub:o.join("/"),ref:i}}const git=(e,t)=>promisify(execFile)("git",e,{cwd:t,env:{...process.env,GIT_TERMINAL_PROMPT:"0"}});async function fetchAction(e,t){const r=path.join("actions",t.owner,t.repo,t.ref.replace(/\//g,"%2F")),n=path.join(cacheDir(e),r),s=path.join(n,".tnc-ready"),o=/^[0-9a-f]{40}$/i.test(t.ref);try{if(o||Date.now()-fs.statSync(s).mtimeMs<864e5)return fs.accessSync(s),{dir:n,rel:r}}catch(e){}const i=`${n}.tmp-${process.pid}-${crypto.randomBytes(3).toString("hex")}`;fs.mkdirSync(path.dirname(n),{recursive:!0});const c=`https://github.com/${t.owner}/${t.repo}.git`;try{o?(fs.mkdirSync(i),await git(["init","-q"],i),await git(["fetch","-q","--depth","1",c,t.ref],i),await git(["checkout","-q","FETCH_HEAD"],i)):await git(["clone","-q","--depth","1","--branch",t.ref,"--",c,i])}catch(e){fs.rmSync(i,{recursive:!0,force:!0});const r=String(e.stderr||e.message).trim().split("\n").pop();throw new Error(`cannot download ${t.owner}/${t.repo}@${t.ref}: ${r}`)}fs.rmSync(path.join(i,".git"),{recursive:!0,force:!0}),fs.writeFileSync(path.join(i,".tnc-ready"),`${t.owner}/${t.repo}@${t.ref}\n`),fs.rmSync(n,{recursive:!0,force:!0});try{fs.renameSync(i,n)}catch(e){fs.rmSync(i,{recursive:!0,force:!0})}return{dir:n,rel:r}}function loadAction(e){const t=["action.yml","action.yaml"].map(t=>path.join(e,t)).find(e=>fs.existsSync(e));if(!t)throw new Error(`no action.yml in ${e}`);const r=yaml.load(fs.readFileSync(t,"utf8"))||{};if(!r.runs||!r.runs.using)throw new Error(`${t} has no runs.using`);return r}function actionInputs(e,t,r){const n={},s=[];for(const[t,s]of Object.entries(e.inputs||{}))s&&void 0!==s.default&&null!==s.default&&(n[t]=String(r(String(s.default))??""));for(const[e,r]of Object.entries(t||{}))n[e]=null==r?"":"object"==typeof r?JSON.stringify(r):String(r);for(const[t,r]of Object.entries(e.inputs||{}))r?.required&&!(t in n)&&s.push(t);return{inputs:n,env:Object.fromEntries(Object.entries(n).map(([e,t])=>[`INPUT_${e.replace(/ /g,"_").toUpperCase()}`,t])),missing:s}}function splitArgs(e){const t=[];let r="",n=null,s=!1;for(const o of e)n?o===n?n=null:r+=o:'"'!==o&&"'"!==o?/\s/.test(o)?((r||s)&&t.push(r),r="",s=!1):r+=o:(n=o,s=!0);return(r||s)&&t.push(r),t}function download(e,t,r=5){return new Promise((n,s)=>{https.get(e,o=>{if([301,302,303,307,308].includes(o.statusCode)&&o.headers.location&&r>0)return o.resume(),n(download(new URL(o.headers.location,e).href,t,r-1));if(200!==o.statusCode)return o.resume(),s(new Error(`${e}: HTTP ${o.statusCode}`));const i=fs.createWriteStream(t);o.pipe(i),i.on("finish",n),i.on("error",s),o.on("error",s)}).on("error",s)})}async function ensureNode(e,t){const r=`node-${NODE.version}-linux-${NODE_ARCH[t]}`,n=path.join("node",r),s=path.join(cacheDir(e),n);if(fs.existsSync(path.join(s,"bin","node")))return path.join(n,"bin","node");if(!NODE.sha256[t])throw new Error(`no node for linux/${t}`);const o=path.join(cacheDir(e),"node",`.tmp-${process.pid}-${crypto.randomBytes(3).toString("hex")}`);fs.mkdirSync(o,{recursive:!0});try{const e=path.join(o,`${r}.tar.gz`);await download(`https://nodejs.org/dist/${NODE.version}/${r}.tar.gz`,e);const n=crypto.createHash("sha256").update(fs.readFileSync(e)).digest("hex");if(n!==NODE.sha256[t])throw new Error(`${r}.tar.gz: sha256 ${n} is not the pinned one`);execFileSync("tar",["-xzf",e,"-C",o]),fs.rmSync(s,{recursive:!0,force:!0});try{fs.renameSync(path.join(o,r),s)}catch(e){}}finally{fs.rmSync(o,{recursive:!0,force:!0})}return path.join(n,"bin","node")}const unescapeData=e=>e.replace(/%0D/gi,"\r").replace(/%0A/gi,"\n").replace(/%25/g,"%"),unescapeProp=e=>unescapeData(e).replace(/%3A/gi,":").replace(/%2C/gi,",");function parseCommand(e){const t=/^::([A-Za-z][A-Za-z-]*)(?:\s+([^:]*))?::(.*)$/.exec(e);if(!t)return null;const r={};for(const e of(t[2]||"").split(",")){const t=e.indexOf("=");t>0&&(r[e.slice(0,t).trim()]=unescapeProp(e.slice(t+1)))}return{cmd:t[1].toLowerCase(),props:r,value:unescapeData(t[3])}}function walkFiles(e){const t=[],r=n=>{let s;try{s=fs.readdirSync(path.join(e,n),{withFileTypes:!0})}catch(e){return}for(const e of s){const s=n?`${n}/${e.name}`:e.name;e.isDirectory()?".git"!==e.name&&r(s):e.isFile()&&t.push(s)}};return r(""),t.sort()}function matchFiles(e,t){const r=[].concat(t).flatMap(e=>String(e??"").split("\n")).map(e=>e.trim()).filter(Boolean).map(t=>{const r=t.startsWith("!");let n=(r?t.slice(1):t).replace(/^\.\//,"");return path.isAbsolute(n)&&(n=toPosix(path.relative(e,n))),{neg:r,re:globToRegExp(n),dir:new RegExp(`^${globToRegExp(n).source.slice(1,-1)}/`)}});return walkFiles(e).filter(e=>{let t=!1;for(const n of r)(n.re.test(e)||n.dir.test(e))&&(t=!n.neg);return t})}function hashFiles(e,t){const r=matchFiles(e,t);if(!r.length)return"";const n=crypto.createHash("sha256");for(const t of r)n.update(crypto.createHash("sha256").update(fs.readFileSync(path.join(e,t))).digest());return n.digest("hex")}function saveArtifact(e,t,r){const n=String(t||"").split("\n").map(e=>e.trim()).filter(Boolean),s=1!==n.length||/[*?!]/.test(n[0])?null:path.resolve(e,n[0]);let o,i=e;s&&fs.existsSync(s)&&fs.statSync(s).isDirectory()?(i=s,o=walkFiles(s)):s&&fs.existsSync(s)?(i=path.dirname(s),o=[path.basename(s)]):o=matchFiles(e,n);for(const e of o){const t=path.join(r,e);fs.mkdirSync(path.dirname(t),{recursive:!0}),fs.copyFileSync(path.join(i,e),t)}return o.length}module.exports={MOUNT,NODE,cacheDir,parseUses,fetchAction,loadAction,actionInputs,ensureNode,splitArgs,parseCommand,matchFiles,hashFiles,saveArtifact,toPosix};
@@ -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"),{execFileSync,spawnSync}=require("child_process"),{DockApi,demuxer,qs}=require("../cli/api"),{pullImage}=require("../cli/commands"),{dockPaths}=require("../paths"),W=require("./workflow"),A=require("./actions"),{packContext}=require("../cli/context"),{BUILD_FILE,BUILD_FILE_PARAM}=require("../api/wire"),WORK="/work",FILES="/__tnc_files",FILE_COMMANDS=["env","path","output","state","summary"],sleep=t=>new Promise(e=>setTimeout(e,t)),short=t=>String(t).replace(/[^a-zA-Z0-9_.-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,40)||"x";function copyProject(t,e){fs.mkdirSync(e,{recursive:!0});let s=null;try{s=execFileSync("git",["-C",t,"ls-files","-z","-co","--exclude-standard"],{maxBuffer:1<<28,stdio:["ignore","pipe","ignore"]}).toString("utf8").split("\0").filter(Boolean)}catch(t){}if(s){if(s.forEach(s=>{const i=path.join(t,s),n=path.join(e,s);let a;try{a=fs.lstatSync(i)}catch(t){return}if(fs.mkdirSync(path.dirname(n),{recursive:!0}),a.isSymbolicLink())try{fs.symlinkSync(fs.readlinkSync(i),n)}catch(t){}else a.isFile()&&fs.copyFileSync(i,n,fs.constants.COPYFILE_FICLONE)}),fs.existsSync(path.join(t,".git"))){const s="darwin"===process.platform?spawnSync("cp",["-cR",path.join(t,".git"),path.join(e,".git")]):null;s&&0===s.status||fs.cpSync(path.join(t,".git"),path.join(e,".git"),{recursive:!0})}}else fs.cpSync(t,e,{recursive:!0,filter:t=>!/[\\/](node_modules|\.git)$/.test(t)})}function gitInfo(t){const e=e=>{try{return execFileSync("git",["-C",t,...e],{stdio:["ignore","pipe","ignore"]}).toString().trim()}catch(t){return""}},s=e(["rev-parse","--abbrev-ref","HEAD"])||"main",i=e(["remote","get-url","origin"]),n=(/[:/]([^/:]+\/[^/]+?)(\.git)?$/.exec(i)||[])[1]||path.basename(t);return{branch:s,sha:e(["rev-parse","HEAD"])||"0".repeat(40),repository:n}}function parseKeyFile(t){const e={},s=String(t||"").split("\n");for(let t=0;t<s.length;t++){const i=s[t];if(!i.trim())continue;const n=/^([^=<\s]+)<<(.+)$/.exec(i);if(n){const i=n[2].trim(),a=[];for(;++t<s.length&&s[t]!==i;)a.push(s[t]);e[n[1]]=a.join("\n");continue}const a=i.indexOf("=");a>0&&(e[i.slice(0,a)]=i.slice(a+1))}return e}class CiRun{constructor(t){this.opts=t,this.api=t.api||new DockApi,this.id=crypto.randomBytes(4).toString("hex"),this.log=e=>{try{t.onLog?.(e)}catch(t){}},this.dir=path.join(dockPaths().home,"ci",this.id),this.created=[],this.arch={arm64:"arm64",x64:"amd64"}[process.arch]||"amd64",this.artifactsDir=path.join(dockPaths().home,"ci","artifacts",this.id),this.masks=new Set,this.fetches=new Map,this.nodes=new Map,this.builds=new Map}async run(){const t=W.parseWorkflow(this.opts.workflowText);this.wf=t,await this.api.ensureRunning({quiet:!0});const e=gitInfo(this.opts.projectDir);this.github={workflow:t.name,ref:`refs/heads/${e.branch}`,ref_name:e.branch,sha:e.sha,repository:e.repository,event_name:this.opts.event||"workflow_dispatch",workspace:WORK,actor:os.userInfo().username,run_id:this.id,run_number:1,run_attempt:1,job:"",repository_owner:e.repository.split("/")[0],server_url:"https://github.com",api_url:"https://api.github.com",graphql_url:"https://api.github.com/graphql"};const s={GITHUB_TOKEN:"",...this.opts.secrets||{}};this.github.token=s.GITHUB_TOKEN;for(const t of Object.values(s))t&&this.masks.add(String(t));this.baseVars={github:this.github,secrets:s,inputs:{},vars:{}};const i=Date.now();this.log({type:"run",text:t.name,id:this.id,jobs:Object.keys(t.jobs)});const n=this.opts.onlyJobs?.length?new Set(this.opts.onlyJobs):null,a={},r=new Set(Object.keys(t.jobs).filter(t=>!n||n.has(t)));try{for(;r.size;){const e=[...r].filter(e=>t.jobs[e].needs.every(t=>a[t]||n&&!n.has(t)));if(!e.length)throw new Error("jobs wait on each other");e.forEach(t=>r.delete(t)),await Promise.all(e.map(async e=>{a[e]=await this.runJob(t.jobs[e],a)}))}}finally{await this.cleanup()}const o=Object.values(a),c=o.every(t=>["success","skipped"].includes(t.status)),u={id:this.id,name:t.name,passed:c,durationMs:Date.now()-i,jobs:o};return fs.existsSync(this.artifactsDir)&&(u.artifacts=this.artifactsDir,this.log({type:"note",text:`artifacts: ${this.artifactsDir}`})),this.log({type:"done",status:c?"success":"failure",summary:u}),u}async runJob(t,e){const s=Object.fromEntries(t.needs.map(t=>[t,{result:e[t]?.status||"skipped",outputs:{}}])),i=t.needs.every(t=>"success"===e[t]?.status||this.opts.onlyJobs?.length&&!e[t]),n={...this.baseVars,env:this.wf.env,needs:s,matrix:{},steps:{},job:{},runner:{}},a={id:t.id,name:t.name,legs:[]};let r,o;try{r=W.condition(t.if,{vars:n,status:i?"success":"failure"})}catch(e){return this.log({type:"note",job:t.id,text:`if: ${e.message}`}),{...a,status:"failure"}}if(!r)return this.log({type:"job",job:t.id,name:t.name,status:"skipped"}),{...a,status:"skipped"};if(t.reusable)return this.log({type:"note",job:t.id,text:`reusable workflow ${t.reusable} is not run locally`}),{...a,status:"skipped"};try{o=W.expandMatrix(W.interpolateDeep(t.strategy.matrix,{vars:n}))}catch(e){return this.log({type:"note",job:t.id,text:`matrix: ${e.message}`}),{...a,status:"failure"}}const c=!1!==t.strategy["fail-fast"],u=Math.max(1,Number(t.strategy["max-parallel"])||this.opts.maxParallel||4);this.log({type:"job",job:t.id,name:t.name,status:"running",legs:o.length});const p=o.map((t,e)=>({n:e,matrix:t,label:Object.values(t).map(t=>"object"==typeof t?JSON.stringify(t):String(t)).join(", "),cancelled:!1})),h=[];let l=0;await Promise.all(Array.from({length:Math.min(u,p.length)},async()=>{for(;l<p.length;){const e=p[l++];if(e.cancelled){h[e.n]={label:e.label,matrix:e.matrix,status:"cancelled",steps:[]};continue}const s=await this.runLeg(t,e,n);h[e.n]=s,"failure"===s.status&&c&&!t.continueOnError&&p.forEach(t=>{t!==e&&this.cancelLeg(t)})}}));const d=h.map(t=>t.status),f=d.includes("failure")?t.continueOnError?"success":"failure":d.includes("cancelled")?"cancelled":d.every(t=>"skipped"===t)?"skipped":"success";return this.log({type:"job",job:t.id,name:t.name,status:f}),{...a,status:f,legs:h}}cancelLeg(t){t.cancelled=!0,t.socket&&t.socket.destroy()}async runLeg(t,e,s){const i=Date.now(),n={job:t.id,leg:e.label},a={...s,matrix:e.matrix,steps:{},job:{status:"success"}},r=()=>({vars:a,status:e.status});e.status="success";let o=t.name;try{o=String(W.interpolate(t.name,r()))}catch(t){}const c={name:o,label:e.label,matrix:e.matrix,status:"success",steps:[]},u=t=>(c.status=t,c.durationMs=Date.now()-i,this.log({type:"leg",...n,status:t,durationMs:c.durationMs}),c);let p;try{p=W.runnerFor(W.interpolateDeep(t.runsOn,r()),W.interpolate(t.platform,r()))}catch(t){return this.log({type:"note",...n,text:t.message}),u("failure")}if("linux"!==p.os)return this.log({type:"note",...n,text:`runs-on ${p.label}: ${p.os} jobs need a ${p.os} machine, skipped here`}),u("skipped");const h=p.arch||this.arch;let l;a.runner={os:"Linux",arch:"arm64"===h?"ARM64":"X64",temp:"/tmp",name:`tnc-${this.id}`};try{const e=W.interpolateDeep(t.container,r()),s=t.steps.find(t=>t.uses&&W.imageFromSetup({uses:t.uses,with:W.interpolateDeep(t.with,r())}));l=e?.image||s&&W.imageFromSetup({uses:s.uses,with:W.interpolateDeep(s.with,r())})||W.DEFAULT_IMAGE,c.container=e}catch(t){return this.log({type:"note",...n,text:t.message}),u("failure")}c.image=l,c.platform=`linux/${h}`,this.log({type:"leg",...n,status:"running",image:l,platform:c.platform});const d=`tnc-ci-${this.id}-${short(t.id)}-${e.n}`;let f=this.opts.projectDir;try{this.opts.inPlace||(f=path.join(this.dir,`${short(t.id)}-${e.n}`),copyProject(this.opts.projectDir,f));const s=W.interpolateDeep(t.services||{},r())||{},i=Object.keys(s),o=Object.entries(c.container?.env||{}).map(([t,e])=>`${t}=${e}`),p=await this.resolveImage(l,h,n),m=path.join(this.dir,"files",d);fs.mkdirSync(m,{recursive:!0}),fs.mkdirSync(A.cacheDir(dockPaths().home),{recursive:!0});const g=await this.createStart(d,{Image:p,Entrypoint:["tail","-f","/dev/null"],Cmd:[],WorkingDir:WORK,Env:o,Labels:{"tnc.ci.run":this.id,"tnc.ci.job":t.id},HostConfig:{Binds:[`${f}:${WORK}`,`${m}:${FILES}`,`${A.cacheDir(dockPaths().home)}:${A.MOUNT}:ro`],ExtraHosts:i.map(t=>`${t}:127.0.0.1`)}},"");e.container=g;for(const t of i){const e="string"==typeof s[t]?{image:s[t]}:s[t];if(!e?.image)continue;const i=W.parseServiceOptions(e.options),a=Array.isArray(e.env)?e.env:Object.entries(e.env||{}).map(([t,e])=>`${t}=${e}`),r=await this.resolveImage(e.image,this.arch,n);await this.createStart(`${d}-${short(t)}`,{Image:r,...i.Entrypoint?{Entrypoint:i.Entrypoint}:{},Env:[...a,...i.Env],Labels:{"tnc.ci.run":this.id,"tnc.ci.service":t},...i.Healthcheck?{Healthcheck:i.Healthcheck}:{},HostConfig:{...i.HostConfig,NetworkMode:`container:${g}`}},""),this.log({type:"note",...n,text:`service ${t} (${e.image}) started`})}for(const t of i)await this.waitHealthy(`${d}-${short(t)}`,t,n);const y={id:g,tag:n,job:t,leg:e,vars:a,work:f,files:m,arch:h,env:W.interpolateDeep({...this.wf.env,...t.env},r()),path:[],posts:[]};this.writeEvent(y);const w={vars:a,env:{},state:e,results:c.steps,prefix:"",depth:0};await this.runSteps(y,t.steps,w);for(const t of y.posts.reverse())await this.runPost(y,t,w);return u(e.cancelled&&"failure"!==e.status?"cancelled":e.status)}catch(t){return this.log({type:"note",...n,text:t.message}),u(e.cancelled?"cancelled":"failure")}finally{this.opts.keep||this.opts.inPlace||fs.rmSync(f,{recursive:!0,force:!0})}}ctxOf(t,e){return{vars:e.vars,status:e.state.status,hashFiles:e=>A.hashFiles(t.work,e)}}setStatus(t,e,s){e.state.status=s,e.state===t.leg&&(t.vars.job.status=s)}async runSteps(t,e,s){for(const i of e){if(t.leg.cancelled){this.setStatus(t,s,"cancelled");break}s.vars.env={...t.env,...s.env};const e=this.ctxOf(t,s),n=s.prefix+(i.name?String(W.interpolate(String(i.name),e)):W.stepName(i)),a={name:n,status:"skipped",exitCode:null,durationMs:0};let r;s.results&&s.results.push(a);try{r=W.condition(i.if,e)}catch(e){this.log({type:"note",...t.tag,text:`step "${n}" if: ${e.message}`}),r=!1}if(!r){this.log({type:"step",...t.tag,step:n,status:"skipped"});continue}const o=Date.now();let c;this.log({type:"step",...t.tag,step:n,status:"running"});try{c=i.uses?await this.runUses(t,i,s,n):await this.runScript(t,i,s,n)}catch(e){this.log({type:"note",...t.tag,step:n,text:e.message}),c={code:1,outputs:{}}}a.status=0===c.code?"success":t.leg.cancelled?"cancelled":"failure",a.exitCode=c.code,a.durationMs=Date.now()-o;const u="failure"===a.status&&W.truthy(W.interpolate(i.continueOnError,e));i.id&&(s.vars.steps[i.id]={outputs:c.outputs||{},outcome:a.status,conclusion:u?"success":a.status}),this.log({type:"step",...t.tag,step:n,status:a.status,exitCode:c.code,durationMs:a.durationMs,ignored:u}),"failure"!==a.status||u||this.setStatus(t,s,"failure"),"cancelled"===a.status&&this.setStatus(t,s,"cancelled")}return s.state.status}async runPost(t,e,s){const i={name:e.name,status:"skipped",exitCode:null,durationMs:0};let n;s.results.push(i);try{n=W.condition(e.if,this.ctxOf(t,s))}catch(t){n=!1}if(!n)return void this.log({type:"step",...t.tag,step:e.name,status:"skipped"});const a=Date.now();let r;this.log({type:"step",...t.tag,step:e.name,status:"running"});try{r=await e.run()}catch(s){this.log({type:"note",...t.tag,step:e.name,text:s.message}),r={code:1}}i.status=0===r.code?"success":"failure",i.exitCode=r.code,i.durationMs=Date.now()-a,this.log({type:"step",...t.tag,step:e.name,status:i.status,exitCode:r.code,durationMs:i.durationMs}),"failure"===i.status&&this.setStatus(t,s,"failure")}stepEnv(t,e,s,i="job"){const n="job"===i?WORK:"/github/workspace",a="job"===i?FILES:"/github/file_commands",r=this.github;return{...t.env,...e.env,...s,CI:"true",TNC_CI:"true",GITHUB_ACTIONS:"true",GITHUB_WORKSPACE:n,GITHUB_REF:r.ref,GITHUB_REF_NAME:r.ref_name,GITHUB_REF_TYPE:"branch",GITHUB_SHA:r.sha,GITHUB_REPOSITORY:r.repository,GITHUB_REPOSITORY_OWNER:r.repository_owner,GITHUB_EVENT_NAME:r.event_name,GITHUB_EVENT_PATH:`${a}/event.json`,GITHUB_JOB:t.job.id,GITHUB_WORKFLOW:r.workflow,GITHUB_ACTOR:r.actor,GITHUB_RUN_ID:this.id,GITHUB_RUN_NUMBER:"1",GITHUB_RUN_ATTEMPT:"1",GITHUB_SERVER_URL:r.server_url,GITHUB_API_URL:r.api_url,GITHUB_GRAPHQL_URL:r.graphql_url,RUNNER_OS:"Linux",RUNNER_ARCH:t.vars.runner.arch,RUNNER_NAME:t.vars.runner.name,RUNNER_ENVIRONMENT:"self-hosted",RUNNER_TEMP:"/tmp",RUNNER_TOOL_CACHE:"/opt/hostedtoolcache",GITHUB_ENV:`${a}/env`,GITHUB_PATH:`${a}/path`,GITHUB_OUTPUT:`${a}/output`,GITHUB_STATE:`${a}/state`,GITHUB_STEP_SUMMARY:`${a}/summary`}}resetFiles(t){for(const e of FILE_COMMANDS){const s=path.join(t.files,e);fs.writeFileSync(s,""),fs.chmodSync(s,438)}}readFiles(t){const e=e=>{try{return fs.readFileSync(path.join(t.files,e),"utf8")}catch(t){return""}};return t.env={...t.env,...parseKeyFile(e("env"))},t.path=[...e("path").split("\n").map(t=>t.trim()).filter(Boolean).reverse(),...t.path],{outputs:parseKeyFile(e("output")),state:parseKeyFile(e("state"))}}writeEvent(t){const e=this.github,[s,i]=e.repository.includes("/")?e.repository.split("/"):[e.actor,e.repository];fs.writeFileSync(path.join(t.files,"event.json"),JSON.stringify({ref:e.ref,after:e.sha,repository:{full_name:e.repository,name:i,owner:{login:s}},sender:{login:e.actor}}))}command(t,e,s,i){switch(s.cmd){case"set-output":return s.props.name&&(i.outputs[s.props.name]=s.value),!0;case"save-state":return s.props.name&&(i.state[s.props.name]=s.value),!0;case"add-mask":return s.value&&this.masks.add(s.value),!0;case"group":return this.log({type:"out",...t.tag,step:e,stream:"stdout",text:`▸ ${this.mask(s.value)}`}),!0;case"endgroup":case"debug":case"echo":case"add-matcher":case"remove-matcher":return!0;case"error":case"warning":case"notice":{const i=s.props.file?` (${s.props.file}${s.props.line?`:${s.props.line}`:""})`:"";return this.log({type:"note",...t.tag,step:e,text:this.mask(`${s.cmd}: ${s.value}${i}`)}),!0}default:return!1}}mask(t){for(const e of this.masks)e&&t.includes(e)&&(t=t.split(e).join("***"));return t}async execStep(t,e,s,i){this.resetFiles(t);const n={outputs:{},state:{}};t.path.length&&(e=["sh","-c",'PATH="$0:$PATH"; export PATH; exec "$@"',t.path.join(":"),...e]);const a=await this.exec(t.id,e,s,{tag:t.tag,step:i.step,workdir:i.workdir||WORK,leg:t.leg,timeoutMs:i.timeoutMs,onCommand:e=>this.command(t,i.step,e,n)}),r=this.readFiles(t);return{code:a,outputs:{...n.outputs,...r.outputs},state:{...n.state,...r.state}}}runScript(t,e,s,i){const n=this.ctxOf(t,s),a=String(W.interpolate(e.run||"",n)),r=this.stepEnv(t,s,W.interpolateDeep(e.env,n)||{}),o=e.shell||(s.depth?"":t.job.defaults.shell),c=e.workingDirectory||(s.depth?"":t.job.defaults["working-directory"]),u=Number(e.timeoutMinutes||t.job.timeoutMinutes)||0;return this.execStep(t,shellArgv(o,a),r,{step:i,workdir:c?path.posix.join(WORK,String(W.interpolate(c,n))):WORK,timeoutMs:6e4*u})}async runUses(t,e,s,i){const n=this.ctxOf(t,s),a=W.interpolateDeep(e.with,n)||{},r=this.builtin(t,e,a,i);if(r)return r;if(s.depth>=8)throw new Error("actions call each other more than 8 levels deep");const o=A.parseUses(e.uses),c=W.interpolateDeep(e.env,n)||{};if("docker"===o.kind){const n={runs:{image:e.uses,entrypoint:a.entrypoint,args:a.args?A.splitArgs(String(a.args)):[]}};return this.runContainerAction(t,s,i,{def:n},{},c)}let u,p,h="";if("local"===o.kind)u=path.join(t.work,o.rel),p=path.posix.join(WORK,o.rel);else{const e=await this.fetchAction(o,t,i);u=path.join(e.dir,o.sub),p=path.posix.join(A.MOUNT,A.toPosix(e.rel),o.sub),h=`${o.owner}/${o.repo}`}const l=A.loadAction(u),d=String(l.runs.using).toLowerCase(),f={...n,vars:{...s.vars,github:{...s.vars.github,action_path:p}}},{inputs:m,env:g,missing:y}=A.actionInputs(l,a,t=>W.interpolate(t,f));y.length&&this.log({type:"note",...t.tag,step:i,text:`required input not given: ${y.join(", ")}`});const w={GITHUB_ACTION_PATH:p,GITHUB_ACTION:e.id||`__${h||o.rel||"action"}`,GITHUB_ACTION_REPOSITORY:h,GITHUB_ACTION_REF:o.ref||""};if(/^node\d+$/.test(d))return this.runNodeAction(t,s,i,l,p,{...c,...g,...w});if("composite"===d)return this.runComposite(t,s,i,l,p,m,{...c,...w});if("docker"===d)return this.runContainerAction(t,s,i,{def:l,hostDir:u},m,{...c,...g,...w});throw new Error(`${e.uses}: runs.using "${l.runs.using}" is not supported`)}builtin(t,e,s,i){const n=String(e.uses).toLowerCase(),a=e=>this.log({type:"note",...t.tag,step:i,text:e}),r=(t={})=>({code:0,outputs:t});if(n.startsWith("actions/checkout@"))return s.repository&&s.repository!==this.github.repository?a(`checkout of ${s.repository} is not done locally; the workspace is this project`):a(`workspace ready in ${WORK}`),r();if(/^actions\/setup-(node|python|go|java)@/.test(n))return a("provided by the job image"),r();if(/^actions\/cache(\/(restore|save))?@/.test(n))return a("cache: not needed locally"),r({"cache-hit":"false"});if(n.startsWith("actions/upload-artifact@")){const e=String(s.name||"artifact");if(!/^[^/\\:*?"<>|]+$/.test(e)||"."===e||".."===e)return a(`invalid artifact name "${e}"`),{code:1,outputs:{}};const i=path.join(this.artifactsDir,e),n=A.saveArtifact(t.work,s.path,i);if(!n){const t=String(s["if-no-files-found"]||"warn");return"error"===t?(a(`no files found for: ${s.path}`),{code:1,outputs:{}}):("warn"===t&&a(`no files found for: ${s.path}; nothing saved`),r())}return a(`artifact "${e}": ${n} file(s) saved in ${i}`),r({"artifact-id":e,"artifact-url":`file://${i}`})}if(n.startsWith("actions/download-artifact@")){const e=path.resolve(t.work,String(s.path||"."));if(path.relative(t.work,e).startsWith(".."))return a(`download path ${s.path} is outside the workspace`),{code:1,outputs:{}};let i=[];try{i=s.name?[String(s.name)]:fs.readdirSync(this.artifactsDir)}catch(t){}for(const t of i){const i=path.join(this.artifactsDir,t);if(path.dirname(i)!==this.artifactsDir||!fs.existsSync(i))return a(`artifact "${t}" not found in this run`),{code:1,outputs:{}};fs.cpSync(i,s.name?e:path.join(e,t),{recursive:!0})}return a(`${i.length} artifact(s) placed in ${s.path||"."}`),r({"download-path":path.posix.join(WORK,A.toPosix(path.relative(t.work,e)))})}return null}fetchAction(t,e,s){const i=`${t.owner}/${t.repo}@${t.ref}`;return this.fetches.has(i)||this.fetches.set(i,A.fetchAction(dockPaths().home,t)),this.fetches.get(i)}async nodeIn(t,e){if(t.node)return t.node;const[s,i]=(await this.capture(t.id,"if ls /lib/ld-musl-* >/dev/null 2>&1; then echo musl; else echo glibc; fi; command -v node || true")).split("\n").map(t=>t.trim());if("glibc"===s)this.nodes.has(t.arch)||(this.log({type:"note",...t.tag,step:e,text:`getting node ${A.NODE.version} (linux/${t.arch}) for JavaScript actions`}),this.nodes.set(t.arch,A.ensureNode(dockPaths().home,t.arch))),t.node=path.posix.join(A.MOUNT,A.toPosix(await this.nodes.get(t.arch)));else{if(!i)throw new Error("JavaScript actions need node: this image uses musl libc and has no node (use a -bookworm image, or one with node)");t.node=i}return t.node}async runNodeAction(t,e,s,i,n,a){const r=await this.nodeIn(t,s),o=this.stepEnv(t,e,a),c=t=>Object.fromEntries(Object.entries(t).map(([t,e])=>[`STATE_${t}`,e])),u=(e,i)=>this.execStep(t,[r,path.posix.join(n,String(e))],{...o,...c(i)},{step:s});let p={};if(i.runs.pre&&W.condition(i.runs["pre-if"]||"always()",this.ctxOf(t,e))){const t=await u(i.runs.pre,p);if(p={...p,...t.state},0!==t.code)return t}const h=await u(i.runs.main,p);return p={...p,...h.state},i.runs.post&&t.posts.push({name:`Post ${s}`,if:i.runs["post-if"]||"always()",run:()=>u(i.runs.post,p)}),h}async runComposite(t,e,s,i,n,a,r){const o={vars:{...e.vars,inputs:a,steps:{},github:{...e.vars.github,action_path:n}},env:{...e.env,...r},state:{status:"success"},results:null,prefix:`${s} › `,depth:e.depth+1},c=await this.runSteps(t,(i.runs.steps||[]).map(W.normalizeStep),o),u=this.ctxOf(t,o),p={};for(const[t,e]of Object.entries(i.outputs||{}))try{p[t]=String(W.interpolate(String(e?.value??""),u)??"")}catch(t){}return{code:"success"===c?0:1,outputs:p}}async runContainerAction(t,e,s,i,n,a){const r=i.def,o=String(r.runs.image||"");let c;c=o.startsWith("docker://")?await this.resolveImage(o.slice(9),t.arch,t.tag):await this.buildAction(t,i.hostDir,o,s);const u=this.ctxOf(t,{...e,vars:{...e.vars,inputs:n}}),p=W.interpolateDeep(r.runs.env||{},u)||{},h=this.stepEnv(t,e,{...p,...a,HOME:"/github/home"},"container"),l=[].concat(r.runs.args||[]).map(t=>String(W.interpolate(String(t),u)??"")),d=(e,i)=>this.runContainer(t,{Image:c,Env:Object.entries(h).map(([t,e])=>`${t}=${e??""}`),...e?{Entrypoint:[String(e)]}:{},...i&&l.length?{Cmd:l}:{},WorkingDir:"/github/workspace",Labels:{"tnc.ci.run":this.id},HostConfig:{Binds:[`${t.work}:/github/workspace`,`${t.files}:/github/file_commands`],NetworkMode:`container:${t.id}`}},s);if(r.runs["pre-entrypoint"]){const t=await d(r.runs["pre-entrypoint"]);if(0!==t.code)return t}const f=await d(r.runs.entrypoint,!0);return r.runs["post-entrypoint"]&&t.posts.push({name:`Post ${s}`,if:r.runs["post-if"]||"always()",run:()=>d(r.runs["post-entrypoint"])}),f}buildAction(t,e,s,i){const n=crypto.createHash("sha256").update(`${e}\0${s}\0${t.arch}`).digest("hex").slice(0,16);return this.builds.has(n)||this.builds.set(n,(async()=>{this.log({type:"note",...t.tag,step:i,text:`building the action's image (${s||BUILD_FILE})`});const{tar:a,buildFile:r}=packContext(e,s?path.join(e,s):void 0),o=`tnc-ci-action:${n}`;return await this.api.progress("POST",`/build${qs({t:o,[BUILD_FILE_PARAM]:r,platform:`linux/${t.arch}`,q:!0})}`,a,()=>{},{"Content-Type":"application/x-tar"}),(await this.inspectImage(o)).Id})()),this.builds.get(n)}async runContainer(t,e,s){this.resetFiles(t);const i={outputs:{},state:{}},n=await this.api.json("POST",`/containers/create${qs({name:`tnc-ci-${this.id}-act-${crypto.randomBytes(3).toString("hex")}`})}`,e);this.created.push(n.Id);const{socket:a}=await this.api.hijack(`/containers/${n.Id}/attach${qs({stream:!0,stdout:!0,stderr:!0})}`,null),r=this.pump(a,{tag:t.tag,step:s,leg:t.leg,onCommand:e=>this.command(t,s,e,i)});await this.api.json("POST",`/containers/${n.Id}/start`),await r;const o=await this.api.json("POST",`/containers/${n.Id}/wait`);await this.api.json("DELETE",`/containers/${n.Id}${qs({force:!0,v:!0})}`).catch(()=>{});const c=this.readFiles(t);return{code:o.StatusCode,outputs:{...i.outputs,...c.outputs},state:{...i.state,...c.state}}}async inspectImage(t){try{return await this.api.json("GET",`/images/${encodeURIComponent(t)}/json`)}catch(t){return null}}async resolveImage(t,e,s){this.locks=this.locks||new Map;const i=this.locks.get(t)||Promise.resolve();let n;const a=new Promise(t=>{n=t});this.locks.set(t,i.then(()=>a)),await i;try{let i=await this.inspectImage(t);if(e===this.arch)return i&&i.Architecture===this.arch||(this.log({type:"note",...s,text:`pulling ${t}`}),await pullImage(this.api,t,{quiet:!0}),i=await this.inspectImage(t)),i.Id;const n=`tnc-ci/${short(t).toLowerCase()}:${e}`,a=await this.inspectImage(n);if(a&&a.Architecture===e)return a.Id;this.log({type:"note",...s,text:`pulling ${t} (linux/${e})`});const r=i&&i.Architecture===this.arch?i.Id:null;await pullImage(this.api,t,{quiet:!0,platform:`linux/${e}`});const o=await this.inspectImage(t),[c,u]=n.split(":");await this.api.json("POST",`/images/${encodeURIComponent(o.Id)}/tag${qs({repo:c,tag:u})}`);const p=t.lastIndexOf(":")>t.lastIndexOf("/")?t.lastIndexOf(":"):-1,h={repo:-1===p?t:t.slice(0,p),tag:-1===p?"latest":t.slice(p+1)};return r?await this.api.json("POST",`/images/${encodeURIComponent(r)}/tag${qs(h)}`):await this.api.json("DELETE",`/images/${encodeURIComponent(t)}`).catch(()=>{}),o.Id}finally{n()}}async createStart(t,e,s){await this.api.json("DELETE",`/containers/${encodeURIComponent(t)}${qs({force:!0,v:!0})}`).catch(()=>{});const i=await this.api.json("POST",`/containers/create${qs({name:t,platform:s||void 0})}`,e);return this.created.push(i.Id),await this.api.json("POST",`/containers/${i.Id}/start`),i.Id}async waitHealthy(t,e,s){const i=Date.now()+18e4;for(;Date.now()<i;){const i=(await this.api.json("GET",`/containers/${encodeURIComponent(t)}/json`)).State||{};if(!i.Running)throw new Error(`service ${e} stopped (exit ${i.ExitCode})`);const n=i.Health?.Status;if(!n||"healthy"===n)return void(n&&this.log({type:"note",...s,text:`service ${e} is healthy`}));if("unhealthy"===n)throw new Error(`service ${e} is unhealthy`);await sleep(1e3)}throw new Error(`service ${e} did not become healthy in 3 minutes`)}async exec(t,e,s,i){const n=await this.api.json("POST",`/containers/${t}/exec`,{Cmd:e,Env:Object.entries(s||{}).map(([t,e])=>`${t}=${e??""}`),WorkingDir:i?.workdir||WORK,AttachStdout:!0,AttachStderr:!0,Tty:!1}),{socket:a}=await this.api.hijack(`/exec/${n.Id}/start`,{Detach:!1,Tty:!1});if(await this.pump(a,i))return this.log({type:"note",...i.tag,step:i.step,text:`timed out after ${i.timeoutMs/6e4} min`}),124;for(let t=0;t<20;t++){const t=await this.api.json("GET",`/exec/${n.Id}/json`).catch(()=>null);if(t&&!t.Running&&null!==t.ExitCode)return t.ExitCode;await sleep(50)}return 255}pump(t,e){e?.leg&&(e.leg.socket=t);const s={stdout:"",stderr:""},i=(t,s)=>{if(e.onCommand&&s.startsWith("::")){const t=A.parseCommand(s.replace(/\r$/,""));if(t&&e.onCommand(t))return}this.log({type:"out",...e.tag,step:e.step,stream:t,text:this.mask(s)})},n=(t,n)=>{if(!e)return;let a;for(s[t]+=n.toString("utf8");-1!==(a=s[t].indexOf("\n"));)i(t,s[t].slice(0,a)),s[t]=s[t].slice(a+1)},a=demuxer(t=>n("stdout",t),t=>n("stderr",t));let r=!1;const o=e?.timeoutMs?setTimeout(()=>{r=!0,t.destroy()},e.timeoutMs):null;return new Promise(n=>{let c=!1;const u=()=>{if(!c){c=!0,clearTimeout(o);for(const t of["stdout","stderr"])e&&s[t]&&i(t,s[t]);e?.leg&&(e.leg.socket=null),n(r)}};t.on("data",a),t.on("end",u),t.on("close",u),t.on("error",u)})}async capture(t,e){const s=await this.api.json("POST",`/containers/${t}/exec`,{Cmd:["sh","-c",e],AttachStdout:!0,AttachStderr:!1}),{socket:i}=await this.api.hijack(`/exec/${s.Id}/start`,{Detach:!1,Tty:!1}),n=[],a=demuxer(t=>n.push(Buffer.from(t)),()=>{});return await new Promise(t=>{i.on("data",a),i.on("end",t),i.on("close",t),i.on("error",t)}),Buffer.concat(n).toString("utf8")}async cleanup(){if(this.opts.keep)this.log({type:"note",text:`kept: containers labelled tnc.ci.run=${this.id}, copies in ${this.dir}`});else{for(const t of this.created.reverse())await this.api.json("DELETE",`/containers/${t}${qs({force:!0,v:!0})}`).catch(()=>{});fs.rmSync(this.dir,{recursive:!0,force:!0})}}}function shellArgv(t,e){const s=String(t||"").trim();return s?"bash"===s?["bash","--noprofile","--norc","-eo","pipefail","-c",e]:"sh"===s?["sh","-e","-c",e]:"python"===s?["python3","-c",e]:["sh","-c",`f=$(mktemp); cat > "$f" <<'TNC_CI_EOF'\n${e}\nTNC_CI_EOF\n${s.replace("{0}",'"$f"')}`]:["sh","-c",'if command -v bash >/dev/null 2>&1; then exec bash --noprofile --norc -eo pipefail -c "$1"; else exec sh -e -c "$1"; fi',"tnc-ci",e]}async function runWorkflow(t){return new CiRun(t).run()}module.exports={runWorkflow,CiRun,copyProject,parseKeyFile,shellArgv};