thinkncollab-shell 0.0.19 → 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 -482
  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
+ "use strict";const yaml=require("js-yaml");function tokenize(e){const t=[];let n=0;for(;n<e.length;){const r=e[n];if(/\s/.test(r)){n++;continue}if("'"===r){let r=n+1,s="";for(;r<e.length;)if("'"!==e[r]||"'"!==e[r+1]){if("'"===e[r])break;s+=e[r++]}else s+="'",r+=2;t.push({t:"str",v:s}),n=r+1;continue}const s=e.slice(n,n+2);if(["==","!=","&&","||","<=",">="].includes(s)){t.push({t:"op",v:s}),n+=2;continue}if("()[],.!<>".includes(r)){t.push({t:"op",v:r}),n++;continue}const o=/^-?\d+(\.\d+)?/.exec(e.slice(n));if(o){t.push({t:"num",v:Number(o[0])}),n+=o[0].length;continue}const i=/^[A-Za-z_][A-Za-z0-9_-]*\*?/.exec(e.slice(n));if(!i)throw new Error(`cannot read expression near "${e.slice(n,n+12)}"`);t.push({t:"id",v:i[0]}),n+=i[0].length}return t}const truthy=e=>!(!1===e||null==e||""===e||0===e||Number.isNaN(e)),loose=e=>"string"==typeof e?e.toLowerCase():e;function evaluate(e,t){const n=tokenize(e);let r=0;const s=()=>n[r],o=t=>{const s=n[r];if(!s||t&&s.v!==t)throw new Error(`expected ${t} in "${e}"`);return r++,s},i={always:()=>!0,success:()=>"success"===t.status,failure:()=>"failure"===t.status,cancelled:()=>"cancelled"===t.status,contains:(e,t)=>Array.isArray(e)?e.some(e=>loose(e)===loose(t)):String(e??"").toLowerCase().includes(String(t??"").toLowerCase()),startswith:(e,t)=>String(e??"").toLowerCase().startsWith(String(t??"").toLowerCase()),endswith:(e,t)=>String(e??"").toLowerCase().endsWith(String(t??"").toLowerCase()),format:(e,...t)=>String(e).replace(/\{(\d+)\}/g,(e,n)=>String(t[n]??"")),join:(e,t=",")=>Array.isArray(e)?e.join(t):String(e??""),tojson:e=>JSON.stringify(e,null,2),fromjson:e=>JSON.parse(e),hashfiles:(...e)=>t.hashFiles?t.hashFiles(e):""};t.usedStatus=t.usedStatus||!1;const a=()=>{const n=s();if(!n)throw new Error(`unexpected end of "${e}"`);if("("===n.v&&"op"===n.t){r++;const e=l();return o(")"),e}if("!"===n.v&&"op"===n.t)return r++,!truthy(a());if("str"===n.t||"num"===n.t)return r++,n.v;if("id"!==n.t)throw new Error(`unexpected "${n.v}" in "${e}"`);if(r++,"true"===n.v)return!0;if("false"===n.v)return!1;if("null"===n.v)return null;let c;if("("===s()?.v){r++;const a=[];if(")"!==s()?.v)for(a.push(l());","===s()?.v;)r++,a.push(l());o(")");const u=i[n.v.toLowerCase()];if(!u)throw new Error(`unknown function ${n.v}() in "${e}"`);["always","success","failure","cancelled"].includes(n.v.toLowerCase())&&(t.usedStatus=!0),c=u(...a)}else c=t.vars[n.v];for(;;){if("."===s()?.v){r++;const e=o().v;c="*"===e?c:null==c?void 0:c[e];continue}if("["===s()?.v){r++;const e=l();o("]"),c=null==c?void 0:c[e];continue}break}return c},c=()=>{let e=a();for(;s()&&["==","!=","<",">","<=",">="].includes(s().v);){const t=o().v,n=a(),r=loose(e),s=loose(n);e="=="===t?r==s:"!="===t?r!=s:"<"===t?r<s:">"===t?r>s:"<="===t?r<=s:r>=s}return e},u=()=>{let e=c();for(;"&&"===s()?.v;){r++;const t=c();e=truthy(e)?t:e}return e},l=()=>{let e=u();for(;"||"===s()?.v;){r++;const t=u();e=truthy(e)?e:t}return e},f=l();if(r<n.length)throw new Error(`unexpected "${n[r].v}" in "${e}"`);return f}function interpolate(e,t){if("string"!=typeof e||!e.includes("${{"))return e;const n=/^\$\{\{([\s\S]*?)\}\}$/.exec(e.trim());return n?evaluate(n[1],t):e.replace(/\$\{\{([\s\S]*?)\}\}/g,(e,n)=>{const r=evaluate(n,t);return null==r?"":"object"==typeof r?JSON.stringify(r):String(r)})}function interpolateDeep(e,t){return Array.isArray(e)?e.map(e=>interpolateDeep(e,t)):e&&"object"==typeof e?Object.fromEntries(Object.entries(e).map(([e,n])=>[e,interpolateDeep(n,t)])):interpolate(e,t)}function condition(e,t){if(null==e||""===e)return"success"===t.status;if("boolean"==typeof e)return e&&"success"===t.status;const n=String(e).trim().replace(/^\$\{\{([\s\S]*)\}\}$/,"$1"),r={...t,usedStatus:!1},s=truthy(evaluate(n,r));return r.usedStatus?s:s&&"success"===t.status}function expandMatrix(e){if(!e||"object"!=typeof e)return[{}];const{include:t=[],exclude:n=[],...r}=e;let s=[{}];for(const[e,t]of Object.entries(r)){const n=Array.isArray(t)?t:[t];s=s.flatMap(t=>n.map(n=>({...t,[e]:n})))}Object.keys(r).length||(s=[]);const o=(e,t)=>Object.entries(t).every(([t,n])=>JSON.stringify(e[t])===JSON.stringify(n));s=s.filter(e=>!n.some(t=>o(e,t)));for(const e of t){const t=Object.fromEntries(Object.entries(e).filter(([e])=>e in r)),n=s.filter(e=>o(e,t)&&Object.keys(t).length);n.length?n.forEach(t=>Object.assign(t,e)):s.push({...e})}return s.length?s:[{}]}const DEFAULT_IMAGE=process.env.TNC_CI_IMAGE||"node:22-bookworm";function versionTag(e){const t=String(e??"").trim();return!t||/^lts/i.test(t)||/[<>=^~*]/.test(t)?"lts":t.replace(/\.x$/i,"").replace(/^v/,"")}function imageFromSetup(e){const t=String(e.uses||"").toLowerCase(),n=e.with||{};return t.startsWith("actions/setup-node")?`node:${versionTag(n["node-version"])}-bookworm`:t.startsWith("actions/setup-python")?`python:${"lts"===versionTag(n["python-version"])?"3":versionTag(n["python-version"])}-bookworm`:t.startsWith("actions/setup-go")?`golang:${"lts"===versionTag(n["go-version"])?"1":versionTag(n["go-version"])}-bookworm`:t.startsWith("actions/setup-java")?`eclipse-temurin:${"lts"===versionTag(n["java-version"])?"21":versionTag(n["java-version"])}-jdk`:null}function runnerFor(e,t){const n=(Array.isArray(e)?e:[e]).filter(Boolean).map(e=>String(e).toLowerCase()),r=n.join(" ");let s="linux";/macos|darwin/.test(r)?s="macos":/windows/.test(r)&&(s="windows");let o=null;const i=String(t||"").toLowerCase();return/arm64|aarch64/.test(i)||/(^|[-\s/])(arm|arm64|aarch64)($|[-\s])/.test(r)?o="arm64":(/amd64|x86_64|x64/.test(i)||/(^|[-\s/])(x64|amd64|x86_64)($|[-\s])/.test(r))&&(o="amd64"),{os:s,arch:o,label:n.join(", ")||"ubuntu-latest"}}function parseServiceOptions(e=""){const t=[],n=/"((?:\\.|[^"])*)"|'([^']*)'|(\S+)/g;let r;for(;r=n.exec(String(e));)t.push(void 0!==r[1]?r[1].replace(/\\(.)/g,"$1"):void 0!==r[2]?r[2]:r[3]);const s=e=>{const t=/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/.exec(String(e));return t?Math.round(Number(t[1])*{ms:1e6,s:1e9,m:6e10,h:36e11}[t[2]||"s"]):0},o={Healthcheck:null,Env:[],HostConfig:{}};for(let e=0;e<t.length;e++){const[n,r]=t[e].split(/=(.*)/s),i=()=>void 0!==r?r:t[++e];switch(n){case"--health-cmd":o.Healthcheck={...o.Healthcheck||{},Test:["CMD-SHELL",i()]};break;case"--health-interval":o.Healthcheck={...o.Healthcheck||{},Interval:s(i())};break;case"--health-timeout":o.Healthcheck={...o.Healthcheck||{},Timeout:s(i())};break;case"--health-retries":o.Healthcheck={...o.Healthcheck||{},Retries:Number(i())};break;case"--health-start-period":o.Healthcheck={...o.Healthcheck||{},StartPeriod:s(i())};break;case"-e":case"--env":o.Env.push(i());break;case"--entrypoint":o.Entrypoint=[i()];break;case"--tmpfs":o.HostConfig.Tmpfs={...o.HostConfig.Tmpfs||{},[i()]:""};break;case"--shm-size":o.HostConfig.ShmSize=i()}}return o}function normalizeStep(e,t){return{index:t,id:e.id,name:e.name,run:e.run,uses:e.uses,with:e.with||{},env:e.env||{},if:e.if,shell:e.shell,workingDirectory:e["working-directory"],continueOnError:e["continue-on-error"],timeoutMinutes:e["timeout-minutes"]}}function parseWorkflow(e){const t=yaml.load(e)||{};if(!t.jobs||"object"!=typeof t.jobs)throw new Error("the workflow has no jobs");const n={};for(const[e,r]of Object.entries(t.jobs))r&&"object"==typeof r&&(r.uses?n[e]={id:e,name:r.name||e,reusable:r.uses,needs:[].concat(r.needs||[]),steps:[]}:n[e]={id:e,name:r.name||e,needs:[].concat(r.needs||[]),if:r.if,runsOn:r["runs-on"],platform:r.platform,container:"string"==typeof r.container?{image:r.container}:r.container||null,services:r.services||{},env:r.env||{},strategy:r.strategy||{},timeoutMinutes:r["timeout-minutes"],continueOnError:r["continue-on-error"],defaults:r.defaults?.run||{},steps:(r.steps||[]).map(normalizeStep)});for(const e of Object.values(n))for(const t of e.needs)if(!n[t])throw new Error(`job ${e.id} needs ${t}, which is not in the workflow`);const r={},s=(e,t)=>{if(2!==r[e]){if(1===r[e])throw new Error(`jobs depend on each other in a cycle: ${[...t,e].join(" → ")}`);r[e]=1;for(const r of n[e].needs)s(r,[...t,e]);r[e]=2}};return Object.keys(n).forEach(e=>s(e,[])),{name:t.name||"workflow",env:t.env||{},jobs:n}}function stepName(e){return e.name?e.name:e.uses?`Run ${e.uses}`:`Run ${(String(e.run||"").split("\n").find(e=>e.trim())||"").trim().slice(0,60)}`}module.exports={parseWorkflow,expandMatrix,evaluate,interpolate,interpolateDeep,condition,imageFromSetup,runnerFor,parseServiceOptions,stepName,normalizeStep,DEFAULT_IMAGE,truthy};
@@ -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"),{spawn}=require("child_process"),path=require("path"),fs=require("fs"),{dockPaths}=require("../paths"),{ping}=require("../client");class EngineError extends Error{constructor(e,t){super(e),this.status=t}}const enc=encodeURIComponent;function qs(e={}){const t=[];for(const[n,r]of Object.entries(e))null!=r&&!1!==r&&(Array.isArray(r)?r.forEach(e=>t.push(`${n}=${enc(e)}`)):"object"==typeof r?t.push(`${n}=${enc(JSON.stringify(r))}`):t.push(`${n}=${enc(!0===r?"1":r)}`));return t.length?`?${t.join("&")}`:""}class DockApi{constructor(e=dockPaths().socket){this.socket=e}_request(e,t,n={}){return http.request({socketPath:this.socket,method:e,path:t,headers:n})}_send(e,t,n,r={}){return new Promise((s,o)=>{const c=null!=n&&"function"==typeof n.pipe,i=null==n||c?null:Buffer.isBuffer(n)?n:Buffer.from(JSON.stringify(n)),a={...r};i&&(a["Content-Length"]=i.length,a["Content-Type"]||="application/json");const u=this._request(e,t,a);u.on("response",s),u.on("error",e=>o(this._connError(e))),c?(n.on("error",e=>u.destroy(e)),n.pipe(u)):u.end(i)})}_connError(e){return["ENOENT","ECONNREFUSED"].includes(e.code)?new EngineError("dock is not running. Start it with: thinknsh dock start",0):e}static async _read(e){const t=[];for await(const n of e)t.push(n);const n=Buffer.concat(t).toString("utf8");if(!(e.headers["content-type"]||"").includes("json")||!n)return n;try{return JSON.parse(n)}catch(e){return n}}static async _check(e){if(e.statusCode<400)return e;const t=await DockApi._read(e);throw new EngineError("object"==typeof t&&t?t.message:String(t||`HTTP ${e.statusCode}`),e.statusCode)}async json(e,t,n){const r=await DockApi._check(await this._send(e,t,n));return DockApi._read(r)}async stream(e,t,n,r){return DockApi._check(await this._send(e,t,n,r))}async progress(e,t,n,r,s){const o=await this.stream(e,t,n,s);let c="",i=null;const a=e=>{if(!e.trim())return;let t;try{t=JSON.parse(e)}catch(e){return}t.error?i=i||t.error:r(t)};for await(const e of o){let t;for(c+=e.toString("utf8");-1!==(t=c.indexOf("\n"));)a(c.slice(0,t)),c=c.slice(t+1)}if(a(c),i)throw new EngineError(i,500)}hijack(e,t){return new Promise((n,r)=>{const s=null==t?null:Buffer.from(JSON.stringify(t)),o={Connection:"Upgrade",Upgrade:"tcp"};s&&(o["Content-Type"]="application/json",o["Content-Length"]=s.length);const c=this._request("POST",e,o);c.on("upgrade",(e,t,r)=>{r?.length&&t.unshift(r),n({socket:t,tty:(e.headers["content-type"]||"").includes("raw-stream")})}),c.on("response",async e=>{try{await DockApi._check(e),n({socket:e,tty:!1,readOnly:!0})}catch(e){r(e)}}),c.on("error",e=>r(this._connError(e))),c.end(s)})}async ensureRunning({quiet:e=!1}={}){if(await ping(this.socket))return;const t=dockPaths();e||process.stderr.write("starting dock…\n"),fs.mkdirSync(t.home,{recursive:!0,mode:448});const n=fs.openSync(t.logFile,"a"),r=spawn(process.execPath,[path.join(__dirname,"..","daemon-main.js")],{detached:!0,stdio:["ignore",n,n],env:process.env,windowsHide:!0});r.unref(),fs.closeSync(n);for(let e=0;e<100;e++){if(await ping(this.socket))return;if(null!==r.exitCode)break;await new Promise(e=>setTimeout(e,100))}throw new EngineError(`dock did not start; see ${t.logFile}`,0)}}function demuxer(e,t){let n=Buffer.alloc(0);return r=>{for(n=n.length?Buffer.concat([n,r]):r;n.length>=8;){const r=n.readUInt32BE(4);if(n.length<8+r)break;const s=n.subarray(8,8+r);(2===n[0]?t:e)(s),n=n.subarray(8+r)}}}module.exports={DockApi,EngineError,demuxer,qs,enc};
@@ -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";class UsageError extends Error{}function parseArgs(e,r,{stopAtPositional:s=!1}={}){const t={},n={};for(const[e,s]of Object.entries(r))s.short&&(n[s.short]=e),t[e]="list"===s.type?[]:void 0!==s.default?s.default:"bool"!==s.type&&void 0;const o=[],i=(e,s)=>{"list"===r[e].type?t[e].push(s):t[e]=s};let a=0;const l=(r,s)=>{if(void 0!==s)return s;if(a+1>=e.length)throw new UsageError(`flag --${r} needs a value`);return e[++a]};for(;a<e.length;a++){const u=e[a];if("--"===u){o.push(...e.slice(a+1));break}if(u.startsWith("--")&&u.length>2){let[e,s]=u.slice(2).split(/=(.*)/s);if(!r[e]&&e.startsWith("no-")&&"bool"===r[e.slice(3)]?.type){t[e.slice(3)]=!1;continue}if(!r[e])throw new UsageError(`unknown flag: --${e}`);"bool"===r[e].type?i(e,void 0===s||!["false","0"].includes(s)):i(e,l(e,s));continue}if(u.startsWith("-")&&u.length>1&&!/^-\d/.test(u)){const e=u.slice(1);for(let s=0;s<e.length;s++){const t=n[e[s]];if(!t)throw new UsageError(`unknown flag: -${e[s]}`);if("bool"!==r[t].type){i(t,l(t,e.slice(s+1).replace(/^=/,"")||void 0));break}i(t,!0)}continue}if(o.push(u),s){o.push(...e.slice(a+1));break}}return{opts:t,args:o}}function parseBytes(e){if(null==e||""===e)return;const r=/^(\d+(?:\.\d+)?)\s*([bkmgt]?)i?b?$/i.exec(String(e).trim());if(!r)throw new UsageError(`invalid size "${e}" (examples: 512m, 2g)`);const s={"":0,b:0,k:1,m:2,g:3,t:4}[r[2].toLowerCase()];return Math.round(parseFloat(r[1])*1024**s)}function parseDuration(e){if(null==e||""===e)return;if(/^\d+(\.\d+)?$/.test(String(e)))return Math.round(1e9*parseFloat(e));let r=0;const s=/(\d+(?:\.\d+)?)(ms|s|m|h)/g;let t,n="";for(;t=s.exec(e);)r+=parseFloat(t[1])*{ms:1e6,s:1e9,m:6e10,h:36e11}[t[2]],n+=t[0];if(n!==String(e))throw new UsageError(`invalid duration "${e}" (examples: 30s, 1m30s)`);return Math.round(r)}function parseEnvList(e,r=process.env){const s=[];for(const t of e)-1===t.indexOf("=")?void 0!==r[t]&&s.push(`${t}=${r[t]}`):s.push(t);return s}function parseLabels(e){return Object.fromEntries(e.map(e=>{const r=e.indexOf("=");return-1===r?[e,""]:[e.slice(0,r),e.slice(r+1)]}))}module.exports={parseArgs,parseBytes,parseDuration,parseEnvList,parseLabels,UsageError};
@@ -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"),chalk=require("chalk"),{DockApi,EngineError,demuxer,qs,enc}=require("./api"),{parseArgs,parseBytes,parseDuration,parseEnvList,parseLabels,UsageError}=require("./args"),fmt=require("./format"),{pipeSession}=require("./session"),{packContext}=require("./context"),{BUILD_FILE,BUILD_IGNORE_FILE,BUILD_FILE_PARAM}=require("../api/wire");function readEnvFile(e){const t=[];for(const s of fs.readFileSync(e,"utf8").split(/\r?\n/)){const e=s.trim();e&&!e.startsWith("#")&&t.push(e.replace(/^export\s+/,""))}return t}function parsePublish(e){const t={},s=e=>{const t=/^(\d+)(?:-(\d+))?$/.exec(e);if(!t)return null;const s=Number(t[1]),r=t[2]?Number(t[2]):s;return r>=s&&r-s<1e4?Array.from({length:r-s+1},(e,t)=>s+t):null};for(const r of e){const[e,n="tcp"]=r.split("/");if(!["tcp","udp"].includes(n))throw new UsageError(`invalid protocol "${n}" in "${r}" (tcp or udp)`);let o="",i=e;const a=/^\[([^\]]+)\]:(.*)$/.exec(e);a&&(o=a[1],i=a[2]);const l=i.split(":"),c=l.pop(),p=l.pop()||"";a||(o=l.join(":")||"");const u=s(c),g=p?s(p):null;if(!u||p&&!g)throw new UsageError(`invalid port "${r}" (examples: -p 8080:80, -p 5353:53/udp, -p 8000-8002:8000-8002)`);if(g&&u.length>1&&g.length!==u.length)throw new UsageError(`port ranges in "${r}" must have the same length`);u.forEach((e,s)=>{const r=g?u.length>1?String(g[s]):g.length>1?`${g[0]}-${g[g.length-1]}`:String(g[0]):"";(t[`${e}/${n}`]||=[]).push({HostIp:o,HostPort:r})})}return t}function parseMountFlag(e){const t={Type:"volume"};for(const s of String(e).split(",")){if(!s)continue;const e=s.indexOf("="),r=(-1===e?s:s.slice(0,e)).toLowerCase(),n=-1===e?"true":s.slice(e+1);if("type"===r)t.Type=n;else if(["source","src"].includes(r))t.Source=n;else if(["target","destination","dst"].includes(r))t.Target=n;else if(["readonly","ro"].includes(r))t.ReadOnly=!["false","0"].includes(n);else if("tmpfs-size"===r)t.TmpfsOptions={...t.TmpfsOptions||{},SizeBytes:parseBytes(n)};else{if("tmpfs-mode"!==r){if("bind-propagation"===r||"consistency"===r||"volume-nocopy"===r)continue;throw new UsageError(`unknown --mount option "${r}"`)}t.TmpfsOptions={...t.TmpfsOptions||{},Mode:parseInt(n,8)}}}if(!t.Target)throw new UsageError(`--mount needs target=… (${e})`);if(!["bind","volume","tmpfs"].includes(t.Type))throw new UsageError(`--mount type must be bind, volume or tmpfs (${e})`);if("bind"===t.Type){if(!t.Source)throw new UsageError(`--mount type=bind needs source=… (${e})`);t.Source=resolveBind(`${t.Source}:/x`).slice(0,-3)}return t}function parseUlimit(e){const t=/^([a-z]+)=(-?\d+)(?::(-?\d+))?$/.exec(String(e));if(!t)throw new UsageError(`invalid ulimit "${e}" (example: --ulimit nofile=1024:2048)`);return{Name:t[1],Soft:Number(t[2]),Hard:Number(t[3]??t[2])}}function parseDevice(e){const[t,s,r]=String(e).split(":");return{PathOnHost:t,PathInContainer:s||t,CgroupPermissions:r||"rwm"}}function resolveBind(e){if(/^[A-Za-z]:[\\/]/.test(e))return e;const t=e.indexOf(":");if(-1===t)return e;let s=e.slice(0,t);return"."===s||s.startsWith("./")||s.startsWith("../")||s.startsWith(".\\")?s=path.resolve(s):("~"===s||s.startsWith("~/"))&&(s=path.join(os.homedir(),s.slice(1))),s+e.slice(t)}function restartPolicy(e){if(!e||"no"===e)return{Name:"no"};const[t,s]=e.split(":");if(!["always","unless-stopped","on-failure"].includes(t))throw new UsageError(`invalid restart policy "${e}" (no, always, unless-stopped, on-failure[:N])`);return{Name:t,MaximumRetryCount:s?parseInt(s,10):0}}function filtersOf(e){const t={};for(const s of e){const e=s.indexOf("=");if(-1===e)throw new UsageError(`invalid filter "${s}" (expected key=value)`);(t[s.slice(0,e)]||=[]).push(s.slice(e+1))}return Object.keys(t).length?t:void 0}function splitRepoTag(e){const t=e.lastIndexOf("/"),s=e.lastIndexOf(":");return s>t?{repo:e.slice(0,s),tag:e.slice(s+1)}:{repo:e,tag:"latest"}}async function pullImage(e,t,{quiet:s=!1,err:r=process.stderr,platform:n}={}){const{repo:o,tag:i}=splitRepoTag(t.includes("@")?t.split("@")[0]:t),a=fmt.pullPrinter(r),l={...t.includes("@")?{fromImage:t.split("@")[0],tag:t.split("@")[1]}:{fromImage:o,tag:i},platform:n};await e.progress("POST",`/images/create${qs(l)}`,null,e=>{s||a.onMessage(e)}),a.done()}const isMissingImage=e=>e instanceof EngineError&&404===e.status&&/image/i.test(e.message);async function createContainer(e,t,s,{pull:r="missing",quiet:n=!1,err:o,platform:i}){"always"===r&&await pullImage(e,s.Image,{quiet:n,err:o,platform:i});try{return await e.json("POST",`/containers/create${qs({name:t,platform:i})}`,s)}catch(a){if(!isMissingImage(a)||"never"===r)throw a;return n||o.write(`Unable to find image '${s.Image}'${i?` for ${i}`:""} locally\n`),await pullImage(e,s.Image,{quiet:n,err:o,platform:i}),e.json("POST",`/containers/create${qs({name:t,platform:i})}`,s)}}const CREATE_FLAGS={name:{type:"string"},platform:{type:"string"},init:{type:"bool"},ulimit:{type:"list"},dns:{type:"list"},"dns-search":{type:"list"},"dns-option":{type:"list"},mount:{type:"list"},sysctl:{type:"list"},"memory-swap":{type:"string"},"memory-reservation":{type:"string"},"cpuset-cpus":{type:"string"},"cpu-shares":{short:"c",type:"string"},device:{type:"list"},gpus:{type:"string"},"volumes-from":{type:"list"},"group-add":{type:"list"},"security-opt":{type:"list"},"log-driver":{type:"string"},"log-opt":{type:"list"},"label-file":{type:"list"},cidfile:{type:"string"},publish:{short:"p",type:"list"},env:{short:"e",type:"list"},"env-file":{type:"list"},volume:{short:"v",type:"list"},tmpfs:{type:"list"},workdir:{short:"w",type:"string"},user:{short:"u",type:"string"},entrypoint:{type:"string"},network:{type:"string"},net:{type:"string"},"network-alias":{type:"list"},hostname:{type:"string"},restart:{type:"string"},memory:{short:"m",type:"string"},cpus:{type:"string"},"pids-limit":{type:"string"},"shm-size":{type:"string"},cgroupns:{type:"string"},privileged:{type:"bool"},"cap-add":{type:"list"},"cap-drop":{type:"list"},"read-only":{type:"bool"},"add-host":{type:"list"},label:{short:"l",type:"list"},"health-cmd":{type:"string"},"health-interval":{type:"string"},"health-timeout":{type:"string"},"health-retries":{type:"string"},"health-start-period":{type:"string"},"no-healthcheck":{type:"bool"},"stop-signal":{type:"string"},"stop-timeout":{type:"string"},pull:{type:"string",default:"missing"},interactive:{short:"i",type:"bool"},tty:{short:"t",type:"bool"},rm:{type:"bool"},quiet:{short:"q",type:"bool"}};function createBody(e,t,s){const r=[];for(const t of e["env-file"])r.push(...readEnvFile(t));r.push(...parseEnvList(e.env));const n=e.network||e.net;if(e.cgroupns&&!["host","private"].includes(e.cgroupns))throw new UsageError(`invalid --cgroupns "${e.cgroupns}" (private or host)`);let o;e["no-healthcheck"]?o={Test:["NONE"]}:(e["health-cmd"]||e["health-interval"]||e["health-timeout"]||e["health-retries"]||e["health-start-period"])&&(o={...e["health-cmd"]?{Test:["CMD-SHELL",e["health-cmd"]]}:{},...e["health-interval"]?{Interval:parseDuration(e["health-interval"])}:{},...e["health-timeout"]?{Timeout:parseDuration(e["health-timeout"])}:{},...e["health-retries"]?{Retries:parseInt(e["health-retries"],10)}:{},...e["health-start-period"]?{StartPeriod:parseDuration(e["health-start-period"])}:{}});const i=parsePublish(e.publish);if(e.gpus)throw new UsageError("--gpus is not supported: containers run in a Linux VM without GPU access");const a={};for(const t of e["label-file"])Object.assign(a,parseLabels(readEnvFile(t)));Object.assign(a,parseLabels(e.label));const l=parseLabels(e["log-opt"]);return{Image:t,...s.length?{Cmd:s}:{},...void 0!==e.entrypoint?{Entrypoint:""===e.entrypoint?[""]:[e.entrypoint]}:{},Env:r,Labels:a,...e.workdir?{WorkingDir:e.workdir}:{},...e.user?{User:e.user}:{},...e.hostname?{Hostname:e.hostname}:{},Tty:e.tty,OpenStdin:e.interactive,StdinOnce:e.interactive,AttachStdin:e.interactive,AttachStdout:!0,AttachStderr:!0,ExposedPorts:Object.fromEntries(Object.keys(i).map(e=>[e,{}])),...o?{Healthcheck:o}:{},...e["stop-signal"]?{StopSignal:e["stop-signal"]}:{},...e["stop-timeout"]?{StopTimeout:parseInt(e["stop-timeout"],10)}:{},HostConfig:{Binds:e.volume.map(resolveBind),PortBindings:i,...n?{NetworkMode:n}:{},RestartPolicy:restartPolicy(e.restart),AutoRemove:e.rm,...e.memory?{Memory:parseBytes(e.memory)}:{},...e.cpus?{NanoCpus:Math.round(1e9*parseFloat(e.cpus))}:{},...e["pids-limit"]?{PidsLimit:parseInt(e["pids-limit"],10)}:{},...e["shm-size"]?{ShmSize:parseBytes(e["shm-size"])}:{},...e.cgroupns?{CgroupnsMode:e.cgroupns}:{},Privileged:e.privileged,CapAdd:e["cap-add"],CapDrop:e["cap-drop"],ReadonlyRootfs:e["read-only"],ExtraHosts:e["add-host"],Tmpfs:Object.fromEntries(e.tmpfs.map(e=>{const t=e.indexOf(":");return-1===t?[e,""]:[e.slice(0,t),e.slice(t+1)]})),...e.init?{Init:!0}:{},...e.mount.length?{Mounts:e.mount.map(parseMountFlag)}:{},...e.ulimit.length?{Ulimits:e.ulimit.map(parseUlimit)}:{},...e.dns.length?{Dns:e.dns}:{},...e["dns-search"].length?{DnsSearch:e["dns-search"]}:{},...e["dns-option"].length?{DnsOptions:e["dns-option"]}:{},...e.sysctl.length?{Sysctls:parseLabels(e.sysctl)}:{},...e["memory-swap"]?{MemorySwap:"-1"===e["memory-swap"]?-1:parseBytes(e["memory-swap"])}:{},...e["memory-reservation"]?{MemoryReservation:parseBytes(e["memory-reservation"])}:{},...e["cpuset-cpus"]?{CpusetCpus:e["cpuset-cpus"]}:{},...e["cpu-shares"]?{CpuShares:parseInt(e["cpu-shares"],10)}:{},...e.device.length?{Devices:e.device.map(parseDevice)}:{},...e["volumes-from"].length?{VolumesFrom:e["volumes-from"]}:{},...e["group-add"].length?{GroupAdd:e["group-add"]}:{},...e["security-opt"].length?{SecurityOpt:e["security-opt"]}:{},...e["log-driver"]||e["log-opt"].length?{LogConfig:{Type:e["log-driver"]||"json-file",Config:l}}:{}},...n&&e["network-alias"].length?{NetworkingConfig:{EndpointsConfig:{[n]:{Aliases:e["network-alias"]}}}}:{}}}async function resizeTo(e,t,s,r,n){await e.json("POST",`/${t}/${enc(s)}/resize${qs({h:r,w:n})}`).catch(()=>{})}async function attachAndWait(e,t,{stdin:s,tty:r,startIt:n,err:o}){const i=await e.hijack(`/containers/${enc(t)}/attach${qs({stream:1,stdout:1,stderr:1,stdin:s?1:0})}`),a=e.stream("POST",`/containers/${enc(t)}/wait${qs({condition:"next-exit"})}`);await a;const l=a.then(e=>DockApi._read(e));n&&await e.json("POST",`/containers/${enc(t)}/start`);const c=["SIGINT","SIGTERM","SIGHUP"],p=s=>e.json("POST",`/containers/${enc(t)}/kill${qs({signal:s})}`).catch(()=>{});r||c.forEach(e=>process.on(e,p));try{const{detached:n}=await pipeSession(i,{stdin:s,resize:r?(s,r)=>resizeTo(e,"containers",t,s,r):null});if(n)return{detached:!0,code:0};const a=await l,c="string"==typeof a?JSON.parse(a.trim().split("\n").pop()||"{}"):a;return c?.Error?.Message&&o.write(`${c.Error.Message}\n`),{detached:!1,code:c?.StatusCode??0}}finally{r||c.forEach(e=>process.removeListener(e,p))}}const RUN_FLAGS={...CREATE_FLAGS,detach:{short:"d",type:"bool"}};async function run(e,{api:t,out:s,err:r}){const{opts:n,args:o}=parseArgs(e,RUN_FLAGS,{stopAtPositional:!0});if(!o.length)throw new UsageError("run needs an image, e.g. thinknsh run -it alpine sh");if(n.tty&&!process.stdin.isTTY&&n.interactive)throw new UsageError("the input device is not a TTY; drop -t when piping input");const[i,...a]=o,l=createBody(n,i,a);if(n.detach&&(l.AttachStdout=l.AttachStderr=!1),n.cidfile&&fs.existsSync(n.cidfile))throw new UsageError(`container ID file found, make sure the other container isn't running or delete ${n.cidfile}`);const{Id:c}=await createContainer(t,n.name,l,{pull:n.pull,quiet:n.quiet,err:r,platform:n.platform});if(n.cidfile&&fs.writeFileSync(n.cidfile,c),n.detach)return await t.json("POST",`/containers/${c}/start`),s.write(`${c}\n`),0;const{detached:p,code:u}=await attachAndWait(t,c,{stdin:n.interactive,tty:n.tty,startIt:!0,err:r});return p&&s.write(`${fmt.shortId(c)}\n`),u}async function create(e,{api:t,out:s,err:r}){const{opts:n,args:o}=parseArgs(e,CREATE_FLAGS,{stopAtPositional:!0});if(!o.length)throw new UsageError("create needs an image");const[i,...a]=o,{Id:l}=await createContainer(t,n.name,createBody(n,i,a),{pull:n.pull,quiet:n.quiet,err:r,platform:n.platform});return n.cidfile&&fs.writeFileSync(n.cidfile,l),s.write(`${l}\n`),0}async function each(e,t,{out:s,err:r},n){if(!e.length)throw new UsageError(`${n} needs at least one container`);let o=0;for(const n of e)try{await t(n),s.write(`${n}\n`)}catch(e){o++,r.write(`Error: ${e.message}\n`)}return o?1:0}async function start(e,t){const{opts:s,args:r}=parseArgs(e,{attach:{short:"a",type:"bool"},interactive:{short:"i",type:"bool"}});if(s.attach||s.interactive){if(1!==r.length)throw new UsageError("start -a takes exactly one container");const e=await t.api.json("GET",`/containers/${enc(r[0])}/json`),{code:n}=await attachAndWait(t.api,e.Id,{stdin:s.interactive,tty:e.Config.Tty,startIt:!0,err:t.err});return n}return each(r,e=>t.api.json("POST",`/containers/${enc(e)}/start`),t,"start")}async function stop(e,t){const{opts:s,args:r}=parseArgs(e,{time:{short:"t",type:"string"},timeout:{type:"string"}}),n=s.time??s.timeout;return each(r,e=>t.api.json("POST",`/containers/${enc(e)}/stop${qs({t:n})}`),t,"stop")}async function restart(e,t){const{opts:s,args:r}=parseArgs(e,{time:{short:"t",type:"string"}});return each(r,e=>t.api.json("POST",`/containers/${enc(e)}/restart${qs({t:s.time})}`),t,"restart")}async function kill(e,t){const{opts:s,args:r}=parseArgs(e,{signal:{short:"s",type:"string",default:"SIGKILL"}});return each(r,e=>t.api.json("POST",`/containers/${enc(e)}/kill${qs({signal:s.signal})}`),t,"kill")}async function rm(e,t){const{opts:s,args:r}=parseArgs(e,{force:{short:"f",type:"bool"},volumes:{short:"v",type:"bool"}});return each(r,e=>t.api.json("DELETE",`/containers/${enc(e)}${qs({force:s.force,v:s.volumes})}`),t,"rm")}async function wait(e,{api:t,out:s}){const{args:r}=parseArgs(e,{});if(!r.length)throw new UsageError("wait needs at least one container");let n=0;for(const e of r){const r=await t.json("POST",`/containers/${enc(e)}/wait`);n=("string"==typeof r?JSON.parse(r.trim()):r).StatusCode,s.write(`${n}\n`)}return 0}async function ps(e,{api:t,out:s}){const{opts:r}=parseArgs(e,{all:{short:"a",type:"bool"},quiet:{short:"q",type:"bool"},filter:{short:"f",type:"list"},"no-trunc":{type:"bool"},json:{type:"bool"},last:{short:"n",type:"string"}}),n=await t.json("GET",`/containers/json${qs({all:r.all||void 0,filters:filtersOf(r.filter),limit:r.last})}`);if(r.json)return s.write(JSON.stringify(n,null,2)+"\n"),0;if(r.quiet){for(const e of n)s.write(`${r["no-trunc"]?e.Id:fmt.shortId(e.Id)}\n`);return 0}return fmt.table(["CONTAINER ID","IMAGE","COMMAND","CREATED","STATUS","PORTS","NAMES"],n.map(e=>{return[r["no-trunc"]?e.Id:fmt.shortId(e.Id),e.Image,`"${t=e.Command||"",r["no-trunc"]||t.length<=20?t:t.slice(0,19)+"…"}"`,fmt.ago(e.Created),e.Status,fmt.ports(e.Ports),(e.Names||[]).map(e=>e.replace(/^\//,"")).join(",")];var t}),{out:s}),0}async function logs(e,{api:t}){const{opts:s,args:r}=parseArgs(e,{follow:{short:"f",type:"bool"},tail:{short:"n",type:"string"},timestamps:{short:"t",type:"bool"},since:{type:"string"},until:{type:"string"}});if(1!==r.length)throw new UsageError("logs takes exactly one container");const n=await t.stream("GET",`/containers/${enc(r[0])}/logs${qs({stdout:1,stderr:1,follow:s.follow,tail:s.tail,timestamps:s.timestamps,since:s.since,until:s.until})}`),o=(n.headers["content-type"]||"").includes("raw-stream")?e=>process.stdout.write(e):demuxer(e=>process.stdout.write(e),e=>process.stderr.write(e));for await(const e of n)o(e);return 0}async function exec(e,{api:t,out:s}){const{opts:r,args:n}=parseArgs(e,{interactive:{short:"i",type:"bool"},tty:{short:"t",type:"bool"},detach:{short:"d",type:"bool"},env:{short:"e",type:"list"},"env-file":{type:"list"},workdir:{short:"w",type:"string"},user:{short:"u",type:"string"},privileged:{type:"bool"}},{stopAtPositional:!0});if(n.length<2)throw new UsageError("exec needs a container and a command, e.g. thinknsh exec -it web sh");const[o,...i]=n,a=[];for(const e of r["env-file"])a.push(...readEnvFile(e));a.push(...parseEnvList(r.env));const{Id:l}=await t.json("POST",`/containers/${enc(o)}/exec`,{Cmd:i,AttachStdin:r.interactive&&!r.detach,AttachStdout:!r.detach,AttachStderr:!r.detach,Tty:r.tty,Env:a,...r.workdir?{WorkingDir:r.workdir}:{},...r.user?{User:r.user}:{},Privileged:r.privileged});if(r.detach)return await t.json("POST",`/exec/${l}/start`,{Detach:!0,Tty:r.tty}),0;const c=await t.hijack(`/exec/${l}/start`,{Detach:!1,Tty:r.tty}),{detached:p}=await pipeSession(c,{stdin:r.interactive,resize:r.tty?(e,s)=>resizeTo(t,"exec",l,e,s):null});return p?0:(await t.json("GET",`/exec/${l}/json`)).ExitCode??0}async function inspect(e,{api:t,out:s}){const{opts:r,args:n}=parseArgs(e,{type:{type:"string"}});if(!n.length)throw new UsageError("inspect needs at least one name");const o={container:e=>`/containers/${enc(e)}/json`,image:e=>`/images/${e}/json`,volume:e=>`/volumes/${enc(e)}`,network:e=>`/networks/${enc(e)}`},i=r.type?[r.type]:Object.keys(o),a=[];let l=0;for(const e of n){let s=null;for(const r of i){if(!o[r])throw new UsageError(`unknown type "${r}" (container, image, volume, network)`);try{s=await t.json("GET",o[r](e));break}catch(e){if(!(e instanceof EngineError)||404!==e.status)throw e}}s?a.push(s):(l++,process.stderr.write(`Error: no such object: ${e}\n`))}return s.write(JSON.stringify(a,null,2)+"\n"),l?1:0}async function images(e,{api:t,out:s}){const{opts:r,args:n}=parseArgs(e,{quiet:{short:"q",type:"bool"},all:{short:"a",type:"bool"},filter:{short:"f",type:"list"},json:{type:"bool"},"no-trunc":{type:"bool"}}),o=filtersOf(r.filter)||{};n[0]&&(o.reference=[n[0]]);const i=await t.json("GET",`/images/json${qs({all:r.all||void 0,filters:Object.keys(o).length?o:void 0})}`);if(r.json)return s.write(JSON.stringify(i,null,2)+"\n"),0;const a=e=>r["no-trunc"]?e.Id:fmt.shortId(e.Id);if(r.quiet)return[...new Set(i.map(a))].forEach(e=>s.write(`${e}\n`)),0;const l=[];for(const e of i){const t=e.RepoTags?.length?e.RepoTags:["<none>:<none>"];for(const s of t){const{repo:t,tag:r}=splitRepoTag(s);l.push([t,r,a(e),fmt.ago(e.Created),fmt.size(e.Size)])}}return fmt.table(["REPOSITORY","TAG","IMAGE ID","CREATED","SIZE"],l,{out:s}),0}async function pull(e,{api:t,err:s}){const{opts:r,args:n}=parseArgs(e,{quiet:{short:"q",type:"bool"},platform:{type:"string"}});if(1!==n.length)throw new UsageError("pull takes one image, e.g. thinknsh pull postgres:16");return await pullImage(t,n[0],{quiet:r.quiet,err:s,platform:r.platform}),r.quiet&&process.stdout.write(`${n[0]}\n`),0}async function rmi(e,{api:t,out:s,err:r}){const{opts:n,args:o}=parseArgs(e,{force:{short:"f",type:"bool"}});if(!o.length)throw new UsageError("rmi needs at least one image");let i=0;for(const e of o)try{const r=await t.json("DELETE",`/images/${e}${qs({force:n.force})}`);for(const e of r||[])s.write(e.Untagged?`Untagged: ${e.Untagged}\n`:`Deleted: ${e.Deleted}\n`)}catch(e){i++,r.write(`Error: ${e.message}\n`)}return i?1:0}async function tag(e,{api:t}){const{args:s}=parseArgs(e,{});if(2!==s.length)throw new UsageError("tag takes SOURCE TARGET, e.g. thinknsh tag myapp:dev myapp:1.0");const{repo:r,tag:n}=splitRepoTag(s[1]);return await t.json("POST",`/images/${s[0]}/tag${qs({repo:r,tag:n})}`),0}async function readStdin(e=process.stdin){const t=[];for await(const s of e)t.push(s);return Buffer.concat(t).toString("utf8")}function promptLine(e,{err:t=process.stderr}={}){const s=require("readline").createInterface({input:process.stdin,output:t});return new Promise(t=>s.question(e,e=>{s.close(),t(e)}))}function promptHidden(e,{err:t=process.stderr}={}){return new Promise((s,r)=>{const n=process.stdin;t.write(e),n.setRawMode(!0),n.resume();let o="";const i=e=>{n.setRawMode(!1),n.pause(),n.removeListener("data",a),t.write("\n"),e instanceof Error?r(e):s(e)},a=e=>{for(const t of e.toString("utf8")){if("\r"===t||"\n"===t||""===t)return i(o);if(""===t)return i(new UsageError("login cancelled"));""===t||"\b"===t?o=o.slice(0,-1):o+=t}};n.on("data",a)})}async function login(e,{api:t,out:s,err:r}){const{opts:n,args:o}=parseArgs(e,{username:{short:"u",type:"string"},"password-stdin":{type:"bool"}});if(o.length>1)throw new UsageError("login takes at most one registry, e.g. thinknsh login -u me ghcr.io");const i=o[0]||"",a=process.stdin.isTTY;let l,c=n.username;if(n["password-stdin"]){if(!c)throw new UsageError("--password-stdin needs --username (-u)");l=(await readStdin()).replace(/\r?\n$/,"")}else{if(!a)throw new UsageError("no terminal to ask for the password; pipe it in with --password-stdin");c||(c=(await promptLine("Username: ",{err:r})).trim()),l=await promptHidden("Password: ",{err:r})}if(!c)throw new UsageError("a username is required");if(!l)throw new UsageError("a password is required");const p=await t.json("POST","/auth",{username:c,password:l,serveraddress:i});return s.write(`Login Succeeded${p.Registry?` (${p.Registry})`:""}\n`),0}async function logout(e,{api:t,out:s}){const{args:r}=parseArgs(e,{});if(r.length>1)throw new UsageError("logout takes at most one registry");const n=await t.json("DELETE",`/auth${qs({serveraddress:r[0]})}`);return s.write(n.Removed?`Removing login credentials for ${n.Registry}\n`:`Not logged in to ${n.Registry}\n`),0}async function push(e,{api:t,out:s,err:r}){const{opts:n,args:o}=parseArgs(e,{"all-tags":{short:"a",type:"bool"},quiet:{short:"q",type:"bool"}});if(1!==o.length)throw new UsageError("push takes one image, e.g. thinknsh push ghcr.io/me/app:1.0");if(o[0].includes("@"))throw new UsageError("push a tag, not a digest");const{repo:i,tag:a}=splitRepoTag(o[0]),l=o[0].lastIndexOf(":")>o[0].lastIndexOf("/");if(n["all-tags"]&&l)throw new UsageError("--all-tags takes a repository without a tag");const c=fmt.pullPrinter(r);let p=null;return await t.progress("POST",`/images/${i}/push${qs({tag:n["all-tags"]?void 0:a})}`,null,e=>{e.aux?.Digest&&(p=e.aux),n.quiet||c.onMessage(e)}),c.done(),n.quiet&&p&&s.write(`${i}:${p.Tag}@${p.Digest}\n`),0}async function build(e,{api:t,out:s,err:r}){const{opts:n,args:o}=parseArgs(e,{tag:{short:"t",type:"list"},file:{short:"f",type:"string"},"build-arg":{type:"list"},target:{type:"string"},"no-cache":{type:"bool"},pull:{type:"bool"},quiet:{short:"q",type:"bool"},label:{type:"list"},network:{type:"string"},platform:{type:"string"},secret:{type:"list"},ssh:{type:"list"}});if(n.ssh.length)throw new UsageError("--ssh is not supported yet; pass keys or tokens with --secret id=NAME,src=FILE");if(n.platform&&n.platform.includes(","))throw new UsageError("building several platforms at once is not supported; run one build per --platform");const i=path.resolve(o[0]||".");if(!fs.existsSync(i)||!fs.statSync(i).isDirectory())throw new UsageError(`build context ${i} is not a folder`);const{tar:a,buildFile:l}=packContext(i,n.file,{secrets:n.secret.map(parseSecret)});n.quiet||r.write(chalk.dim(`sending build context (${fmt.size(a.length)})\n`));const c=Object.fromEntries(n["build-arg"].map(e=>{const t=e.indexOf("=");return-1===t?[e,process.env[e]??null]:[e.slice(0,t),e.slice(t+1)]}));let p=null;return await t.progress("POST",`/build${qs({t:n.tag,[BUILD_FILE_PARAM]:l,buildargs:c,target:n.target,nocache:n["no-cache"],pull:n.pull,q:n.quiet,labels:n.label.length?parseLabels(n.label):void 0,networkmode:n.network,platform:n.platform})}`,a,e=>{e.aux?.ID&&(p=e.aux.ID),e.stream&&!n.quiet?s.write(e.stream):e.status&&!n.quiet&&r.write(`${e.status}\n`)},{"Content-Type":"application/x-tar"}),n.quiet&&p&&s.write(`${p}\n`),0}function parseSecret(e){const t=parseLabels(String(e).split(",")),s=t.id;if(!s||!/^[A-Za-z0-9._-]+$/.test(s))throw new UsageError(`--secret needs id=NAME (letters, digits, . _ -): ${e}`);const r=t.src||t.source;if(r){const e=path.resolve(r.replace(/^~(?=\/|$)/,os.homedir()));if(!fs.existsSync(e))throw new UsageError(`secret file ${e} not found`);return{id:s,data:fs.readFileSync(e)}}const n=t.env||s;if(void 0===process.env[n])throw new UsageError(`--secret ${s}: set src=FILE or env=VAR (${n} is not set)`);return{id:s,data:Buffer.from(process.env[n])}}async function confirm(e,t){if(t)return!0;if(!process.stdin.isTTY)throw new UsageError("refusing to prune without confirmation; pass -f");const s=require("readline").createInterface({input:process.stdin,output:process.stderr}),r=await new Promise(t=>s.question(`${e} [y/N] `,t));return s.close(),/^y(es)?$/i.test(r.trim())}async function volume(e,t){const{api:s,out:r}=t,[n="ls",...o]=e;switch(n){case"ls":case"list":{const{opts:e}=parseArgs(o,{quiet:{short:"q",type:"bool"},filter:{short:"f",type:"list"}}),t=(await s.json("GET",`/volumes${qs({filters:filtersOf(e.filter)})}`)).Volumes||[];return e.quiet?t.forEach(e=>r.write(`${e.Name}\n`)):fmt.table(["DRIVER","VOLUME NAME"],t.map(e=>[e.Driver,e.Name]),{out:r}),0}case"create":{const{opts:e,args:t}=parseArgs(o,{label:{type:"list"}}),n=await s.json("POST","/volumes/create",{Name:t[0]||"",Labels:parseLabels(e.label)});return r.write(`${n.Name}\n`),0}case"rm":case"remove":{const{opts:e,args:r}=parseArgs(o,{force:{short:"f",type:"bool"}});return each(r,t=>s.json("DELETE",`/volumes/${enc(t)}${qs({force:e.force})}`),t,"volume rm")}case"inspect":return inspect(["--type","volume",...o],t);case"prune":{const{opts:e}=parseArgs(o,{force:{short:"f",type:"bool"},all:{short:"a",type:"bool"}});if(!await confirm("Remove all unused volumes?",e.force))return 0;const t=await s.json("POST",`/volumes/prune${qs({filters:e.all?{all:["true"]}:void 0})}`);return(t.VolumesDeleted||[]).forEach(e=>r.write(`${e}\n`)),r.write(`Total reclaimed space: ${fmt.size(t.SpaceReclaimed||0)}\n`),0}default:throw new UsageError(`unknown volume command "${n}" (ls, create, rm, inspect, prune)`)}}async function network(e,t){const{api:s,out:r}=t,[n="ls",...o]=e;switch(n){case"ls":case"list":{const{opts:e}=parseArgs(o,{quiet:{short:"q",type:"bool"},filter:{short:"f",type:"list"}}),t=await s.json("GET",`/networks${qs({filters:filtersOf(e.filter)})}`);return e.quiet?t.forEach(e=>r.write(`${fmt.shortId(e.Id)}\n`)):fmt.table(["NETWORK ID","NAME","DRIVER","SCOPE"],t.map(e=>[fmt.shortId(e.Id),e.Name,e.Driver,e.Scope]),{out:r}),0}case"create":{const{opts:e,args:t}=parseArgs(o,{label:{type:"list"},internal:{type:"bool"},driver:{short:"d",type:"string"}});if(1!==t.length)throw new UsageError("network create takes one name");const n=await s.json("POST","/networks/create",{Name:t[0],Driver:e.driver||"bridge",Internal:e.internal,Labels:parseLabels(e.label)});return r.write(`${n.Id}\n`),0}case"rm":case"remove":{const{args:e}=parseArgs(o,{});return each(e,e=>s.json("DELETE",`/networks/${enc(e)}`),t,"network rm")}case"inspect":return inspect(["--type","network",...o],t);case"connect":case"disconnect":{const{opts:e,args:t}=parseArgs(o,{alias:{type:"list"},force:{short:"f",type:"bool"}});if(2!==t.length)throw new UsageError(`network ${n} takes NETWORK CONTAINER`);const r="connect"===n?{Container:t[1],EndpointConfig:{Aliases:e.alias}}:{Container:t[1],Force:e.force};return await s.json("POST",`/networks/${enc(t[0])}/${n}`,r),0}case"prune":{const{opts:e}=parseArgs(o,{force:{short:"f",type:"bool"}});return await confirm("Remove all unused networks?",e.force)?(((await s.json("POST","/networks/prune")).NetworksDeleted||[]).forEach(e=>r.write(`${e}\n`)),0):0}default:throw new UsageError(`unknown network command "${n}" (ls, create, rm, inspect, connect, disconnect, prune)`)}}async function system(e,t){const{api:s,out:r}=t,[n="info",...o]=e;switch(n){case"df":{const e=await s.json("GET","/system/df"),t=(e.Images||[]).reduce((e,t)=>e+(t.Size||0),0),n=(e.Containers||[]).reduce((e,t)=>e+(t.SizeRw||0),0),o=(e.Volumes||[]).reduce((e,t)=>e+(t.UsageData?.Size>0?t.UsageData.Size:0),0),i=(e.BuildCache||[]).reduce((e,t)=>e+(t.Size||0),0);return fmt.table(["TYPE","TOTAL","ACTIVE","SIZE"],[["Images",(e.Images||[]).length,(e.Images||[]).filter(e=>e.Containers>0).length,fmt.size(t)],["Containers",(e.Containers||[]).length,(e.Containers||[]).filter(e=>"running"===e.State).length,fmt.size(n)],["Local Volumes",(e.Volumes||[]).length,(e.Volumes||[]).filter(e=>e.UsageData?.RefCount>0).length,fmt.size(o)],["Build Cache",(e.BuildCache||[]).length,0,fmt.size(i)]],{out:r}),0}case"prune":{const{opts:e}=parseArgs(o,{force:{short:"f",type:"bool"},all:{short:"a",type:"bool"},volumes:{type:"bool"}}),t=["all stopped containers","all unused networks",e.all?"all unused images":"all dangling images","the build cache",...e.volumes?["all unused volumes"]:[]];if(!await confirm(`This removes:\n - ${t.join("\n - ")}\nContinue?`,e.force))return 0;let n=0;return n+=(await s.json("POST","/containers/prune")).SpaceReclaimed||0,await s.json("POST","/networks/prune"),n+=(await s.json("POST",`/images/prune${qs({filters:{dangling:[e.all?"false":"true"]}})}`)).SpaceReclaimed||0,n+=(await s.json("POST",`/build/prune${qs({all:e.all})}`)).SpaceReclaimed||0,e.volumes&&(n+=(await s.json("POST","/volumes/prune")).SpaceReclaimed||0),r.write(`Total reclaimed space: ${fmt.size(n)}\n`),0}case"info":{const[e,t]=await Promise.all([s.json("GET","/version"),s.json("GET","/info")]);return r.write(`dock ${e.Version} (${t.OperatingSystem||""} ${t.Architecture||""})\n`),r.write(` containers ${t.Containers} (${t.ContainersRunning} running, ${t.ContainersStopped} stopped)\n`),r.write(` images ${t.Images}\n`),r.write(` cpus ${t.NCPU} memory ${fmt.size(t.MemTotal)}\n`),0}default:throw new UsageError(`unknown system command "${n}" (info, df, prune)`)}}const COMMANDS={run,create,start,stop,restart,kill,rm,wait,ps,logs,exec,inspect,images,pull,push,rmi,tag,build,login,logout,volume,network,system};for(const e of["cp","diff","save","load","import"])COMMANDS[e]=(t,s)=>require("./files")[e](t,s);COMMANDS.export=(e,t)=>require("./files").exportCmd(e,t);for(const e of["stats","top","pause","unpause","rename","update","commit","history","port"])COMMANDS[e]=(t,s)=>require("./manage")[e](t,s);async function container(e,t){const[s="ls",...r]=e;if("export"===s)return COMMANDS.export(r,t);if("prune"===s){const{opts:e}=parseArgs(r,{force:{short:"f",type:"bool"}});if(!await confirm("Remove all stopped containers?",e.force))return 0;const s=await t.api.json("POST","/containers/prune");return(s.ContainersDeleted||[]).forEach(e=>t.out.write(`${e}\n`)),t.out.write(`Total reclaimed space: ${fmt.size(s.SpaceReclaimed||0)}\n`),0}const n={ls:"ps",list:"ps",remove:"rm",prune:null}[s]||s;if(!COMMANDS[n]||["images","pull","push","rmi","tag","build","login","logout","volume","network","system","save","load","history","import"].includes(n))throw new UsageError(`unknown container command "${s}"`);return COMMANDS[n](r,t)}async function image(e,t){const[s="ls",...r]=e;if("prune"===s){const{opts:e}=parseArgs(r,{force:{short:"f",type:"bool"},all:{short:"a",type:"bool"}});if(!await confirm(e.all?"Remove all images without a container?":"Remove all dangling images?",e.force))return 0;const s=await t.api.json("POST",`/images/prune${qs({filters:{dangling:[e.all?"false":"true"]}})}`);return(s.ImagesDeleted||[]).forEach(e=>t.out.write(e.Untagged?`Untagged: ${e.Untagged}\n`:`Deleted: ${e.Deleted}\n`)),t.out.write(`Total reclaimed space: ${fmt.size(s.SpaceReclaimed||0)}\n`),0}const n={ls:"images",list:"images",rm:"rmi",remove:"rmi",pull:"pull",push:"push",tag:"tag",build:"build",inspect:null,save:"save",load:"load",history:"history",import:"import"};if("inspect"===s)return inspect(["--type","image",...r],t);if(!n[s])throw new UsageError(`unknown image command "${s}" (ls, pull, push, build, tag, rm, inspect, prune, save, load, history, import)`);return COMMANDS[n[s]](r,t)}Object.assign(COMMANDS,{container,image}),COMMANDS.compose=(e,t)=>require("./compose").compose(e,t),COMMANDS.workspace=(e,t)=>require("./workspace").workspace(e,t),COMMANDS.service=(e,t)=>require("./services").service(e,t),COMMANDS.registry=(e,t)=>require("./services").registry(e,t);for(const e of["capsule","vault","mesh","stack","vm"])COMMANDS[e]=(t,s)=>require("./moved")[e](t,s);const USAGE={run:"Usage: thinknsh run [OPTIONS] IMAGE [COMMAND] [ARG…]\n\nCreate and start a container. Pulls the image if it is not here yet.\n\n -d, --detach Run in the background and print the container ID\n -i, --interactive Keep stdin open -t, --tty Allocate a terminal\n --rm Remove the container when it exits\n --name NAME Container name\n -p, --publish H:C Publish container port C on host port H (C/udp for udp, ranges 8000-8002:8000-8002)\n -e, --env K=V Set an environment variable --env-file FILE\n -v, --volume SRC:DST Named volume or host folder (./relative works)\n -w, --workdir DIR -u, --user USER --entrypoint CMD --hostname NAME\n --network NET --network-alias A --add-host HOST:IP\n --restart POLICY no | always | unless-stopped | on-failure[:N]\n -m, --memory 512m --cpus 1.5 --pids-limit N --shm-size 64m\n --cgroupns private|host (private: the container sees only its own cgroup)\n --health-cmd CMD --health-interval 30s --health-retries N --no-healthcheck\n --privileged --cap-add CAP --cap-drop CAP --read-only\n -l, --label K=V --pull missing|always|never -q, --quiet\n --platform linux/amd64 Run an image built for another CPU (Rosetta 2 on Apple Silicon)\n --init A tiny PID 1 that forwards signals and reaps zombies\n --mount type=bind|volume|tmpfs,source=…,target=…[,readonly]\n --ulimit nofile=1024:2048 --sysctl net.core.somaxconn=1024\n --dns IP --dns-search DOMAIN --dns-option OPT\n --memory-swap 1g --memory-reservation 256m --cpuset-cpus 0-1 -c, --cpu-shares N\n --device /dev/x --volumes-from NAME[:ro] --group-add GROUP --security-opt OPT\n --log-opt max-size=10m --cidfile FILE --label-file FILE\n\nDetach from a -it session with Ctrl-P Ctrl-Q.",create:"Usage: thinknsh create [OPTIONS] IMAGE [COMMAND] [ARG…]\n\nLike run, without starting the container. Same options as run.",start:"Usage: thinknsh start [-a] [-i] CONTAINER…\n\n -a, --attach Attach to its output and wait\n -i Attach stdin too",stop:"Usage: thinknsh stop [-t SECONDS] CONTAINER…\n\nSends the stop signal, then SIGKILL after the timeout (default 10s).",restart:"Usage: thinknsh restart [-t SECONDS] CONTAINER…",kill:"Usage: thinknsh kill [-s SIGNAL] CONTAINER…",rm:"Usage: thinknsh rm [-f] [-v] CONTAINER…\n\n -f, --force Stop it first if running\n -v, --volumes Also remove its anonymous volumes",wait:"Usage: thinknsh wait CONTAINER…\n\nBlocks until each container stops, then prints its exit code.",ps:"Usage: thinknsh ps [-a] [-q] [-f KEY=VALUE] [--no-trunc] [--json]\n\n -a All containers (default: running only)\n -q IDs only\n -f Filter: name=, label=, status=, ancestor=, id=",logs:"Usage: thinknsh logs [-f] [-n N] [-t] [--since T] CONTAINER\n\n -f, --follow Keep streaming\n -n, --tail N Last N lines\n -t Show timestamps",exec:"Usage: thinknsh exec [-it] [-d] [-e K=V] [-w DIR] [-u USER] CONTAINER COMMAND [ARG…]\n\nRuns a command in a running container and exits with its code.",inspect:"Usage: thinknsh inspect [--type container|image|volume|network] NAME…",images:"Usage: thinknsh images [-a] [-q] [REPOSITORY[:TAG]]",pull:"Usage: thinknsh pull [-q] [--platform linux/amd64] IMAGE[:TAG|@DIGEST]",push:"Usage: thinknsh push [-a] [-q] IMAGE[:TAG]\n\nUpload an image to its registry (the host in its name; no host = the public\ndefault registry). Tag it first: thinknsh tag myapp:dev ghcr.io/me/myapp:1.0\nUses the login saved by thinknsh login for that registry.\n\n -a, --all-tags Push every local tag of the repository\n -q, --quiet Print only NAME:TAG@DIGEST",login:'Usage: thinknsh login [-u USER] [--password-stdin] [REGISTRY]\n\nCheck a registry login and save it for pull and push (default: the public\ndefault registry). Saved per registry host in ~/.tnc/dock/auth.json (mode 0600).\nAsks for the password without echo, or reads it from stdin:\n echo "$TOKEN" | thinknsh login -u me --password-stdin ghcr.io\n\n -u, --username USER --password-stdin Read the password from stdin',logout:"Usage: thinknsh logout [REGISTRY]\n\nForget the saved login for REGISTRY (default: the public default registry).",rmi:"Usage: thinknsh rmi [-f] IMAGE…",tag:"Usage: thinknsh tag SOURCE TARGET",build:`Usage: thinknsh build [OPTIONS] [FOLDER]\n\nBuild an image from the build file (${BUILD_FILE} or Containerfile) in FOLDER (default: .).\nFiles listed in .tncignore (or an existing ${BUILD_IGNORE_FILE}) are not sent.\n\n -t, --tag NAME:TAG Name the image (repeatable)\n -f, --file FILE Build file to use\n --build-arg K=V --target STAGE --no-cache --pull -q, --quiet\n --platform linux/amd64 Build for another CPU (FROM --platform, TARGETARCH, … work)\n --secret id=NAME,src=FILE | id=NAME,env=VAR For RUN --mount=type=secret,id=NAME\n\nBuild files may use RUN --mount=type=cache|secret|bind|tmpfs and heredocs (RUN <<EOF, COPY <<EOF /file).`,cp:'Usage: thinknsh cp [-a] [-L] SRC DEST\n\nCopy files or folders between a container and this machine. One side is CONTAINER:PATH.\n"PATH/." copies a folder\'s contents. "-" as SRC or DEST is a tar on stdin or stdout.\n\n -a, --archive Keep owners (uid/gid) -L, --follow-link Follow a symlink in SRC\n\n thinknsh cp web:/etc/nginx/nginx.conf ./nginx.conf\n thinknsh cp ./site/. web:/usr/share/nginx/html',stats:"Usage: thinknsh stats [-a] [--no-stream] [--json] [CONTAINER…]\n\nLive CPU, memory, network, disk and process counts, read from the kernel (cgroups).",top:"Usage: thinknsh top CONTAINER",pause:"Usage: thinknsh pause CONTAINER…\n\nFreezes every process (cgroup freezer); unpause thaws them.",unpause:"Usage: thinknsh unpause CONTAINER…",rename:"Usage: thinknsh rename CONTAINER NEW_NAME",update:"Usage: thinknsh update [OPTIONS] CONTAINER…\n\n -m, --memory 1g --memory-swap 2g --memory-reservation 256m --cpus 1.5\n -c, --cpu-shares N --cpuset-cpus 0-1 --pids-limit N --restart POLICY\n\nA running container gets the new limits at once.",diff:"Usage: thinknsh diff CONTAINER\n\nA added, C changed, D deleted, compared with its image.",port:"Usage: thinknsh port CONTAINER [PRIVATE_PORT[/PROTO]]",commit:"Usage: thinknsh commit [-m MSG] [-a AUTHOR] [-c CHANGE]… [--no-pause] CONTAINER [REPO[:TAG]]\n\nMakes an image from the container's changes. -c applies build file instructions (CMD, ENV, EXPOSE, …).",save:"Usage: thinknsh save [-o FILE] IMAGE…\n\nWrites images (OCI layout + manifest.json) to FILE or stdout; loads in any engine.",load:"Usage: thinknsh load [-i FILE] [-q]\n\nReads images from FILE or stdin (plain or gzip tar).",import:"Usage: thinknsh import [-c CHANGE]… [-m MSG] [--platform P] FILE|- [REPO[:TAG]]\n\nMakes a one-layer image from a root filesystem tar.",export:"Usage: thinknsh container export [-o FILE] CONTAINER\n\nWrites the container's filesystem (without volumes) as a tar.",volume:"Usage: thinknsh volume ls | create [NAME] | rm [-f] NAME… | inspect NAME… | prune [-f] [-a]",network:"Usage: thinknsh network ls | create NAME | rm NAME… | inspect NAME… | connect NET CONTAINER | disconnect NET CONTAINER | prune [-f]",system:"Usage: thinknsh system info | df | prune [-f] [-a] [--volumes]",container:"Usage: thinknsh container ls | run | create | start | stop | restart | kill | rm | logs | exec | inspect | wait | prune\n | cp | export | diff | stats | top | pause | unpause | rename | update | commit | port",image:"Usage: thinknsh image ls | pull | push | build | tag | rm | inspect | prune | save | load | history | import",history:"Usage: thinknsh image history [-q] [--no-trunc] IMAGE",get workspace(){return require("./workspace").USAGE}};async function runCommand(e,t,{api:s=new DockApi,out:r=process.stdout,err:n=process.stderr,autoStart:o=!0}={}){const i=COMMANDS[e];if(!i)return n.write(`unknown command ${e}\n`),2;if((["--help","-h","help"].includes(t[0])||!["run","create","exec","compose","workspace"].includes(e)&&t.includes("--help"))&&USAGE[e])return r.write(USAGE[e]+"\n"),0;try{return o&&await s.ensureRunning({quiet:!0}),await i(t,{api:s,out:r,err:n})}catch(t){return t instanceof UsageError?(n.write(`${chalk.red("Error:")} ${t.message}\n${chalk.dim(`See: thinknsh ${e} --help`)}\n`),2):(n.write(`${chalk.red("Error:")} ${t.message}\n`),"run"===e?125:1)}}module.exports={COMMANDS,USAGE,runCommand,parsePublish,parseMountFlag,parseUlimit,resolveBind,splitRepoTag,createBody,pullImage,readStdin,confirm,each,CREATE_FLAGS};
@@ -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"),yaml=require("js-yaml"),chalk=require("chalk"),{EngineError,demuxer,qs,enc}=require("./api"),{parseArgs,parseBytes,UsageError}=require("./args"),os=require("os"),{splitWords}=require("../build/buildfile"),{readDotEnv,interpolate}=require("../deploy/compose"),{packContext}=require("./context"),{pullImage}=require("./commands"),{pipeSession}=require("./session"),fmt=require("./format"),{COMPOSE_LEGACY_FILES,BUILD_FILE_PARAM}=require("../api/wire"),FILES=["compose.yaml","compose.yml",...COMPOSE_LEGACY_FILES],L={project:"tnc.compose.project",service:"tnc.compose.service",number:"tnc.compose.number",hash:"tnc.compose.config-hash",file:"tnc.compose.file"},COLORS=[chalk.cyan,chalk.yellow,chalk.green,chalk.magenta,chalk.blue,chalk.red],sleep=e=>new Promise(t=>setTimeout(t,e)),seconds=e=>{if(null==e)return;if("number"==typeof e)return e;let t=0;const r=/(\d+(?:\.\d+)?)(ms|s|m|h)/g;let s;for(;s=r.exec(String(e));)t+=parseFloat(s[1])*{ms:.001,s:1,m:60,h:3600}[s[2]];return!t&&/^\d+(\.\d+)?$/.test(String(e))&&(t=parseFloat(e)),t},ns=e=>void 0===e?void 0:Math.round(1e9*seconds(e)),toList=e=>null==e?void 0:Array.isArray(e)?e.map(String):splitWords(String(e)),mapOrList=e=>e?Array.isArray(e)?Object.fromEntries(e.map(e=>{const t=String(e),r=t.indexOf("=");return-1===r?[t,null]:[t.slice(0,r),t.slice(r+1)]})):Object.fromEntries(Object.entries(e).map(([e,t])=>[e,null===t?null:String(t)])):{};function findFile(e){let t=path.resolve(e);for(;;){for(const e of FILES)if(fs.existsSync(path.join(t,e)))return path.join(t,e);const e=path.dirname(t);if(e===t)return null;t=e}}const isObj=e=>e&&"object"==typeof e&&!Array.isArray(e),APPEND=new Set(["ports","expose","dns","dns_search","dns_opt","cap_add","cap_drop","devices","tmpfs","env_file","external_links","security_opt","group_add","volumes_from","profiles","links"]),AS_MAP=new Set(["environment","labels","extra_hosts","sysctls","annotations"]);function volumeTarget(e){if(isObj(e))return e.target;const t=String(e).replace(/^[A-Za-z]:[\\/]/,"X").split(":");return 1===t.length?t[0]:t[1]}function extraHostsMap(e){return Array.isArray(e)?Object.fromEntries(e.map(e=>{const t=String(e),r=t.search(/[:=]/);return[t.slice(0,r),t.slice(r+1)]})):mapOrList(e)}function mergeDeep(e,t){if(!isObj(e)||!isObj(t))return t;const r={...e};for(const[e,s]of Object.entries(t))r[e]=isObj(r[e])&&isObj(s)?mergeDeep(r[e],s):s;return r}function mergeService(e={},t={}){const r={...e};for(const[e,s]of Object.entries(t)){const t=r[e];if(void 0!==t&&null!==s)if("command"===e||"entrypoint"===e)r[e]=s;else if(AS_MAP.has(e))r[e]="extra_hosts"===e?{...extraHostsMap(t),...extraHostsMap(s)}:{...mapOrList(t),...mapOrList(s)};else if(APPEND.has(e)){const o=[...[].concat(t),...[].concat(s)];r[e]=o.filter((e,t)=>o.findIndex(t=>JSON.stringify(t)===JSON.stringify(e))===t)}else if("volumes"===e){const o=new Map([...t,...s].map(e=>[volumeTarget(e),e]));r[e]=[...o.values()]}else if("secrets"===e||"configs"===e){const o=e=>isObj(e)?e.source:e,n=new Map([...t,...s].map(e=>[o(e),e]));r[e]=[...n.values()]}else if("depends_on"===e)r[e]={...depsOf({depends_on:t}),...depsOf({depends_on:s})};else if("networks"===e){const o=e=>Array.isArray(e)?Object.fromEntries(e.map(e=>[e,null])):e||{};r[e]=mergeDeep(o(t),o(s))}else"build"!==e||"string"!=typeof t&&"string"!=typeof s?isObj(t)&&isObj(s)?r[e]=mergeDeep(t,s):r[e]=s:r[e]=mergeDeep("string"==typeof t?{context:t}:t,"string"==typeof s?{context:s}:s);else r[e]=s}return r}function mergeDocs(e,t){const r={...e};for(const[s,o]of Object.entries(t))if("services"===s){r.services={...e.services||{}};for(const[e,t]of Object.entries(o||{}))r.services[e]=mergeService(r.services[e],t||{})}else["networks","volumes","secrets","configs"].includes(s)?r[s]=mergeDeep(e[s]||{},o||{}):"include"===s?r.include=[...e.include||[],...o||[]]:r[s]=o;return r}function absolutizeService(e,t){if(!isObj(e))return e||{};const r=e=>"string"!=typeof e?e:e.startsWith("~")?path.join(os.homedir(),e.slice(1)):path.isAbsolute(e)||/^[A-Za-z]:[\\/]/.test(e)?e:path.resolve(t,e),s={...e};return"string"==typeof s.build?s.build={context:r(s.build)}:isObj(s.build)&&(s.build={...s.build,context:r(s.build.context||".")}),s.env_file&&(s.env_file=[].concat(s.env_file).map(e=>isObj(e)?{...e,path:r(e.path)}:r(e))),Array.isArray(s.volumes)&&(s.volumes=s.volumes.map(e=>{if(isObj(e))return e.source&&/^[.~]/.test(e.source)?{...e,source:r(e.source)}:e;const t=String(e);return/^[.~]/.test(t)?r(t.split(":")[0])+t.slice(t.split(":")[0].length):t})),isObj(s.extends)&&s.extends.file&&(s.extends={...s.extends,file:r(s.extends.file)}),s}function absolutizeDoc(e,t){const r={...e};isObj(e.services)&&(r.services=Object.fromEntries(Object.entries(e.services).map(([e,r])=>[e,absolutizeService(r,t)])));for(const s of["secrets","configs"])isObj(e[s])&&(r[s]=Object.fromEntries(Object.entries(e[s]).map(([e,r])=>[e,isObj(r)&&r.file?{...r,file:path.resolve(t,r.file)}:r])));return r}function readComposeFile(e,t,r=new Set){if(r.has(e))throw new UsageError(`include cycle: ${[...r,e].map(e=>path.basename(e)).join(" → ")}`);if(!fs.existsSync(e))throw new UsageError(`compose file ${e} not found`);const s=path.dirname(e);let o=absolutizeDoc(interpolate(yaml.load(fs.readFileSync(e,"utf8"))||{},t),s);for(const n of[].concat(o.include||[])){const i=isObj(n)?[].concat(n.path):[n];for(const a of i){const i=path.resolve(isObj(n)&&n.project_directory?path.resolve(s,n.project_directory):s,String(a)),c=readComposeFile(i,isObj(n)&&n.env_file?{...[].concat(n.env_file).reduce((e,t)=>({...e,...readDotEnv(path.resolve(s,t))}),{}),...process.env}:t,new Set([...r,e]));for(const t of Object.keys(c.services||{}))if(o.services?.[t])throw new UsageError(`service ${t} is defined in ${path.basename(e)} and in the included ${path.basename(i)}`);const{include:l,name:p,...m}=c;o=mergeDocs(m,o)}}return delete o.include,o}function resolveExtends(e,t){const r={},s=new Map,o=(e,r,n)=>{const i=e[r];if(!i)throw new UsageError(`cannot extend service "${r}": it is not defined`);if(!i.extends)return i;if(n.includes(`${r}`))throw new UsageError(`extends cycle: ${[...n,r].join(" → ")}`);const a="string"==typeof i.extends?{service:i.extends}:i.extends;let c=e;a.file&&(s.has(a.file)||s.set(a.file,readComposeFile(a.file,t).services||{}),c=s.get(a.file));const l=o(c,a.service,[...n,r]),{extends:p,...m}=i;return mergeService(l,m)};for(const t of Object.keys(e))r[t]=o(e,t,[]);return r}function loadProject({file:e,files:t,projectName:r,profiles:s=[],envFile:o}={}){const n=[].concat(t||e||[]).filter(Boolean).map(e=>path.resolve(e)),i=n[0]||findFile(process.cwd());if(!i||!fs.existsSync(i))throw new UsageError(`no compose file found (looked for ${FILES.slice(0,2).join(", ")} here and in parent folders); pass -f <file>`);const a=path.dirname(i),c={...readDotEnv(o?path.resolve(o):path.join(a,".env")),...process.env};let l={};for(const e of n.length?n:[i])l=mergeDocs(l,readComposeFile(e,c));if(!l.services||"object"!=typeof l.services)throw new UsageError(`${path.basename(i)} has no services`);l.services=resolveExtends(l.services,c);const p=String(r||process.env.TNC_COMPOSE_PROJECT||l.name||path.basename(a)).toLowerCase().replace(/[^a-z0-9_-]/g,"");if(!p)throw new UsageError("could not derive a project name; pass -p <name>");const m={};for(const[e,t]of Object.entries(l.services))t.profiles?.length&&!t.profiles.some(e=>s.includes(e))||(m[e]=t||{});for(const[e,t]of Object.entries(m)){for(const r of Object.keys(depsOf(t)))if(!m[r])throw new UsageError(`service ${e} depends on undefined service ${r}`);for(const r of["secrets","configs"])for(const s of t[r]||[]){const t=isObj(s)?s.source:s;if(!l[r]?.[t])throw new UsageError(`service ${e} refers to undefined ${r.slice(0,-1)} ${t}; declare it under top-level "${r}:"`)}}return{file:i,files:n.length?n:[i],dir:a,name:p,doc:l,services:m,varEnv:c}}function fileMounts(e,t,r){const s=[];for(const o of["secrets","configs"])for(const n of r[o]||[]){const r=isObj(n)?n.source:String(n),i=e.doc[o][r]||{};if(i.external)throw new UsageError(`service ${t}: external ${o} need a cluster; use file: or environment: here`);const a=isObj(n)&&n.target?n.target.startsWith("/")?n.target:"secrets"===o?`/run/secrets/${n.target}`:`/${n.target}`:"secrets"===o?`/run/secrets/${r}`:`/${r}`;let c=i.file;if(c&&"darwin"===process.platform&&fs.existsSync(c)&&!path.resolve(c).startsWith(os.homedir()+path.sep)){const t=path.join(os.homedir(),".tnc","dock","compose",e.name,o);fs.mkdirSync(t,{recursive:!0,mode:448});const s=path.join(t,r);fs.writeFileSync(s,fs.readFileSync(c),{mode:384}),c=s}if(!c){let t;if(void 0!==i.environment){if(t=e.varEnv[i.environment],void 0===t)throw new UsageError(`${o.slice(0,-1)} ${r}: environment variable ${i.environment} is not set`)}else{if(void 0===i.content)throw new UsageError(`${o.slice(0,-1)} ${r} needs file:, environment: or content:`);t=String(i.content)}const s=path.join(os.homedir(),".tnc","dock","compose",e.name,o);fs.mkdirSync(s,{recursive:!0,mode:448}),c=path.join(s,r),fs.writeFileSync(c,t,{mode:384})}if(!fs.existsSync(c))throw new UsageError(`${o.slice(0,-1)} ${r}: file ${c} not found`);s.push(`${c}:${a}:ro`)}return s}function depsOf(e){const t=e.depends_on,r=/^service:(.+)$/.exec(e.network_mode||"")?.[1],s=r?{[r]:{condition:"service_started"}}:{};return t?Array.isArray(t)?{...s,...Object.fromEntries(t.map(e=>[e,{condition:"service_started"}]))}:{...s,...Object.fromEntries(Object.entries(t).map(([e,t])=>[e,{condition:t?.condition||"service_started"}]))}:s}function order(e,t){const r=[],s={},o=(t,n)=>{if(2!==s[t]){if(1===s[t])throw new UsageError(`dependency cycle: ${[...n,t].join(" → ")}`);s[t]=1;for(const r of Object.keys(depsOf(e[t])))o(r,[...n,t]);s[t]=2,r.push(t)}};for(const r of t?.length?t:Object.keys(e)){if(!e[r])throw new UsageError(`no such service: ${r}`);o(r,[])}return r}const netName=(e,t)=>{const r=e.doc.networks?.[t]||{};return r.external?r.name||t:r.name||`${e.name}_${t}`},volName=(e,t)=>{const r=e.doc.volumes?.[t]||{};return r.external?r.name||t:r.name||`${e.name}_${t}`},imageOf=(e,t,r)=>r.image||`${e.name}-${t}`,replicas=e=>e.deploy?.replicas??e.scale??1;function serviceNetworks(e,t){if(t.network_mode)return null;const r=t.networks;return r?Array.isArray(r)?Object.fromEntries(r.map(e=>[e,{aliases:[]}])):Object.fromEntries(Object.entries(r).map(([e,t])=>[e,{aliases:t?.aliases||[]}])):{default:{aliases:[]}}}function bindSpecs(e,t,r){const s=[],o={},n=[],i=t=>t.startsWith(".")?path.resolve(e.dir,t):t.startsWith("~")?path.join(require("os").homedir(),t.slice(1)):t,a=e=>e.startsWith(".")||e.startsWith("/")||e.startsWith("~")||/^[A-Za-z]:[\\/]/.test(e);for(const c of r.volumes||[]){if("object"==typeof c){if("tmpfs"===c.type){o[c.target]=c.tmpfs?.size?`size=${c.tmpfs.size}`:"";continue}if("bind"===c.type||c.source&&a(c.source)){s.push(`${i(c.source)}:${c.target}${c.read_only?":ro":""}`);continue}if(c.source){const t=volName(e,c.source);n.push(c.source),s.push(`${t}:${c.target}${c.read_only?":ro":""}`);continue}s.push(`${e.name}-${t}-${crypto.createHash("sha1").update(c.target).digest("hex").slice(0,8)}:${c.target}`);continue}const r=String(c),l=/^[A-Za-z]:[\\/]/.test(r)?[r.slice(0,2)+r.slice(2).split(":")[0],...r.slice(2).split(":").slice(1)]:r.split(":");if(1===l.length){s.push(`${e.name}-${t}-${crypto.createHash("sha1").update(l[0]).digest("hex").slice(0,8)}:${l[0]}`);continue}const[p,m,d]=l;if(a(p))s.push(`${i(p)}:${m}${d?`:${d}`:""}`);else{if(!e.doc.volumes||!(p in e.doc.volumes))throw new UsageError(`service ${t} refers to undefined volume ${p}; declare it under top-level "volumes:"`);n.push(p),s.push(`${volName(e,p)}:${m}${d?`:${d}`:""}`)}}for(const e of[].concat(r.tmpfs||[])){const[t,r]=String(e).split(/:(.*)/s);o[t]=r||""}return{binds:s,tmpfs:o,vols:n}}function portBindings(e,t){const{parsePublish:r}=require("./commands"),s={},o={};for(const n of t.ports||[]){let t,i;if("object"==typeof n){const e=n.protocol||"tcp",r=n.host_ip?n.host_ip.includes(":")?`[${n.host_ip}]:`:`${n.host_ip}:`:"";t=`${n.published?`${r}${n.published}:`:""}${n.target}/${e}`}else t=String(n);try{i=r([t])}catch(t){throw new UsageError(`service ${e}: ${t.message}`)}for(const[e,t]of Object.entries(i))(s[e]||=[]).push(...t),o[e]={}}for(const e of t.expose||[]){const[t,r="tcp"]=String(e).split("/");o[`${t}/${r}`]={}}return{bindings:s,exposed:o}}function envOf(e,t){const r={};for(const s of[].concat(t.env_file||[])){const t=path.resolve(e.dir,"object"==typeof s?s.path:s);if(!fs.existsSync(t)){if("object"==typeof s&&!1===s.required)continue;throw new UsageError(`env file ${t} not found`)}Object.assign(r,readDotEnv(t))}for(const[s,o]of Object.entries(mapOrList(t.environment)))null===o?void 0!==e.varEnv[s]&&(r[s]=e.varEnv[s]):r[s]=o;return Object.entries(r).map(([e,t])=>`${e}=${t}`)}function healthOf(e){const t=e.healthcheck;if(!t)return;if(t.disable)return{Test:["NONE"]};const r=void 0===t.test?void 0:Array.isArray(t.test)?t.test.map(String):["CMD-SHELL",String(t.test)];return{...r?{Test:r}:{},...t.interval?{Interval:ns(t.interval)}:{},...t.timeout?{Timeout:ns(t.timeout)}:{},...void 0!==t.retries?{Retries:Number(t.retries)}:{},...t.start_period?{StartPeriod:ns(t.start_period)}:{},...t.start_interval?{StartInterval:ns(t.start_interval)}:{}}}function restartOf(e){const t=e.restart||e.deploy?.restart_policy?.condition;if(!t||"no"===t||"none"===t)return{Name:"no"};if("any"===t)return{Name:"always"};const[r,s]=String(t).split(":");return{Name:r,MaximumRetryCount:s?Number(s):0}}function containerBody(e,t,r,s,o){const n=serviceNetworks(e,r),{binds:i,tmpfs:a}=bindSpecs(e,t,r);i.push(...fileMounts(e,t,r));const{bindings:c,exposed:l}=portBindings(t,r),p=r.deploy?.resources?.limits||{},m=r.deploy?.resources?.reservations||{};if((m.devices||[]).some(e=>(e.capabilities||[]).flat().includes("gpu"))||r.gpus)throw new UsageError(`service ${t}: GPUs cannot be passed to containers by dock yet`);const d=Object.entries(r.ulimits||{}).map(([e,t])=>isObj(t)?{Name:e,Soft:Number(t.soft),Hard:Number(t.hard)}:{Name:e,Soft:Number(t),Hard:Number(t)}),f=r.mem_limit||p.memory,u=r.cpus||p.cpus,h=n?Object.keys(n):[],g={...mapOrList(r.labels)},v={Image:imageOf(e,t,r),...void 0!==r.command?{Cmd:toList(r.command)}:{},...void 0!==r.entrypoint?{Entrypoint:toList(r.entrypoint)}:{},Env:envOf(e,r),...r.working_dir?{WorkingDir:r.working_dir}:{},...void 0!==r.user?{User:String(r.user)}:{},...r.hostname?{Hostname:r.hostname}:{},Tty:!!r.tty,OpenStdin:!!r.stdin_open,ExposedPorts:l,...healthOf(r)?{Healthcheck:healthOf(r)}:{},...r.stop_signal?{StopSignal:r.stop_signal}:{},...r.stop_grace_period?{StopTimeout:Math.ceil(seconds(r.stop_grace_period))}:{},HostConfig:{Binds:i,PortBindings:replicas(r)>1&&Object.values(c).some(e=>e.some(e=>e.HostPort))&&s>1?{}:c,NetworkMode:/^service:/.test(r.network_mode||"")?`container:${e.name}-${r.network_mode.slice(8)}-1`:r.network_mode||netName(e,h[0]),RestartPolicy:restartOf(r),...f?{Memory:parseBytes(f)}:{},...u?{NanoCpus:Math.round(1e9*parseFloat(u))}:{},...r.pids_limit?{PidsLimit:Number(r.pids_limit)}:{},...r.shm_size?{ShmSize:parseBytes(r.shm_size)}:{},...r.cgroup?{CgroupnsMode:r.cgroup}:{},Privileged:!!r.privileged,CapAdd:r.cap_add||[],CapDrop:r.cap_drop||[],ReadonlyRootfs:!!r.read_only,ExtraHosts:Array.isArray(r.extra_hosts)?r.extra_hosts:Object.entries(r.extra_hosts||{}).map(([e,t])=>`${e}:${t}`),Tmpfs:a,...r.init?{Init:!0}:{},...d.length?{Ulimits:d}:{},...r.sysctls?{Sysctls:mapOrList(r.sysctls)}:{},...r.dns?{Dns:[].concat(r.dns)}:{},...r.dns_search?{DnsSearch:[].concat(r.dns_search)}:{},...r.dns_opt?{DnsOptions:[].concat(r.dns_opt)}:{},...r.devices?.length?{Devices:r.devices.map(e=>{const[t,r,s]=String(isObj(e)?`${e.source}:${e.target}:${e.permissions||"rwm"}`:e).split(":");return{PathOnHost:t,PathInContainer:r||t,CgroupPermissions:s||"rwm"}})}:{},...r.group_add?{GroupAdd:r.group_add.map(String)}:{},...r.security_opt?{SecurityOpt:r.security_opt}:{},...r.mem_reservation||m.memory?{MemoryReservation:parseBytes(r.mem_reservation||m.memory)}:{},...r.memswap_limit?{MemorySwap:"-1"===String(r.memswap_limit)?-1:parseBytes(r.memswap_limit)}:{},...r.cpuset?{CpusetCpus:String(r.cpuset)}:{},...r.cpu_shares?{CpuShares:Number(r.cpu_shares)}:{},...r.logging?{LogConfig:{Type:r.logging.driver||"json-file",Config:r.logging.options||{}}}:{},...r.volumes_from?{VolumesFrom:r.volumes_from.map(t=>{const[r,s]=String(t).replace(/^service:/,"").split(":");return`${e.services[r]?`${e.name}-${r}-1`:r.replace(/^container:/,"")}${s?`:${s}`:""}`})}:{}},...n?{NetworkingConfig:{EndpointsConfig:Object.fromEntries(h.map(r=>[netName(e,r),{Aliases:[t,...n[r].aliases]}]))}}:{}},w=crypto.createHash("sha256").update(JSON.stringify([v,g,o])).digest("hex");return v.Labels={...g,[L.project]:e.name,[L.service]:t,[L.number]:String(s),[L.hash]:w,[L.file]:e.file},v}async function projectContainers(e,t,{all:r=!0,service:s}={}){const o=[`${L.project}=${t}`,...s?[`${L.service}=${s}`]:[]];return e.json("GET",`/containers/json${qs({all:r||void 0,filters:{label:o}})}`)}const cname=e=>(e.Names?.[0]||"").replace(/^\//,"");function reporter(e){const t=e.isTTY;return(r,s,o,n=!0)=>{const i=n?chalk.green("✔"):chalk.red("✘");e.write(` ${t?i:n?"ok ":"err"} ${r.padEnd(9)} ${s.padEnd(28)} ${n?o:chalk.red(o)}\n`)}}async function exists(e,t){try{return await e.json("GET",t),!0}catch(e){if(e instanceof EngineError&&404===e.status)return!1;throw e}}async function ensureNetworks(e,t,r,s){const o=new Set;for(const e of Object.values(r)){const r=serviceNetworks(t,e);r&&Object.keys(r).forEach(e=>o.add(e))}for(const r of o){if(!("default"===r||t.doc.networks&&r in t.doc.networks))throw new UsageError(`undefined network ${r}; declare it under top-level "networks:"`);const o=t.doc.networks?.[r]||{},n=netName(t,r);if(!await exists(e,`/networks/${enc(n)}`)){if(o.external)throw new UsageError(`network ${n} is declared external but does not exist; create it with: thinknsh network create ${n}`);await e.json("POST","/networks/create",{Name:n,Driver:o.driver||"bridge",Internal:!!o.internal,Labels:{...mapOrList(o.labels),[L.project]:t.name,"tnc.compose.network":r}}),s("Network",n,"Created")}}}function projectNetworks(e){const t=new Set;for(const r of Object.values(e.services)){const s=serviceNetworks(e,r);s&&Object.keys(s).forEach(e=>t.add(e))}return[...t].map(t=>({key:t,name:netName(e,t),external:!!e.doc.networks?.[t]?.external}))}async function ensureVolumes(e,t,r,s){const o=new Set;for(const[e,s]of Object.entries(r))bindSpecs(t,e,s).vols.forEach(e=>o.add(e));for(const r of o){const o=t.doc.volumes?.[r]||{},n=volName(t,r);if(!await exists(e,`/volumes/${enc(n)}`)){if(o.external)throw new UsageError(`volume ${n} is declared external but does not exist; create it with: thinknsh volume create ${n}`);await e.json("POST","/volumes/create",{Name:n,Labels:{...mapOrList(o.labels),[L.project]:t.name,"tnc.compose.volume":r}}),s("Volume",n,"Created")}}}async function buildService(e,t,r,s,{out:o,noCache:n=!1,pull:i=!1}={}){const a="string"==typeof s.build?{context:s.build}:s.build,c=path.resolve(t.dir,a.context||"."),l=a[BUILD_FILE_PARAM]?path.resolve(c,a[BUILD_FILE_PARAM]):void 0,p=(a.secrets||[]).map(e=>{const s=isObj(e)?e.source:String(e),o=t.doc.secrets?.[s];if(!o)throw new UsageError(`service ${r}: build secret ${s} is not declared under top-level "secrets:"`);const n=o.file?fs.readFileSync(o.file):void 0!==o.environment?Buffer.from(String(t.varEnv[o.environment]??"")):Buffer.from(String(o.content??""));return{id:isObj(e)&&e.target?path.posix.basename(e.target):s,data:n}}),{tar:m,buildFile:d}=packContext(c,l,{secrets:p}),f=chalk.cyan;o.write(`${f(`[${r}]`)} building ${imageOf(t,r,s)} (context ${fmt.size(m.length)})\n`);const u=Object.fromEntries(Object.entries(mapOrList(a.args)).map(([e,r])=>[e,null===r?t.varEnv[e]??null:r]));let h="";await e.progress("POST",`/build${qs({t:[imageOf(t,r,s),...a.tags||[]],[BUILD_FILE_PARAM]:d,buildargs:u,target:a.target,nocache:n,pull:i,labels:Object.keys(mapOrList(a.labels)).length?mapOrList(a.labels):void 0,networkmode:a.network,platform:(a.platforms||[])[0]||s.platform})}`,m,e=>{if(!e.stream)return;h+=e.stream;const t=h.split("\n");h=t.pop();for(const e of t)e.trim()&&o.write(`${f(`[${r}]`)} ${e}\n`)},{"Content-Type":"application/x-tar"}),h.trim()&&o.write(`${f(`[${r}]`)} ${h}\n`)}async function imageId(e,t){try{return(await e.json("GET",`/images/${t}/json`)).Id}catch(e){if(e instanceof EngineError&&404===e.status)return null;throw e}}async function ensureImage(e,t,r,s,{build:o,out:n,err:i}){const a=imageOf(t,r,s),c=s.pull_policy||"missing";if(!s.build||!o&&"build"!==c&&await imageId(e,a)){if("always"===c||!await imageId(e,a)&&"never"!==c){if(s.build&&"never"===c)throw new UsageError(`image ${a} for service ${r} is missing`);n.write(`${chalk.cyan(`[${r}]`)} pulling ${a}\n`),await pullImage(e,a,{err:i,platform:s.platform})}}else await buildService(e,t,r,s,{out:n});const l=await imageId(e,a);if(l&&s.platform){const t=await e.json("GET",`/images/${a}/json`),o=s.platform.split("/")[1];if(t.Architecture&&o&&t.Architecture!==o&&"never"!==c)return n.write(`${chalk.cyan(`[${r}]`)} pulling ${a} for ${s.platform}\n`),await pullImage(e,a,{err:i,platform:s.platform}),imageId(e,a)}if(!l)throw new UsageError(`image ${a} for service ${r} is missing and has no build section`);return l}async function waitForCondition(e,t,r,s,o=3e5){const n=Date.now()+o;for(;;){const o=await projectContainers(e,t.name,{service:r});if(!o.length)throw new Error(`dependency ${r} has no container`);const i=await Promise.all(o.map(t=>e.json("GET",`/containers/${t.Id}/json`)));if("service_healthy"===s){const e=i[0].Config.Healthcheck?.Test;if(!e||"NONE"===e[0])throw new Error(`dependency ${r} has no healthcheck, so "service_healthy" can never be met`);const t=i.find(e=>!e.State.Running);if(t)throw new Error(`dependency failed to start: ${r} exited with code ${t.State.ExitCode}; see: thinknsh compose logs ${r}`);const s=i.map(e=>e.State.Health?.Status||"starting");if(s.includes("unhealthy"))throw new Error(`dependency failed to start: ${r} is unhealthy; see: thinknsh compose logs ${r}`);if(s.every(e=>"healthy"===e))return}else{if("service_completed_successfully"!==s)return;{const e=i.filter(e=>"exited"===e.State.Status),t=e.find(e=>0!==e.State.ExitCode);if(t)throw new Error(`service ${r} didn't complete successfully: exit ${t.State.ExitCode}`);if(e.length===i.length)return}}if(Date.now()>n)throw new Error(`timed out waiting for ${r} (${s})`);await sleep(500)}}const GLOBAL={file:{short:"f",type:"list"},"project-name":{short:"p",type:"string"},profile:{type:"list"},"env-file":{type:"string"}};async function up(e,t,r){const{api:s,out:o,err:n}=r,{opts:i,args:a}=parseArgs(t,{detach:{short:"d",type:"bool"},build:{type:"bool"},"force-recreate":{type:"bool"},"no-recreate":{type:"bool"},"remove-orphans":{type:"bool"},"no-start":{type:"bool"},"abort-on-container-exit":{type:"bool"},timeout:{short:"t",type:"string"},wait:{type:"bool"}}),c=order(e.services,a),l=reporter(o),p=Object.fromEntries(c.map(t=>[t,e.services[t]])),m=await projectContainers(s,e.name);for(const t of m.filter(t=>!e.services[t.Labels[L.service]]))i["remove-orphans"]?(await s.json("DELETE",`/containers/${t.Id}?force=1`),l("Container",cname(t),"Removed")):n.write(chalk.yellow(`found orphan container ${cname(t)} (service ${t.Labels[L.service]} is no longer in the file); remove it with --remove-orphans\n`));await ensureNetworks(s,e,p,l),await ensureVolumes(s,e,p,l);const d={};for(const t of c)d[t]=await ensureImage(s,e,t,e.services[t],{build:i.build,out:o,err:n});const f=[];for(const t of c){const r=e.services[t];for(const[t,{condition:n}]of Object.entries(depsOf(r)))"service_started"===n||i["no-start"]||(o.write(chalk.dim(` … waiting for ${t} (${n.replace("service_","")})\n`)),await waitForCondition(s,e,t,n));const n=(await projectContainers(s,e.name,{service:t})).sort((e,t)=>Number(e.Labels[L.number])-Number(t.Labels[L.number])),a=replicas(r);for(const e of n.filter(e=>Number(e.Labels[L.number])>a))await s.json("DELETE",`/containers/${e.Id}?force=1`),l("Container",cname(e),"Removed");for(let o=1;o<=a;o++){const c=containerBody(e,t,r,o,d[t]),p=r.container_name&&1===a?r.container_name:`${e.name}-${t}-${o}`,m=n.find(e=>Number(e.Labels[L.number])===o);let u,h=m?.Id;!m||i["force-recreate"]||!i["no-recreate"]&&m.Labels[L.hash]!==c.Labels[L.hash]?(m&&await s.json("DELETE",`/containers/${m.Id}?force=1`),h=(await s.json("POST",`/containers/create${qs({name:p,platform:r.platform})}`,c)).Id,u=m?"Recreated":"Created"):u="running"===m.State?"Running":"Started",i["no-start"]||"Running"===u||(await s.json("POST",`/containers/${h}/start`),"Created"!==u&&"Recreated"!==u||(u=`${u}, started`)),l("Container",p,u),f.push({id:h,name:p,service:t,fresh:"Running"!==u})}}if(i.wait){for(const t of c)e.services[t].healthcheck&&await waitForCondition(s,e,t,"service_healthy");o.write(chalk.green(" all services are up and healthy\n"))}if(i.detach||i["no-start"]||i.wait)return 0;o.write(chalk.dim("Attaching to "+f.map(e=>e.name).join(", ")+" (Ctrl-C stops them)\n"));let u=!1;const h=async()=>{u&&process.exit(130),u=!0,o.write(chalk.yellow("\nGracefully stopping… (press Ctrl-C again to force)\n")),await Promise.all(f.map(e=>s.json("POST",`/containers/${e.id}/stop${qs({t:i.timeout})}`).catch(()=>{})))};process.on("SIGINT",h);const g=Math.max(...f.map(e=>e.name.length));let v=0;try{await Promise.all(f.map((e,t)=>followLogs(s,e,{prefix:COLORS[t%COLORS.length](`${e.name.padEnd(g)} |`),tail:e.fresh?"all":"0",follow:!0,out:o}).then(async()=>{const t=await s.json("GET",`/containers/${e.id}/json`).catch(()=>null);t&&!u&&o.write(`${e.name} exited with code ${t.State.ExitCode}\n`),i["abort-on-container-exit"]&&!u&&(v=t?.State.ExitCode||0,await h())})))}finally{process.removeListener("SIGINT",h)}return v}async function followLogs(e,t,{prefix:r,tail:s="all",follow:o=!1,timestamps:n=!1,out:i}){const a=await e.stream("GET",`/containers/${t.id}/logs${qs({stdout:1,stderr:1,follow:o,tail:s,timestamps:n})}`),c=(a.headers["content-type"]||"").includes("raw-stream");let l={out:"",err:""};const p=(e,t)=>{l[e]+=t.toString("utf8");const s=l[e].split("\n");l[e]=s.pop();for(const t of s)("err"===e?process.stderr:i).write(`${r} ${t.replace(/\r$/,"")}\n`)},m=c?e=>p("out",e):demuxer(e=>p("out",e),e=>p("err",e));for await(const e of a)m(e);for(const e of["out","err"])l[e]&&("err"===e?process.stderr:i).write(`${r} ${l[e]}\n`)}async function down(e,t,{api:r,out:s}){const{opts:o}=parseArgs(t,{volumes:{short:"v",type:"bool"},rmi:{type:"string"},timeout:{short:"t",type:"string"},"remove-orphans":{type:"bool"}}),n=reporter(s),i=await projectContainers(r,e.name),a=new Set;await Promise.all(i.filter(e=>"running"===e.State).map(e=>r.json("POST",`/containers/${e.Id}/stop${qs({t:o.timeout})}`).catch(()=>{})));for(const e of i)a.add(e.Image),await r.json("DELETE",`/containers/${e.Id}${qs({force:!0,v:o.volumes})}`),n("Container",cname(e),"Removed");const c=await r.json("GET",`/networks${qs({filters:{label:[`${L.project}=${e.name}`]}})}`);for(const e of c)await r.json("DELETE",`/networks/${e.Id}`).catch(()=>{}),n("Network",e.Name,"Removed");if(o.volumes){const t=await r.json("GET",`/volumes${qs({filters:{label:[`${L.project}=${e.name}`]}})}`);for(const e of t.Volumes||[])await r.json("DELETE",`/volumes/${enc(e.Name)}`).catch(()=>{}),n("Volume",e.Name,"Removed")}if(o.rmi){if(!["local","all"].includes(o.rmi))throw new UsageError("--rmi takes local or all");for(const[t,s]of Object.entries(e.services)){if("local"===o.rmi&&s.image)continue;const i=imageOf(e,t,s);try{await r.json("DELETE",`/images/${i}`),n("Image",i,"Removed")}catch(e){}}}return 0}async function psCmd(e,t,{api:r,out:s}){const{opts:o,args:n}=parseArgs(t,{all:{short:"a",type:"bool"},quiet:{short:"q",type:"bool"},json:{type:"bool"}});let i=await projectContainers(r,e.name,{all:o.all});return n.length&&(i=i.filter(e=>n.includes(e.Labels[L.service]))),o.json?(s.write(JSON.stringify(i,null,2)+"\n"),0):o.quiet?(i.forEach(e=>s.write(`${e.Id}\n`)),0):(fmt.table(["NAME","IMAGE","SERVICE","CREATED","STATUS","PORTS"],i.sort((e,t)=>cname(e).localeCompare(cname(t))).map(e=>[cname(e),e.Image,e.Labels[L.service],fmt.ago(e.Created),e.Status,fmt.ports(e.Ports)]),{out:s}),0)}async function logsCmd(e,t,{api:r,out:s}){const{opts:o,args:n}=parseArgs(t,{follow:{short:"f",type:"bool"},tail:{short:"n",type:"string"},timestamps:{short:"t",type:"bool"},"no-prefix":{type:"bool"}});let i=await projectContainers(r,e.name);n.length&&(i=i.filter(e=>n.includes(e.Labels[L.service]))),i.sort((e,t)=>cname(e).localeCompare(cname(t)));const a=Math.max(0,...i.map(e=>cname(e).length));return await Promise.all(i.map((e,t)=>followLogs(r,{id:e.Id},{prefix:o["no-prefix"]?"":COLORS[t%COLORS.length](`${cname(e).padEnd(a)} |`),tail:o.tail||"all",follow:o.follow,timestamps:o.timestamps,out:s}))),0}async function forEachContainer(e,t,{api:r,out:s},o,n){const{opts:i,args:a}=parseArgs(t,{timeout:{short:"t",type:"string"},force:{short:"f",type:"bool"},stop:{short:"s",type:"bool"},volumes:{short:"v",type:"bool"}});let c=await projectContainers(r,e.name);a.length&&(c=c.filter(e=>a.includes(e.Labels[L.service])));const l=reporter(s);for(const e of c)await n(e,i),l("Container",cname(e),o);return 0}async function execCmd(e,t,{api:r}){const{opts:s,args:o}=parseArgs(t,{"no-tty":{short:"T",type:"bool"},index:{type:"string",default:"1"},env:{short:"e",type:"list"},workdir:{short:"w",type:"string"},user:{short:"u",type:"string"},detach:{short:"d",type:"bool"},privileged:{type:"bool"}},{stopAtPositional:!0});if(o.length<2)throw new UsageError("compose exec takes SERVICE COMMAND…, e.g. thinknsh compose exec db psql -U postgres");const[n,...i]=o,a=(await projectContainers(r,e.name,{all:!1,service:n})).find(e=>e.Labels[L.number]===String(s.index));if(!a)throw new UsageError(`service ${n} is not running`);const c=!s["no-tty"]&&process.stdin.isTTY&&process.stdout.isTTY,{Id:l}=await r.json("POST",`/containers/${a.Id}/exec`,{Cmd:i,AttachStdin:!s.detach,AttachStdout:!s.detach,AttachStderr:!s.detach,Tty:c,Env:s.env,...s.workdir?{WorkingDir:s.workdir}:{},...s.user?{User:s.user}:{},Privileged:s.privileged});if(s.detach)return await r.json("POST",`/exec/${l}/start`,{Detach:!0}),0;const p=await r.hijack(`/exec/${l}/start`,{Detach:!1,Tty:c});return await pipeSession(p,{stdin:!0,resize:c?(e,t)=>r.json("POST",`/exec/${l}/resize${qs({h:e,w:t})}`).catch(()=>{}):null}),(await r.json("GET",`/exec/${l}/json`)).ExitCode??0}const HELP='Usage: thinknsh compose [-f FILE] [-p NAME] [--profile P] COMMAND\n\nCommands:\n up [-d] [--build] [--wait] [services…] Create and start the app (foreground unless -d)\n down [-v] [--rmi local|all] Stop and remove containers and networks (-v: volumes too)\n ps [-a] [-q] List the app\'s containers\n logs [-f] [--tail N] [services…] Show output, one color per container\n exec [-T] SERVICE CMD… Run a command in a running service\n build [--no-cache] [services…] Build the services that have "build:"\n pull [services…] Pull service images\n start | stop | restart [services…]\n rm [-f] [-s] [-v] [services…] Remove stopped containers\n config Print the resolved file\n';async function compose(e,t){const{opts:r,args:s}=parseArgs(e,GLOBAL,{stopAtPositional:!0}),[o,...n]=s;if(!o||"help"===o||n.includes("--help"))return t.out.write(HELP),o?0:2;const i=loadProject({files:r.file,projectName:r["project-name"],profiles:r.profile,envFile:r["env-file"]}),{api:a}=t;switch(o){case"up":return up(i,n,t);case"down":return down(i,n,t);case"ps":return psCmd(i,n,t);case"logs":return logsCmd(i,n,t);case"exec":return execCmd(i,n,t);case"config":return t.out.write(yaml.dump({name:i.name,...i.doc,services:i.services},{lineWidth:-1})),0;case"build":{const{opts:e,args:r}=parseArgs(n,{"no-cache":{type:"bool"},pull:{type:"bool"}});for(const s of order(i.services,r))i.services[s].build&&await buildService(a,i,s,i.services[s],{out:t.out,noCache:e["no-cache"],pull:e.pull});return 0}case"pull":{const{args:e}=parseArgs(n,{quiet:{short:"q",type:"bool"}});for(const r of order(i.services,e)){const e=i.services[r];e.image&&(t.out.write(`${chalk.cyan(`[${r}]`)} pulling ${e.image}\n`),await pullImage(a,e.image,{err:t.err}))}return 0}case"start":return forEachContainer(i,n,t,"Started",e=>a.json("POST",`/containers/${e.Id}/start`));case"stop":return forEachContainer(i,n,t,"Stopped",(e,t)=>a.json("POST",`/containers/${e.Id}/stop${qs({t:t.timeout})}`));case"restart":return forEachContainer(i,n,t,"Restarted",(e,t)=>a.json("POST",`/containers/${e.Id}/restart${qs({t:t.timeout})}`));case"rm":return forEachContainer(i,n,t,"Removed",async(e,t)=>{if("running"===e.State&&!t.stop)throw new UsageError(`${cname(e)} is running; stop it first or pass -s`);await a.json("DELETE",`/containers/${e.Id}${qs({force:!0,v:t.volumes})}`)});default:throw new UsageError(`unknown compose command "${o}"`)}}module.exports={compose,loadProject,containerBody,order,L,FILES,projectNetworks,ensureNetworks,mergeService,mergeDocs};
@@ -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"),{BUILD_FILE,BUILD_IGNORE_FILE}=require("../api/wire"),IGNORE_FILES=[".tncignore",BUILD_IGNORE_FILE],BUILD_FILES=[BUILD_FILE,"Containerfile"];function globToRegExp(e){let t="";for(let n=0;n<e.length;n++){const r=e[n];"*"===r?"*"===e[n+1]?(n++,"/"===e[n+1]?(n++,t+="(?:.*/)?"):t+=".*"):t+="[^/]*":t+="?"===r?"[^/]":r.replace(/[.+^${}()|[\]\\]/g,"\\$&")}return new RegExp(`^${t}$`)}function readIgnore(e){for(const t of IGNORE_FILES){const n=path.join(e,t);if(fs.existsSync(n))return{name:t,rules:fs.readFileSync(n,"utf8").split(/\r?\n/).map(e=>e.trim()).filter(e=>e&&!e.startsWith("#")).map(e=>{const t=e.startsWith("!");let n=(t?e.slice(1):e).trim().replace(/\\/g,"/").replace(/^\/+/,"").replace(/^\.\//,"");const r=n.endsWith("/");return n=n.replace(/\/+$/,""),{negate:t,re:globToRegExp(n),dirOnly:r}})}}return{name:null,rules:[]}}function makeFilter(e){const t=(t,n)=>{let r=!1;for(const i of e)i.dirOnly&&!n||i.re.test(t)&&(r=!i.negate);return r};return(n,r)=>{const i=n.split("/");for(let r=1;r<i.length;r++)if(t(i.slice(0,r).join("/"),!0)&&!e.some(e=>e.negate&&e.re.test(n)))return!0;return t(n,r)}}const{entry}=require("../image/tar");function packContext(e,t,{secrets:n=[]}={}){const r=path.resolve(e);let i,o=null;if(t){const e=path.resolve(t),n=path.relative(r,e);if(!fs.existsSync(e))throw new Error(`build file ${t} not found`);n.startsWith("..")||path.isAbsolute(n)?(i=".tnc-buildfile",o=e):i=n.split(path.sep).join("/")}else if(i=BUILD_FILES.find(e=>fs.existsSync(path.join(r,e))),!i)throw new Error(`no build file (${BUILD_FILES.join(" or ")}) in ${r}; pass -f <file>`);const s=readIgnore(r),l=makeFilter(s.rules),a=new Set([i,s.name].filter(Boolean)),c="win32"!==process.platform,f=[];let p=0;const m=e=>{const t=path.join(r,e);for(const n of fs.readdirSync(t).sort()){const r=e?`${e}/${n}`:n,i=path.join(t,n),o=fs.lstatSync(i),s=o.isDirectory();if(!a.has(r)&&l(r,s))continue;if(".tnc-build-secrets"===r)continue;const u=o.mtimeMs/1e3;if(o.isSymbolicLink())entry(f,{name:r,mode:511,size:0,mtime:u,type:"2",linkname:fs.readlinkSync(i).split(path.sep).join("/")});else if(s)entry(f,{name:`${r}/`,mode:c?o.mode:493,size:0,mtime:u,type:"5"}),m(r);else if(o.isFile()){const e=fs.readFileSync(i);entry(f,{name:r,mode:c?o.mode:420,size:e.length,mtime:u,type:"0"},e),p++}}};if(m(""),o){const e=fs.readFileSync(o);entry(f,{name:i,mode:420,size:e.length,mtime:Date.now()/1e3,type:"0"},e)}for(const{id:e,data:t}of n)entry(f,{name:`.tnc-build-secrets/${e}`,mode:384,size:t.length,mtime:Date.now()/1e3,type:"0"},t);return f.push(Buffer.alloc(1024)),{tar:Buffer.concat(f),buildFile:i,files:p}}module.exports={packContext,makeFilter,readIgnore,globToRegExp};
@@ -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"),{Readable}=require("stream"),{EngineError,qs,enc}=require("./api"),{parseArgs,UsageError}=require("./args"),{entryHeader,pad,END,TarReader}=require("../image/tar"),{safeRel}=require("../image/archive"),{PATH_STAT_HEADER}=require("../api/wire"),DIR_BIT=2**31,LINK_BIT=2**27,isDirMode=t=>t>=DIR_BIT,isLinkMode=t=>Math.floor(t/LINK_BIT)%2==1;function splitSpec(t){if(/^[A-Za-z]:[\\/]/.test(t))return{local:t};const e=/^([a-zA-Z0-9][a-zA-Z0-9_.-]*):(.*)$/.exec(t);return e?{container:e[1],path:e[2]||"/"}:{local:t}}async function containerStat(t,e,r){try{const n=await t.stream("HEAD",`/containers/${enc(e)}/archive${qs({path:r})}`);n.resume();const a=n.headers[PATH_STAT_HEADER.toLowerCase()];return a?JSON.parse(Buffer.from(a,"base64").toString("utf8")):null}catch(t){if(t instanceof EngineError&&404===t.status)return null;throw t}}function lstatOrNull(t){try{return fs.lstatSync(t)}catch(t){return null}}function statOrNull(t){try{return fs.statSync(t)}catch(t){return null}}function assertNoLinkParents(t,e){const r=path.relative(t,e);if(r.startsWith("..")||path.isAbsolute(r))throw new Error(`refusing to write outside ${t}: ${e}`);let n=t;for(const t of r.split(path.sep).slice(0,-1)){n=path.join(n,t);const e=lstatOrNull(n);if(e&&e.isSymbolicLink())throw new Error(`refusing to write through the symlink ${n}`)}}async function extractLocal(t,e){const r=path.resolve(e),n=[],a=t=>{if("."===t||t.startsWith("./")){const e=t.replace(/^\.\/?/,"").replace(/\/+$/,"");return""===e?"":safeRel(e)}const e=safeRel(t);return null===e?null:e.split("/").slice(1).join("/")},o=new TarReader(t=>{const e=a(t.name);if(null===e)return null;const o=e?path.join(r,...e.split("/")):r;if(assertNoLinkParents(path.dirname(r),o),"5"===t.type)return fs.mkdirSync(o,{recursive:!0}),n.push([o,t.mtime]),null;if(fs.mkdirSync(path.dirname(o),{recursive:!0}),"2"===t.type){try{fs.unlinkSync(o)}catch(t){}return fs.symlinkSync(t.linkname,o),null}if("1"===t.type){const e=a(t.linkname);if(null!==e){try{fs.unlinkSync(o)}catch(t){}fs.copyFileSync(e?path.join(r,...e.split("/")):r,o)}return null}if("0"!==t.type&&"7"!==t.type)return null;lstatOrNull(o)?.isSymbolicLink()&&fs.unlinkSync(o);const i=fs.openSync(o,"w",511&t.mode||420);return{write:t=>fs.writeSync(i,t),end:()=>{fs.closeSync(i);try{fs.chmodSync(o,4095&t.mode),fs.utimesSync(o,t.mtime,t.mtime)}catch(t){}}}});for await(const e of t)o.push(e);o.end();for(const[t,e]of n.reverse())try{fs.utimesSync(t,e,e)}catch(t){}}function packLocal(t,e,{follow:r=!1,keepOwner:n=!1}={}){const a=path.resolve(t);return Readable.from(async function*o(){const t=function*(e,a,o){const i=o&&r?fs.statSync(e):fs.lstatSync(e),s={mode:i.mode,mtime:i.mtimeMs/1e3,uid:n?i.uid:0,gid:n?i.gid:0};if(i.isSymbolicLink())yield*entryHeader({...s,name:a,size:0,type:"2",linkname:fs.readlinkSync(e).split(path.sep).join("/")});else if(i.isDirectory()){yield*entryHeader({...s,name:`${a}/`,size:0,type:"5"});for(const r of fs.readdirSync(e).sort())yield*t(path.join(e,r),`${a}/${r}`,!1)}else if(i.isFile()){yield*entryHeader({...s,name:a,size:i.size,type:"0"});const t=fs.openSync(e,"r");try{const e=Buffer.alloc(1<<20);let r;for(;(r=fs.readSync(t,e,0,e.length,null))>0;)yield Buffer.from(e.subarray(0,r))}finally{fs.closeSync(t)}yield pad(i.size)}};yield*t(a,e||".",!0),yield END}())}const CP_USAGE="cp takes SRC DEST where one side is CONTAINER:PATH, e.g. thinknsh cp web:/etc/nginx/nginx.conf ./ or thinknsh cp ./site web:/usr/share/nginx/html";async function cp(t,{api:e,out:r,err:n}){const{opts:a,args:o}=parseArgs(t,{archive:{short:"a",type:"bool"},"follow-link":{short:"L",type:"bool"},quiet:{short:"q",type:"bool"}});if(2!==o.length)throw new UsageError(CP_USAGE);const[i,s]=o.map(splitSpec);if(!!i.container==!!s.container)throw new UsageError(i.container?"copying between containers is not supported; copy to this machine first":CP_USAGE);if(i.container){let t=i.path,o=await containerStat(e,i.container,t);if(!o)throw new EngineError(`Could not find the file ${t} in container ${i.container}`,404);a["follow-link"]&&isLinkMode(o.mode)&&o.linkTarget&&(t=o.linkTarget,o=await containerStat(e,i.container,t));const c=await e.stream("GET",`/containers/${enc(i.container)}/archive${qs({path:t})}`);if("-"===s.local){for await(const t of c)r.write(t);return 0}const l=/\/\.$/.test(t),p=isDirMode(o.mode),f=path.resolve(s.local),u=statOrNull(f);if(!u&&/[\\/]$/.test(s.local)&&!p)throw new UsageError(`destination directory ${s.local} does not exist`);if(u&&!u.isDirectory()&&p)throw new UsageError(`cannot copy a directory to a file: ${s.local}`);if(!u&&!statOrNull(path.dirname(f)))throw new UsageError(`destination directory ${path.dirname(f)} does not exist`);const h=!u?.isDirectory()||p&&l?f:path.join(f,o.name);return await extractLocal(c,h),!a.quiet&&n.isTTY&&n.write(`Successfully copied ${o.size>=0?o.size:0}B to ${s.local}\n`),0}const c=s.container;if("-"===i.local){const t=await containerStat(e,c,s.path);if(!t||!isDirMode(t.mode))throw new UsageError(`destination "${s.path}" must be a directory when the source is a tar on stdin`);return await(await e.stream("PUT",`/containers/${enc(c)}/archive${qs({path:s.path,copyUIDGID:a.archive})}`,process.stdin,{"Content-Type":"application/x-tar"})).resume(),0}const l=path.resolve(i.local),p=a["follow-link"]?statOrNull(l):lstatOrNull(l);if(!p)throw new UsageError(`could not find ${i.local}`);const f=/[\\/]\.$/.test(i.local);let u,h,d=await containerStat(e,c,s.path);if(d&&isLinkMode(d.mode)&&d.linkTarget){const t=await containerStat(e,c,d.linkTarget);t&&isDirMode(t.mode)&&(d=t)}if(d&&isDirMode(d.mode))u=s.path,h=p.isDirectory()&&f?"":path.basename(l);else{if(d&&p.isDirectory())throw new UsageError(`cannot copy a directory to a file: ${s.path}`);if(!d&&/\/$/.test(s.path))throw new UsageError(`destination directory ${s.path} does not exist in ${c}`);u=path.posix.dirname(path.posix.resolve("/",s.path));const t=await containerStat(e,c,u);if(!t||!isDirMode(t.mode))throw new UsageError(`destination directory ${u} does not exist in ${c}`);h=path.posix.basename(s.path)}const m=packLocal(l,h,{follow:a["follow-link"],keepOwner:a.archive});return(await e.stream("PUT",`/containers/${enc(c)}/archive${qs({path:u,copyUIDGID:a.archive})}`,m,{"Content-Type":"application/x-tar"})).resume(),!a.quiet&&n.isTTY&&n.write(`Successfully copied ${i.local} to ${c}:${s.path}\n`),0}async function toFileOrStdout(t,e,r,n){if(!e&&r.isTTY)throw new UsageError(`refusing to write ${n} to a terminal; use -o FILE or redirect the output`);const a=e?fs.createWriteStream(e):r;for await(const e of t)a.write(e)||await new Promise(t=>a.once("drain",t));e&&await new Promise((t,e)=>a.end(r=>r?e(r):t()))}async function exportCmd(t,{api:e,out:r}){const{opts:n,args:a}=parseArgs(t,{output:{short:"o",type:"string"}});if(1!==a.length)throw new UsageError("export takes one container, e.g. thinknsh container export -o web.tar web");const o=await e.stream("GET",`/containers/${enc(a[0])}/export`);return await toFileOrStdout(o,n.output,r,"the export"),0}async function diff(t,{api:e,out:r}){const{args:n}=parseArgs(t,{});if(1!==n.length)throw new UsageError("diff takes one container");const a=await e.json("GET",`/containers/${enc(n[0])}/changes`)||[];for(const t of a)r.write(`${["C","A","D"][t.Kind]} ${t.Path}\n`);return 0}async function save(t,{api:e,out:r}){const{opts:n,args:a}=parseArgs(t,{output:{short:"o",type:"string"}});if(!a.length)throw new UsageError("save needs at least one image, e.g. thinknsh save -o app.tar myapp:1.0");const o=await e.stream("GET",`/images/get${qs({names:a})}`);return await toFileOrStdout(o,n.output,r,"image data"),0}async function load(t,{api:e,out:r}){const{opts:n}=parseArgs(t,{input:{short:"i",type:"string"},quiet:{short:"q",type:"bool"}});if(!n.input&&process.stdin.isTTY)throw new UsageError("load reads a tar: use -i FILE or pipe it in (thinknsh load < app.tar)");const a=n.input?fs.createReadStream(path.resolve(n.input)):process.stdin;return await e.progress("POST",`/images/load${qs({quiet:n.quiet})}`,a,t=>{t.stream&&r.write(t.stream)},{"Content-Type":"application/x-tar"}),0}async function importCmd(t,{api:e,out:r}){const{opts:n,args:a}=parseArgs(t,{change:{short:"c",type:"list"},message:{short:"m",type:"string"},platform:{type:"string"}});if(!a.length||a.length>2)throw new UsageError("import takes FILE|- [REPO[:TAG]], e.g. thinknsh import rootfs.tar myimage:1");const[o,i]=a;if(/^https?:\/\//.test(o))throw new UsageError("importing from a URL is not supported; download the file first");if("-"===o&&process.stdin.isTTY)throw new UsageError("import - reads a tar from stdin; pipe one in");let s,c;if(i){const t=i.lastIndexOf("/"),e=i.lastIndexOf(":");[s,c]=e>t?[i.slice(0,e),i.slice(e+1)]:[i,"latest"]}const l="-"===o?process.stdin:fs.createReadStream(path.resolve(o));let p=null;return await e.progress("POST",`/images/create${qs({fromSrc:"-",repo:s,tag:c,changes:n.change,message:n.message,platform:n.platform})}`,l,t=>{t.status&&(p=t.status)},{"Content-Type":"application/x-tar"}),p&&r.write(`${p}\n`),0}module.exports={cp,exportCmd,diff,save,load,import:importCmd,splitSpec,extractLocal,packLocal};
@@ -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 chalk=require("chalk"),shortId=t=>String(t||"").replace(/^sha256:/,"").slice(0,12);function size(t){if(null==t||t<0)return"-";const o=["B","kB","MB","GB","TB"];let r=t,e=0;for(;r>=1e3&&e<o.length-1;)r/=1e3,e++;return`${r<10&&e?r.toFixed(2):r<100&&e?r.toFixed(1):Math.round(r)}${o[e]}`}function ago(t){const o=Math.max(0,Math.floor(Date.now()/1e3-t));if(o<60)return o<2?"just now":`${o} seconds ago`;const r=[[60,"minute"],[3600,"hour"],[86400,"day"],[604800,"week"],[2592e3,"month"],[31536e3,"year"]];let[e,n]=r[0];for(const t of r)o>=t[0]&&([e,n]=t);const s=Math.floor(o/e);return 1===s?`${"hour"===n?"an":"a"} ${n} ago`:`${s} ${n}s ago`}function duration(t){const o=Math.floor(t/1e3);return o<60?`${o<1?"less than a":o} second${1===o?"":"s"}`:o<3600?`${Math.floor(o/60)} minute${o<120?"":"s"}`:o<86400?`${Math.floor(o/3600)} hour${o<7200?"":"s"}`:`${Math.floor(o/86400)} day${o<172800?"":"s"}`}function table(t,o,{out:r=process.stdout}={}){const e=t.map((t,r)=>Math.max(t.length,...o.map(t=>String(t[r]??"").length))),n=t=>t.map((o,r)=>r===t.length-1?String(o??""):String(o??"").padEnd(e[r])).join(" ").trimEnd();r.write(chalk.bold(n(t))+"\n");for(const t of o)r.write(n(t)+"\n")}function ports(t=[]){const o=new Set;return t.filter(t=>t.PublicPort).map(t=>`${t.IP&&"0.0.0.0"!==t.IP?t.IP+":":""}${t.PublicPort}->${t.PrivatePort}/${t.Type}`).filter(t=>!o.has(t)&&o.add(t)).join(", ")}function pullPrinter(t=process.stderr){const o=t.isTTY,r=[],e=new Map;let n=0;const s=()=>{if(o){n&&t.write(`[${n}A`);for(const o of r)t.write(`${e.get(o)}\n`);n=r.length}};return{onMessage(n){if(!n.progressDetail)return void(n.status&&t.write(`${n.id?`${n.id}: `:""}${n.status}\n`));const a=n.progressDetail||{},i=a.total?` ${Math.floor(a.current/a.total*100)}% of ${size(a.total)}`:"",l=`${n.id}: ${n.status}${i}`;e.has(n.id)||r.push(n.id),e.set(n.id,l),o?s():a.total||t.write(l+"\n")},done(){o&&n&&s()}}}module.exports={shortId,size,ago,duration,table,ports,pullPrinter};
@@ -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 chalk=require("chalk"),{EngineError,qs,enc}=require("./api"),{parseArgs,parseBytes,UsageError}=require("./args"),fmt=require("./format");async function each(t,e,{out:s,err:r},o){if(!t.length)throw new UsageError(`${o} needs at least one container`);let a=0;for(const o of t)try{await e(o),s.write(`${o}\n`)}catch(t){a++,r.write(`Error: ${t.message}\n`)}return a?1:0}function statsRow(t){const e=(t.cpu_stats?.cpu_usage?.total_usage||0)-(t.precpu_stats?.cpu_usage?.total_usage||0),s=(t.cpu_stats?.system_cpu_usage||0)-(t.precpu_stats?.system_cpu_usage||0),r=t.cpu_stats?.online_cpus||1,o=s>0&&e>0?e/s*r*100:0,a=t.memory_stats||{},n=Math.max(0,(a.usage||0)-(a.stats?.inactive_file||0)),i=a.limit||0,p=Object.values(t.networks||{}).reduce((t,e)=>[t[0]+e.rx_bytes,t[1]+e.tx_bytes],[0,0]),c=(t.blkio_stats?.io_service_bytes_recursive||[]).reduce((t,e)=>("read"!==e.op&&"Read"!==e.op||(t[0]+=e.value),"write"!==e.op&&"Write"!==e.op||(t[1]+=e.value),t),[0,0]);return{id:fmt.shortId(t.id),name:(t.name||"").replace(/^\//,""),cpu:`${o.toFixed(2)}%`,mem:`${fmt.size(n)} / ${fmt.size(i)}`,memPct:`${i?(n/i*100).toFixed(2):"0.00"}%`,net:`${fmt.size(p[0])} / ${fmt.size(p[1])}`,block:`${fmt.size(c[0])} / ${fmt.size(c[1])}`,pids:String(t.pids_stats?.current??0)}}const STATS_COLUMNS=["CONTAINER ID","NAME","CPU %","MEM USAGE / LIMIT","MEM %","NET I/O","BLOCK I/O","PIDS"],rowCells=t=>[t.id,t.name,t.cpu,t.mem,t.memPct,t.net,t.block,t.pids];async function stats(t,{api:e,out:s}){const{opts:r,args:o}=parseArgs(t,{all:{short:"a",type:"bool"},"no-stream":{type:"bool"},json:{type:"bool"},"no-trunc":{type:"bool"}}),a=o.length?o:(await e.json("GET",`/containers/json${qs({all:r.all||void 0})}`)).map(t=>t.Id);if(!a.length)return fmt.table(STATS_COLUMNS,[],{out:s}),0;if(r["no-stream"]){const t=await Promise.all(a.map(async t=>{const s=await e.stream("GET",`/containers/${enc(t)}/stats${qs({stream:0})}`);let r="";for await(const t of s)r+=t;return JSON.parse(r.trim().split("\n").pop())}));return r.json?s.write(JSON.stringify(t.map(statsRow),null,2)+"\n"):fmt.table(STATS_COLUMNS,t.map(statsRow).map(rowCells),{out:s}),0}const n=new Map,i=[];let p=!1;for(const t of a){const s=await e.stream("GET",`/containers/${enc(t)}/stats${qs({stream:1})}`);i.push(s),(async()=>{let e="";for await(const r of s){let s;for(e+=r;-1!==(s=e.indexOf("\n"));){const r=e.slice(0,s);e=e.slice(s+1),r.trim()&&n.set(t,JSON.parse(r))}}n.delete(t)})().catch(()=>n.delete(t))}const c=()=>{const t=a.filter(t=>n.has(t)).map(t=>statsRow(n.get(t)));r.json?s.write(JSON.stringify(t)+"\n"):(s.isTTY&&s.write(""),fmt.table(STATS_COLUMNS,t.map(rowCells),{out:s}))};return await new Promise(t=>{const e=setInterval(c,1e3),s=()=>{p=!0,clearInterval(e);for(const t of i)t.destroy();t()};process.once("SIGINT",s),Promise.allSettled(i.map(t=>new Promise(e=>t.on("close",e)))).then(()=>{p||s()})}),0}async function top(t,{api:e,out:s}){const{args:r}=parseArgs(t,{});if(1!==r.length)throw new UsageError("top takes one container");const o=await e.json("GET",`/containers/${enc(r[0])}/top`);return fmt.table(o.Titles,o.Processes,{out:s}),0}const pause=(t,e)=>each(parseArgs(t,{}).args,t=>e.api.json("POST",`/containers/${enc(t)}/pause`),e,"pause"),unpause=(t,e)=>each(parseArgs(t,{}).args,t=>e.api.json("POST",`/containers/${enc(t)}/unpause`),e,"unpause");async function rename(t,{api:e}){const{args:s}=parseArgs(t,{});if(2!==s.length)throw new UsageError("rename takes CONTAINER NEW_NAME");return await e.json("POST",`/containers/${enc(s[0])}/rename${qs({name:s[1]})}`),0}async function update(t,e){const{opts:s,args:r}=parseArgs(t,{memory:{short:"m",type:"string"},"memory-swap":{type:"string"},"memory-reservation":{type:"string"},cpus:{type:"string"},"cpu-shares":{short:"c",type:"string"},"cpuset-cpus":{type:"string"},"cpu-quota":{type:"string"},"cpu-period":{type:"string"},"pids-limit":{type:"string"},restart:{type:"string"},"blkio-weight":{type:"string"}}),o={...s.memory?{Memory:parseBytes(s.memory)}:{},...s["memory-swap"]?{MemorySwap:"-1"===s["memory-swap"]?-1:parseBytes(s["memory-swap"])}:{},...s["memory-reservation"]?{MemoryReservation:parseBytes(s["memory-reservation"])}:{},...s.cpus?{NanoCpus:Math.round(1e9*parseFloat(s.cpus))}:{},...s["cpu-shares"]?{CpuShares:parseInt(s["cpu-shares"],10)}:{},...s["cpuset-cpus"]?{CpusetCpus:s["cpuset-cpus"]}:{},...s["cpu-quota"]?{CpuQuota:parseInt(s["cpu-quota"],10)}:{},...s["cpu-period"]?{CpuPeriod:parseInt(s["cpu-period"],10)}:{},...s["pids-limit"]?{PidsLimit:parseInt(s["pids-limit"],10)}:{},...s["blkio-weight"]?{BlkioWeight:parseInt(s["blkio-weight"],10)}:{}};if(s.restart){const[t,e]=s.restart.split(":");if(!["no","always","unless-stopped","on-failure"].includes(t))throw new UsageError(`invalid restart policy "${s.restart}"`);o.RestartPolicy={Name:t,MaximumRetryCount:e?parseInt(e,10):0}}if(!Object.keys(o).length)throw new UsageError("update needs at least one limit to change, e.g. thinknsh update --memory 1g web");return each(r,async t=>{const s=await e.api.json("POST",`/containers/${enc(t)}/update`,o);for(const t of s?.Warnings||[])e.err.write(chalk.yellow(`Warning: ${t}\n`))},e,"update")}async function commit(t,{api:e,out:s}){const{opts:r,args:o}=parseArgs(t,{message:{short:"m",type:"string"},author:{short:"a",type:"string"},change:{short:"c",type:"list"},pause:{short:"p",type:"bool",default:!0}});if(!o.length||o.length>2)throw new UsageError("commit takes CONTAINER [REPO[:TAG]], e.g. thinknsh commit web myweb:snapshot");const[a,n]=o;let i,p;if(n){const t=n.lastIndexOf("/"),e=n.lastIndexOf(":");[i,p]=e>t?[n.slice(0,e),n.slice(e+1)]:[n,"latest"]}const c=await e.json("POST",`/commit${qs({container:a,repo:i,tag:p,comment:r.message,author:r.author,changes:r.change,pause:r.pause?void 0:"0"})}`,{});return s.write(`${c.Id}\n`),0}async function history(t,{api:e,out:s}){const{opts:r,args:o}=parseArgs(t,{quiet:{short:"q",type:"bool"},"no-trunc":{type:"bool"},human:{short:"H",type:"bool",default:!0}});if(1!==o.length)throw new UsageError("history takes one image");const a=await e.json("GET",`/images/${o[0]}/history`);return r.quiet?(a.forEach(t=>s.write(`${r["no-trunc"]||"<missing>"===t.Id?t.Id:fmt.shortId(t.Id)}\n`)),0):(fmt.table(["IMAGE","CREATED","CREATED BY","SIZE","COMMENT"],a.map(t=>{return["<missing>"===t.Id||r["no-trunc"]?t.Id:fmt.shortId(t.Id),t.Created?fmt.ago(t.Created):"",(e=t.CreatedBy.replace(/\s+/g," "),r["no-trunc"]||e.length<=45?e:`${e.slice(0,44)}…`),fmt.size(t.Size),t.Comment];var e}),{out:s}),0)}async function port(t,{api:e,out:s}){const{args:r}=parseArgs(t,{});if(!r.length||r.length>2)throw new UsageError("port takes CONTAINER [PRIVATE_PORT[/PROTO]]");const o=await e.json("GET",`/containers/${enc(r[0])}/json`),a=o.NetworkSettings?.Ports||{};if(r[1]){const t=r[1].includes("/")?r[1]:`${r[1]}/tcp`,e=a[t];if(!e?.length)throw new EngineError(`no public port '${t}' published for ${r[0]}`,404);for(const t of e)s.write(`${t.HostIp||"0.0.0.0"}:${t.HostPort}\n`);return 0}for(const[t,e]of Object.entries(a))for(const r of e||[])s.write(`${t} -> ${r.HostIp||"0.0.0.0"}:${r.HostPort}\n`);return 0}module.exports={stats,top,pause,unpause,rename,update,commit,history,port,statsRow};
@@ -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"),chalk=require("chalk"),{UsageError}=require("./args"),BACKUP_IMAGE="alpine:3.20";function note(e,r,t){e.write(chalk.dim(`note: "${r}" is now "thinknsh ${t}"\n`))}function run(e,r,t){return require("./commands").COMMANDS[e](r,t)}async function capsule(e,r){const[t="ls",...n]=e,a=(e,n)=>(note(r.err,`capsule ${t}`,[e,...n].join(" ")),run(e,n,r));switch(t){case"seal":return a("build",["-t",n[0]||`${path.basename(process.cwd()).toLowerCase().replace(/[^a-z0-9._-]/g,"-")}:latest`,"."]);case"ls":case"list":return a("images",n);case"open":if(!n[0])throw new UsageError("capsule open takes an image tag");return a("run",["--rm",...process.stdin.isTTY?["-it"]:[],n[0],...n.slice(1)]);case"rm":case"remove":return a("rmi",n);case"inspect":return a("inspect",["--type","image",...n]);case"export":case"import":throw new UsageError(`capsule ${t} is not available yet; move images through a registry instead: thinknsh registry start, then thinknsh registry push|pull IMAGE`);default:throw new UsageError(`unknown capsule command "${t}"; images are managed with: thinknsh build, images, rmi, inspect`)}}async function vault(e,r){const[t="ls",...n]=e;switch(t){case"create":case"ls":case"list":case"inspect":case"rm":case"remove":return note(r.err,`vault ${t}`,["volume",t,...n].join(" ")),run("volume",[t,...n],r);case"backup":{const e=n[0],t=n.indexOf("-o"),a=-1!==t?n[t+1]:null;if(!e||!a)throw new UsageError("vault backup takes NAME -o FILE.tar.gz");const s=path.resolve(a);fs.mkdirSync(path.dirname(s),{recursive:!0}),note(r.err,"vault backup",`run --rm -v ${e}:/data:ro … tar czf`);const o=await run("run",["--rm","-v",`${e}:/data:ro`,"-v",`${path.dirname(s)}:/backup`,BACKUP_IMAGE,"tar","czf",`/backup/${path.basename(s)}`,"-C","/data","."],r);return o||r.out.write(`${s}\n`),o}case"restore":{const e=n.find(e=>!e.startsWith("-")),t=n.indexOf("--name");if(!e)throw new UsageError("vault restore takes FILE.tar.gz [--name VOLUME]");const a=path.resolve(e);if(!fs.existsSync(a))throw new UsageError(`${a} not found`);const s=-1!==t?n[t+1]:path.basename(a).replace(/\.(tar\.gz|tgz|tar)$/,"");note(r.err,"vault restore",`volume create ${s} + run --rm … tar xzf`);return await run("volume",["create",s],r)||run("run",["--rm","-v",`${s}:/data`,"-v",`${path.dirname(a)}:/backup:ro`,BACKUP_IMAGE,"tar","xzf",`/backup/${path.basename(a)}`,"-C","/data"],r)}default:throw new UsageError(`unknown vault command "${t}"; volumes are managed with: thinknsh volume ls|create|rm|inspect|prune`)}}async function mesh(e,r){const[t="ls",...n]=e;if(!["create","ls","list","rm","remove","connect","disconnect","inspect"].includes(t))throw new UsageError(`unknown mesh command "${t}"; networks are managed with: thinknsh network ls|create|rm|connect|disconnect`);return note(r.err,`mesh ${t}`,["network",t,...n].join(" ")),run("network",[t,...n],r)}async function stack(e,r){return note(r.err,`stack ${e.join(" ")}`.trim(),["compose",...e].join(" ")),run("compose",e,r)}async function vm(e,r){const[t="status",...n]=e;if("ps"===t)return note(r.err,"vm ps","ps"),run("ps",n,r);if(!["setup","start","stop","status"].includes(t))throw new UsageError(`unknown vm command "${t}"; the engine and its VM are managed with: thinknsh dock setup|start|stop|status`);note(r.err,`vm ${t}`,`dock ${t}`);const a=process.exitCode;await require("../../commands/dock/dock").execute([t,...n]);const s=process.exitCode||0;return process.exitCode=a,s}module.exports={capsule,vault,mesh,stack,vm};
@@ -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 chalk=require("chalk"),{EngineError,qs,enc}=require("./api"),{parseArgs,UsageError}=require("./args"),{pullImage,runCommand,splitRepoTag}=require("./commands"),fmt=require("./format"),LABEL="tnc.service",SERVICES={postgres:{image:"postgres:16-alpine",port:5432,data:"/var/lib/postgresql/data",env:["POSTGRES_HOST_AUTH_METHOD=trust"],health:["CMD-SHELL","pg_isready -U postgres"],url:t=>`postgresql://postgres@localhost:${t}/postgres`},redis:{image:"redis:7-alpine",port:6379,data:"/data",env:[],health:["CMD","redis-cli","ping"],url:t=>`redis://localhost:${t}`},mysql:{image:"mysql:8.4",port:3306,data:"/var/lib/mysql",env:["MYSQL_ALLOW_EMPTY_PASSWORD=yes"],health:["CMD-SHELL","mysqladmin ping -h 127.0.0.1 --silent"],url:t=>`mysql://root@localhost:${t}`}},ALIASES={pg:"postgres",postgresql:"postgres",mariadb:"mysql"},REGISTRY={name:"tnc-registry",image:"registry:2",port:5e3,data:"/var/lib/registry"},containerName=t=>`tnc-svc-${t}`,sleep=t=>new Promise(e=>setTimeout(e,t));function serviceOf(t){const e=ALIASES[t]||t;if(!SERVICES[e])throw new UsageError(`unknown service "${t}" (${Object.keys(SERVICES).join(", ")})`);return e}async function inspectOrNull(t,e){try{return await t.json("GET",`/containers/${enc(e)}/json`)}catch(t){if(t instanceof EngineError&&404===t.status)return null;throw t}}const hostPortOf=t=>Number(t?.HostConfig?.PortBindings&&Object.values(t.HostConfig.PortBindings)[0]?.[0]?.HostPort)||null;async function ensureContainer(t,{name:e,image:r,port:s,hostPort:n,data:a,env:o=[],health:i,labels:l,err:c}){let u=await inspectOrNull(t,e);if(u&&n&&hostPortOf(u)!==n)throw new UsageError(`${e} already exists on port ${hostPortOf(u)}; remove it first (data stays in its volume): thinknsh rm -f ${e}`);if(!u){const h={Image:r,Env:o,Labels:l,ExposedPorts:{[`${s}/tcp`]:{}},...i?{Healthcheck:{Test:i,Interval:2e9,Timeout:5e9,Retries:30}}:{},HostConfig:{Binds:[`${e}:${a}`],PortBindings:{[`${s}/tcp`]:[{HostIp:"127.0.0.1",HostPort:String(n||s)}]},RestartPolicy:{Name:"unless-stopped"}}};try{await t.json("POST",`/containers/create${qs({name:e})}`,h)}catch(s){if(!(s instanceof EngineError&&404===s.status))throw s;c.write(`pulling ${r}\n`),await pullImage(t,r,{err:c}),await t.json("POST",`/containers/create${qs({name:e})}`,h)}u=await inspectOrNull(t,e)}return u.State.Running||(await t.json("POST",`/containers/${enc(e)}/start`),u=await inspectOrNull(t,e)),u}async function waitHealthy(t,e,r,s=12e4){const n=Date.now()+s;for(;;){const r=await inspectOrNull(t,e);if(!r?.State.Running)throw new Error(`${e} stopped while starting; see: thinknsh logs ${e}`);const a=r.State.Health?.Status;if(!r.Config.Healthcheck||"healthy"===a)return;if("unhealthy"===a)throw new Error(`${e} is unhealthy; see: thinknsh logs ${e}`);if(Date.now()>n)throw new Error(`${e} did not become healthy in ${s/1e3}s; see: thinknsh logs ${e}`);await sleep(500)}}const SERVICE_USAGE=`Usage: thinknsh service start NAME [-p PORT] | stop NAME | restart NAME | status [NAME] | logs [-f] NAME | rm [-v] NAME\n\nDev databases as containers: ${Object.keys(SERVICES).join(", ")}. Each listens on 127.0.0.1 only,\nkeeps its data in the volume tnc-svc-NAME, and restarts with dock unless you stop it.\n\n start NAME [-p PORT] Create or start it and wait until it is healthy (default ports: ${Object.entries(SERVICES).map(([t,e])=>`${t} ${e.port}`).join(", ")})\n rm NAME [-v] Remove the container; -v also deletes its data volume`;async function service(t,e){const{api:r,out:s,err:n}=e,[a="status",...o]=t;if(["--help","-h","help"].includes(a))return s.write(SERVICE_USAGE+"\n"),0;switch(a){case"start":{const{opts:t,args:e}=parseArgs(o,{port:{short:"p",type:"string"}});if(1!==e.length)throw new UsageError("service start takes one name, e.g. thinknsh service start postgres");const a=serviceOf(e[0]),i=SERVICES[a],l=t.port?Number(t.port):void 0;if(t.port&&!(l>0&&l<65536))throw new UsageError(`invalid port "${t.port}"`);const c=containerName(a),u=await ensureContainer(r,{name:c,image:i.image,port:i.port,hostPort:l,data:i.data,env:i.env,health:i.health,labels:{[LABEL]:a},err:n});n.write(chalk.dim(`waiting for ${a} to be ready…\n`)),await waitHealthy(r,c,n);const h=hostPortOf(u)||i.port;return s.write(`${chalk.green("✓")} ${a} is running ${i.url(h)}\n`),s.write(chalk.dim(` container ${c}, data in volume ${c}\n`)),0}case"stop":case"restart":{if(1!==o.length)throw new UsageError(`service ${a} takes one name`);const t=containerName(serviceOf(o[0]));return await r.json("POST",`/containers/${enc(t)}/${a}`),s.write(`${t}\n`),0}case"logs":{const{opts:t,args:r}=parseArgs(o,{follow:{short:"f",type:"bool"},tail:{short:"n",type:"string"}});if(1!==r.length)throw new UsageError("service logs takes one name");return runCommand("logs",[...t.follow?["-f"]:[],...t.tail?["-n",t.tail]:[],containerName(serviceOf(r[0]))],{...e,autoStart:!1})}case"rm":case"remove":{const{opts:t,args:e}=parseArgs(o,{volumes:{short:"v",type:"bool"}});if(1!==e.length)throw new UsageError("service rm takes one name");const n=containerName(serviceOf(e[0]));return await r.json("DELETE",`/containers/${enc(n)}${qs({force:!0})}`),t.volumes&&await r.json("DELETE",`/volumes/${enc(n)}`),s.write(`${n}${t.volumes?" (and its data)":""}\n`),0}case"status":case"ls":{const t=await r.json("GET",`/containers/json${qs({all:!0,filters:{label:[LABEL]}})}`),e=o[0]?serviceOf(o[0]):null,n=Object.keys(SERVICES).filter(t=>!e||t===e).map(e=>{const r=t.find(t=>t.Labels[LABEL]===e),s=r?.Ports?.find(t=>t.PublicPort)?.PublicPort;return[e,r?r.Status:"not created",r&&"running"===r.State&&s?SERVICES[e].url(s):"-"]});return fmt.table(["SERVICE","STATUS","CONNECT"],n,{out:s}),0}default:throw new UsageError(`unknown service command "${a}" (start, stop, restart, status, logs, rm)`)}}const REGISTRY_USAGE=`Usage: thinknsh registry start [PORT] | stop | status | push IMAGE | pull IMAGE\n\nA local image registry as the container ${REGISTRY.name} (127.0.0.1:${REGISTRY.port} by default, data in the volume ${REGISTRY.name}).\n push IMAGE tags IMAGE as localhost:PORT/IMAGE and pushes it there\n pull IMAGE pulls localhost:PORT/IMAGE`;async function registryPort(t){const e=await inspectOrNull(t,REGISTRY.name);if(!e?.State.Running)throw new UsageError("the registry is not running; start it with: thinknsh registry start");return hostPortOf(e)||REGISTRY.port}async function registry(t,e){const{api:r,out:s,err:n}=e,[a="status",...o]=t;if(["--help","-h","help"].includes(a))return s.write(REGISTRY_USAGE+"\n"),0;switch(a){case"start":{const t=o[0]?Number(o[0]):void 0;if(o[0]&&!(t>0&&t<65536))throw new UsageError(`invalid port "${o[0]}"`);const e=await ensureContainer(r,{...REGISTRY,hostPort:t,labels:{[LABEL]:"registry"},err:n}),a=hostPortOf(e)||REGISTRY.port;return s.write(`${chalk.green("✓")} registry is running on localhost:${a}\n`),s.write(chalk.dim(` push: thinknsh registry push myapp:1 (or thinknsh push localhost:${a}/myapp:1)\n`)),0}case"stop":return await r.json("POST",`/containers/${REGISTRY.name}/stop`),s.write(`${REGISTRY.name}\n`),0;case"status":{const t=await inspectOrNull(r,REGISTRY.name);if(!t)return s.write("registry: not created (thinknsh registry start)\n"),0;const e=hostPortOf(t)||REGISTRY.port;if(s.write(`registry: ${t.State.Running?`running on localhost:${e}`:"stopped"}\n`),t.State.Running)try{const t=await fetch(`http://127.0.0.1:${e}/v2/_catalog`,{signal:AbortSignal.timeout(5e3)}),r=(await t.json()).repositories||[];s.write(r.length?r.map(t=>` ${t}`).join("\n")+"\n":" (no repositories yet)\n")}catch(t){s.write(chalk.dim(" (could not list repositories)\n"))}return 0}case"push":case"pull":{if(1!==o.length)throw new UsageError(`registry ${a} takes one image, e.g. thinknsh registry ${a} myapp:1`);const t=await registryPort(r),{repo:s,tag:n}=splitRepoTag(o[0]),i=`localhost:${t}/${s}:${n}`;if("push"===a){return await runCommand("tag",[o[0],i],{...e,autoStart:!1})||runCommand("push",[i],{...e,autoStart:!1})}return runCommand("pull",[i],{...e,autoStart:!1})}default:throw new UsageError(`unknown registry command "${a}" (start, stop, status, push, pull)`)}}module.exports={service,registry,SERVICES,serviceOf,SERVICE_USAGE,REGISTRY_USAGE};
@@ -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{demuxer}=require("./api"),DETACH=Buffer.from([16,17]);function pipeSession(e,{stdin:s=!1,resize:t=null}={}){const{socket:o,tty:r}=e,n=process.stdin,d=r&&s&&n.isTTY;let i=!1,c=null;return new Promise(u=>{const p=()=>{if(d)try{n.setRawMode(!1)}catch(e){}n.pause(),n.removeListener("data",a),process.stdout.removeListener("resize",f)},a=e=>{if(d){if((null!==c?Buffer.concat([Buffer.from([c]),e]):e).includes(DETACH))return i=!0,process.stderr.write("\r\nread escape sequence\r\n"),void o.destroy();c=e[e.length-1]}o.write(e)},f=()=>{t&&process.stdout.columns&&t(process.stdout.rows,process.stdout.columns)};if(r)o.on("data",e=>process.stdout.write(e));else{const e=demuxer(e=>process.stdout.write(e),e=>process.stderr.write(e));o.on("data",e)}o.on("error",()=>{}),o.on("close",()=>{p(),u({detached:i})}),o.on("end",()=>{p(),u({detached:i})}),s&&!e.readOnly&&(d&&n.setRawMode(!0),n.on("data",a),n.on("end",()=>{"function"==typeof o.end&&o.end()}),n.resume()),r&&t&&process.stdout.isTTY&&(f(),process.stdout.on("resize",f))})}module.exports={pipeSession};
@@ -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"),chalk=require("chalk"),{EngineError,qs}=require("./api"),{parseArgs,parseBytes,UsageError}=require("./args"),{packContext}=require("./context"),{dockPaths}=require("../paths"),fmt=require("./format"),ws=require("../workspace/workspace"),{BUILD_FILE_PARAM,COMPOSE_LEGACY_FILES}=require("../api/wire"),home=()=>dockPaths().home;async function findContainer(e,o){try{return await e.json("GET",`/containers/${ws.containerName(o)}/json`)}catch(e){if(e instanceof EngineError&&404===e.status)return null;throw e}}async function imageExists(e,o){try{return await e.json("GET",`/images/${o}/json`),!0}catch(e){if(e instanceof EngineError&&404===e.status)return!1;throw e}}async function buildImage(e,o,t,{err:n,quiet:s}){const r=fs.mkdtempSync(path.join(os.tmpdir(),"tnc-workspace-image-"));try{ws.writeImageContext(r);const{tar:a,buildFile:i}=packContext(r);s||n.write(chalk.dim(`building the workspace image ${o} (once; agents: ${t.join(" ")||"none"})\n`)),await e.progress("POST",`/build${qs({t:o,[BUILD_FILE_PARAM]:i,buildargs:{AGENTS:t.join(" ")}})}`,a,e=>{e.stream&&!s&&/^Step |^Successfully/.test(e.stream)&&n.write(chalk.dim(e.stream))},{"Content-Type":"application/x-tar"})}finally{fs.rmSync(r,{recursive:!0,force:!0})}}function resolveName(e,o=!0){return e[0]?ws.workspaceName(e[0]):o?ws.workspaceName(path.basename(process.cwd())):null}const pathsOf=e=>e.paths||(e.path?[e.path]:[]),mountsOf=e=>ws.folderMounts(pathsOf(e)),openTarget=e=>pathsOf(e).length>1?`/run/tnc-workspace/${e.name}.code-workspace`:"/work";function connectHelp(e,{out:o,sshConfigChanged:t,sshConfigFile:n,sshConfigWritten:s,projects:r=[]}){const{detectApps:a}=require("../workspace/apps"),i=ws.sshHost(e.name),c=s?`ssh ${i}`:`ssh -F ${n} ${i}`,p=chalk.bold,w=chalk.cyan,h=mountsOf(e),l=(h.length,"/work");o.write(`\n${chalk.green("✓")} Workspace ${p(e.name)} is running. SSH host: ${w(i)}\n\n`),o.write(`${p("Your folders inside it")} (same files as on your Mac, live):\n`);for(const e of h)o.write(` ${e.host} → ${w(e.target)}\n`);if(o.write("\n"),e.joined?.length){o.write(`${p("Services it reaches by name")} (networks: ${e.joined.join(", ")}):\n`);for(const e of r)o.write(` compose project ${e.p.name}: ${Object.keys(e.p.services).map(e=>w(e)).join(", ")}\n`);const t=e.networks||[];t.length&&o.write(` any container on ${t.join(", ")}, by its name or alias\n`),o.write(chalk.dim(` e.g. thinknsh workspace exec ${e.name} getent hosts ${r[0]?Object.keys(r[0].p.services)[0]:"NAME"} · names resolve once those containers run (thinknsh compose up)\n\n`))}o.write(`${p("Open it in your apps")} (all of them share this one workspace):\n\n`);const m=a();for(const t of m){const n=t.ready?chalk.green("●"):chalk.dim("○");o.write(` ${n} ${p(t.label)}${t.ready?"":chalk.dim(` (${t.note})`)}\n`),"editor"===t.kind?(o.write(` ${w(`thinknsh workspace open ${e.name} --app ${t.id}`)}\n`),o.write(chalk.dim(` or in the app: Cmd+Shift+P → "Remote-SSH: Connect to Host" → ${i} → open ${openTarget(e)}\n`))):"claude"===t.id?(o.write(` Code tab → environment picker (where it says Local) → SSH → ${w(i)} → folder ${w(l)}\n`),o.write(chalk.dim(" Claude installs its engine in the workspace the first time; your Claude login is used inside it.\n"))):"codex"===t.id&&(o.write(` Settings → Connections → enable ${w(i)} → pick folder ${w(l)}\n`),o.write(chalk.dim(` First, log Codex in inside the workspace once: thinknsh workspace exec ${e.name} codex login --device-auth\n`))),o.write("\n")}o.write(` ${chalk.green("●")} ${p("Terminal")}\n ${w(c)} ${chalk.dim("(then: claude, codex, npm test, …)")}\n\n`),o.write(chalk.dim(`Status of every app: thinknsh workspace apps · Stop: thinknsh workspace stop ${e.name} · Remove: thinknsh workspace rm ${e.name} (folders are never touched)\n`)),s?o.write(chalk.dim(`SSH: 127.0.0.1:${e.port}, key-only, host key pinned.${t?` Host ${i} written to ${ws.userSshConfig()} (inside the thinknsh block).`:""}\n`)):o.write(chalk.dim("~/.ssh/config was not changed (--no-ssh-config), so apps will not list the host; only the ssh -F command above works.\n"))}const UP_FLAGS={name:{type:"string"},port:{type:"string"},memory:{short:"m",type:"string",default:"2g"},cpus:{type:"string",default:"2"},agents:{type:"string"},rebuild:{type:"bool"},"no-ssh-config":{type:"bool"},quiet:{short:"q",type:"bool"},network:{type:"list"},compose:{type:"string"},"no-compose":{type:"bool"}};function parseUpArgs(e){const o=e.map((o,t)=>"--compose"===o&&(t+1>=e.length||e[t+1].startsWith("-"))?"--compose=":o);return parseArgs(o,UP_FLAGS)}function composeFileIn(e){const{FILES:o}=require("./compose");for(const t of o)if(fs.existsSync(path.join(e,t)))return path.join(e,t);return null}function composeFileArg(e){const o=path.resolve(e);if(!fs.existsSync(o))throw new UsageError(`--compose: ${o} does not exist`);if(!fs.statSync(o).isDirectory())return o;const t=composeFileIn(o);if(!t)throw new UsageError(`--compose: no compose.yaml, compose.yml, ${COMPOSE_LEGACY_FILES.join(", ")} in ${o}`);return t}function composeProjects(e,o,{warn:t=()=>{},required:n=!1}={}){const{loadProject:s,projectNetworks:r}=require("./compose");if(!e)return[];const a="auto"===e?o.map(composeFileIn).filter(Boolean):[e];if(n&&!a.length)throw new UsageError(`--compose: no compose file in ${o.join(", ")}; pass --compose FILE|DIR`);if("auto"!==e&&!fs.existsSync(e))throw new UsageError(`compose file ${e} (remembered for this workspace) is gone; pass --compose FILE|DIR or --no-compose`);const i=[];for(const o of a)try{const e=s({file:o});i.push({p:e,nets:r(e)})}catch(s){if("auto"!==e||n)throw s;t(`skipping ${o}: ${s.message}`)}return i}async function networkExists(e,o){try{return await e.json("GET",`/networks/${encodeURIComponent(o)}`),!0}catch(e){if(e instanceof EngineError&&404===e.status)return!1;throw e}}async function joinNetworks(e,o,{networks:t,projects:n,joinedBefore:s=[],explicitCompose:r=!1,err:a,quiet:i}){const{ensureNetworks:c}=require("./compose"),p=e=>{i||a.write(chalk.dim(e+"\n"))};for(const o of t)if(!await networkExists(e,o))throw new UsageError(`network ${o} does not exist; create it with: thinknsh network create ${o}`);const w=[];for(const o of n)try{await c(e,o.p,o.p.services,(e,t)=>p(`created network ${t} for compose project ${o.p.name} (thinknsh compose up will use it)`)),w.push(o)}catch(e){if(r)throw e;p(`not joining compose project ${o.p.name}: ${e.message}`)}const h=ws.wantedNetworks(t,w.flatMap(e=>e.nets)).filter(e=>"bridge"!==e),l=await e.json("GET",`/containers/${o}/json`),m=Object.keys(l.NetworkSettings?.Networks||{});for(const t of h)m.includes(t)||await e.json("POST",`/networks/${encodeURIComponent(t)}/connect`,{Container:o});for(const t of s)"bridge"!==t&&!h.includes(t)&&m.includes(t)&&(await e.json("POST",`/networks/${encodeURIComponent(t)}/disconnect`,{Container:o}).catch(()=>{}),p(`left network ${t}`));return{joined:h,projects:w}}async function up(e,{api:o,out:t,err:n}){const{opts:s,args:r}=parseUpArgs(e),a=(r.length?r:["."]).map(e=>{const o=fs.realpathSync(path.resolve(e));if(!fs.statSync(o).isDirectory())throw new UsageError(`${o} is not a folder`);if(o===os.homedir()||"/"===o)throw new UsageError("pick project folders, not your whole home or /");return o});if(new Set(a).size!==a.length)throw new UsageError("the same folder is listed twice");if(a.length>1&&!s.name)throw new UsageError("several folders need a workspace name: --name NAME");const i=ws.folderMounts(a),c=ws.workspaceName(s.name||path.basename(a[0])),p=home(),w=ws.agentPackages(s.agents),h=ws.imageTag(w),l=a.join("\n");let m=ws.readMeta(p,c);const u=ws.networkChoice(m,{network:s.network,noCompose:s["no-compose"],compose:void 0===s.compose||""===s.compose?s.compose:composeFileArg(s.compose)}),d=composeProjects(u.compose,a,{warn:e=>n.write(chalk.yellow(`warning: ${e}\n`)),required:""===s.compose});let f=await findContainer(o,c);const g=f?.Config?.Labels?.[`${ws.LABEL}.path`],k=!(!f||!g||g===l);if(k&&0===r.length)throw new UsageError(`workspace "${c}" serves ${g.split("\n").join(", ")}; pass its folders or a different --name`);if(f&&(s.rebuild||k||void 0!==s.agents&&f.Config.Image!==h||s.port&&m&&String(m.port)!==s.port)&&(await o.json("DELETE",`/containers/${f.Id}?force=1`),f=null,k&&n.write(chalk.dim(`folders changed; recreating workspace ${c} (logins in its home volume are kept)\n`))),!f){const e=ws.listMeta(p).filter(e=>e.name!==c).map(e=>e.port),t=s.port?parseInt(s.port,10):m?.port&&await ws.portFree(m.port)?m.port:await ws.pickPort(e);if(!Number.isInteger(t)||t<1||t>65535)throw new UsageError(`invalid --port ${s.port}`);const r=ws.ensureKeys(p,c,t);ws.writeCodeWorkspace(r.server,c,i),!s.rebuild&&await imageExists(o,h)||await buildImage(o,h,w,{err:n,quiet:s.quiet}),m={name:c,path:a[0],paths:a,port:t,image:h,agents:w,createdAt:m?.createdAt||(new Date).toISOString()},ws.saveMeta(p,m);const l=ws.containerBody({name:c,image:h,mounts:i,serverDir:r.server,port:t,memory:parseBytes(s.memory),nanoCpus:Math.round(1e9*parseFloat(s.cpus))});f={Id:(await o.json("POST",`/containers/create${qs({name:ws.containerName(c)})}`,l)).Id,State:{Running:!1}}}const $=await joinNetworks(o,f.Id,{networks:u.networks,projects:d,joinedBefore:m?.joined||[],explicitCompose:void 0!==s.compose,err:n,quiet:s.quiet});if(m={...ws.readMeta(p,c)||m,networks:u.networks,compose:u.compose,joined:$.joined},ws.saveMeta(p,m),f.State?.Running||await o.json("POST",`/containers/${f.Id}/start`),!await waitForSsh(o,f.Id)){const e=await o.stream("GET",`/containers/${f.Id}/logs${qs({stdout:1,stderr:1,tail:20})}`).then(async e=>{const o=[];for await(const t of e)o.push(t);return Buffer.concat(o).toString("utf8").replace(/[\x00-\x08]/g,"")}).catch(()=>"");throw new Error(`workspace ssh did not come up on 127.0.0.1:${m.port}\n${e.trim()}`)}const E=ws.writeSshConfig(p),y=!s["no-ssh-config"]&&ws.syncUserSshConfig(p);return s.quiet?(t.write(`${ws.sshHost(c)}\n`),0):(connectHelp(m,{out:t,sshConfigChanged:y,sshConfigFile:E,sshConfigWritten:!s["no-ssh-config"],projects:$.projects}),0)}async function open(e,{api:o,out:t}){const{detectApps:n,openEditor:s,APPS:r}=require("../workspace/apps"),{opts:a,args:i}=parseArgs(e,{app:{short:"a",type:"string"}}),c=resolveName(i),p=ws.readMeta(home(),c);if(!p)throw new UsageError(`no workspace "${c}"; start one with: thinknsh workspace up`);const w=await findContainer(o,c);if(!w?.State?.Running)throw new UsageError(`workspace "${c}" is not running; run: thinknsh workspace up ${pathsOf(p).join(" ")}`);const h=n(),l=h.filter(e=>"editor"===e.kind);let m;if(a.app){if(!r[a.app])throw new UsageError(`unknown app "${a.app}" (${Object.keys(r).join(", ")})`);if(m=h.find(e=>e.id===a.app),"editor"!==m.kind)throw new UsageError(`${m.label} is connected inside the app, not from here: ${m.note}. Host: ${ws.sshHost(c)}`);if(!m.ready)throw new UsageError(`${m.label}: ${m.note}`)}else{const e=l.filter(e=>e.ready);if(1!==e.length)throw new UsageError(`pick one with --app (${e.map(e=>e.id).join(", ")||"no editor app is ready; see thinknsh workspace apps"})`);m=e[0]}return s(m,ws.sshHost(c),openTarget(p)),t.write(`opening ${m.label} on ${ws.sshHost(c)}:${openTarget(p)}\n`),t.write(chalk.dim(" first time: it installs its server in the workspace (a minute or two)\n")),0}async function appsCmd(e,{out:o}){const{detectApps:t}=require("../workspace/apps");parseArgs(e,{});const n=t().map(e=>[e.label,e.bundle?"installed":"-",e.ready?"ready":"no",e.note]);return fmt.table(["APP","INSTALLED","READY","HOW"],n,{out:o}),0}async function waitForSsh(e,o){for(let t=0;t<60;t++){if((await e.json("GET",`/containers/${o}/probe${qs({port:ws.SSH_PORT_IN})}`).catch(()=>({ok:!1}))).ok)return!0;const t=await e.json("GET",`/containers/${o}/json`).catch(()=>null);if(t&&!t.State.Running&&"exited"===t.State.Status)return!1;await new Promise(e=>setTimeout(e,250))}return!1}async function ls(e,{api:o,out:t}){parseArgs(e,{});const n=[];for(const e of ws.listMeta(home())){const t=await findContainer(o,e.name),s=t?Object.keys(t.NetworkSettings?.Networks||{}).filter(e=>"bridge"!==e):e.joined||[];n.push([e.name,t?t.State.Status:"removed",`${ws.sshHost(e.name)} (127.0.0.1:${e.port})`,s.join(", ")||"-",pathsOf(e).join(", ")])}return n.length?(fmt.table(["NAME","STATUS","SSH HOST","NETWORKS","PROJECT"],n,{out:t}),0):(t.write("no workspaces yet: thinknsh workspace up [FOLDER]\n"),0)}async function inside(e,o,t,{root:n=!1,tty:s=process.stdin.isTTY}={}){const{COMMANDS:r}=require("./commands"),a=await findContainer(t.api,e);if(!a)throw new UsageError(`no workspace "${e}"; start one with: thinknsh workspace up`);if(!a.State.Running)throw new UsageError(`workspace "${e}" is stopped; run: thinknsh workspace up ${a.Config.Labels[`${ws.LABEL}.path`]||""}`.trim());const i=["-i",...s?["-t"]:[],"-w","/work","-e",`TERM=${process.env.TERM||"xterm-256color"}`],c=n?[...i,"-u","root",ws.containerName(e),...o]:[...i,"-u","root",ws.containerName(e),...ws.DROP.split(" "),"bash","-lc",'exec "$@"',"bash",...o];return r.exec(c,t)}async function shell(e,o){const{args:t}=parseArgs(e,{});return inside(resolveName(t),["bash","-l"],o)}async function exec(e,o){const{opts:t,args:n}=parseArgs(e,{root:{type:"bool"}},{stopAtPositional:!0});if(n.length<2)throw new UsageError("exec takes NAME COMMAND…, e.g. thinknsh workspace exec myapp claude");const[s,...r]=n;return inside(ws.workspaceName(s),r,o,{root:t.root})}async function sshConfig(e,{out:o}){const{args:t}=parseArgs(e,{}),n=resolveName(t,!1),s=ws.listMeta(home()).filter(e=>!n||e.name===n);if(n&&!s.length)throw new UsageError(`no workspace "${n}"`);for(const e of s)o.write(ws.hostEntry(e,home())+"\n");return 0}async function stop(e,{api:o,out:t}){const{args:n}=parseArgs(e,{}),s=resolveName(n),r=await findContainer(o,s);if(!r)throw new UsageError(`no workspace "${s}"`);return await o.json("POST",`/containers/${r.Id}/stop`),t.write(`stopped workspace ${s}\n`),0}async function rm(e,{api:o,out:t}){const{opts:n,args:s}=parseArgs(e,{volumes:{short:"v",type:"bool"}}),r=resolveName(s),a=home(),i=await findContainer(o,r);if(!i&&!ws.readMeta(a,r))throw new UsageError(`no workspace "${r}"`);return i&&await o.json("DELETE",`/containers/${i.Id}?force=1`),n.volumes&&await o.json("DELETE",`/volumes/${ws.homeVolume(r)}`).catch(()=>{}),ws.removeState(a,r),ws.writeSshConfig(a),ws.syncUserSshConfig(a),t.write(`removed workspace ${r}${n.volumes?" and its home volume":` (home volume ${ws.homeVolume(r)} kept; -v removes it)`}\n`),t.write(chalk.dim(" your project folder was not touched\n")),0}const SUB={up,open,apps:appsCmd,ls,list:ls,shell,exec,"ssh-config":sshConfig,stop,rm,remove:rm};async function workspace(e,o){const[t="ls",...n]=e;if("exec"!==t&&n.some(e=>"--help"===e||"-h"===e))return o.out.write(USAGE+"\n"),0;const s=SUB[t];if(!s)throw new UsageError(`unknown workspace command "${t}" (up, open, apps, ls, shell, exec, ssh-config, stop, rm)`);try{return await s(n,o)}catch(e){if(e.workspace)throw new UsageError(e.message);throw e}}const USAGE='Usage: thinknsh workspace up [FOLDER…] | open [NAME] --app APP | apps | ls | shell [NAME] | exec [--root] NAME CMD… | ssh-config [NAME] | stop NAME | rm [-v] NAME\n\nOne secure TNC container that all your coding agents and editors share: Claude desktop,\nCodex app, Cursor, Antigravity IDE, VS Code, and the claude / codex CLIs. No sandbox per tool.\n\n up [FOLDER…] Create/start the workspace (default: .). One folder is /work;\n several are /work/<folder name> each, e.g.\n thinknsh workspace up ~/web ~/api ~/infra --name main\n --name NAME Workspace name (default: folder name; required for several folders)\n --agents LIST Agent CLIs to bake in: claude codex gemini or npm packages; "none" (default: claude codex)\n -m, --memory 2g --cpus 2 --port PORT (default: first free from 22220)\n --rebuild Rebuild the image and recreate the container (home volume kept)\n --no-ssh-config Do not add the Include line to ~/.ssh/config\n --compose [FILE|DIR] Join that compose project\'s networks, so services are\n reachable by name (db, redis, …). Default: the compose file\n in the workspace folder, if there is one\n --no-compose Do not join any compose project\n --network NET Also join NET (repeatable; "none" clears). Remembered for next up\n open [NAME] --app APP Open an editor on the workspace: cursor, antigravity, vscode\n apps Which desktop apps are installed and how each one connects\n shell [NAME] Login shell inside as user tnc\n exec NAME CMD… Run CMD inside as tnc (--root for system packages, e.g. apt-get)\n ssh-config [NAME] Print the SSH host entries (host tnc-NAME)\n rm [-v] NAME Remove the container (-v: also the home volume). The project folder is never touched.\n\nSecurity: SSH listens on 127.0.0.1 only, key-only, host key pinned; sessions run as tnc\nwith no capabilities, no sudo, no setuid binaries (no-new-privileges); memory, CPU and\nprocess limits apply. Agent logins live in the workspace\'s home volume, not on your Mac.';module.exports={workspace,USAGE,parseUpArgs,composeProjects,composeFileArg};
@@ -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");function request(t,e,n,{body:o=null,timeout:r=5e3,headers:s={}}={}){return new Promise((u,i)=>{const a=null==o?null:JSON.stringify(o),d=http.request({socketPath:t,method:e,path:n,headers:a?{...s,"Content-Type":"application/json","Content-Length":Buffer.byteLength(a)}:s,timeout:r},t=>{let e="";t.setEncoding("utf8"),t.on("data",t=>{e+=t}),t.on("end",()=>{let n=e;if((t.headers["content-type"]||"").includes("application/json")&&e)try{n=JSON.parse(e)}catch(t){}u({status:t.statusCode,headers:t.headers,body:n})})});d.on("timeout",()=>d.destroy(new Error(`dock request timed out: ${e} ${n}`))),d.on("error",i),a&&d.write(a),d.end()})}async function ping(t,e=1e3){try{const n=await request(t,"GET","/_ping",{timeout:e});return 200===n.status&&"OK"===n.body}catch(t){return!1}}module.exports={request,ping};
@@ -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_STAT_HEADER,BUILD_FILE,HOST_ALIAS,ENGINE_SOCKET,ENGINE_CLI,ENGINE_CLI_IMAGE}=require("../api/wire"),isJson=e=>e.body&&"object"==typeof e.body,status=e=>t=>t.status===e||`HTTP ${t.status}, expected ${e}`,API_CASES=[{id:"ping",phase:"P0",area:"system",req:["GET","/_ping"],check:e=>200===e.status&&"OK"===e.body&&void 0!==e.headers["api-version"]||"ping must return OK with Api-Version header"},{id:"ping-head",phase:"P0",area:"system",req:["HEAD","/_ping"],check:status(200)},{id:"version",phase:"P0",area:"system",req:["GET","/version"],check:e=>200===e.status&&isJson(e)&&e.body.ApiVersion&&"linux"===e.body.Os||"version body incomplete"},{id:"version-prefixed",phase:"P0",area:"system",req:["GET","/v1.43/version"],check:status(200)},{id:"version-too-new",phase:"P0",area:"system",req:["GET","/v9.99/version"],check:e=>400===e.status&&/too new/.test(e.body?.message||"")||"too-new client must get 400"},{id:"info",phase:"P0",area:"system",req:["GET","/info"],check:e=>200===e.status&&Number.isInteger(e.body?.Containers)&&!!e.body?.Swarm||"info body incomplete"},{id:"unimplemented-501",phase:"P0",area:"system",req:["GET","/swarm"],check:e=>501===e.status&&"string"==typeof e.body?.message||"unimplemented endpoints must return 501 + JSON message"},{id:"containers-list",phase:"P1",area:"container",req:["GET","/containers/json?all=1"],check:status(200)},{id:"images-list",phase:"P1",area:"image",req:["GET","/images/json"],check:status(200)},{id:"image-pull",phase:"P1",area:"image",req:["POST","/images/create?fromImage=alpine&tag=3.20"],check:status(200)},{id:"container-create",phase:"P1",area:"container",req:["POST","/containers/create?name=dock-compat",{Image:"alpine:3.20",Cmd:["echo","hi"]}],check:status(201)},{id:"container-start",phase:"P1",area:"container",req:["POST","/containers/dock-compat/start"],check:status(204)},{id:"container-wait",phase:"P1",area:"container",req:["POST","/containers/dock-compat/wait"],check:status(200)},{id:"container-logs",phase:"P1",area:"container",req:["GET","/containers/dock-compat/logs?stdout=1"],check:status(200)},{id:"archive-stat",phase:"P7",area:"container",req:["HEAD","/containers/dock-compat/archive?path=/etc/alpine-release"],check:e=>200===e.status&&!!e.headers[PATH_STAT_HEADER.toLowerCase()]||`HTTP ${e.status}, expected 200 with a path-stat header`},{id:"archive-get",phase:"P7",area:"container",req:["GET","/containers/dock-compat/archive?path=/etc/alpine-release"],check:e=>200===e.status&&/x-tar/.test(e.headers["content-type"]||"")&&/alpine-release/.test(e.body)||`HTTP ${e.status}, expected a tar`},{id:"archive-missing",phase:"P7",area:"container",req:["GET","/containers/dock-compat/archive?path=/no/such/file"],check:status(404)},{id:"container-changes",phase:"P7",area:"container",req:["GET","/containers/dock-compat/changes"],check:status(200)},{id:"container-export",phase:"P7",area:"container",req:["GET","/containers/dock-compat/export"],check:e=>200===e.status&&/etc\/alpine-release/.test(e.body)||`HTTP ${e.status}, expected the filesystem tar`},{id:"container-stats",phase:"P7",area:"container",req:["GET","/containers/dock-compat/stats?stream=0"],check:e=>200===e.status&&/"memory_stats"/.test("string"==typeof e.body?e.body:JSON.stringify(e.body))||`HTTP ${e.status}`},{id:"commit",phase:"P7",area:"image",req:["POST","/commit?container=dock-compat&repo=dock-compat-commit&tag=api",{}],check:e=>201===e.status&&/^sha256:/.test(e.body?.Id||"")||`HTTP ${e.status}, expected 201 with an image Id`},{id:"image-history",phase:"P7",area:"image",req:["GET","/images/alpine:3.20/history"],check:e=>200===e.status&&Array.isArray(e.body)&&e.body.length>0||`HTTP ${e.status}`},{id:"image-save",phase:"P7",area:"image",req:["GET","/images/get?names=alpine:3.20"],check:e=>200===e.status&&/manifest\.json/.test(e.body)&&/oci-layout/.test(e.body)||`HTTP ${e.status}, expected an image archive`},{id:"container-remove",phase:"P1",area:"container",req:["DELETE","/containers/dock-compat?force=1"],check:status(204)},{id:"volumes-list",phase:"P1",area:"volume",req:["GET","/volumes"],check:status(200)},{id:"networks-list",phase:"P1",area:"network",req:["GET","/networks"],check:status(200)},{id:"events",phase:"P1",area:"system",req:["GET","/events?until=0"],check:status(200)},{id:"network-create",phase:"P2",area:"network",req:["POST","/networks/create",{Name:"dock-compat-net",Labels:{"tnc.compose.project":"compat"}}],check:status(201)},{id:"network-remove",phase:"P2",area:"network",req:["DELETE","/networks/dock-compat-net"],check:status(204)},{id:"system-df",phase:"P2",area:"system",req:["GET","/system/df"],check:status(200)},{id:"containers-prune",phase:"P2",area:"container",req:["POST",`/containers/prune?filters=${encodeURIComponent('{"label":["tnc.compat=1"]}')}`],check:status(200)},{id:"volumes-prune",phase:"P2",area:"volume",req:["POST",`/volumes/prune?filters=${encodeURIComponent('{"label":["tnc.compat=1"]}')}`],check:status(200)},{id:"auth-missing",phase:"P2",area:"registry",req:["POST","/auth",{serveraddress:"localhost:1"}],check:e=>400===e.status&&/required/.test(e.body?.message||"")||"login without credentials must get 400"},{id:"image-push-missing",phase:"P2",area:"registry",req:["POST","/images/localhost:1/dock-compat-none/push?tag=1"],check:status(404)}],CLI_CASES=[{id:"cli-info",phase:"P0",argv:["system","info"],expect:/dock \d/},{id:"cli-ps",phase:"P1",argv:["ps","-a"],expect:/CONTAINER ID/},{id:"cli-run",phase:"P1",argv:["run","--rm","alpine:3.20","echo","dock-ok"],expect:/dock-ok/},{id:"cli-exit",phase:"P1",argv:["run","--rm","alpine:3.20","sh","-c","exit 3"],status:3,expect:/^/},{id:"cli-port",phase:"P1",argv:["run","-d","--name","dock-web","-p","18081:80","nginx:alpine"],expect:/^[0-9a-f]{12,}/m},{id:"cli-exec",phase:"P1",argv:["exec","dock-web","nginx","-v"],expect:/nginx version/},{id:"cli-logs",phase:"P1",argv:["logs","dock-web"],expect:/^/},{id:"cli-build",phase:"P2",argv:["build","-q","-t","dock-compat-img:1","{dir}"],files:{[BUILD_FILE]:"FROM alpine:3.20 AS a\nRUN echo hi > /x\nFROM alpine:3.20\nCOPY --from=a /x /x\n"},expect:/sha256:/,teardown:[["rmi","-f","dock-compat-img:1"]]},{id:"cli-push",phase:"P2",argv:["push","localhost:15001/dock-compat/app:1"],expect:/Pushed|Layer already exists/,setup:[["rm","-f","dock-compat-registry"],["run","-d","--name","dock-compat-registry","-p","15001:5000","registry:2"],["run","--rm","alpine:3.20","sleep","2"],["tag","alpine:3.20","localhost:15001/dock-compat/app:1"]],teardown:[["rmi","localhost:15001/dock-compat/app:1"],["rm","-f","dock-compat-registry"]]},{id:"cli-net-dns",phase:"P2",argv:["run","--rm","--network","dock-compat-net","alpine:3.20","sh","-c","cat /etc/hosts | grep -c localhost"],expect:/^2$/m,setup:[["network","create","dock-compat-net"]],teardown:[["network","rm","dock-compat-net"]]},{id:"cli-run-i",phase:"P2",argv:["run","--rm","-i","alpine:3.20","cat"],stdin:"stdin-ok\n",expect:/stdin-ok/},{id:"cli-health",phase:"P2",argv:["run","--rm","--health-cmd","true","--health-interval","1s","alpine:3.20","sh","-c","echo health-ok"],expect:/health-ok/},{id:"cli-limits",phase:"P3",argv:["run","--rm","-m","64m","--cpus","0.5","--pids-limit","50","alpine:3.20","cat","/sys/fs/cgroup/memory.max","/sys/fs/cgroup/cpu.max","/sys/fs/cgroup/pids.max"],expect:/^67108864\s+50000 100000\s+50\s*$/m},{id:"cli-oom",phase:"P3",argv:["run","--rm","-m","32m","alpine:3.20","sh","-c","tail /dev/zero"],status:137,expect:/^/},{id:"cli-compose",phase:"P3",argv:["compose","-f","{dir}/compose.yaml","-p","dockcompat","up","-d","--wait"],files:{"compose.yaml":'services:\n web:\n image: nginx:alpine\n healthcheck: { test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/ >/dev/null"], interval: 1s, retries: 30 }\n probe:\n image: alpine:3.20\n command: ["sh", "-c", "wget -qO- http://web/ | grep -q nginx && sleep 300"]\n depends_on: { web: { condition: service_healthy } }\n'},expect:/healthy/,teardown:[["compose","-f","{dir}/compose.yaml","-p","dockcompat","down","-v"]]},{id:"cli-cp-in",phase:"P7",argv:["exec","dock-cp","cat","/tmp/hello.txt"],files:{"hello.txt":"cp-in-ok\n"},expect:/cp-in-ok/,setup:[["rm","-f","dock-cp"],["run","-d","--name","dock-cp","alpine:3.20","sleep","300"],["cp","{dir}/hello.txt","dock-cp:/tmp/"]]},{id:"cli-cp-out",phase:"P7",argv:["cp","dock-cp:/tmp/hello.txt","-"],expect:/cp-in-ok/,teardown:[["rm","-f","dock-cp"]]},{id:"cli-stats",phase:"P7",argv:["stats","--no-stream","dock-stats"],expect:/dock-stats\s+\d+\.\d+%/,setup:[["rm","-f","dock-stats"],["run","-d","--name","dock-stats","alpine:3.20","sleep","300"]]},{id:"cli-top",phase:"P7",argv:["top","dock-stats"],expect:/sleep 300/,teardown:[["rm","-f","dock-stats"]]},{id:"cli-pause",phase:"P7",argv:["ps","-f","status=paused"],expect:/dock-pause/,setup:[["rm","-f","dock-pause"],["run","-d","--name","dock-pause","alpine:3.20","sleep","300"],["pause","dock-pause"]],teardown:[["rm","-f","dock-pause"]]},{id:"cli-rename",phase:"P7",argv:["ps","-a","-f","name=dock-ren2"],expect:/dock-ren2/,setup:[["rm","-f","dock-ren1","dock-ren2"],["create","--name","dock-ren1","alpine:3.20","true"],["rename","dock-ren1","dock-ren2"]],teardown:[["rm","-f","dock-ren2"]]},{id:"cli-update",phase:"P7",argv:["stats","--no-stream","dock-upd"],expect:/\/ 67\.1MB/,setup:[["rm","-f","dock-upd"],["run","-d","--name","dock-upd","alpine:3.20","sleep","300"],["update","--memory","64m","dock-upd"]],teardown:[["rm","-f","dock-upd"]]},{id:"cli-init",phase:"P7",argv:["run","--rm","--init","alpine:3.20","sh","-c",'tr "\\0" " " < /proc/1/cmdline'],expect:/tnc-init init/},{id:"cli-ulimit",phase:"P7",argv:["run","--rm","--ulimit","nofile=1024:2048","alpine:3.20","sh","-c","ulimit -n"],expect:/^1024$/m},{id:"cli-dns",phase:"P7",argv:["run","--rm","--dns","9.9.9.9","--dns-search","example.test","alpine:3.20","cat","/etc/resolv.conf"],expect:/nameserver 9\.9\.9\.9[\s\S]*search example\.test/},{id:"cli-mount",phase:"P7",argv:["run","--rm","--mount","type=tmpfs,target=/scratch","alpine:3.20","sh","-c",'grep " /scratch " /proc/mounts'],expect:/tmpfs/},{id:"cli-build-mounts",phase:"P7",argv:["build","--no-cache","-t","dock-compat-mounts:1","--secret","id=tok,src={dir}/tok.txt","{dir}"],files:{"tok.txt":"s3cret","ctx.txt":"bind-ok",[BUILD_FILE]:'FROM alpine:3.20\nRUN --mount=type=cache,target=/cache echo x >> /cache/f && test -s /cache/f\nRUN --mount=type=secret,id=tok test "$(cat /run/secrets/tok)" = s3cret && echo secret-ok\nRUN --mount=type=bind,source=ctx.txt,target=/mnt/ctx.txt cat /mnt/ctx.txt\nRUN <<EOF\necho heredoc-ok > /hd\nEOF\nCOPY <<EOF /greeting\nhello-heredoc\nEOF\nRUN test ! -e /run/secrets/tok && test ! -e /cache/f && cat /hd /greeting\n'},expect:/secret-ok[\s\S]*bind-ok[\s\S]*heredoc-ok\s+hello-heredoc/,teardown:[["rmi","-f","dock-compat-mounts:1"]]},{id:"cli-save-load",phase:"P7",argv:["load","-i","{dir}/a.tar"],files:{".keep":""},expect:/Loaded image: alpine:3\.20/,setup:[["save","-o","{dir}/a.tar","alpine:3.20"]]},{id:"cli-commit",phase:"P7",argv:["run","--rm","dock-compat-commit:1","cat","/c.txt"],expect:/committed/,setup:[["rm","-f","dock-cm"],["run","--name","dock-cm","alpine:3.20","sh","-c","echo committed > /c.txt"],["commit","-c","ENV X=1","dock-cm","dock-compat-commit:1"]],teardown:[["rm","-f","dock-cm"],["rmi","-f","dock-compat-commit:1"]]},{id:"cli-diff",phase:"P7",argv:["diff","dock-df"],expect:/^A \/newfile$/m,setup:[["rm","-f","dock-df"],["run","--name","dock-df","alpine:3.20","touch","/newfile"]],teardown:[["rm","-f","dock-df"]]},{id:"cli-history",phase:"P7",argv:["image","history","alpine:3.20"],expect:/CREATED BY/},{id:"cli-import",phase:"P7",argv:["run","--rm","dock-compat-import:1","cat","/etc/alpine-release"],files:{".keep":""},expect:/^3\.20/m,setup:[["rm","-f","dock-ex"],["create","--name","dock-ex","alpine:3.20","true"],["container","export","-o","{dir}/fs.tar","dock-ex"],["import","-c",'CMD ["sh"]',"{dir}/fs.tar","dock-compat-import:1"]],teardown:[["rm","-f","dock-ex"],["rmi","-f","dock-compat-import:1"]]},{id:"cli-compose-extends",phase:"P7",argv:["compose","-f","{dir}/compose.yaml","-p","dockcompat2","exec","-T","app","sh","-c","cat /run/secrets/tok /cfg; echo; echo $FROM_BASE"],files:{"compose.yaml":'include: [inc.yaml]\nservices:\n base:\n image: alpine:3.20\n command: ["sleep", "300"]\n environment: { FROM_BASE: "from-base" }\n app:\n extends: base\n secrets: [tok]\n configs: [{ source: cfg, target: /cfg }]\nsecrets:\n tok: { file: ./tok.txt }\nconfigs:\n cfg: { content: "config-ok" }\n',"inc.yaml":'services:\n extra:\n image: alpine:3.20\n command: ["sleep", "300"]\n',"tok.txt":"tok-ok\n"},expect:/tok-ok[\s\S]*config-ok[\s\S]*from-base/,setup:[["compose","-f","{dir}/compose.yaml","-p","dockcompat2","up","-d"]],teardown:[["compose","-f","{dir}/compose.yaml","-p","dockcompat2","down"]]},{id:"cli-host-internal",phase:"P7",argv:["run","--rm","alpine:3.20","getent","hosts",HOST_ALIAS],expect:new RegExp(HOST_ALIAS.replace(/\./g,"\\."))},{id:"cli-engine-socket",phase:"P7",argv:["run","--rm","-v",`${ENGINE_SOCKET}:${ENGINE_SOCKET}`,ENGINE_CLI_IMAGE,ENGINE_CLI,"version","--format","server={{.Server.APIVersion}}"],expect:/server=1\.\d+/},{id:"cli-bind-tmpdir",phase:"P7",argv:["run","--rm","-v","{dir}:/d","alpine:3.20","cat","/d/hello.txt"],files:{"hello.txt":"tmpdir-share-ok\n"},expect:/tmpdir-share-ok/},{id:"cli-platform",phase:"P8",argv:["run","--rm","--platform","linux/amd64","busybox:1.36","uname","-m"],expect:/x86_64/,teardown:[["rmi","-f","busybox:1.36"]]}],CLEANUP=[["DELETE","/containers/dock-compat?force=1"],["DELETE","/containers/dock-web?force=1"],["DELETE","/networks/dock-compat-net"],["DELETE","/containers/dock-compat-registry?force=1"],["DELETE","/images/dock-compat-commit:api?force=1"],...["dock-cp","dock-stats","dock-pause","dock-ren1","dock-ren2","dock-upd","dock-cm","dock-df","dock-ex"].map(e=>["DELETE",`/containers/${e}?force=1`])];module.exports={API_CASES,CLI_CASES,CLEANUP};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const fs=require("fs"),os=require("os"),path=require("path"),{spawnSync}=require("child_process"),{request}=require("../client"),{PHASE}=require("../version"),{API_CASES,CLI_CASES,CLEANUP}=require("./cases"),CLI=[process.execPath,path.join(__dirname,"..","..","..","bin","thinknsh.js")];function classify(e,t,s){const n=e<=s;return t?n?"pass":"early":n?"fail":"pending"}async function runApiCase(e,t,s){const[n,r,a]=t.req;let i;try{const s=await request(e,n,r,{body:a,timeout:1e4});i=t.check(s)}catch(e){i=e.message}const o=!0===i;return{id:t.id,kind:"api",area:t.area,phase:t.phase,result:classify(t.phase,o,s),detail:o?"":String(i)}}function runCliCase(e,t,s){const n=t.files?fs.mkdtempSync(path.join(os.tmpdir(),"dock-compat-")):null;for(const[e,s]of Object.entries(t.files||{}))fs.writeFileSync(path.join(n,e),s);const r=e=>e.map(e=>n?e.replace("{dir}",n):e),a={...process.env,TNC_DOCK_HOME:e},i=e=>spawnSync(CLI[0],[CLI[1],...r(e)],{encoding:"utf8",env:a,input:void 0,timeout:18e4});for(const e of t.setup||[])i(e);const o=spawnSync(CLI[0],[CLI[1],...r(t.argv)],{encoding:"utf8",input:t.stdin,timeout:18e4,env:a});for(const e of t.teardown||[])i(e);n&&fs.rmSync(n,{recursive:!0,force:!0});const c=`${o.stdout||""}${o.stderr||""}`,u=o.status===(t.status??0)&&t.expect.test(c);return{id:t.id,kind:"cli",area:"cli",phase:t.phase,result:classify(t.phase,u,s),detail:u?"":(c.trim().split("\n").pop()||`exit ${o.status}`).slice(0,160)}}async function cleanup(e){for(const[t,s]of CLEANUP)await request(e,t,s,{timeout:3e4}).catch(()=>{})}async function runCompat({socketPath:e,home:t=path.dirname(e),currentPhase:s=PHASE,cli:n=!0,throughPhase:r=null}){const a=e=>!r||e.phase<=r;await cleanup(e);const i=[];for(const t of API_CASES.filter(a))i.push(await runApiCase(e,t,s));if(n)for(const e of CLI_CASES.filter(a))i.push(runCliCase(t,e,s));await cleanup(e);const o={};for(const e of i)o[e.result]=(o[e.result]||0)+1;return{phase:s,results:i,tally:o,ok:!i.some(e=>"fail"===e.result)}}function toMarkdown(e){const t={pass:"✅",fail:"❌",pending:"⏳",early:"🟢"};return[`# dock check report (phase ${e.phase})`,"","| Case | Kind | Phase | Result | Detail |","| --- | --- | --- | --- | --- |",...e.results.map(e=>`| ${e.id} | ${e.kind} | ${e.phase} | ${t[e.result]} ${e.result} | ${e.detail.replace(/\|/g,"\\|")} |`)].join("\n")+"\n"}module.exports={runCompat,toMarkdown};
@@ -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");function configFile(){return path.join(process.env.TNC_DOCK_HOME||path.join(os.homedir(),".tnc","dock"),"config.json")}function readConfig(){try{return JSON.parse(fs.readFileSync(configFile(),"utf8"))||{}}catch(e){return{}}}function writeConfig(e){fs.mkdirSync(path.dirname(configFile()),{recursive:!0,mode:448}),fs.writeFileSync(configFile(),JSON.stringify(e,null,2)+"\n")}function defaults(){const e=os.cpus().length||2;return{cpus:Math.max(2,Math.floor(e/2)),memoryMB:Math.min(4096,Math.floor(os.totalmem()/1024/1024/2)),diskGB:64,shares:[],publishAddress:"127.0.0.1"}}function effective(){const e={...defaults(),...readConfig()};return Number(process.env.TNC_DOCK_VM_CPUS)&&(e.cpus=Number(process.env.TNC_DOCK_VM_CPUS)),Number(process.env.TNC_DOCK_VM_MEMORY_MB)&&(e.memoryMB=Number(process.env.TNC_DOCK_VM_MEMORY_MB)),e.shares=(e.shares||[]).map(e=>path.resolve(e)),process.env.TNC_DOCK_PUBLISH_ADDRESS&&(e.publishAddress=process.env.TNC_DOCK_PUBLISH_ADDRESS),e}module.exports={readConfig,writeConfig,defaults,effective,configFile};