thinkncollab-shell 2.0.0-beta.1.1 → 2.0.0-beta.3

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 (51) hide show
  1. package/Readme.md +41 -10
  2. package/dist/native/tnc-tray/build-macos.sh +54 -0
  3. package/dist/native/tnc-tray/main.swift +201 -0
  4. package/dist/native/tnc-tray/tray.ps1 +136 -0
  5. package/dist/src/commands/dock/containers.js +1 -1
  6. package/dist/src/commands/dock/dock.js +1 -1
  7. package/dist/src/dock/api/routes.js +1 -1
  8. package/dist/src/dock/api/server.js +1 -1
  9. package/dist/src/dock/api/websocket.js +7 -0
  10. package/dist/src/dock/api/wire.js +1 -1
  11. package/dist/src/dock/build/builder.js +1 -1
  12. package/dist/src/dock/build/buildkit.js +1 -1
  13. package/dist/src/dock/build/cache.js +7 -0
  14. package/dist/src/dock/build/sshagent.js +7 -0
  15. package/dist/src/dock/cli/api.js +1 -1
  16. package/dist/src/dock/cli/commands.js +1 -1
  17. package/dist/src/dock/cli/compose.js +1 -1
  18. package/dist/src/dock/cli/trust.js +7 -0
  19. package/dist/src/dock/compat/cases.js +1 -1
  20. package/dist/src/dock/config.js +1 -1
  21. package/dist/src/dock/daemon-main.js +1 -1
  22. package/dist/src/dock/daemon.js +1 -1
  23. package/dist/src/dock/engine.js +1 -1
  24. package/dist/src/dock/image/auth.js +1 -1
  25. package/dist/src/dock/image/credhelpers.js +7 -0
  26. package/dist/src/dock/image/keychain.js +7 -0
  27. package/dist/src/dock/image/registry.js +1 -1
  28. package/dist/src/dock/image/scan.js +7 -0
  29. package/dist/src/dock/image/sign.js +7 -0
  30. package/dist/src/dock/linux/host.js +1 -1
  31. package/dist/src/dock/paths.js +1 -1
  32. package/dist/src/dock/runtime/cdi.js +7 -0
  33. package/dist/src/dock/runtime/fsops.js +1 -1
  34. package/dist/src/dock/runtime/network.js +1 -1
  35. package/dist/src/dock/runtime/overlaydns.js +7 -0
  36. package/dist/src/dock/runtime/ports.js +1 -1
  37. package/dist/src/dock/runtime/rootfs.js +1 -1
  38. package/dist/src/dock/runtime/spec.js +1 -1
  39. package/dist/src/dock/runtime/stats.js +1 -1
  40. package/dist/src/dock/runtime/userns.js +7 -0
  41. package/dist/src/dock/runtime/volumes.js +1 -1
  42. package/dist/src/dock/setup/autostart.js +1 -1
  43. package/dist/src/dock/setup/diagnose.js +7 -0
  44. package/dist/src/dock/setup/index.js +1 -1
  45. package/dist/src/dock/state/store.js +1 -1
  46. package/dist/src/dock/tray/agent.js +7 -0
  47. package/dist/src/dock/tray/install.js +7 -0
  48. package/dist/src/dock/ui/page.js +228 -15
  49. package/dist/src/dock/ui/server.js +1 -1
  50. package/package.json +1 -1
  51. package/dist/src/dock/setup/app.js +0 -7
@@ -4,4 +4,4 @@
4
4
  * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
5
  * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
6
  */
7
- "use strict";const 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};
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]),n=t[2]?Number(t[2]):s;return n>=s&&n-s<1e4?Array.from({length:n-s+1},(e,t)=>s+t):null};for(const n of e){const[e,r="tcp"]=n.split("/");if(!["tcp","udp"].includes(r))throw new UsageError(`invalid protocol "${r}" in "${n}" (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),h=p?s(p):null;if(!u||p&&!h)throw new UsageError(`invalid port "${n}" (examples: -p 8080:80, -p 5353:53/udp, -p 8000-8002:8000-8002)`);if(h&&u.length>1&&h.length!==u.length)throw new UsageError(`port ranges in "${n}" must have the same length`);u.forEach((e,s)=>{const n=h?u.length>1?String(h[s]):h.length>1?`${h[0]}-${h[h.length-1]}`:String(h[0]):"";(t[`${e}/${r}`]||=[]).push({HostIp:o,HostPort:n})})}return t}function parseMountFlag(e){const t={Type:"volume"};for(const s of String(e).split(",")){if(!s)continue;const e=s.indexOf("="),n=(-1===e?s:s.slice(0,e)).toLowerCase(),r=-1===e?"true":s.slice(e+1);if("type"===n)t.Type=r;else if(["source","src"].includes(n))t.Source=r;else if(["target","destination","dst"].includes(n))t.Target=r;else if(["readonly","ro"].includes(n))t.ReadOnly=!["false","0"].includes(r);else if("tmpfs-size"===n)t.TmpfsOptions={...t.TmpfsOptions||{},SizeBytes:parseBytes(r)};else{if("tmpfs-mode"!==n){if("bind-propagation"===n||"consistency"===n||"volume-nocopy"===n)continue;throw new UsageError(`unknown --mount option "${n}"`)}t.TmpfsOptions={...t.TmpfsOptions||{},Mode:parseInt(r,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 gpuRequest(e){const t={count:[],device:[],capabilities:[],driver:[]};let s="count";for(const n of String(e).replace(/"/g,"").split(",").map(e=>e.trim()).filter(Boolean)){const e=/^(count|device|capabilities|driver)=(.*)$/.exec(n);e?(s=e[1],t[s].push(e[2])):t[s].push(n)}const n=t.count[0];if(!t.device.length&&!/^(all|\d+)$/.test(n||""))throw new UsageError(`invalid --gpus value "${e}" (all, a number, or device=0,1)`);return{Driver:t.driver[0]||"nvidia",Count:t.device.length?0:"all"===n?-1:Number(n),DeviceIDs:t.device,Capabilities:[["gpu",...t.capabilities]]}}function parseDevice(e){const[t,s,n]=String(e).split(":");return{PathOnHost:t,PathInContainer:s||t,CgroupPermissions:n||"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:n=process.stderr,platform:r}={}){const{repo:o,tag:i}=splitRepoTag(t.includes("@")?t.split("@")[0]:t),a=fmt.pullPrinter(n),l={...t.includes("@")?{fromImage:t.split("@")[0],tag:t.split("@")[1]}:{fromImage:o,tag:i},platform:r};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:n="missing",quiet:r=!1,err:o,platform:i}){"always"===n&&await pullImage(e,s.Image,{quiet:r,err:o,platform:i});try{return await e.json("POST",`/containers/create${qs({name:t,platform:i})}`,s)}catch(a){if(!isMissingImage(a)||"never"===n)throw a;return r||o.write(`Unable to find image '${s.Image}'${i?` for ${i}`:""} locally\n`),await pullImage(e,s.Image,{quiet:r,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"},userns:{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 n=[];for(const t of e["env-file"])n.push(...readEnvFile(t));n.push(...parseEnvList(e.env));const r=e.network||e.net;if(e.cgroupns&&!["host","private"].includes(e.cgroupns))throw new UsageError(`invalid --cgroupns "${e.cgroupns}" (private or host)`);if(e.userns&&"host"!==e.userns)throw new UsageError(`invalid --userns "${e.userns}" (host: leave out userns-remap for this container)`);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),a=e.gpus?[gpuRequest(e.gpus)]:[],l={};for(const t of e["label-file"])Object.assign(l,parseLabels(readEnvFile(t)));Object.assign(l,parseLabels(e.label));const c=parseLabels(e["log-opt"]);return{Image:t,...s.length?{Cmd:s}:{},...void 0!==e.entrypoint?{Entrypoint:""===e.entrypoint?[""]:[e.entrypoint]}:{},Env:n,Labels:l,...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,...r?{NetworkMode:r}:{},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}:{},...e.userns?{UsernsMode:e.userns}:{},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)}:{},...a.length?{DeviceRequests:a}:{},...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:c}}:{}},...r&&e["network-alias"].length?{NetworkingConfig:{EndpointsConfig:{[r]:{Aliases:e["network-alias"]}}}}:{}}}async function resizeTo(e,t,s,n,r){await e.json("POST",`/${t}/${enc(s)}/resize${qs({h:n,w:r})}`).catch(()=>{})}async function attachAndWait(e,t,{stdin:s,tty:n,startIt:r,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));r&&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(()=>{});n||c.forEach(e=>process.on(e,p));try{const{detached:r}=await pipeSession(i,{stdin:s,resize:n?(s,n)=>resizeTo(e,"containers",t,s,n):null});if(r)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{n||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:n}){const{opts:r,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(r.tty&&!process.stdin.isTTY&&r.interactive)throw new UsageError("the input device is not a TTY; drop -t when piping input");const[i,...a]=o,l=createBody(r,i,a);if(r.detach&&(l.AttachStdout=l.AttachStderr=!1),r.cidfile&&fs.existsSync(r.cidfile))throw new UsageError(`container ID file found, make sure the other container isn't running or delete ${r.cidfile}`);const{Id:c}=await createContainer(t,r.name,l,{pull:r.pull,quiet:r.quiet,err:n,platform:r.platform});if(r.cidfile&&fs.writeFileSync(r.cidfile,c),r.detach)return await t.json("POST",`/containers/${c}/start`),s.write(`${c}\n`),0;const{detached:p,code:u}=await attachAndWait(t,c,{stdin:r.interactive,tty:r.tty,startIt:!0,err:n});return p&&s.write(`${fmt.shortId(c)}\n`),u}async function create(e,{api:t,out:s,err:n}){const{opts:r,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,r.name,createBody(r,i,a),{pull:r.pull,quiet:r.quiet,err:n,platform:r.platform});return r.cidfile&&fs.writeFileSync(r.cidfile,l),s.write(`${l}\n`),0}async function each(e,t,{out:s,err:n},r){if(!e.length)throw new UsageError(`${r} needs at least one container`);let o=0;for(const r of e)try{await t(r),s.write(`${r}\n`)}catch(e){o++,n.write(`Error: ${e.message}\n`)}return o?1:0}async function start(e,t){const{opts:s,args:n}=parseArgs(e,{attach:{short:"a",type:"bool"},interactive:{short:"i",type:"bool"}});if(s.attach||s.interactive){if(1!==n.length)throw new UsageError("start -a takes exactly one container");const e=await t.api.json("GET",`/containers/${enc(n[0])}/json`),{code:r}=await attachAndWait(t.api,e.Id,{stdin:s.interactive,tty:e.Config.Tty,startIt:!0,err:t.err});return r}return each(n,e=>t.api.json("POST",`/containers/${enc(e)}/start`),t,"start")}async function stop(e,t){const{opts:s,args:n}=parseArgs(e,{time:{short:"t",type:"string"},timeout:{type:"string"}}),r=s.time??s.timeout;return each(n,e=>t.api.json("POST",`/containers/${enc(e)}/stop${qs({t:r})}`),t,"stop")}async function restart(e,t){const{opts:s,args:n}=parseArgs(e,{time:{short:"t",type:"string"}});return each(n,e=>t.api.json("POST",`/containers/${enc(e)}/restart${qs({t:s.time})}`),t,"restart")}async function kill(e,t){const{opts:s,args:n}=parseArgs(e,{signal:{short:"s",type:"string",default:"SIGKILL"}});return each(n,e=>t.api.json("POST",`/containers/${enc(e)}/kill${qs({signal:s.signal})}`),t,"kill")}async function rm(e,t){const{opts:s,args:n}=parseArgs(e,{force:{short:"f",type:"bool"},volumes:{short:"v",type:"bool"}});return each(n,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:n}=parseArgs(e,{});if(!n.length)throw new UsageError("wait needs at least one container");let r=0;for(const e of n){const n=await t.json("POST",`/containers/${enc(e)}/wait`);r=("string"==typeof n?JSON.parse(n.trim()):n).StatusCode,s.write(`${r}\n`)}return 0}async function ps(e,{api:t,out:s}){const{opts:n}=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"}}),r=await t.json("GET",`/containers/json${qs({all:n.all||void 0,filters:filtersOf(n.filter),limit:n.last})}`);if(n.json)return s.write(JSON.stringify(r,null,2)+"\n"),0;if(n.quiet){for(const e of r)s.write(`${n["no-trunc"]?e.Id:fmt.shortId(e.Id)}\n`);return 0}return fmt.table(["CONTAINER ID","IMAGE","COMMAND","CREATED","STATUS","PORTS","NAMES"],r.map(e=>{return[n["no-trunc"]?e.Id:fmt.shortId(e.Id),e.Image,`"${t=e.Command||"",n["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:n}=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!==n.length)throw new UsageError("logs takes exactly one container");const r=await t.stream("GET",`/containers/${enc(n[0])}/logs${qs({stdout:1,stderr:1,follow:s.follow,tail:s.tail,timestamps:s.timestamps,since:s.since,until:s.until})}`),o=(r.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 r)o(e);return 0}async function exec(e,{api:t,out:s}){const{opts:n,args:r}=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(r.length<2)throw new UsageError("exec needs a container and a command, e.g. thinknsh exec -it web sh");const[o,...i]=r,a=[];for(const e of n["env-file"])a.push(...readEnvFile(e));a.push(...parseEnvList(n.env));const{Id:l}=await t.json("POST",`/containers/${enc(o)}/exec`,{Cmd:i,AttachStdin:n.interactive&&!n.detach,AttachStdout:!n.detach,AttachStderr:!n.detach,Tty:n.tty,Env:a,...n.workdir?{WorkingDir:n.workdir}:{},...n.user?{User:n.user}:{},Privileged:n.privileged});if(n.detach)return await t.json("POST",`/exec/${l}/start`,{Detach:!0,Tty:n.tty}),0;const c=await t.hijack(`/exec/${l}/start`,{Detach:!1,Tty:n.tty}),{detached:p}=await pipeSession(c,{stdin:n.interactive,resize:n.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:n,args:r}=parseArgs(e,{type:{type:"string"}});if(!r.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=n.type?[n.type]:Object.keys(o),a=[];let l=0;for(const e of r){let s=null;for(const n of i){if(!o[n])throw new UsageError(`unknown type "${n}" (container, image, volume, network)`);try{s=await t.json("GET",o[n](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:n,args:r}=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(n.filter)||{};r[0]&&(o.reference=[r[0]]);const i=await t.json("GET",`/images/json${qs({all:n.all||void 0,filters:Object.keys(o).length?o:void 0})}`);if(n.json)return s.write(JSON.stringify(i,null,2)+"\n"),0;const a=e=>n["no-trunc"]?e.Id:fmt.shortId(e.Id);if(n.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:n}=splitRepoTag(s);l.push([t,n,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:n,args:r}=parseArgs(e,{quiet:{short:"q",type:"bool"},platform:{type:"string"}});if(1!==r.length)throw new UsageError("pull takes one image, e.g. thinknsh pull postgres:16");return await pullImage(t,r[0],{quiet:n.quiet,err:s,platform:n.platform}),n.quiet&&process.stdout.write(`${r[0]}\n`),0}async function rmi(e,{api:t,out:s,err:n}){const{opts:r,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 n=await t.json("DELETE",`/images/${e}${qs({force:r.force})}`);for(const e of n||[])s.write(e.Untagged?`Untagged: ${e.Untagged}\n`:`Deleted: ${e.Deleted}\n`)}catch(e){i++,n.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:n,tag:r}=splitRepoTag(s[1]);return await t.json("POST",`/images/${s[0]}/tag${qs({repo:n,tag:r})}`),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,n)=>{const r=process.stdin;t.write(e),r.setRawMode(!0),r.resume();let o="";const i=e=>{r.setRawMode(!1),r.pause(),r.removeListener("data",a),t.write("\n"),e instanceof Error?n(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}};r.on("data",a)})}async function login(e,{api:t,out:s,err:n}){const{opts:r,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=r.username;if(r["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:n})).trim()),l=await promptHidden("Password: ",{err:n})}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:n}=parseArgs(e,{});if(n.length>1)throw new UsageError("logout takes at most one registry");const r=await t.json("DELETE",`/auth${qs({serveraddress:n[0]})}`);return s.write(r.Removed?`Removing login credentials for ${r.Registry}\n`:`Not logged in to ${r.Registry}\n`),0}async function push(e,{api:t,out:s,err:n}){const{opts:r,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(r["all-tags"]&&l)throw new UsageError("--all-tags takes a repository without a tag");const c=fmt.pullPrinter(n);let p=null;return await t.progress("POST",`/images/${i}/push${qs({tag:r["all-tags"]?void 0:a})}`,null,e=>{e.aux?.Digest&&(p=e.aux),r.quiet||c.onMessage(e)}),c.done(),r.quiet&&p&&s.write(`${i}:${p.Tag}@${p.Digest}\n`),0}async function build(e,{api:t,out:s,err:n}){const{opts:r,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"},push:{type:"bool"},"cache-from":{type:"list"},"cache-to":{type:"list"}}),{parseSshSpec:i,openSession:a}=require("../build/sshagent");let l;try{l=r.ssh.map(e=>i(e))}catch(e){throw new UsageError(e.message)}const c=r["cache-from"].map(localCachePath),p=r["cache-to"].map(localCachePath),u=(r.platform||"").split(",").map(e=>e.trim()).filter(Boolean);if(r.push&&!r.tag.length)throw new UsageError("--push needs a name: -t registry/repo:tag");const h=path.resolve(o[0]||".");if(!fs.existsSync(h)||!fs.statSync(h).isDirectory())throw new UsageError(`build context ${h} is not a folder`);const{tar:m,buildFile:g}=packContext(h,r.file,{secrets:r.secret.map(parseSecret)});r.quiet||n.write(chalk.dim(`sending build context (${fmt.size(m.length)})\n`));const f=Object.fromEntries(r["build-arg"].map(e=>{const t=e.indexOf("=");return-1===t?[e,process.env[e]??null]:[e.slice(0,t),e.slice(t+1)]})),d=l.length?await a(t,l):null;try{return await buildWith({api:t,out:s,err:n,o:r,tar:m,buildFile:g,buildargs:f,platforms:u,cacheFrom:c,cacheTo:p,agent:d})}finally{d?.close()}}function localCachePath(e){return String(e).replace(/(^|,)(src|dest)=([^,]+)/,(e,t,s,n)=>`${t}${s}=${path.resolve(n)}`)}async function buildWith({api:e,out:t,err:s,o:n,tar:r,buildFile:o,buildargs:i,platforms:a,cacheFrom:l,cacheTo:c,agent:p}){const u=async(a,u,h="")=>{let m=null;return await e.progress("POST",`/build${qs({t:u,[BUILD_FILE_PARAM]:o,buildargs:i,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:a,cachefrom:l.length?JSON.stringify(l):void 0,cacheto:c.length?JSON.stringify(c):void 0,sshsession:p?.session})}`,r,e=>{e.aux?.ID&&(m=e.aux.ID),e.stream&&!n.quiet?t.write(h?e.stream.replace(/^(?=.)/gm,h):e.stream):e.status&&!n.quiet&&s.write(`${h}${e.status}\n`)},{"Content-Type":"application/x-tar"}),m};if(a.length<=1){const r=await u(a[0],n.tag);if(n.quiet&&r&&t.write(`${r}\n`),n.push)for(const r of n.tag)await push([r,...n.quiet?["-q"]:[]],{api:e,out:t,err:s});return 0}const h=[];for(const e of a)n.quiet||s.write(chalk.bold(`\n▸ ${e}\n`)),h.push({platform:e,id:await u(e,[],chalk.dim(`[${e}] `))});const m={arm64:"arm64",x64:"amd64"}[process.arch],g=h.find(e=>e.platform.split("/")[1]===m)||h[0];for(const t of n.tag){const{repo:s,tag:n}=splitRepoTag(t);await e.json("POST",`/images/${enc(g.id)}/tag${qs({repo:s,tag:n})}`)}if(n.push)for(const r of n.tag){const o=fmt.pullPrinter(s);let i=null;await e.progress("POST",`/tnc/manifests/${r}/push`,{Images:h.map(e=>e.id)},e=>{e.aux?.Digest&&(i=e.aux),n.quiet||o.onMessage(e)}),o.done(),i&&t.write(`${r}@${i.Digest} (${h.length} platforms)\n`)}else n.quiet||(s.write(chalk.dim(`\n ${n.tag.length?`${n.tag.join(", ")} → ${g.platform} here`:`${g.platform}: ${g.id}`}; ${h.filter(e=>e!==g).map(e=>`${e.platform}: ${e.id.replace(/^sha256:/,"").slice(0,12)}`).join(", ")}\n`)),s.write(chalk.dim(" publish all platforms as one image with --push (or: thinknsh manifest create NAME IMAGE… && thinknsh manifest push NAME)\n")));if(n.quiet)for(const e of h)t.write(`${e.id}\n`);return 0}function manifestFile(e){const{dockPaths:t}=require("../paths");return path.join(t().home,"manifests",`${encodeURIComponent(e)}.json`)}async function manifest(e,{api:t,out:s,err:n}){const[r,...o]=e;switch(r){case"create":{const{opts:e,args:n}=parseArgs(o,{amend:{short:"a",type:"bool"}});if(n.length<2)throw new UsageError("manifest create NAME IMAGE… (the images are local, one per platform)");const[r,...i]=n,a=manifestFile(r),l=e.amend&&fs.existsSync(a)?JSON.parse(fs.readFileSync(a,"utf8")).images:[];if(!e.amend&&fs.existsSync(a))throw new UsageError(`a manifest list ${r} exists; add to it with --amend or remove it: thinknsh manifest rm ${r}`);const c=[...l];for(const e of i){const s=await t.json("GET",`/images/${e}/json`);c.push({ref:e,id:s.Id,platform:`${s.Os||"linux"}/${s.Architecture}${s.Variant?"/"+s.Variant:""}`})}return fs.mkdirSync(path.dirname(a),{recursive:!0}),fs.writeFileSync(a,JSON.stringify({name:r,images:c},null,2)+"\n"),s.write(`Created manifest list ${r} (${c.map(e=>e.platform).join(", ")})\n`),0}case"inspect":{const{args:e}=parseArgs(o,{});if(1!==e.length)throw new UsageError("manifest inspect NAME");const n=manifestFile(e[0]);if(fs.existsSync(n))return s.write(fs.readFileSync(n,"utf8")),0;const r=await t.json("GET",`/tnc/manifests/${e[0]}`);return s.write(JSON.stringify(r.Manifest,null,2)+"\n"),0}case"push":{const{opts:e,args:r}=parseArgs(o,{purge:{short:"p",type:"bool"},quiet:{short:"q",type:"bool"}});if(1!==r.length)throw new UsageError("manifest push NAME");const i=manifestFile(r[0]);if(!fs.existsSync(i))throw new UsageError(`no manifest list ${r[0]}: thinknsh manifest create ${r[0]} IMAGE…`);const a=JSON.parse(fs.readFileSync(i,"utf8")),l=fmt.pullPrinter(n);let c=null;return await t.progress("POST",`/tnc/manifests/${r[0]}/push`,{Images:a.images.map(e=>e.id)},t=>{t.aux?.Digest&&(c=t.aux),e.quiet||l.onMessage(t)}),l.done(),c&&s.write(`${c.Digest}\n`),e.purge&&fs.rmSync(i),0}case"rm":{const{args:e}=parseArgs(o,{});for(const t of e){const e=manifestFile(t);if(!fs.existsSync(e))throw new UsageError(`no manifest list ${t}`);fs.rmSync(e),s.write(`${t}\n`)}return 0}default:throw new UsageError("manifest create | inspect | push | rm")}}async function search(e,{api:t,out:s}){const{opts:n,args:r}=parseArgs(e,{limit:{type:"string"},filter:{short:"f",type:"list"},"no-trunc":{type:"bool"},json:{type:"bool"}});if(1!==r.length)throw new UsageError("search takes one term, e.g. thinknsh search postgres");const o=await t.json("GET",`/images/search${qs({term:r[0],limit:n.limit||25,filters:filtersOf(n.filter)})}`);return n.json?(s.write(JSON.stringify(o,null,2)+"\n"),0):(fmt.table(["NAME","DESCRIPTION","STARS","OFFICIAL"],o.map(e=>{return[e.name,(t=e.description.replace(/\s+/g," "),n["no-trunc"]||t.length<=54?t:t.slice(0,53)+"…"),e.star_count,e.is_official?"[OK]":""];var t}),{out:s}),0)}async function events(e,{api:t,out:s}){const{opts:n}=parseArgs(e,{since:{type:"string"},until:{type:"string"},filter:{short:"f",type:"list"},json:{type:"bool"}}),r=e=>{if(!e)return;const t=/^(\d+)(s|m|h)$/.exec(e);return t?String(Math.floor(Date.now()/1e3)-Number(t[1])*{s:1,m:60,h:3600}[t[2]]):e},o=await t.stream("GET",`/events${qs({since:r(n.since),until:r(n.until),filters:filtersOf(n.filter)})}`);let i="";const a=e=>{if(!e.trim())return;let t;try{t=JSON.parse(e)}catch(e){return}if(n.json)return void s.write(e.trim()+"\n");const r=new Date(t.timeNano?t.timeNano/1e6:1e3*t.time).toISOString(),o=Object.entries(t.Actor?.Attributes||{}).filter(([e])=>!e.includes(".")).map(([e,t])=>`${e}=${t}`).join(", ");s.write(`${r} ${t.Type} ${t.Action} ${t.Actor?.ID||""}${o?` (${o})`:""}\n`)};for await(const e of o){let t;for(i+=e.toString("utf8");-1!==(t=i.indexOf("\n"));)a(i.slice(0,t)),i=i.slice(t+1)}return a(i),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 n=t.src||t.source;if(n){const e=path.resolve(n.replace(/^~(?=\/|$)/,os.homedir()));if(!fs.existsSync(e))throw new UsageError(`secret file ${e} not found`);return{id:s,data:fs.readFileSync(e)}}const r=t.env||s;if(void 0===process.env[r])throw new UsageError(`--secret ${s}: set src=FILE or env=VAR (${r} is not set)`);return{id:s,data:Buffer.from(process.env[r])}}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}),n=await new Promise(t=>s.question(`${e} [y/N] `,t));return s.close(),/^y(es)?$/i.test(n.trim())}async function volume(e,t){const{api:s,out:n}=t,[r="ls",...o]=e;switch(r){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=>n.write(`${e.Name}\n`)):fmt.table(["DRIVER","VOLUME NAME"],t.map(e=>[e.Driver,e.Name]),{out:n}),0}case"create":{const{opts:e,args:t}=parseArgs(o,{label:{type:"list"}}),r=await s.json("POST","/volumes/create",{Name:t[0]||"",Labels:parseLabels(e.label)});return n.write(`${r.Name}\n`),0}case"rm":case"remove":{const{opts:e,args:n}=parseArgs(o,{force:{short:"f",type:"bool"}});return each(n,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=>n.write(`${e}\n`)),n.write(`Total reclaimed space: ${fmt.size(t.SpaceReclaimed||0)}\n`),0}default:throw new UsageError(`unknown volume command "${r}" (ls, create, rm, inspect, prune)`)}}async function network(e,t){const{api:s,out:n}=t,[r="ls",...o]=e;switch(r){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=>n.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:n}),0}case"create":{const{opts:e,args:t}=parseArgs(o,{label:{type:"list"},internal:{type:"bool"},driver:{short:"d",type:"string"},ipv6:{type:"bool"},subnet:{type:"list"},opt:{short:"o",type:"list"},parent:{type:"string"},gateway:{type:"list"},"ip-range":{type:"list"}});if(1!==t.length)throw new UsageError("network create takes one name");const r=parseLabels(e.opt);e.parent&&(r.parent=e.parent);const i=e.subnet.map((t,s)=>({Subnet:t,...e.gateway[s]?{Gateway:e.gateway[s]}:{},...e["ip-range"][s]?{IPRange:e["ip-range"][s]}:{}})),a=await s.json("POST","/networks/create",{Name:t[0],Driver:e.driver||"bridge",Internal:e.internal,Labels:parseLabels(e.label),EnableIPv6:e.ipv6,IPAM:{Config:i},Options:r});return n.write(`${a.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 ${r} takes NETWORK CONTAINER`);const n="connect"===r?{Container:t[1],EndpointConfig:{Aliases:e.alias}}:{Container:t[1],Force:e.force};return await s.json("POST",`/networks/${enc(t[0])}/${r}`,n),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=>n.write(`${e}\n`)),0):0}default:throw new UsageError(`unknown network command "${r}" (ls, create, rm, inspect, connect, disconnect, prune)`)}}async function system(e,t){const{api:s,out:n}=t,[r="info",...o]=e;switch(r){case"df":{const e=await s.json("GET","/system/df"),t=(e.Images||[]).reduce((e,t)=>e+(t.Size||0),0),r=(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(r)],["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:n}),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 r=0;return r+=(await s.json("POST","/containers/prune")).SpaceReclaimed||0,await s.json("POST","/networks/prune"),r+=(await s.json("POST",`/images/prune${qs({filters:{dangling:[e.all?"false":"true"]}})}`)).SpaceReclaimed||0,r+=(await s.json("POST",`/build/prune${qs({all:e.all})}`)).SpaceReclaimed||0,e.volumes&&(r+=(await s.json("POST","/volumes/prune")).SpaceReclaimed||0),n.write(`Total reclaimed space: ${fmt.size(r)}\n`),0}case"info":{const[e,t]=await Promise.all([s.json("GET","/version"),s.json("GET","/info")]);n.write(`dock ${e.Version} (${t.OperatingSystem||""} ${t.Architecture||""})\n`),n.write(` containers ${t.Containers} (${t.ContainersRunning} running, ${t.ContainersStopped} stopped)\n`),n.write(` images ${t.Images}\n`),n.write(` cpus ${t.NCPU} memory ${fmt.size(t.MemTotal)}\n`),(t.SecurityOptions||[]).includes("name=rootless")&&n.write(" mode rootless\n"),(t.SecurityOptions||[]).includes("name=userns")&&n.write(" userns remapped: the root of containers is an unprivileged id range\n");for(const e of t.Warnings||[])n.write(chalk.yellow(` warning ${e}\n`));return 0}default:throw new UsageError(`unknown system command "${r}" (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,manifest,search,events};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",...n]=e;if("export"===s)return COMMANDS.export(n,t);if("prune"===s){const{opts:e}=parseArgs(n,{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 r={ls:"ps",list:"ps",remove:"rm",prune:null}[s]||s;if(!COMMANDS[r]||["images","pull","push","rmi","tag","build","login","logout","volume","network","system","save","load","history","import","manifest","search","events"].includes(r))throw new UsageError(`unknown container command "${s}"`);return COMMANDS[r](n,t)}async function image(e,t){const[s="ls",...n]=e;if("prune"===s){const{opts:e}=parseArgs(n,{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 r={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",...n],t);if("scan"===s)return require("./trust").scan(n,t);if(!r[s])throw new UsageError(`unknown image command "${s}" (ls, pull, push, build, tag, rm, inspect, prune, save, load, history, import, scan)`);return COMMANDS[r[s]](n,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.trust=(e,t)=>require("./trust").trust(e,t),COMMANDS.scan=(e,t)=>require("./trust").scan(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 --userns host Run as real root although dock config --userns-remap is on\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…",manifest:"Usage: thinknsh manifest create [--amend] NAME IMAGE… | inspect NAME | push [--purge] NAME | rm NAME…\n\nA multi-platform image from local images (one per platform), e.g.\n thinknsh build --platform linux/arm64 -t app:arm64 . && thinknsh build --platform linux/amd64 -t app:amd64 .\n thinknsh manifest create ghcr.io/me/app:1 app:arm64 app:amd64 && thinknsh manifest push ghcr.io/me/app:1\ninspect on a name with no local list shows the registry's manifest (or index).",search:"Usage: thinknsh search [--limit N] [-f is-official=true|stars=N] [--no-trunc] [--json] TERM\n\nSearches the public default registry.",events:"Usage: thinknsh events [--since 10m|TIME] [--until TIME] [-f type=container|event=start|container=NAME|label=K=V] [--json]\n\nStreams engine events (Ctrl-C stops). With --until it ends there.",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 --platform linux/amd64,linux/arm64 Several at once: one build each; this machine's gets the tags\n --push Push the tags when done (several platforms: as one multi-platform image)\n --secret id=NAME,src=FILE | id=NAME,env=VAR For RUN --mount=type=secret,id=NAME\n --ssh default | ID=AGENT_SOCKET This machine's SSH agent for RUN --mount=type=ssh (git clone of private repos)\n --cache-from REF | type=registry,ref=REF | type=local,src=DIR Reuse a cache another build exported\n --cache-to type=registry,ref=REF[,mode=max] | type=local,dest=DIR | type=inline Export this build's cache\n\nBuild files may use RUN --mount=type=cache|secret|bind|tmpfs|ssh and heredocs (RUN <<EOF, COPY <<EOF /file).\nIndependent stages are built at the same time.`,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 [--ipv6] [--subnet fd00:1::/64] [--internal] NAME | rm NAME… | inspect NAME… | connect NET CONTAINER | disconnect NET CONTAINER | prune [-f]\n\n --ipv6 containers get an IPv6 address too (a /64: given with --subnet, or a unique local one); names resolve to both\n -d macvlan|ipvlan --subnet 192.168.1.0/24 --gateway 192.168.1.1 [--ip-range 192.168.1.192/27] [--parent eth0] [-o macvlan_mode=bridge]\n containers on this machine's LAN with their own addresses (Linux as root only)\n -d overlay --subnet 10.20.0.0/16 --ip-range 10.20.1.0/24 -o peers=192.168.1.11,192.168.1.12 [-o vni=4242] [-o local=IP]\n one network across machines (VXLAN, UDP 4789): create it on each, same subnet, own --ip-range (Linux as root only)",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 | scan",get trust(){return require("./trust").USAGE},get scan(){return require("./trust").SCAN_USAGE},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:n=process.stdout,err:r=process.stderr,autoStart:o=!0}={}){const i=COMMANDS[e];if(!i)return r.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 n.write(USAGE[e]+"\n"),0;try{return o&&await s.ensureRunning({quiet:!0}),await i(t,{api:s,out:n,err:r})}catch(t){return t instanceof UsageError?(r.write(`${chalk.red("Error:")} ${t.message}\n${chalk.dim(`See: thinknsh ${e} --help`)}\n`),2):(r.write(`${chalk.red("Error:")} ${t.message}\n`),"run"===e?125:1)}}module.exports={COMMANDS,USAGE,runCommand,promptHidden,parsePublish,parseMountFlag,parseUlimit,resolveBind,splitRepoTag,createBody,pullImage,readStdin,confirm,each,CREATE_FLAGS,attachAndWait,gpuRequest};
@@ -4,4 +4,4 @@
4
4
  * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
5
  * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
6
  */
7
- "use strict";const fs=require("fs"),path=require("path"),crypto=require("crypto"),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};
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 s=/(\d+(?:\.\d+)?)(ms|s|m|h)/g;let r;for(;r=s.exec(String(e));)t+=parseFloat(r[1])*{ms:.001,s:1,m:60,h:3600}[r[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),s=t.indexOf("=");return-1===s?[t,null]:[t.slice(0,s),t.slice(s+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),s=t.search(/[:=]/);return[t.slice(0,s),t.slice(s+1)]})):mapOrList(e)}function mergeDeep(e,t){if(!isObj(e)||!isObj(t))return t;const s={...e};for(const[e,r]of Object.entries(t))s[e]=isObj(s[e])&&isObj(r)?mergeDeep(s[e],r):r;return s}function mergeService(e={},t={}){const s={...e};for(const[e,r]of Object.entries(t)){const t=s[e];if(void 0!==t&&null!==r)if("command"===e||"entrypoint"===e)s[e]=r;else if(AS_MAP.has(e))s[e]="extra_hosts"===e?{...extraHostsMap(t),...extraHostsMap(r)}:{...mapOrList(t),...mapOrList(r)};else if(APPEND.has(e)){const n=[...[].concat(t),...[].concat(r)];s[e]=n.filter((e,t)=>n.findIndex(t=>JSON.stringify(t)===JSON.stringify(e))===t)}else if("volumes"===e){const n=new Map([...t,...r].map(e=>[volumeTarget(e),e]));s[e]=[...n.values()]}else if("secrets"===e||"configs"===e){const n=e=>isObj(e)?e.source:e,o=new Map([...t,...r].map(e=>[n(e),e]));s[e]=[...o.values()]}else if("depends_on"===e)s[e]={...depsOf({depends_on:t}),...depsOf({depends_on:r})};else if("networks"===e){const n=e=>Array.isArray(e)?Object.fromEntries(e.map(e=>[e,null])):e||{};s[e]=mergeDeep(n(t),n(r))}else"build"!==e||"string"!=typeof t&&"string"!=typeof r?isObj(t)&&isObj(r)?s[e]=mergeDeep(t,r):s[e]=r:s[e]=mergeDeep("string"==typeof t?{context:t}:t,"string"==typeof r?{context:r}:r);else s[e]=r}return s}function mergeDocs(e,t){const s={...e};for(const[r,n]of Object.entries(t))if("services"===r){s.services={...e.services||{}};for(const[e,t]of Object.entries(n||{}))s.services[e]=mergeService(s.services[e],t||{})}else["networks","volumes","secrets","configs"].includes(r)?s[r]=mergeDeep(e[r]||{},n||{}):"include"===r?s.include=[...e.include||[],...n||[]]:s[r]=n;return s}function absolutizeService(e,t){if(!isObj(e))return e||{};const s=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),r={...e};return"string"==typeof r.build?r.build={context:s(r.build)}:isObj(r.build)&&(r.build={...r.build,context:s(r.build.context||".")}),r.env_file&&(r.env_file=[].concat(r.env_file).map(e=>isObj(e)?{...e,path:s(e.path)}:s(e))),Array.isArray(r.volumes)&&(r.volumes=r.volumes.map(e=>{if(isObj(e))return e.source&&/^[.~]/.test(e.source)?{...e,source:s(e.source)}:e;const t=String(e);return/^[.~]/.test(t)?s(t.split(":")[0])+t.slice(t.split(":")[0].length):t})),isObj(r.extends)&&r.extends.file&&(r.extends={...r.extends,file:s(r.extends.file)}),r}function absolutizeDoc(e,t){const s={...e};isObj(e.services)&&(s.services=Object.fromEntries(Object.entries(e.services).map(([e,s])=>[e,absolutizeService(s,t)])));for(const r of["secrets","configs"])isObj(e[r])&&(s[r]=Object.fromEntries(Object.entries(e[r]).map(([e,s])=>[e,isObj(s)&&s.file?{...s,file:path.resolve(t,s.file)}:s])));return s}function readComposeFile(e,t,s=new Set){if(s.has(e))throw new UsageError(`include cycle: ${[...s,e].map(e=>path.basename(e)).join(" → ")}`);if(!fs.existsSync(e))throw new UsageError(`compose file ${e} not found`);const r=path.dirname(e);let n=absolutizeDoc(interpolate(yaml.load(fs.readFileSync(e,"utf8"))||{},t),r);for(const o of[].concat(n.include||[])){const i=isObj(o)?[].concat(o.path):[o];for(const a of i){const i=path.resolve(isObj(o)&&o.project_directory?path.resolve(r,o.project_directory):r,String(a)),c=readComposeFile(i,isObj(o)&&o.env_file?{...[].concat(o.env_file).reduce((e,t)=>({...e,...readDotEnv(path.resolve(r,t))}),{}),...process.env}:t,new Set([...s,e]));for(const t of Object.keys(c.services||{}))if(n.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,...u}=c;n=mergeDocs(u,n)}}return delete n.include,n}function resolveExtends(e,t){const s={},r=new Map,n=(e,s,o)=>{const i=e[s];if(!i)throw new UsageError(`cannot extend service "${s}": it is not defined`);if(!i.extends)return i;if(o.includes(`${s}`))throw new UsageError(`extends cycle: ${[...o,s].join(" → ")}`);const a="string"==typeof i.extends?{service:i.extends}:i.extends;let c=e;a.file&&(r.has(a.file)||r.set(a.file,readComposeFile(a.file,t).services||{}),c=r.get(a.file));const l=n(c,a.service,[...o,s]),{extends:p,...u}=i;return mergeService(l,u)};for(const t of Object.keys(e))s[t]=n(e,t,[]);return s}function loadProject({file:e,files:t,projectName:s,profiles:r=[],envFile:n}={}){const o=[].concat(t||e||[]).filter(Boolean).map(e=>path.resolve(e)),i=o[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(n?path.resolve(n):path.join(a,".env")),...process.env};let l={};for(const e of o.length?o:[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(s||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 u={};for(const[e,t]of Object.entries(l.services))t.profiles?.length&&!t.profiles.some(e=>r.includes(e))||(u[e]=t||{});for(const[e,t]of Object.entries(u)){for(const s of Object.keys(depsOf(t)))if(!u[s])throw new UsageError(`service ${e} depends on undefined service ${s}`);for(const s of["secrets","configs"])for(const r of t[s]||[]){const t=isObj(r)?r.source:r;if(!l[s]?.[t])throw new UsageError(`service ${e} refers to undefined ${s.slice(0,-1)} ${t}; declare it under top-level "${s}:"`)}}return{file:i,files:o.length?o:[i],dir:a,name:p,doc:l,services:u,varEnv:c}}function fileMounts(e,t,s){const r=[];for(const n of["secrets","configs"])for(const o of s[n]||[]){const s=isObj(o)?o.source:String(o),i=e.doc[n][s]||{};if(i.external)throw new UsageError(`service ${t}: external ${n} need a cluster; use file: or environment: here`);const a=isObj(o)&&o.target?o.target.startsWith("/")?o.target:"secrets"===n?`/run/secrets/${o.target}`:`/${o.target}`:"secrets"===n?`/run/secrets/${s}`:`/${s}`;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,n);fs.mkdirSync(t,{recursive:!0,mode:448});const r=path.join(t,s);fs.writeFileSync(r,fs.readFileSync(c),{mode:384}),c=r}if(!c){let t;if(void 0!==i.environment){if(t=e.varEnv[i.environment],void 0===t)throw new UsageError(`${n.slice(0,-1)} ${s}: environment variable ${i.environment} is not set`)}else{if(void 0===i.content)throw new UsageError(`${n.slice(0,-1)} ${s} needs file:, environment: or content:`);t=String(i.content)}const r=path.join(os.homedir(),".tnc","dock","compose",e.name,n);fs.mkdirSync(r,{recursive:!0,mode:448}),c=path.join(r,s),fs.writeFileSync(c,t,{mode:384})}if(!fs.existsSync(c))throw new UsageError(`${n.slice(0,-1)} ${s}: file ${c} not found`);r.push(`${c}:${a}:ro`)}return r}function depsOf(e){const t=e.depends_on,s=/^service:(.+)$/.exec(e.network_mode||"")?.[1],r=s?{[s]:{condition:"service_started"}}:{};return t?Array.isArray(t)?{...r,...Object.fromEntries(t.map(e=>[e,{condition:"service_started"}]))}:{...r,...Object.fromEntries(Object.entries(t).map(([e,t])=>[e,{condition:t?.condition||"service_started"}]))}:r}function order(e,t){const s=[],r={},n=(t,o)=>{if(2!==r[t]){if(1===r[t])throw new UsageError(`dependency cycle: ${[...o,t].join(" → ")}`);r[t]=1;for(const s of Object.keys(depsOf(e[t])))n(s,[...o,t]);r[t]=2,s.push(t)}};for(const s of t?.length?t:Object.keys(e)){if(!e[s])throw new UsageError(`no such service: ${s}`);n(s,[])}return s}const netName=(e,t)=>{const s=e.doc.networks?.[t]||{};return s.external?s.name||t:s.name||`${e.name}_${t}`},volName=(e,t)=>{const s=e.doc.volumes?.[t]||{};return s.external?s.name||t:s.name||`${e.name}_${t}`},imageOf=(e,t,s)=>s.image||`${e.name}-${t}`,replicas=e=>e.deploy?.replicas??e.scale??1;function serviceNetworks(e,t){if(t.network_mode)return null;const s=t.networks;return s?Array.isArray(s)?Object.fromEntries(s.map(e=>[e,{aliases:[]}])):Object.fromEntries(Object.entries(s).map(([e,t])=>[e,{aliases:t?.aliases||[]}])):{default:{aliases:[]}}}function bindSpecs(e,t,s){const r=[],n={},o=[],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 s.volumes||[]){if("object"==typeof c){if("tmpfs"===c.type){n[c.target]=c.tmpfs?.size?`size=${c.tmpfs.size}`:"";continue}if("bind"===c.type||c.source&&a(c.source)){r.push(`${i(c.source)}:${c.target}${c.read_only?":ro":""}`);continue}if(c.source){const t=volName(e,c.source);o.push(c.source),r.push(`${t}:${c.target}${c.read_only?":ro":""}`);continue}r.push(`${e.name}-${t}-${crypto.createHash("sha1").update(c.target).digest("hex").slice(0,8)}:${c.target}`);continue}const s=String(c),l=/^[A-Za-z]:[\\/]/.test(s)?[s.slice(0,2)+s.slice(2).split(":")[0],...s.slice(2).split(":").slice(1)]:s.split(":");if(1===l.length){r.push(`${e.name}-${t}-${crypto.createHash("sha1").update(l[0]).digest("hex").slice(0,8)}:${l[0]}`);continue}const[p,u,m]=l;if(a(p))r.push(`${i(p)}:${u}${m?`:${m}`:""}`);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:"`);o.push(p),r.push(`${volName(e,p)}:${u}${m?`:${m}`:""}`)}}for(const e of[].concat(s.tmpfs||[])){const[t,s]=String(e).split(/:(.*)/s);n[t]=s||""}return{binds:r,tmpfs:n,vols:o}}function portBindings(e,t){const{parsePublish:s}=require("./commands"),r={},n={};for(const o of t.ports||[]){let t,i;if("object"==typeof o){const e=o.protocol||"tcp",s=o.host_ip?o.host_ip.includes(":")?`[${o.host_ip}]:`:`${o.host_ip}:`:"";t=`${o.published?`${s}${o.published}:`:""}${o.target}/${e}`}else t=String(o);try{i=s([t])}catch(t){throw new UsageError(`service ${e}: ${t.message}`)}for(const[e,t]of Object.entries(i))(r[e]||=[]).push(...t),n[e]={}}for(const e of t.expose||[]){const[t,s="tcp"]=String(e).split("/");n[`${t}/${s}`]={}}return{bindings:r,exposed:n}}function envOf(e,t){const s={};for(const r of[].concat(t.env_file||[])){const t=path.resolve(e.dir,"object"==typeof r?r.path:r);if(!fs.existsSync(t)){if("object"==typeof r&&!1===r.required)continue;throw new UsageError(`env file ${t} not found`)}Object.assign(s,readDotEnv(t))}for(const[r,n]of Object.entries(mapOrList(t.environment)))null===n?void 0!==e.varEnv[r]&&(s[r]=e.varEnv[r]):s[r]=n;return Object.entries(s).map(([e,t])=>`${e}=${t}`)}function healthOf(e){const t=e.healthcheck;if(!t)return;if(t.disable)return{Test:["NONE"]};const s=void 0===t.test?void 0:Array.isArray(t.test)?t.test.map(String):["CMD-SHELL",String(t.test)];return{...s?{Test:s}:{},...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[s,r]=String(t).split(":");return{Name:s,MaximumRetryCount:r?Number(r):0}}function containerBody(e,t,s,r,n){const o=serviceNetworks(e,s),{binds:i,tmpfs:a}=bindSpecs(e,t,s);i.push(...fileMounts(e,t,s));const{bindings:c,exposed:l}=portBindings(t,s),p=s.deploy?.resources?.limits||{},u=s.deploy?.resources?.reservations||{},m=(u.devices||[]).map(e=>({Driver:e.driver||((e.capabilities||[]).flat().includes("gpu")?"nvidia":""),Count:e.device_ids?0:void 0===e.count||"all"===e.count?-1:Number(e.count),DeviceIDs:(e.device_ids||[]).map(String),Capabilities:[[...new Set((e.capabilities||[]).flat().map(String))]]}));s.gpus&&m.push({Driver:"nvidia",Count:"all"===s.gpus?-1:0,DeviceIDs:"all"===s.gpus?[]:[].concat(s.gpus).map(e=>String(e.device_ids||e)),Capabilities:[["gpu"]]});const d=Object.entries(s.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=s.mem_limit||p.memory,h=s.cpus||p.cpus,g=o?Object.keys(o):[],w={...mapOrList(s.labels)},v={Image:imageOf(e,t,s),...void 0!==s.command?{Cmd:toList(s.command)}:{},...void 0!==s.entrypoint?{Entrypoint:toList(s.entrypoint)}:{},Env:envOf(e,s),...s.working_dir?{WorkingDir:s.working_dir}:{},...void 0!==s.user?{User:String(s.user)}:{},...s.hostname?{Hostname:s.hostname}:{},Tty:!!s.tty,OpenStdin:!!s.stdin_open,ExposedPorts:l,...healthOf(s)?{Healthcheck:healthOf(s)}:{},...s.stop_signal?{StopSignal:s.stop_signal}:{},...s.stop_grace_period?{StopTimeout:Math.ceil(seconds(s.stop_grace_period))}:{},HostConfig:{Binds:i,PortBindings:replicas(s)>1&&Object.values(c).some(e=>e.some(e=>e.HostPort))&&r>1?{}:c,NetworkMode:/^service:/.test(s.network_mode||"")?`container:${e.name}-${s.network_mode.slice(8)}-1`:s.network_mode||netName(e,g[0]),RestartPolicy:restartOf(s),...f?{Memory:parseBytes(f)}:{},...h?{NanoCpus:Math.round(1e9*parseFloat(h))}:{},...s.pids_limit?{PidsLimit:Number(s.pids_limit)}:{},...s.shm_size?{ShmSize:parseBytes(s.shm_size)}:{},...s.cgroup?{CgroupnsMode:s.cgroup}:{},...s.userns_mode?{UsernsMode:s.userns_mode}:{},Privileged:!!s.privileged,CapAdd:s.cap_add||[],CapDrop:s.cap_drop||[],ReadonlyRootfs:!!s.read_only,ExtraHosts:Array.isArray(s.extra_hosts)?s.extra_hosts:Object.entries(s.extra_hosts||{}).map(([e,t])=>`${e}:${t}`),Tmpfs:a,...s.init?{Init:!0}:{},...d.length?{Ulimits:d}:{},...s.sysctls?{Sysctls:mapOrList(s.sysctls)}:{},...s.dns?{Dns:[].concat(s.dns)}:{},...s.dns_search?{DnsSearch:[].concat(s.dns_search)}:{},...s.dns_opt?{DnsOptions:[].concat(s.dns_opt)}:{},...m.length?{DeviceRequests:m}:{},...s.devices?.length?{Devices:s.devices.map(e=>{const[t,s,r]=String(isObj(e)?`${e.source}:${e.target}:${e.permissions||"rwm"}`:e).split(":");return{PathOnHost:t,PathInContainer:s||t,CgroupPermissions:r||"rwm"}})}:{},...s.group_add?{GroupAdd:s.group_add.map(String)}:{},...s.security_opt?{SecurityOpt:s.security_opt}:{},...s.mem_reservation||u.memory?{MemoryReservation:parseBytes(s.mem_reservation||u.memory)}:{},...s.memswap_limit?{MemorySwap:"-1"===String(s.memswap_limit)?-1:parseBytes(s.memswap_limit)}:{},...s.cpuset?{CpusetCpus:String(s.cpuset)}:{},...s.cpu_shares?{CpuShares:Number(s.cpu_shares)}:{},...s.logging?{LogConfig:{Type:s.logging.driver||"json-file",Config:s.logging.options||{}}}:{},...s.volumes_from?{VolumesFrom:s.volumes_from.map(t=>{const[s,r]=String(t).replace(/^service:/,"").split(":");return`${e.services[s]?`${e.name}-${s}-1`:s.replace(/^container:/,"")}${r?`:${r}`:""}`})}:{}},...o?{NetworkingConfig:{EndpointsConfig:Object.fromEntries(g.map(s=>[netName(e,s),{Aliases:[t,...o[s].aliases]}]))}}:{}},y=crypto.createHash("sha256").update(JSON.stringify([v,w,n])).digest("hex");return v.Labels={...w,[L.project]:e.name,[L.service]:t,[L.number]:String(r),[L.hash]:y,[L.file]:e.file},v}async function projectContainers(e,t,{all:s=!0,service:r}={}){const n=[`${L.project}=${t}`,...r?[`${L.service}=${r}`]:[]];return e.json("GET",`/containers/json${qs({all:s||void 0,filters:{label:n}})}`)}const cname=e=>(e.Names?.[0]||"").replace(/^\//,"");function reporter(e){const t=e.isTTY;return(s,r,n,o=!0)=>{const i=o?chalk.green("✔"):chalk.red("✘");e.write(` ${t?i:o?"ok ":"err"} ${s.padEnd(9)} ${r.padEnd(28)} ${o?n:chalk.red(n)}\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,s,r){const n=new Set;for(const e of Object.values(s)){const s=serviceNetworks(t,e);s&&Object.keys(s).forEach(e=>n.add(e))}for(const s of n){if(!("default"===s||t.doc.networks&&s in t.doc.networks))throw new UsageError(`undefined network ${s}; declare it under top-level "networks:"`);const n=t.doc.networks?.[s]||{},o=netName(t,s);if(!await exists(e,`/networks/${enc(o)}`)){if(n.external)throw new UsageError(`network ${o} is declared external but does not exist; create it with: thinknsh network create ${o}`);await e.json("POST","/networks/create",{Name:o,Driver:n.driver||"bridge",Internal:!!n.internal,Labels:{...mapOrList(n.labels),[L.project]:t.name,"tnc.compose.network":s},EnableIPv6:!!n.enable_ipv6,IPAM:{Config:(n.ipam?.config||[]).map(e=>({Subnet:e.subnet,Gateway:e.gateway,IPRange:e.ip_range}))},Options:mapOrList(n.driver_opts)}),r("Network",o,"Created")}}}function projectNetworks(e){const t=new Set;for(const s of Object.values(e.services)){const r=serviceNetworks(e,s);r&&Object.keys(r).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,s,r){const n=new Set;for(const[e,r]of Object.entries(s))bindSpecs(t,e,r).vols.forEach(e=>n.add(e));for(const s of n){const n=t.doc.volumes?.[s]||{},o=volName(t,s);if(!await exists(e,`/volumes/${enc(o)}`)){if(n.external)throw new UsageError(`volume ${o} is declared external but does not exist; create it with: thinknsh volume create ${o}`);await e.json("POST","/volumes/create",{Name:o,Labels:{...mapOrList(n.labels),[L.project]:t.name,"tnc.compose.volume":s}}),r("Volume",o,"Created")}}}async function buildService(e,t,s,r,{out:n,noCache:o=!1,pull:i=!1}={}){const a="string"==typeof r.build?{context:r.build}:r.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 r=isObj(e)?e.source:String(e),n=t.doc.secrets?.[r];if(!n)throw new UsageError(`service ${s}: build secret ${r} is not declared under top-level "secrets:"`);const o=n.file?fs.readFileSync(n.file):void 0!==n.environment?Buffer.from(String(t.varEnv[n.environment]??"")):Buffer.from(String(n.content??""));return{id:isObj(e)&&e.target?path.posix.basename(e.target):r,data:o}}),{tar:u,buildFile:m}=packContext(c,l,{secrets:p}),{parseSshSpec:d,openSession:f}=require("../build/sshagent"),h=Array.isArray(a.ssh)?a.ssh.map(String):isObj(a.ssh)?Object.entries(a.ssh).map(([e,t])=>t?`${e}=${t}`:e):a.ssh?[String(a.ssh)]:[];let g=null;try{g=h.length?await f(e,h.map(e=>d(e.replace(/=(\.{0,2}\/)/,(e,s)=>`=${path.resolve(t.dir,s)}`)))):null}catch(e){throw new UsageError(`service ${s}: ${e.message}`)}const w=e=>String(e).replace(/(^|,)(src|dest)=([^,]+)/,(e,s,r,n)=>`${s}${r}=${path.resolve(t.dir,n)}`),v=(a.cache_from||[]).map(w),y=(a.cache_to||[]).map(w);try{const c=chalk.cyan;n.write(`${c(`[${s}]`)} building ${imageOf(t,s,r)} (context ${fmt.size(u.length)})\n`);const l=Object.fromEntries(Object.entries(mapOrList(a.args)).map(([e,s])=>[e,null===s?t.varEnv[e]??null:s]));let p="";await e.progress("POST",`/build${qs({t:[imageOf(t,s,r),...a.tags||[]],[BUILD_FILE_PARAM]:m,buildargs:l,target:a.target,nocache:o,pull:i,labels:Object.keys(mapOrList(a.labels)).length?mapOrList(a.labels):void 0,networkmode:a.network,platform:(a.platforms||[])[0]||r.platform,cachefrom:v.length?JSON.stringify(v):void 0,cacheto:y.length?JSON.stringify(y):void 0,sshsession:g?.session})}`,u,e=>{if(!e.stream)return;p+=e.stream;const t=p.split("\n");p=t.pop();for(const e of t)e.trim()&&n.write(`${c(`[${s}]`)} ${e}\n`)},{"Content-Type":"application/x-tar"}),p.trim()&&n.write(`${c(`[${s}]`)} ${p}\n`)}finally{g?.close()}}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,s,r,{build:n,out:o,err:i}){const a=imageOf(t,s,r),c=r.pull_policy||"missing";if(!r.build||!n&&"build"!==c&&await imageId(e,a)){if("always"===c||!await imageId(e,a)&&"never"!==c){if(r.build&&"never"===c)throw new UsageError(`image ${a} for service ${s} is missing`);o.write(`${chalk.cyan(`[${s}]`)} pulling ${a}\n`),await pullImage(e,a,{err:i,platform:r.platform})}}else await buildService(e,t,s,r,{out:o});const l=await imageId(e,a);if(l&&r.platform){const t=await e.json("GET",`/images/${a}/json`),n=r.platform.split("/")[1];if(t.Architecture&&n&&t.Architecture!==n&&"never"!==c)return o.write(`${chalk.cyan(`[${s}]`)} pulling ${a} for ${r.platform}\n`),await pullImage(e,a,{err:i,platform:r.platform}),imageId(e,a)}if(!l)throw new UsageError(`image ${a} for service ${s} is missing and has no build section`);return l}async function waitForCondition(e,t,s,r,n=3e5){const o=Date.now()+n;for(;;){const n=await projectContainers(e,t.name,{service:s});if(!n.length)throw new Error(`dependency ${s} has no container`);const i=await Promise.all(n.map(t=>e.json("GET",`/containers/${t.Id}/json`)));if("service_healthy"===r){const e=i[0].Config.Healthcheck?.Test;if(!e||"NONE"===e[0])throw new Error(`dependency ${s} 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: ${s} exited with code ${t.State.ExitCode}; see: thinknsh compose logs ${s}`);const r=i.map(e=>e.State.Health?.Status||"starting");if(r.includes("unhealthy"))throw new Error(`dependency failed to start: ${s} is unhealthy; see: thinknsh compose logs ${s}`);if(r.every(e=>"healthy"===e))return}else{if("service_completed_successfully"!==r)return;{const e=i.filter(e=>"exited"===e.State.Status),t=e.find(e=>0!==e.State.ExitCode);if(t)throw new Error(`service ${s} didn't complete successfully: exit ${t.State.ExitCode}`);if(e.length===i.length)return}}if(Date.now()>o)throw new Error(`timed out waiting for ${s} (${r})`);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,s){const{api:r,out:n,err:o}=s,{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(n),p=Object.fromEntries(c.map(t=>[t,e.services[t]])),u=await projectContainers(r,e.name);for(const t of u.filter(t=>!e.services[t.Labels[L.service]]))i["remove-orphans"]?(await r.json("DELETE",`/containers/${t.Id}?force=1`),l("Container",cname(t),"Removed")):o.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(r,e,p,l),await ensureVolumes(r,e,p,l);const m={};for(const t of c)m[t]=await ensureImage(r,e,t,e.services[t],{build:i.build,out:n,err:o});const d=[];for(const t of c){const s=e.services[t];for(const[t,{condition:o}]of Object.entries(depsOf(s)))"service_started"===o||i["no-start"]||(n.write(chalk.dim(` … waiting for ${t} (${o.replace("service_","")})\n`)),await waitForCondition(r,e,t,o));const o=(await projectContainers(r,e.name,{service:t})).sort((e,t)=>Number(e.Labels[L.number])-Number(t.Labels[L.number])),a=replicas(s);for(const e of o.filter(e=>Number(e.Labels[L.number])>a))await r.json("DELETE",`/containers/${e.Id}?force=1`),l("Container",cname(e),"Removed");for(let n=1;n<=a;n++){const c=containerBody(e,t,s,n,m[t]),p=s.container_name&&1===a?s.container_name:`${e.name}-${t}-${n}`,u=o.find(e=>Number(e.Labels[L.number])===n);let f,h=u?.Id;!u||i["force-recreate"]||!i["no-recreate"]&&u.Labels[L.hash]!==c.Labels[L.hash]?(u&&await r.json("DELETE",`/containers/${u.Id}?force=1`),h=(await r.json("POST",`/containers/create${qs({name:p,platform:s.platform})}`,c)).Id,f=u?"Recreated":"Created"):f="running"===u.State?"Running":"Started",i["no-start"]||"Running"===f||(await r.json("POST",`/containers/${h}/start`),"Created"!==f&&"Recreated"!==f||(f=`${f}, started`)),l("Container",p,f),d.push({id:h,name:p,service:t,fresh:"Running"!==f})}}if(i.wait){for(const t of c)e.services[t].healthcheck&&await waitForCondition(r,e,t,"service_healthy");n.write(chalk.green(" all services are up and healthy\n"))}if(i.detach||i["no-start"]||i.wait)return 0;n.write(chalk.dim("Attaching to "+d.map(e=>e.name).join(", ")+" (Ctrl-C stops them)\n"));let f=!1;const h=async()=>{f&&process.exit(130),f=!0,n.write(chalk.yellow("\nGracefully stopping… (press Ctrl-C again to force)\n")),await Promise.all(d.map(e=>r.json("POST",`/containers/${e.id}/stop${qs({t:i.timeout})}`).catch(()=>{})))};process.on("SIGINT",h);const g=Math.max(...d.map(e=>e.name.length));let w=0;try{await Promise.all(d.map((e,t)=>followLogs(r,e,{prefix:COLORS[t%COLORS.length](`${e.name.padEnd(g)} |`),tail:e.fresh?"all":"0",follow:!0,out:n}).then(async()=>{const t=await r.json("GET",`/containers/${e.id}/json`).catch(()=>null);t&&!f&&n.write(`${e.name} exited with code ${t.State.ExitCode}\n`),i["abort-on-container-exit"]&&!f&&(w=t?.State.ExitCode||0,await h())})))}finally{process.removeListener("SIGINT",h)}return w}async function followLogs(e,t,{prefix:s,tail:r="all",follow:n=!1,timestamps:o=!1,out:i}){const a=await e.stream("GET",`/containers/${t.id}/logs${qs({stdout:1,stderr:1,follow:n,tail:r,timestamps:o})}`),c=(a.headers["content-type"]||"").includes("raw-stream");let l={out:"",err:""};const p=(e,t)=>{l[e]+=t.toString("utf8");const r=l[e].split("\n");l[e]=r.pop();for(const t of r)("err"===e?process.stderr:i).write(`${s} ${t.replace(/\r$/,"")}\n`)},u=c?e=>p("out",e):demuxer(e=>p("out",e),e=>p("err",e));for await(const e of a)u(e);for(const e of["out","err"])l[e]&&("err"===e?process.stderr:i).write(`${s} ${l[e]}\n`)}async function down(e,t,{api:s,out:r}){const{opts:n}=parseArgs(t,{volumes:{short:"v",type:"bool"},rmi:{type:"string"},timeout:{short:"t",type:"string"},"remove-orphans":{type:"bool"}}),o=reporter(r),i=await projectContainers(s,e.name),a=new Set;await Promise.all(i.filter(e=>"running"===e.State).map(e=>s.json("POST",`/containers/${e.Id}/stop${qs({t:n.timeout})}`).catch(()=>{})));for(const e of i)a.add(e.Image),await s.json("DELETE",`/containers/${e.Id}${qs({force:!0,v:n.volumes})}`),o("Container",cname(e),"Removed");const c=await s.json("GET",`/networks${qs({filters:{label:[`${L.project}=${e.name}`]}})}`);for(const e of c)await s.json("DELETE",`/networks/${e.Id}`).catch(()=>{}),o("Network",e.Name,"Removed");if(n.volumes){const t=await s.json("GET",`/volumes${qs({filters:{label:[`${L.project}=${e.name}`]}})}`);for(const e of t.Volumes||[])await s.json("DELETE",`/volumes/${enc(e.Name)}`).catch(()=>{}),o("Volume",e.Name,"Removed")}if(n.rmi){if(!["local","all"].includes(n.rmi))throw new UsageError("--rmi takes local or all");for(const[t,r]of Object.entries(e.services)){if("local"===n.rmi&&r.image)continue;const i=imageOf(e,t,r);try{await s.json("DELETE",`/images/${i}`),o("Image",i,"Removed")}catch(e){}}}return 0}async function psCmd(e,t,{api:s,out:r}){const{opts:n,args:o}=parseArgs(t,{all:{short:"a",type:"bool"},quiet:{short:"q",type:"bool"},json:{type:"bool"}});let i=await projectContainers(s,e.name,{all:n.all});return o.length&&(i=i.filter(e=>o.includes(e.Labels[L.service]))),n.json?(r.write(JSON.stringify(i,null,2)+"\n"),0):n.quiet?(i.forEach(e=>r.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:r}),0)}async function logsCmd(e,t,{api:s,out:r}){const{opts:n,args:o}=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(s,e.name);o.length&&(i=i.filter(e=>o.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(s,{id:e.Id},{prefix:n["no-prefix"]?"":COLORS[t%COLORS.length](`${cname(e).padEnd(a)} |`),tail:n.tail||"all",follow:n.follow,timestamps:n.timestamps,out:r}))),0}async function forEachContainer(e,t,{api:s,out:r},n,o){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(s,e.name);a.length&&(c=c.filter(e=>a.includes(e.Labels[L.service])));const l=reporter(r);for(const e of c)await o(e,i),l("Container",cname(e),n);return 0}async function execCmd(e,t,{api:s}){const{opts:r,args:n}=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(n.length<2)throw new UsageError("compose exec takes SERVICE COMMAND…, e.g. thinknsh compose exec db psql -U postgres");const[o,...i]=n,a=(await projectContainers(s,e.name,{all:!1,service:o})).find(e=>e.Labels[L.number]===String(r.index));if(!a)throw new UsageError(`service ${o} is not running`);const c=!r["no-tty"]&&process.stdin.isTTY&&process.stdout.isTTY,{Id:l}=await s.json("POST",`/containers/${a.Id}/exec`,{Cmd:i,AttachStdin:!r.detach,AttachStdout:!r.detach,AttachStderr:!r.detach,Tty:c,Env:r.env,...r.workdir?{WorkingDir:r.workdir}:{},...r.user?{User:r.user}:{},Privileged:r.privileged});if(r.detach)return await s.json("POST",`/exec/${l}/start`,{Detach:!0}),0;const p=await s.hijack(`/exec/${l}/start`,{Detach:!1,Tty:c});return await pipeSession(p,{stdin:!0,resize:c?(e,t)=>s.json("POST",`/exec/${l}/resize${qs({h:e,w:t})}`).catch(()=>{}):null}),(await s.json("GET",`/exec/${l}/json`)).ExitCode??0}async function serviceContainers(e,t,s,{all:r=!0}={}){for(const e of s)if(!t.services[e])throw new UsageError(`no such service: ${e}`);let n=await projectContainers(e,t.name,{all:r});return s.length&&(n=n.filter(e=>s.includes(e.Labels[L.service]))),n.filter(e=>"True"!==e.Labels["tnc.compose.oneoff"]).sort((e,t)=>cname(e).localeCompare(cname(t)))}async function runCmd(e,t,s){const{api:r,out:n,err:o}=s,{opts:i,args:a}=parseArgs(t,{detach:{short:"d",type:"bool"},rm:{type:"bool"},"no-deps":{type:"bool"},"no-TTY":{short:"T",type:"bool"},env:{short:"e",type:"list"},entrypoint:{type:"string"},user:{short:"u",type:"string"},workdir:{short:"w",type:"string"},publish:{short:"p",type:"list"},"service-ports":{type:"bool"},name:{type:"string"},build:{type:"bool"},label:{short:"l",type:"list"},volume:{short:"v",type:"list"}},{stopAtPositional:!0});if(!a.length)throw new UsageError("compose run takes SERVICE [COMMAND…], e.g. thinknsh compose run --rm web npm test");const[c,...l]=a,p=e.services[c];if(!p)throw new UsageError(`no such service: ${c}`);const u=Object.keys(depsOf(p));u.length&&!i["no-deps"]&&await up(e,["-d",...u],{...s,out:{write:()=>{},isTTY:!1}});const m=reporter(o),d={[c]:p};await ensureNetworks(r,e,d,m),await ensureVolumes(r,e,d,m);const f=containerBody(e,c,p,1,await ensureImage(r,e,c,p,{build:i.build,out:o,err:o})),{parsePublish:h,resolveBind:g}=require("./commands");l.length&&(f.Cmd=l),void 0!==i.entrypoint&&(f.Entrypoint=i.entrypoint?toList(i.entrypoint):[""]),i.user&&(f.User=i.user),i.workdir&&(f.WorkingDir=i.workdir),f.Env=[...f.Env||[],...i.env];const w=!i["no-TTY"]&&!i.detach&&process.stdin.isTTY&&process.stdout.isTTY;Object.assign(f,{Tty:w,OpenStdin:!i.detach,AttachStdin:!i.detach,StdinOnce:!i.detach}),f.HostConfig.PortBindings=i["service-ports"]?f.HostConfig.PortBindings:h(i.publish),f.HostConfig.Binds=[...f.HostConfig.Binds||[],...i.volume.map(g)],f.HostConfig.AutoRemove=!!i.rm,f.HostConfig.RestartPolicy={Name:"no"},f.Labels={...f.Labels,"tnc.compose.oneoff":"True",...Object.fromEntries(i.label.map(e=>[e.split("=")[0],e.split("=").slice(1).join("=")]))},delete f.Labels[L.number],delete f.Healthcheck;const v=i.name||`${e.name}-${c}-run-${crypto.randomBytes(6).toString("hex")}`,{Id:y}=await r.json("POST",`/containers/create${qs({name:v,platform:p.platform})}`,f);if(i.detach)return await r.json("POST",`/containers/${y}/start`),n.write(`${v}\n`),0;const{attachAndWait:b}=require("./commands");return(await b(r,y,{stdin:!0,tty:w,startIt:!0,err:o})).code}async function killCmd(e,t,{api:s,out:r}){const{opts:n,args:o}=parseArgs(t,{signal:{short:"s",type:"string",default:"SIGKILL"}}),i=reporter(r);for(const t of(await serviceContainers(s,e,o)).filter(e=>"running"===e.State))await s.json("POST",`/containers/${t.Id}/kill${qs({signal:n.signal})}`),i("Container",cname(t),"Killed");return 0}async function pauseCmd(e,t,{api:s,out:r},n){const{args:o}=parseArgs(t,{}),i=reporter(r);for(const t of await serviceContainers(s,e,o))(n?"running"===t.State:"paused"===t.State)&&(await s.json("POST",`/containers/${t.Id}/${n?"pause":"unpause"}`),i("Container",cname(t),n?"Paused":"Unpaused"));return 0}async function topCmd(e,t,{api:s,out:r}){const{args:n}=parseArgs(t,{});for(const t of(await serviceContainers(s,e,n)).filter(e=>"running"===e.State)){const e=await s.json("GET",`/containers/${t.Id}/top`);r.write(`${chalk.bold(cname(t))}\n`),fmt.table(e.Titles,e.Processes,{out:r}),r.write("\n")}return 0}async function portCmd(e,t,{api:s,out:r}){const{opts:n,args:o}=parseArgs(t,{protocol:{type:"string",default:"tcp"},index:{type:"string",default:"1"}});if(2!==o.length)throw new UsageError("compose port SERVICE PRIVATE_PORT, e.g. thinknsh compose port web 80");const i=(await serviceContainers(s,e,[o[0]],{all:!1})).find(e=>e.Labels[L.number]===String(n.index));if(!i)throw new UsageError(`service ${o[0]} is not running`);const a=(i.Ports||[]).find(e=>String(e.PrivatePort)===String(o[1])&&(e.Type||"tcp")===n.protocol&&e.PublicPort);if(!a)throw new UsageError(`no port ${o[1]}/${n.protocol} published for ${cname(i)}`);return r.write(`${a.IP&&"::"!==a.IP?a.IP:"0.0.0.0"}:${a.PublicPort}\n`),0}async function imagesCmd(e,t,{api:s,out:r}){const{opts:n,args:o}=parseArgs(t,{quiet:{short:"q",type:"bool"}}),i=[];for(const t of await serviceContainers(s,e,o)){const e=await s.json("GET",`/images/${enc(t.ImageID||t.Image)}/json`).catch(()=>null);if(n.quiet){e&&r.write(`${e.Id}\n`);continue}const[o,a]=t.Image.includes(":")&&!t.Image.startsWith("sha256:")?[t.Image.slice(0,t.Image.lastIndexOf(":")),t.Image.slice(t.Image.lastIndexOf(":")+1)]:[t.Image,""];i.push([cname(t),o,a,e?e.Id.replace(/^sha256:/,"").slice(0,12):"",e?fmt.size(e.Size):""])}return n.quiet||fmt.table(["CONTAINER","REPOSITORY","TAG","IMAGE ID","SIZE"],i,{out:r}),0}async function lsCmd(e,{api:t,out:s}){const{opts:r}=parseArgs(e,{all:{short:"a",type:"bool"},quiet:{short:"q",type:"bool"},json:{type:"bool"}}),n=await t.json("GET",`/containers/json${qs({all:!0,filters:{label:[L.project]}})}`),o=new Map;for(const e of n){const t=e.Labels[L.project];o.has(t)||o.set(t,{Name:t,states:{},files:new Set});const s=o.get(t);s.states[e.State]=(s.states[e.State]||0)+1,e.Labels[L.file]&&s.files.add(e.Labels[L.file])}let i=[...o.values()].map(e=>({Name:e.Name,Status:Object.entries(e.states).map(([e,t])=>`${e}(${t})`).join(", "),ConfigFiles:[...e.files].join(","),running:!!e.states.running}));return r.all||(i=i.filter(e=>e.running)),i.sort((e,t)=>e.Name.localeCompare(t.Name)),r.quiet?(i.forEach(e=>s.write(`${e.Name}\n`)),0):r.json?(s.write(JSON.stringify(i.map(({running:e,...t})=>t),null,2)+"\n"),0):(fmt.table(["NAME","STATUS","CONFIG FILES"],i.map(e=>[e.Name,e.Status,e.ConfigFiles]),{out:s}),0)}async function waitCmd(e,t,{api:s,out:r}){const{args:n}=parseArgs(t,{"down-project":{type:"bool"}});if(!n.length)throw new UsageError("compose wait SERVICE…");let o=0;for(const t of await serviceContainers(s,e,n)){const e=await s.stream("POST",`/containers/${t.Id}/wait`);let n="";for await(const t of e)n+=t.toString("utf8");const i=JSON.parse(n.trim().split("\n").pop()||"{}").StatusCode??0;r.write(`${i}\n`),o=o||i}return o}async function pushCmd(e,t,s){const{opts:r,args:n}=parseArgs(t,{"ignore-push-failures":{type:"bool"},quiet:{short:"q",type:"bool"}}),{COMMANDS:o}=require("./commands");for(const t of order(e.services,n)){const n=e.services[t];if(n.image){s.out.write(`${chalk.cyan(`[${t}]`)} pushing ${n.image}\n`);try{await o.push([n.image,...r.quiet?["-q"]:[]],s)}catch(e){if(!r["ignore-push-failures"])throw e;s.err.write(chalk.yellow(`[${t}] ${e.message}\n`))}}}return 0}async function eventsCmd(e,t,{api:s,out:r}){const{opts:n,args:o}=parseArgs(t,{json:{type:"bool"}}),i=[`${L.project}=${e.name}`],a=await s.stream("GET",`/events${qs({filters:{type:["container"],label:i}})}`);let c="";const l=e=>{if(!e.trim())return;let t;try{t=JSON.parse(e)}catch(e){return}const s=t.Actor?.Attributes||{};o.length&&!o.includes(s[L.service])||(n.json?r.write(JSON.stringify({time:new Date(1e3*t.time).toISOString(),type:t.Type,action:t.Action,id:t.Actor?.ID,service:s[L.service],attributes:{name:s.name,image:s.image}})+"\n"):r.write(`${new Date(t.timeNano?t.timeNano/1e6:1e3*t.time).toISOString()} container ${t.Action} ${t.Actor?.ID||""} (image=${s.image||""}, name=${s.name||""}, service=${s[L.service]||""})\n`))};for await(const e of a){let t;for(c+=e.toString("utf8");-1!==(t=c.indexOf("\n"));)l(c.slice(0,t)),c=c.slice(t+1)}return 0}async function cpCmd(e,t,s){const{opts:r,args:n}=parseArgs(t,{index:{type:"string",default:"1"},archive:{short:"a",type:"bool"},"follow-link":{short:"L",type:"bool"}});if(2!==n.length)throw new UsageError("compose cp SERVICE:PATH DEST, or SRC SERVICE:PATH");const o=[];for(const t of n){const n=/^[A-Za-z]:[\\/]/.test(t)?null:/^([A-Za-z0-9][A-Za-z0-9_.-]*):(.*)$/.exec(t);if(n&&e.services[n[1]]){const t=(await serviceContainers(s.api,e,[n[1]])).find(e=>e.Labels[L.number]===String(r.index));if(!t)throw new UsageError(`service ${n[1]} has no container ${r.index}`);o.push(`${cname(t)}:${n[2]}`)}else o.push(t)}const{COMMANDS:i}=require("./commands");return i.cp([...r.archive?["-a"]:[],...r["follow-link"]?["-L"]:[],...o],s)}async function scaleCmd(e,t,s){const{opts:r,args:n}=parseArgs(t,{"no-deps":{type:"bool"}});if(!n.length)throw new UsageError("compose scale SERVICE=N…");const o=[];for(const t of n){const s=/^([^=]+)=(\d+)$/.exec(t);if(!s||!e.services[s[1]])throw new UsageError(`expected SERVICE=N for a service of this app: ${t}`);e.services[s[1]]={...e.services[s[1]],scale:Number(s[2]),deploy:{...e.services[s[1]].deploy||{},replicas:Number(s[2])}},o.push(s[1])}return up(e,["-d","--no-recreate",...o],s)}async function watchCmd(e,t,s){const{api:r,out:n,err:o}=s,{opts:i,args:a}=parseArgs(t,{"no-up":{type:"bool"}}),c=[];for(const t of order(e.services,a))for(const s of e.services[t].develop?.watch||[]){if(!["sync","rebuild","sync+restart","restart"].includes(s.action))throw new UsageError(`service ${t}: watch action must be sync, sync+restart, restart or rebuild`);if(s.action.startsWith("sync")&&!s.target)throw new UsageError(`service ${t}: watch action ${s.action} needs a target`);c.push({service:t,action:s.action,path:path.resolve(e.dir,s.path||"."),target:s.target,ignore:[].concat(s.ignore||[])})}if(!c.length)throw new UsageError("no service has develop.watch rules (see: develop: { watch: [{ action: sync, path: ./src, target: /app/src }] })");i["no-up"]||await up(e,["-d",...new Set(c.map(e=>e.service))],s);const{COMMANDS:l}=require("./commands"),p=async t=>(await serviceContainers(r,e,[t],{all:!1}))[0],u=new Map;let m=null,d=Promise.resolve();const f=()=>{const t=[...u.entries()];u.clear(),d=d.then(async()=>{for(const[i,a]of t){const t=c[i],u=chalk.cyan(`[${t.service}]`);try{if("rebuild"===t.action){n.write(`${u} rebuilding (${a.size} change${1===a.size?"":"s"})\n`),await buildService(r,e,t.service,e.services[t.service],{out:n}),await up(e,["-d","--no-deps",t.service],{...s,out:{write:()=>{},isTTY:!1}}),n.write(`${u} recreated\n`);continue}const i=await p(t.service);if(!i){o.write(`${u} not running; skipped ${a.size} change(s)\n`);continue}if(t.action.startsWith("sync")){for(const e of a){const n=path.join(t.path,e),o=path.posix.join(t.target,e.split(path.sep).join("/"));if(fs.existsSync(n)){if(fs.statSync(n).isDirectory())continue;await r.json("POST",`/containers/${i.Id}/exec`,{Cmd:["mkdir","-p",path.posix.dirname(o)]}).then(e=>r.json("POST",`/exec/${e.Id}/start`,{Detach:!0})),await l.cp([n,`${cname(i)}:${o}`],{...s,out:{write:()=>{}}})}else{const e=await r.json("POST",`/containers/${i.Id}/exec`,{Cmd:["rm","-rf",o]});await r.json("POST",`/exec/${e.Id}/start`,{Detach:!0})}}n.write(`${u} synced ${a.size} file${1===a.size?"":"s"} to ${t.target}\n`)}t.action.endsWith("restart")&&(await r.json("POST",`/containers/${i.Id}/restart`),n.write(`${u} restarted\n`))}catch(e){o.write(`${u} ${chalk.red(e.message)}\n`)}}})},h=c.map((e,t)=>{if(!fs.existsSync(e.path))throw new UsageError(`service ${e.service}: watch path ${e.path} does not exist`);const s=fs.statSync(e.path).isDirectory(),r=s?e.path:path.dirname(e.path);return fs.watch(r,{recursive:s},(r,n)=>{if(!n)return;const o=s?String(n):path.basename(e.path);(s||String(n)===path.basename(e.path))&&(((e,t)=>t.split(path.sep).includes(".git")||e.ignore.some(e=>new RegExp("^"+e.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*\*/g,"\0").replace(/\*/g,"[^/]*").replace(/\u0000/g,".*").replace(/\?/g,".")+"(/|$)").test(t.split(path.sep).join("/"))))(e,o)||(u.has(t)||u.set(t,new Set),u.get(t).add(o),clearTimeout(m),m=setTimeout(f,300)))})});return n.write(chalk.dim(`watching ${c.length} path${1===c.length?"":"s"} (${[...new Set(c.map(e=>e.service))].join(", ")}) Ctrl-C stops\n`)),await new Promise(e=>process.once("SIGINT",e)),h.forEach(e=>e.close()),await d,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 run [--rm] [-d] [-e K=V] [--no-deps] [--service-ports] SERVICE [CMD…]\n A one-off container of a service (dependencies start first)\n create [services…] Create the containers without starting them\n kill [-s SIGNAL] | pause | unpause [services…]\n top [services…] Processes of the running services\n port SERVICE PRIVATE_PORT The published address of a port\n images [services…] Images the app's containers use\n ls [-a] Every compose app on this engine\n wait SERVICE… Block until those services stop; print exit codes\n push [services…] Push the services' images\n events [services…] Stream container events of the app\n cp SERVICE:PATH DEST | SRC SERVICE:PATH Copy files to or from a service\n scale SERVICE=N… Change the number of containers of services\n watch [services…] Apply develop.watch rules (sync, sync+restart, rebuild) while it runs\n";async function compose(e,t){const{opts:s,args:r}=parseArgs(e,GLOBAL,{stopAtPositional:!0}),[n,...o]=r;if(!n||"help"===n||o.includes("--help"))return t.out.write(HELP),n?0:2;if("ls"===n)return lsCmd(o,t);const i=loadProject({files:s.file,projectName:s["project-name"],profiles:s.profile,envFile:s["env-file"]}),{api:a}=t;switch(n){case"run":return runCmd(i,o,t);case"create":return up(i,["--no-start",...o],t);case"kill":return killCmd(i,o,t);case"pause":return pauseCmd(i,o,t,!0);case"unpause":return pauseCmd(i,o,t,!1);case"top":return topCmd(i,o,t);case"port":return portCmd(i,o,t);case"images":return imagesCmd(i,o,t);case"wait":return waitCmd(i,o,t);case"push":return pushCmd(i,o,t);case"events":return eventsCmd(i,o,t);case"cp":return cpCmd(i,o,t);case"scale":return scaleCmd(i,o,t);case"watch":return watchCmd(i,o,t);case"up":return up(i,o,t);case"down":return down(i,o,t);case"ps":return psCmd(i,o,t);case"logs":return logsCmd(i,o,t);case"exec":return execCmd(i,o,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:s}=parseArgs(o,{"no-cache":{type:"bool"},pull:{type:"bool"}});for(const r of order(i.services,s))i.services[r].build&&await buildService(a,i,r,i.services[r],{out:t.out,noCache:e["no-cache"],pull:e.pull});return 0}case"pull":{const{args:e}=parseArgs(o,{quiet:{short:"q",type:"bool"}});for(const s of order(i.services,e)){const e=i.services[s];e.image&&(t.out.write(`${chalk.cyan(`[${s}]`)} pulling ${e.image}\n`),await pullImage(a,e.image,{err:t.err}))}return 0}case"start":return forEachContainer(i,o,t,"Started",e=>a.json("POST",`/containers/${e.Id}/start`));case"stop":return forEachContainer(i,o,t,"Stopped",(e,t)=>a.json("POST",`/containers/${e.Id}/stop${qs({t:t.timeout})}`));case"restart":return forEachContainer(i,o,t,"Restarted",(e,t)=>a.json("POST",`/containers/${e.Id}/restart${qs({t:t.timeout})}`));case"rm":return forEachContainer(i,o,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 "${n}"`)}}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"),chalk=require("chalk"),{parseArgs,UsageError}=require("./args"),USAGE="Usage: thinknsh trust key generate [--output DIR] [--name NAME]\n thinknsh trust sign --key KEY [-a KEY=VALUE]… IMAGE\n thinknsh trust verify --key KEY.pub IMAGE\n thinknsh trust sign --keyless IMAGE (cosign: signs with your GitHub/Google/… login)\n thinknsh trust verify --keyless --identity EMAIL|URL --issuer URL IMAGE\n\nSigns images in their registry and checks those signatures (the Sigstore\nsimple-signing format: cosign verify --key KEY.pub --insecure-ignore-tlog\nchecks them too). Push the image first. The key's password comes from\nTNC_SIGN_PASSWORD or a prompt. Only signed pulls: thinknsh dock config --trust-key 'ghcr.io/me/*=KEY.pub'.",SCAN_USAGE="Usage: thinknsh image scan [OPTIONS] IMAGE\n\nLists the packages in an image (apk, deb, npm, Python) and the known\nvulnerabilities that affect them, from OSV (osv.dev). Only package names and\nversions are sent; --offline sends nothing and lists the packages only.\n\n --severity LEVEL Show LEVEL and worse (LOW, MEDIUM, HIGH, CRITICAL)\n --exit-code N Exit with N when something at --severity or worse is found (CI)\n --json The report as JSON\n --sbom cyclonedx|spdx Print an SBOM instead of the report\n -o, --output FILE Write the JSON or SBOM to FILE\n --offline Do not ask OSV";async function promptSecret(e,t){if(void 0!==process.env.TNC_SIGN_PASSWORD)return process.env.TNC_SIGN_PASSWORD;if(!process.stdin.isTTY)return"";const{promptHidden:n}=require("./commands");return n(e,{err:t})}async function keyless(e,t,n,{out:s,err:i}){const{spawnSync:r}=require("child_process");if(0!==r("win32"===process.platform?"where":"which",["cosign"],{encoding:"utf8"}).status)throw new UsageError("keyless signing needs cosign on the PATH (https://docs.sigstore.dev/cosign/system_config/installation/); signing with a key needs nothing: thinknsh trust key generate");if(!("verify"!==e||t.identity&&t.issuer))throw new UsageError("trust verify --keyless needs --identity (the signer: an email or a workflow URL) and --issuer (e.g. https://token.actions.githubusercontent.com)");const o=require("os"),{parseReference:a}=require("../image/reference"),{AuthStore:c}=require("../image/auth"),{ENGINE_CLI_CONFIG_VAR:g,DEFAULT_REGISTRY:l,DEFAULT_REGISTRY_HOST:u}=require("../api/wire"),{dockPaths:h}=require("../paths"),y=a(n),p=fs.mkdtempSync(path.join(o.tmpdir(),"tnc-cosign-"));try{let o=null;try{o=new c(h().auth).get(y.domain)}catch(e){}if(!o)try{o=require("../image/credhelpers").lookup(require("../image/auth").normalizeRegistry(y.domain))}catch(e){}const a=o?{[y.domain===u?l:y.domain]:{auth:Buffer.from(`${o.username}:${o.password}`).toString("base64")}}:{};fs.writeFileSync(path.join(p,"config.json"),JSON.stringify({auths:a}),{mode:384});const f=r("cosign","sign"===e?["sign","--yes",...(t.annotation||[]).flatMap(e=>["-a",e]),n]:["verify","--certificate-identity",t.identity,"--certificate-oidc-issuer",t.issuer,n],{stdio:["inherit","verify"===e?"pipe":"inherit","inherit"],env:{...process.env,[g]:p}});return"verify"===e&&(0===f.status?s.write(`${chalk.green("✓")} ${n}: signed by ${t.identity} (${t.issuer}), in the transparency log\n`):i.write(`${chalk.red("✗")} ${n}: no valid keyless signature by ${t.identity}\n`)),0===f.status?0:1}finally{fs.rmSync(p,{recursive:!0,force:!0})}}async function trust(e,{api:t,out:n,err:s}){const[i,...r]=e;if(!i||"--help"===i||"-h"===i)return n.write(USAGE+"\n"),0;const o=require("../image/sign");if("key"===i){const[e,...t]=r;if("generate"!==e)throw new UsageError("trust key generate [--output DIR] [--name NAME]");const{opts:i}=parseArgs(t,{output:{short:"o",type:"string"},name:{type:"string"}}),a=path.resolve(i.output||"."),c=i.name||"tnc-sign",g=path.join(a,`${c}.key`),l=path.join(a,`${c}.pub`);if(fs.existsSync(g))throw new UsageError(`${g} exists already`);let u=await promptSecret("Password for the private key (empty: none): ",s);if(u&&process.stdin.isTTY&&void 0===process.env.TNC_SIGN_PASSWORD&&await promptSecret("Again: ",s)!==u)throw new UsageError("the passwords differ");const{privatePem:h,publicPem:y}=o.generateKeyPair(u);return fs.mkdirSync(a,{recursive:!0}),fs.writeFileSync(g,h,{mode:384}),fs.writeFileSync(l,y),u=null,n.write(`private key: ${g}${h.includes("ENCRYPTED")?" (encrypted)":chalk.yellow(" (not encrypted: keep it safe)")}\npublic key: ${l}\n`),0}if("sign"===i||"verify"===i){const{opts:e,args:o}=parseArgs(r,{key:{short:"k",type:"string"},annotation:{short:"a",type:"list"},json:{type:"bool"},keyless:{type:"bool"},identity:{type:"string"},issuer:{type:"string"}});if(e.keyless){if(1!==o.length)throw new UsageError(`trust ${i} --keyless IMAGE`);return keyless(i,e,o[0],{out:n,err:s})}if(!e.key||1!==o.length)throw new UsageError(`trust ${i} --key ${"sign"===i?"KEY":"KEY.pub"} IMAGE`);const a=fs.readFileSync(path.resolve(e.key),"utf8");if("sign"===i){const i=Object.fromEntries((e.annotation||[]).map(e=>{const t=e.indexOf("=");if(t<1)throw new UsageError(`-a takes KEY=VALUE, not ${e}`);return[e.slice(0,t),e.slice(t+1)]})),r=a.includes("ENCRYPTED")?await promptSecret("Password of the key: ",s):"",c=await t.json("POST","/tnc/images/sign",{Image:o[0],Key:a,Passphrase:r,Annotations:Object.keys(i).length?i:null});return n.write(e.json?JSON.stringify(c,null,2)+"\n":`Signed ${c.identity}@${c.digest}\n${chalk.dim(` signature stored as ${c.tag} (${c.signatures} signature${1===c.signatures?"":"s"})`)}\n`),0}const c=await t.json("POST","/tnc/images/verify",{Image:o[0],Key:a});return e.json?n.write(JSON.stringify(c,null,2)+"\n"):c.verified.length?n.write(`${chalk.green("✓")} ${c.identity}@${c.digest}: ${c.verified.length} valid signature${1===c.verified.length?"":"s"} by ${path.basename(e.key)}\n`):s.write(`${chalk.red("✗")} ${c.identity}@${c.digest}: no signature by ${path.basename(e.key)}\n`),c.verified.length?0:1}throw new UsageError(`unknown trust command "${i}" (key generate, sign, verify)`)}const SEV=["UNKNOWN","LOW","MEDIUM","HIGH","CRITICAL"],COLOR={CRITICAL:chalk.red.bold,HIGH:chalk.red,MEDIUM:chalk.yellow,LOW:chalk.cyan,UNKNOWN:chalk.dim};async function scan(e,{api:t,out:n}){const{opts:s,args:i}=parseArgs(e,{severity:{type:"string"},"exit-code":{type:"string"},json:{type:"bool"},sbom:{type:"string"},output:{short:"o",type:"string"},offline:{type:"bool"}});if("--help"===i[0]||1!==i.length)return n.write(SCAN_USAGE+"\n"),1===i.length?0:2;const r=(s.severity||"UNKNOWN").toUpperCase();if(!SEV.includes(r))throw new UsageError("--severity takes LOW, MEDIUM, HIGH or CRITICAL");if(s.sbom&&!["cyclonedx","spdx"].includes(s.sbom))throw new UsageError("--sbom takes cyclonedx or spdx");const{qs:o}=require("./api"),a=await t.json("GET",`/tnc/images/scan${o({image:i[0],offline:s.offline||!!s.sbom,sbom:s.sbom})}`),c=e=>{s.output?fs.writeFileSync(path.resolve(s.output),e):n.write(e)};if(s.sbom)return c(JSON.stringify(a,null,2)+"\n"),0;const g=a.vulnerabilities.filter(e=>SEV.indexOf(e.severity)>=SEV.indexOf(r));if(s.json)c(JSON.stringify({...a,vulnerabilities:g},null,2)+"\n");else{n.write(`${chalk.bold(i[0])} ${a.os.PRETTY_NAME||a.os.ID||"unknown OS"} ${a.packages.length} packages\n`);for(const e of a.notes||[])n.write(chalk.yellow(` note: ${e}\n`));if(a.scanned)if(g.length){n.write(` ${SEV.slice().reverse().filter(e=>a.counts[e]).map(e=>COLOR[e](`${e} ${a.counts[e]}`)).join(" ")}\n\n`);for(const e of g)n.write(` ${COLOR[e.severity](e.severity.padEnd(8))} ${e.aliases[0]||e.id} ${e.package} ${e.version}${e.fixed?chalk.green(` → ${e.fixed}`):chalk.dim(" (no fix yet)")}\n`),e.summary&&n.write(chalk.dim(` ${e.summary}\n`))}else n.write(chalk.green(` no known vulnerabilities${"UNKNOWN"!==r?` at ${r} or worse`:""}\n`));else n.write(chalk.dim(" offline: not checked against OSV\n"))}const l=void 0!==s["exit-code"]?Number(s["exit-code"]):0;return g.length&&l?l:0}module.exports={trust,scan,USAGE,SCAN_USAGE};