thinkncollab-shell 2.0.0-beta.5 → 2.0.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +64 -17
- package/dist/bin/thinknsh.js +1 -1
- package/dist/src/commands/auth/auth.js +7 -0
- package/dist/src/commands/auth/login.js +1 -1
- package/dist/src/commands/auth/logout.js +1 -1
- package/dist/src/commands/auth/register.js +1 -1
- package/dist/src/commands/auth/whoami.js +1 -1
- package/dist/src/commands/dock/cluster.js +1 -1
- package/dist/src/commands/dock/containers.js +1 -1
- package/dist/src/commands/index.js +1 -1
- package/dist/src/commands/room/connect.js +7 -0
- package/dist/src/commands/room/join.js +1 -1
- package/dist/src/commands/room/sync.js +1 -1
- package/dist/src/commands/sandbox/common.js +7 -0
- package/dist/src/commands/sandbox/console.js +1 -1
- package/dist/src/commands/sandbox/dep.js +1 -1
- package/dist/src/commands/sandbox/deploy.js +1 -1
- package/dist/src/commands/sandbox/dev.js +1 -1
- package/dist/src/commands/sandbox/exec.js +1 -1
- package/dist/src/commands/sandbox/push.js +1 -1
- package/dist/src/commands/sandbox/sandbox.js +1 -1
- package/dist/src/commands/sandbox/share.js +1 -1
- package/dist/src/commands/sandbox/snapshot.js +1 -1
- package/dist/src/commands/shell/doctor.js +1 -1
- package/dist/src/commands/tasks/build.js +1 -1
- package/dist/src/commands/terminal/share.js +1 -1
- package/dist/src/core/api-client.js +1 -1
- package/dist/src/core/auth/browser-login.js +7 -0
- package/dist/src/core/auth/gate.js +7 -0
- package/dist/src/core/auth/session-store.js +7 -0
- package/dist/src/core/crypto/seal.js +7 -0
- package/dist/src/core/dependency-tracker.js +1 -1
- package/dist/src/core/foreground.js +7 -0
- package/dist/src/core/platform-socket.js +7 -0
- package/dist/src/core/room/sync-manager.js +7 -0
- package/dist/src/core/room-link.js +7 -0
- package/dist/src/core/shell.js +1 -1
- package/dist/src/core/{sandbox/snapshot-manager.js → snapshot-manager.js} +1 -1
- package/dist/src/core/tunnel/client.js +7 -0
- package/dist/src/core/tunnel/daemon.js +7 -0
- package/dist/src/core/tunnel/index.js +7 -0
- package/dist/src/core/websocket.js +1 -1
- package/dist/src/core/window-launcher.js +7 -0
- package/dist/src/dock/bench/memory.js +1 -1
- package/dist/src/dock/cli/commands.js +1 -1
- package/dist/src/dock/cli/k8s.js +7 -0
- package/dist/src/dock/cli/services.js +1 -1
- package/dist/src/dock/cli/session.js +1 -1
- package/dist/src/dock/cli/workspace.js +1 -1
- package/dist/src/dock/engine.js +1 -1
- package/dist/src/dock/image/certs.js +7 -0
- package/dist/src/dock/image/registry.js +1 -1
- package/dist/src/dock/room-data.js +7 -0
- package/dist/src/dock/runtime/network.js +1 -1
- package/dist/src/dock/sandbox.js +7 -0
- package/dist/src/dock/setup/index.js +1 -1
- package/dist/src/dock/workspace/share.js +7 -0
- package/dist/src/dock/workspace/workspace.js +1 -1
- package/package.json +1 -1
- package/dist/src/core/sandbox/arch-shim.js +0 -7
- package/dist/src/core/sandbox/capsule-builder.js +0 -7
- package/dist/src/core/sandbox/cgroups.js +0 -7
- package/dist/src/core/sandbox/container-monitor.js +0 -7
- package/dist/src/core/sandbox/crun-manager.js +0 -7
- package/dist/src/core/sandbox/engine.js +0 -7
- package/dist/src/core/sandbox/image-manager.js +0 -7
- package/dist/src/core/sandbox/image-store.js +0 -7
- package/dist/src/core/sandbox/index.js +0 -7
- package/dist/src/core/sandbox/macos-sandbox.js +0 -7
- package/dist/src/core/sandbox/microvm-manager.js +0 -7
- package/dist/src/core/sandbox/native-service-manager.js +0 -7
- package/dist/src/core/sandbox/network.js +0 -7
- package/dist/src/core/sandbox/oci-exporter.js +0 -7
- package/dist/src/core/sandbox/oci-puller.js +0 -7
- package/dist/src/core/sandbox/oci-runtime-spec.js +0 -7
- package/dist/src/core/sandbox/port-proxy-daemon.js +0 -7
- package/dist/src/core/sandbox/room-sync-manager.js +0 -7
- package/dist/src/core/sandbox/runtime-manager.js +0 -7
- package/dist/src/core/sandbox/storage.js +0 -7
- package/dist/src/core/sandbox/supervisor.js +0 -7
- package/dist/src/core/sandbox/tunnel-daemon.js +0 -7
- package/dist/src/core/sandbox/tunnel.js +0 -7
- package/dist/src/core/sandbox/window-launcher.js +0 -7
- package/dist/src/core/sandbox/windows-wsl2.js +0 -7
- package/dist/src/core/sandbox-window.js +0 -8
- /package/dist/src/core/{sandbox/policy-engine.js → policy-engine.js} +0 -0
|
@@ -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
|
-
const chalk=require("chalk"),path=require("path"),fs=require("fs-extra"),net=require("net"),{
|
|
7
|
+
const chalk=require("chalk"),path=require("path"),fs=require("fs-extra"),net=require("net"),{SandboxTunnel}=require("../../core/tunnel"),{getEngine,owner,serverOf,syncToDashboard}=require("./common");function isPortInUse(e){return new Promise(o=>{const n=net.createServer();n.once("error",e=>{"EADDRINUSE"===e.code?o(!0):o(!1)}),n.once("listening",()=>{n.close(),o(!1)}),n.listen(e)})}async function findFreePort(e=4e3){let o=e;for(;o<65535;){if(!await isPortInUse(o))return o;o++}return 4e3}function detectProjectConfig(e){const o=path.join(e,"package.json");if(fs.existsSync(o))try{const n=fs.readJsonSync(o);let t="node server.js",s=3e3;return n.scripts?.dev?t="npm run dev":n.scripts?.start?t="npm start":fs.existsSync(path.join(e,"server.js"))?t="node server.js":fs.existsSync(path.join(e,"app.js"))?t="node app.js":fs.existsSync(path.join(e,"index.js"))&&(t="node index.js"),n.config?.port&&(s=parseInt(n.config.port,10)),{type:"Node.js",cmd:t,containerPort:s}}catch(e){}return fs.existsSync(path.join(e,"CMakeLists.txt"))?{type:"C++",cmd:"cmake -B build && cmake --build build && (./build/app || ./build/main || ./build/server || true)",containerPort:8080}:fs.existsSync(path.join(e,"Makefile"))?{type:"C++",cmd:"make && (./app || ./main || ./server || true)",containerPort:8080}:fs.existsSync(path.join(e,"main.cpp"))?{type:"C++",cmd:"g++ -std=c++17 main.cpp -o app && ./app",containerPort:8080}:fs.existsSync(path.join(e,"app.py"))?{type:"Python",cmd:"python3 app.py",containerPort:5e3}:fs.existsSync(path.join(e,"main.py"))?{type:"Python",cmd:"python3 main.py",containerPort:8e3}:fs.existsSync(path.join(e,"main.go"))||fs.existsSync(path.join(e,"go.mod"))?{type:"Go",cmd:"go run .",containerPort:8080}:fs.existsSync(path.join(e,"Cargo.toml"))?{type:"Rust",cmd:"cargo run",containerPort:8080}:{type:"Generic",cmd:"node server.js",containerPort:3e3}}module.exports={name:"dev",description:"Zero-config smart auto-detection & sandbox launcher for current directory",aliases:["up","run-app"],requiresAuth:!1,async execute(e,o){const n=process.cwd(),t=path.basename(n).toLowerCase().replace(/[^a-z0-9_-]/g,"-").replace(/^-+|-+$/g,"")||"app";let s=null;const c=[];for(let o=0;o<e.length;o++)"--name"!==e[o]&&"-n"!==e[o]||!e[o+1]?e[o].startsWith("--name=")?s=e[o].slice(7):c.push(e[o]):(s=e[o+1],o++);const r=s||c[0]||t;console.log(chalk.cyan(`\n[Inspect] Project in: ${chalk.bold(n)}`));const a=detectProjectConfig(n);console.log(chalk.dim(` Type Detected : ${chalk.green(a.type)}`)),console.log(chalk.dim(` Launch Command: ${chalk.white(a.cmd)}`));let i=a.containerPort;for(let o=0;o<e.length;o++)if(("-p"===e[o]||"--port"===e[o])&&e[o+1]){const n=e[o+1].split(":");i=n[1]?parseInt(n[1],10):parseInt(n[0],10)}let l=i;await isPortInUse(l)&&(l=await findFreePort(4e3),console.log(chalk.yellow(` [Notice] Port ${i} is busy on host. Auto-routing to Host Port ${chalk.bold(l)}`))),console.log(chalk.dim(` Port Mapping : Host:${chalk.bold(l)} -> Container:${i}`)),console.log(chalk.cyan(`\n[Launch] Auto-Launching TNC Sandbox [${chalk.bold(r)}]...`));const d=e.indexOf("--image"),p=await getEngine().start(r,{cmd:a.cmd,cwd:n,hostPort:l,containerPort:i,...-1!==d&&e[d+1]?{image:e[d+1]}:{},...owner(o)});if(p.success||p.meta&&p.message?.includes("already running")){const n=p.meta,t=!p.success;t?console.log(chalk.green(`\n[OK] Sandbox "${r}" is ALREADY UP and running!`)):console.log(chalk.green(`\n[OK] Sandbox "${r}" is UP and running!`));const s=serverOf(o),c=`${s}/thinknsh/proxy/${encodeURIComponent(r)}/`,a=`${s}/thinknsh/preview/${encodeURIComponent(r)}`;console.log(` App URL : ${chalk.bold.cyan(c)}`),console.log(` Preview URL : ${chalk.cyan(a)}`),console.log(` Local URL : ${chalk.bold.cyan(`http://localhost:${n.hostPort}`)}`),console.log(` Web Dashboard : ${chalk.underline(`${s}/thinknsh/sandboxes${n.roomId?"?roomId="+n.roomId:""}`)}`),console.log(` Container : ${n.container} (${n.image})`),n.roomId&&console.log(` Workspace Room : ${n.roomName||n.roomId}`),console.log(chalk.dim("\n Quick Controls:")),console.log(chalk.dim(` Console window : thinknsh console ${r}`)),console.log(chalk.dim(` Stream Logs : thinknsh sandbox logs -f ${r}`)),console.log(chalk.dim(` Shell inside : thinknsh sandbox exec ${r}`)),console.log(chalk.dim(` Pause / Freeze : thinknsh sandbox freeze ${r}`)),console.log(chalk.dim(` Wakeup : thinknsh sandbox resume ${r}`)),console.log(chalk.dim(` Destroy : thinknsh sandbox destroy ${r}\n`)),e.includes("--no-window")||e.includes("--headless")||require("../../core/window-launcher").spawn(r),await syncToDashboard(n,t?"updated":"started",o);const i=new SandboxTunnel;i.connect(r,n.hostPort||l,o),console.log(chalk.green(" Tunnel : ACTIVE")),console.log(chalk.dim(` Remote preview streams via thinkncollab.com/thinknsh/proxy/${encodeURIComponent(r)}/`)),process.on("SIGINT",()=>{i.stop(),process.exit(0)}),process.on("SIGTERM",()=>{i.stop(),process.exit(0)})}else console.log(chalk.red(`\n[Error] Failed to start sandbox: ${p.message}\n`))}};
|
|
@@ -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
|
-
const chalk=require("chalk"),{
|
|
7
|
+
const chalk=require("chalk"),{getEngine}=require("./common");module.exports={name:"sandbox-exec",description:"Run a command inside a sandbox (thinknsh sandbox exec ID [cmd])",requiresAuth:!1,async execute(e){const n=[];let o=0;for(;o<e.length;o++){const s=e[o];if("-it"!==s&&"-ti"!==s&&"-i"!==s&&"-t"!==s){if("-e"!==s&&"-w"!==s&&"-u"!==s||!e[o+1])break;n.push(s,e[++o])}}const s=e[o];if(!s)return void console.log(chalk.yellow("Usage: thinknsh sandbox exec [-e KEY=VAL] [-w DIR] <sandbox-id> [command...]"));const t=getEngine(),i=await t.loadMeta(s);if(!i||"RUNNING"!==i.state)return void console.log(chalk.red(`[FAIL] Sandbox "${s}" is not running (${i?i.state:"not found"}). Start it: thinknsh sandbox resume ${s}`));const a=e.slice(o+1),c=process.stdin.isTTY&&process.stdout.isTTY,{runCommand:d}=require("../../dock/cli/commands");process.exitCode=await d("exec",[c?"-it":"-i",...n,t.name(s),...a.length?a:["sh"]])}};
|
|
@@ -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
|
-
const chalk=require("chalk"),
|
|
7
|
+
const chalk=require("chalk"),{execFileSync}=require("child_process"),sh=(e,s)=>execFileSync(e,s,{encoding:"utf8",stdio:["ignore","pipe","pipe"]}).trim();function cloudCli(e,s,t){try{return sh(e,s)}catch(s){const o="ENOENT"===s.code?`${e} is not installed`:(s.stderr||s.message).trim().split("\n").pop();throw new Error(`${o}\n Setup: ${t}`)}}function resolveTarget(e,s){const t=/^([a-z]+):\/\/(.+)$/.exec(e);if(!t)throw new Error(`"${e}" is not a scheme://name target (ecr://, gar://, gcr://, ghcr://)`);const[,o,r]=t,n=r.lastIndexOf(":")>r.lastIndexOf("/")?r.lastIndexOf(":"):-1,a=-1===n?r:r.slice(0,n),c=-1===n?"latest":r.slice(n+1);switch(o){case"ecr":{const e=process.env.AWS_REGION||process.env.AWS_DEFAULT_REGION||cloudCli("aws",["configure","get","region"],"aws configure");return{host:`${cloudCli("aws",["sts","get-caller-identity","--query","Account","--output","text"],"aws configure")}.dkr.ecr.${e}.amazonaws.com`,path:a,tag:c,username:"AWS",password:cloudCli("aws",["ecr","get-login-password","--region",e],"aws configure")}}case"gar":case"gcr":{const[e,...s]=a.split("/");return{host:e.includes(".")?e:"gcr.io",path:e.includes(".")?s.join("/"):a,tag:c,username:"oauth2accesstoken",password:cloudCli("gcloud",["auth","print-access-token"],"gcloud auth login")}}case"ghcr":{const e=s.token||process.env.GITHUB_TOKEN||null;return{host:"ghcr.io",path:a,tag:c,username:e?s.username||process.env.GITHUB_ACTOR||"token":null,password:e}}default:throw new Error(`unknown registry scheme "${o}://" (ecr, gar, gcr, ghcr)`)}}module.exports={name:"push",aliases:["image-push","export"],description:"Push an image to AWS ECR, Google Artifact Registry or GHCR by scheme (ecr:// gar:// ghcr://)",usage:"thinknsh push [LOCAL] <scheme://name:tag> | push --export IMAGE [out.tar]",examples:["thinknsh push ecr://my-app:latest","thinknsh push gar://us-docker.pkg.dev/my-project/apps/my-app:v1","thinknsh push ghcr://username/my-app:latest","thinknsh push --export postgres:16 ./postgres.tar"],async execute(e=[],s){const{runCommand:t}=require("../../dock/cli/commands"),{DockApi:o}=require("../../dock/cli/api"),r={},n=[];for(let s=0;s<e.length;s++){const t=e[s];"--export"===t||"export"===t?r.export=!0:/^--(username|token|output)$/.test(t)||"-o"===t?r[t.replace(/^-+/,"").replace(/^o$/,"output")]=e[++s]:n.push(t)}if(r.export){const e=n[0];if(!e)return void console.log(chalk.red("Usage: thinknsh push --export <image>:<tag> [output.tar]"));const s=r.output||n[1]||`${e.replace(/[/:@]+/g,"-")}.tar`,o=await t("save",["-o",s,e]);return 0===o&&(console.log(chalk.green(`[OK] ${e} → ${s}`)),console.log(chalk.dim(` Load it on another machine: thinknsh load -i ${s}`))),void(process.exitCode=o)}const a=n[n.length-1];if(a)try{const e=resolveTarget(a,r),s=n.length>=2?n[0]:`${e.path.split("/").pop()}:${e.tag}`,c=`${e.host}/${e.path}:${e.tag}`,i=new o;await i.ensureRunning({quiet:!0});try{await i.json("GET",`/images/${encodeURIComponent(s)}/json`)}catch(e){throw new Error(`no local image "${s}"; build it (thinknsh build -t ${s} .) or name it: thinknsh push IMAGE ${a}`)}if(e.username&&await i.json("POST","/auth",{username:e.username,password:e.password,serveraddress:e.host}),s!==c&&0!==await t("tag",[s,c]))return void(process.exitCode=1);console.log(chalk.cyan(`Pushing ${s} → ${c}`));const l=await t("push",[c]);0===l&&console.log(chalk.green(`\n[OK] Pushed: ${c}`)),process.exitCode=l}catch(e){console.log(chalk.red(`\n[Error] Push failed: ${e.message}`)),process.exitCode=1}else console.log(chalk.yellow(`Usage: ${module.exports.usage}`))}},module.exports.resolveTarget=resolveTarget;
|
|
@@ -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
|
-
const chalk=require("chalk"),Table=require("cli-table3"),path=require("path"),fs=require("fs-extra"),{SandboxEngine}=require("../../core/sandbox"),SandboxTunnel=require("../../core/sandbox/tunnel"),CgroupsV2=require("../../core/sandbox/cgroups"),engine=new SandboxEngine,cgroups=new CgroupsV2;function syncToDashboard(e,o,n){if(n?.ws?.isConnected())try{n.ws.emitSandboxSync(e,o)}catch(e){}return new Promise(s=>{let a=!1;const l=()=>{a||(a=!0,s())},t=(n?.ws?.config?.serverUrl||n?.api?.apiUrl||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com").replace(/\/$/,""),r=n?.ws?.getCurrentRoom?.()||n?.config?.roomId||process.env.THINKNCOLLAB_ROOM_ID||e?.roomId||null,c=n?.api?.getUser?.()?._id||n?.api?.getUser?.()?.id||n?.api?.userId||n?.ws?.userId||n?.config?.user?._id||n?.config?.userId||process.env.THINKNCOLLAB_USER_ID||e?.userId||null,i=n?.config?.roomName||e?.roomName||null,d=n?.api?.getUser?.()?.name||n?.config?.user?.name||n?.config?.userName||n?.config?.username||e?.userName||process.env.THINKNCOLLAB_USER_NAME||null,m={event:o,sandbox:{...e,roomId:r||e?.roomId||null,roomName:i||e?.roomName||null,userId:c||e?.userId||null,userName:d||e?.userName||null},roomId:r||e?.roomId||null,roomName:i||e?.roomName||null,userId:c||e?.userId||null,userName:d||e?.userName||null,timestamp:(new Date).toISOString()};try{const e=require("socket.io-client")(t+"/thinknsh",{transports:["websocket","polling"],timeout:1200,reconnection:!1});e.on("connect",()=>{e.emit("sandbox:sync",m),setTimeout(()=>{e.disconnect(),l()},60)}),e.on("connect_error",()=>{e.disconnect(),l()})}catch(e){l()}try{const e=t.startsWith("https"),o=e?require("https"):require("http"),n=new URL(t),s=JSON.stringify(m),a=o.request({hostname:n.hostname,port:n.port||(e?443:80),path:"/thinknsh/api/sandboxes/sync",method:"POST",headers:{"Content-Type":"application/json","Content-Length":Buffer.byteLength(s)},timeout:1e3},()=>l());a.on("error",()=>l()),a.on("timeout",()=>{a.destroy(),l()}),a.write(s),a.end()}catch(e){l()}setTimeout(l,1500)})}module.exports={name:"sandbox",description:"Manage isolated lightweight sandboxes (Start, Freeze, Resume, Destroy)",aliases:["sb"],requiresAuth:!1,async execute(e,o){const n=(e[0]||"help").toLowerCase();switch(n){case"up":case"dev":{const n=require("./dev");return await n.execute(e.slice(1),o)}case"start":{const n=e[1]||`app-${Date.now().toString().slice(-4)}`;let s="node server.js",a=null,l=null,t=null;const r=new Set(["-p","--port","--memory","--mem","--ephemeral","--node","--node-version"]);for(let o=2;o<e.length;o++)if("--cmd"===e[o]){const n=[];for(o++;o<e.length&&!r.has(e[o]);)n.push(e[o]),o++;o--,n.length>0&&(s=n.join(" "))}else if("-p"!==e[o]&&"--port"!==e[o]||!e[o+1])"--node"!==e[o]&&"--node-version"!==e[o]||!e[o+1]||(t=e[++o]);else{const n=e[++o].split(":");a=parseInt(n[0],10),l=n[1]?parseInt(n[1],10):a}console.log(chalk.cyan(`\n[Start] Starting TNC Sandbox [${n}]...`)),console.log(chalk.dim(` Command : ${s}`)),a&&console.log(chalk.dim(` Port : Host:${a} -> Container:${l}`)),t&&console.log(chalk.dim(` Node : ${t}`));const c=o?.ws?.getCurrentRoom?.()||o?.config?.roomId||process.env.THINKNCOLLAB_ROOM_ID||null,i=o?.api?.getUser?.()?._id||o?.api?.getUser?.()?.id||o?.api?.userId||o?.ws?.userId||o?.config?.user?._id||o?.config?.userId||process.env.THINKNCOLLAB_USER_ID||null,d=o?.config?.roomName||null,m=o?.api?.getUser?.()?.name||o?.config?.user?.name||o?.config?.userName||o?.config?.username||process.env.THINKNCOLLAB_USER_NAME||null,g=await engine.start(n,{cmd:s,cwd:process.cwd(),hostPort:a,containerPort:l,nodeVersion:t,roomId:c,roomName:d,userId:i,userName:m});if(g.success){const e={"linux-unshare":chalk.green("linux-unshare (kernel)"),"macos-seatbelt":chalk.magenta("macos-seatbelt (syscall)"),"wsl2-kernel":chalk.blue("wsl2-kernel (linux)"),"host-process":chalk.gray("host-process (basic)")}[g.meta.isolationLevel]||chalk.gray(g.meta.isolationLevel);console.log(chalk.green(`\n[OK] ${g.message}`)),console.log(chalk.dim(` PID : ${g.meta.pid}`)),console.log(chalk.dim(` State : ${chalk.green(g.meta.state)}`)),console.log(` Isolation : ${e}`),g.meta.runtime&&g.meta.runtime.node&&console.log(chalk.dim(` Node.js : v${g.meta.runtime.node}${g.meta.runtime.isHermetic?" (hermetic runtime)":" (host)"}`)),g.meta.roomId&&console.log(` Room : ${g.meta.roomName||g.meta.roomId}`),g.meta.memoryLimitMB&&console.log(chalk.dim(` Mem Limit : ${g.meta.memoryLimitMB} MB`)),console.log(chalk.dim(` To freeze later: thinknsh sandbox freeze ${n}\n`)),await syncToDashboard(g.meta,"started",o),(new SandboxTunnel).connect(n,a||g.meta.hostPort,o)}else console.log(chalk.yellow(`\n[Notice] ${g.message}\n`));break}case"freeze":case"stop":{const n=e[1];if(!n)return void console.log(chalk.red("[Error] Usage: thinknsh sandbox freeze <sandbox-id>"));console.log(chalk.cyan(`\n[Freeze] Freezing Sandbox [${n}]...`));const s=await engine.freeze(n);s.success?(console.log(chalk.green(`[OK] ${s.message}`)),console.log(chalk.dim(" RAM released to 0 MB. All project files and node_modules preserved!")),console.log(chalk.dim(` To wake up: thinknsh sandbox resume ${n}\n`)),await syncToDashboard(s.meta,"frozen",o)):console.log(chalk.red(`[Error] ${s.message}\n`));break}case"resume":case"unfreeze":{const n=e[1];if(!n)return void console.log(chalk.red("[Error] Usage: thinknsh sandbox resume <sandbox-id>"));console.log(chalk.cyan(`\n[Resume] Resuming Sandbox [${n}]...`));const s=await engine.resume(n);s.success?(console.log(chalk.green(`[OK] ${s.message}`)),console.log(chalk.dim(` PID: ${s.meta.pid} | State: ${chalk.green(s.meta.state)}\n`)),await syncToDashboard(s.meta,"resumed",o)):console.log(chalk.red(`[Error] ${s.message}\n`));break}case"ps":case"list":case"ls":{const e=engine.list();if(0===e.length)return console.log(chalk.yellow("\nNo active or frozen sandboxes found.")),void console.log(chalk.dim('Start one with: thinknsh sandbox start <name> --cmd "<command>"\n'));const o=new Table({head:[chalk.cyan("ID"),chalk.cyan("ROOM"),chalk.cyan("STATE"),chalk.cyan("PID"),chalk.cyan("PORTS"),chalk.cyan("ISOLATION"),chalk.cyan("COMMAND"),chalk.cyan("STARTED")]}),n={"linux-unshare":chalk.green("unshare"),"macos-seatbelt":chalk.magenta("seatbelt"),"wsl2-kernel":chalk.blue("wsl2"),"host-process":chalk.gray("host"),"macos-microvm-vz":chalk.cyan("microvm-vz"),"linux-microvm-vz":chalk.cyan("microvm-vz"),"linux-crun-oci":chalk.green("crun-oci")};for(const s of e){const e="RUNNING"===s.state?chalk.green(s.state):"FROZEN"===s.state?chalk.blue(s.state):chalk.gray(s.state),a=s.hostPort?`${s.hostPort}:${s.containerPort||s.hostPort}`:"-",l=n[s.isolationLevel]||chalk.gray(s.isolationLevel||"?"),t=s.roomName||(s.roomId?`Room ${s.roomId.slice(-6)}`:"-");o.push([chalk.bold(s.id),t,e,s.pid||chalk.dim("frozen"),a,l,s.cmd||"-",s.startedAt?new Date(s.startedAt).toLocaleTimeString():"-"])}console.log(chalk.cyan("\nTNC Sandboxes:")),console.log(o.toString()),console.log("");break}case"logs":{const o=e[1];if(!o)return void console.log(chalk.red("[Error] Usage: thinknsh sandbox logs <sandbox-id>"));const n=path.join(engine.baseDir,o,"output.log");if(fs.existsSync(n)){const e=fs.readFileSync(n,"utf8");console.log(chalk.dim(`\n--- Logs for ${o} (last 50 lines) ---`)),console.log(e.split("\n").slice(-50).join("\n"))}else console.log(chalk.yellow(`No log file found for sandbox "${o}".`));break}case"destroy":case"rm":case"prune":{const s=e[1];if("--all"===s||"-a"===s||"prune"===n){const e=engine.list();if(0===e.length)return void console.log(chalk.yellow("\nNo active or frozen sandboxes to destroy.\n"));console.log(chalk.yellow(`\n[Destroy] Destroying all ${e.length} sandboxes...`));for(const n of e)await engine.destroy(n.id),await syncToDashboard({id:n.id,state:"DESTROYED"},"destroyed",o),console.log(chalk.green(`[OK] Destroyed "${n.id}"`));return void console.log(chalk.green("\n[OK] All sandboxes cleaned up successfully.\n"))}if(!s)return void console.log(chalk.red("[Error] Usage: thinknsh sandbox destroy <sandbox-id> (or --all)"));console.log(chalk.yellow(`\n[Destroy] Destroying Sandbox [${s}]...`));const a=await engine.destroy(s);console.log(chalk.green(`[OK] ${a.message}\n`)),await syncToDashboard({id:s,state:"DESTROYED"},"destroyed",o);break}case"stats":case"top":{const o=e[1];if(!o)return void console.log(chalk.red("[Error] Usage: thinknsh sandbox stats <sandbox-id>"));const n=engine.loadMeta(o);if(!n)return void console.log(chalk.red(`[Error] Sandbox "${o}" not found.`));if(console.log(chalk.cyan(`\nStats: ${chalk.bold(o)}`)),console.log(chalk.dim("─────────────────────────────────")),n.pid){const e=(new(require("../../core/sandbox/macos-sandbox"))).getMemoryUsage(n.pid);console.log(` PID : ${chalk.yellow(n.pid)}`),console.log(` RAM (RSS) : ${chalk.yellow(e+" MB")} (live)`)}else console.log(` State : ${chalk.blue("FROZEN")} — RAM = 0 MB`);if("linux"===process.platform&&cgroups.isAvailable()){const e=cgroups.getStats(o);e&&(console.log(` cgroup RAM : ${chalk.green(e.memoryMB+" MB")} / ${e.memoryLimitMB?e.memoryLimitMB+" MB":"unlimited"}`),console.log(` CPU time : ${chalk.green(e.cpuUsageMs+" ms")}`),console.log(` PIDs alive : ${chalk.green(e.pids)} / ${e.pidsLimit||"∞"}`),e.oomKills>0?console.log(` OOM kills : ${chalk.red("[Notice] "+e.oomKills)}`):console.log(` OOM kills : ${chalk.dim("0 (healthy)")}`))}else"linux"!==process.platform&&console.log(chalk.dim(` cgroups v2 : Linux only (you're on ${process.platform})`));console.log(` Isolation : ${chalk.magenta(n.isolationLevel||"?")}`),n.roomId&&console.log(` Room : ${n.roomName||n.roomId}`),console.log(` Started : ${n.startedAt?new Date(n.startedAt).toLocaleString():"-"}`),n.frozenAt&&console.log(` Frozen at : ${chalk.blue(new Date(n.frozenAt).toLocaleString())}`),console.log("");break}case"exec":{const n=require("./exec");return await n.execute(e.slice(1),o)}default:console.log(chalk.cyan("\nThinkNCollab Sandbox Manager")),console.log(chalk.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")),console.log(` ${chalk.green('thinknsh sandbox start <id> [--cmd "..."] [-p host:cont]')}`),console.log(` ${chalk.green("thinknsh sandbox freeze <id>")}`),console.log(` ${chalk.green("thinknsh sandbox resume <id>")}`),console.log(` ${chalk.green("thinknsh sandbox ps")}`),console.log(` ${chalk.green("thinknsh sandbox stats <id>")} ${chalk.dim("<- live memory/CPU/PID stats")}`),console.log(` ${chalk.green("thinknsh sandbox logs <id>")}`),console.log(` ${chalk.green("thinknsh sandbox destroy <id>")}\n`)}}};
|
|
7
|
+
const chalk=require("chalk"),Table=require("cli-table3"),{SandboxTunnel}=require("../../core/tunnel"),{getEngine,owner,syncToDashboard}=require("./common"),dockCommand=(e,o)=>require("../../dock/cli/commands").runCommand(e,o),STATE_COLOR={RUNNING:chalk.green,FROZEN:chalk.blue};function parseStart(e){const o={cmd:null,hostPort:null,containerPort:null,nodeVersion:null,image:null,memoryLimit:null},n=new Set(["-p","--port","--memory","--mem","-m","--node","--node-version","--image"]);for(let s=0;s<e.length;s++){const t=e[s];if("--cmd"===t){const t=[];for(;s+1<e.length&&!n.has(e[s+1]);)t.push(e[++s]);t.length&&(o.cmd=t.join(" "))}else if("-p"!==t&&"--port"!==t||!e[s+1])"--node"!==t&&"--node-version"!==t||!e[s+1]?"--image"===t&&e[s+1]?o.image=e[++s]:"--memory"!==t&&"--mem"!==t&&"-m"!==t||!e[s+1]||(o.memoryLimit=e[++s]):o.nodeVersion=e[++s];else{const[n,t]=e[++s].split(":");o.hostPort=parseInt(n,10),o.containerPort=t?parseInt(t,10):o.hostPort}}return o}async function list(){const e=await getEngine().list();if(!e.length)return console.log(chalk.yellow("\nNo sandboxes yet.")),void console.log(chalk.dim("Start one in a project folder with: thinknsh dev\n"));const o=new Table({head:["ID","ROOM","STATE","PORTS","IMAGE","COMMAND","STARTED"].map(e=>chalk.cyan(e))});for(const n of e)o.push([chalk.bold(n.id),n.roomName||(n.roomId?`Room ${n.roomId.slice(-6)}`:"-"),(STATE_COLOR[n.state]||chalk.gray)(n.state),n.hostPort?`${n.hostPort}:${n.containerPort||n.hostPort}`:"-",n.image,n.cmd||"-",n.startedAt?new Date(n.startedAt).toLocaleTimeString():"-"]);console.log(chalk.cyan("\nTNC Sandboxes:")),console.log(o.toString()),console.log(chalk.dim(" containers tnc-sb-<id>: thinknsh ps, logs, exec and stats work on them too\n"))}module.exports={name:"sandbox",description:"Project folders as dock containers (start, freeze, resume, destroy)",aliases:["sb"],requiresAuth:!1,async execute(e,o){try{return await this.run(e,o)}catch(e){console.log(chalk.red(`[Error] ${e.message}`)),process.exitCode=1}},async run(e,o){const n=(e[0]||"help").toLowerCase(),s=getEngine(),t=e[1],a=e=>(t||console.log(chalk.red(`[Error] Usage: thinknsh sandbox ${e}`)),!t);switch(n){case"up":case"dev":return require("./dev").execute(e.slice(1),o);case"start":{const n=t||`app-${Date.now().toString().slice(-4)}`,a=parseStart(e.slice(2));console.log(chalk.cyan(`\n[Start] Sandbox [${n}]...`));const r=await s.start(n,{...a,cwd:process.cwd(),...owner(o)});return r.success?(console.log(chalk.green(`\n[OK] ${r.message}`)),console.log(chalk.dim(` Container : ${r.meta.container}`)),r.meta.hostPort&&console.log(chalk.dim(` URL : http://localhost:${r.meta.hostPort}`)),r.meta.memoryLimitMB&&console.log(chalk.dim(` Mem Limit : ${r.meta.memoryLimitMB} MB`)),console.log(chalk.dim(` Freeze later: thinknsh sandbox freeze ${n}\n`)),await syncToDashboard(r.meta,"started",o),void(r.meta.hostPort&&(new SandboxTunnel).connect(n,r.meta.hostPort,o))):void console.log(chalk.yellow(`\n[Notice] ${r.message}\n`))}case"freeze":case"stop":{if(a("freeze <sandbox-id>"))return;const e=await s.freeze(t);return e.success?(s.stopTunnelDaemon(t),console.log(chalk.green(`[OK] ${e.message}`)),console.log(chalk.dim(` Wake it up: thinknsh sandbox resume ${t}\n`)),void await syncToDashboard(e.meta,"frozen",o)):void console.log(chalk.red(`[Error] ${e.message}\n`))}case"resume":case"unfreeze":{if(a("resume <sandbox-id>"))return;const e=await s.resume(t);return e.success?(console.log(chalk.green(`[OK] ${e.message}`)),e.meta.hostPort&&console.log(chalk.dim(` http://localhost:${e.meta.hostPort}\n`)),void await syncToDashboard(e.meta,"resumed",o)):void console.log(chalk.red(`[Error] ${e.message}\n`))}case"ps":case"list":case"ls":return list();case"logs":{const o=e.includes("-f")||e.includes("--follow"),n=e.slice(1).find(e=>!e.startsWith("-"));return n?void await dockCommand("logs",[...o?["-f"]:["-n","200"],s.name(n)]):void console.log(chalk.red("[Error] Usage: thinknsh sandbox logs [-f] <sandbox-id>"))}case"stats":case"top":if(a("stats <sandbox-id>"))return;return void await dockCommand("stats",["--no-stream",s.name(t)]);case"destroy":case"rm":case"prune":{const e="--all"===t||"-a"===t;if(e||"prune"===n){const n=(await s.list()).filter(o=>e||"RUNNING"!==o.state);if(!n.length)return void console.log(chalk.yellow("\nNo sandboxes to remove.\n"));for(const e of n)await s.destroy(e.id),s.stopTunnelDaemon(e.id),await syncToDashboard({id:e.id,state:"DESTROYED"},"destroyed",o),console.log(chalk.green(`[OK] Destroyed "${e.id}"`));return}if(a("destroy <sandbox-id> (or --all)"))return;const r=await s.destroy(t);return s.stopTunnelDaemon(t),console.log((r.success?chalk.green:chalk.yellow)(`[${r.success?"OK":"Notice"}] ${r.message}\n`)),void(r.success&&await syncToDashboard({id:t,state:"DESTROYED"},"destroyed",o))}case"exec":return require("./exec").execute(e.slice(1),o);case"share":case"tunnel":return require("./share").execute(e.slice(1),o);default:console.log(chalk.cyan("\nThinkNCollab sandboxes (dock containers)")),console.log(chalk.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")),console.log(` ${chalk.green("thinknsh dev")} ${chalk.dim("detect the project and run it")}`),console.log(` ${chalk.green('thinknsh sandbox start <id> [--cmd "..."] [-p host:cont] [--image IMG]')}`),console.log(` ${chalk.green("thinknsh sandbox freeze <id>")} ${chalk.dim("0 RAM, files and dependencies kept")}`),console.log(` ${chalk.green("thinknsh sandbox resume <id>")}`),console.log(` ${chalk.green("thinknsh sandbox ps")}`),console.log(` ${chalk.green("thinknsh sandbox logs [-f] <id>")}`),console.log(` ${chalk.green("thinknsh sandbox stats <id>")}`),console.log(` ${chalk.green("thinknsh sandbox exec <id> [cmd]")}`),console.log(` ${chalk.green("thinknsh sandbox share <id>")} ${chalk.dim("preview URL for the room")}`),console.log(` ${chalk.green("thinknsh sandbox destroy <id> | --all")}\n`)}}};
|
|
@@ -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
|
-
const chalk=require("chalk"),{
|
|
7
|
+
const chalk=require("chalk"),{getEngine,serverOf}=require("./common");module.exports={name:"tunnel",description:"Generate instant encrypted room preview URL & WebSocket tunnel for teammates",aliases:["preview"],requiresAuth:!1,async execute(e,n){const o=getEngine(),t=await o.list(),r=e[0]||(t.find(e=>"RUNNING"===e.state)||{}).id||null;if(!r)return console.log(chalk.red("\n[Error] No active sandboxes found to share.")),void console.log(chalk.dim(" Run: thinknsh dev first to start a sandbox.\n"));const l=t.find(e=>e.id===r);if(!l)return void console.log(chalk.red(`\n[Error] Sandbox "${r}" not found.`));if("RUNNING"!==l.state)return void console.log(chalk.yellow(`\n[Warning] Sandbox "${r}" is currently ${l.state}. Resume it first: thinknsh sandbox resume ${r}`));const s=l.hostPort||3e3,a=Math.random().toString(36).substring(2,10),i=serverOf(n),c=`${i}/thinknsh/preview/${encodeURIComponent(r)}?token=${a}`,d=`${i}/thinknsh/proxy/${encodeURIComponent(r)}/?token=${a}`;if(console.log(chalk.cyan(`\n[Tunnel] Generating Collaborative Live Tunnel for [${chalk.bold(r)}]...`)),console.log(chalk.green("\n[OK] Live Tunnel Active!")),console.log(` Sandbox ID : ${chalk.bold(r)}`),console.log(` Proxy Stream : ${chalk.cyan(d)}`),console.log(` Team Preview : ${chalk.bold.underline(c)}`),console.log(" Room Members : Instant access enabled with live hot-reload sync"),o.startTunnelDaemon(r,s,i,n?.api?.getUser?.()?._id||null)||console.log(chalk.yellow(" [Notice] Could not start the background tunnel.")),console.log(chalk.dim("\n Share this URL with your teammates to inspect your app in real time!\n")),n?.ws?.isConnected())try{n.ws.socket.emit("sandbox:share-created",{sandboxId:r,previewUrl:c,hostPort:s})}catch(e){}}};
|
|
@@ -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
|
-
const chalk=require("chalk"),Table=require("cli-table3"),SnapshotManager=require("../../core/
|
|
7
|
+
const chalk=require("chalk"),Table=require("cli-table3"),SnapshotManager=require("../../core/snapshot-manager");module.exports={name:"snapshot",aliases:["snap"],description:"Instant volume snapshots with CoW clones (APFS / btrfs / reflink)",usage:"thinknsh snapshot <subcommand> [options]",examples:["thinknsh snapshot create myapp-v1","thinknsh snapshot create myapp-v1 ./data","thinknsh snapshot list","thinknsh snapshot restore myapp-v1-abc123","thinknsh snapshot diff snap1-id snap2-id","thinknsh snapshot delete snap1-id","thinknsh snapshot prune --keep 5","thinknsh snapshot info"],async execute(e=[],s){const o={},n=[];for(let s=0;s<e.length;s++){const a=e[s];if(a.startsWith("--")){const n=a.slice(2);if(n.includes("=")){const[e,s]=n.split("=");o[e]=s}else e[s+1]&&!e[s+1].startsWith("-")?o[n]=e[++s]:o[n]=!0}else if(a.startsWith("-")){const n=a.slice(1);e[s+1]&&!e[s+1].startsWith("-")?o[n]=e[++s]:o[n]=!0}else n.push(a)}return this.run(n,o,s)},async run(e,s={},o){const n=new SnapshotManager,a=e[0]||"list";switch(a){case"create":case"take":case"new":{const o=e[1];if(!o)return void console.log(chalk.red("Usage: thinknsh snapshot create <name> [path]"));const a=e[2]||process.cwd(),t=s.container||s.c||null;try{let e;if(t){const{getEngine:l}=require("./common"),c=l();e=await n.snapshotSafe(a,o,c,t,{description:s.desc||s.description||"",tags:s.tag?{tag:s.tag}:{}})}else e=await n.snapshot(a,o,{description:s.desc||s.description||"",tags:s.tag?{tag:s.tag}:{}});console.log(chalk.dim(`\n Snapshot ID: ${e.id}`)),console.log(chalk.dim(` Strategy: ${e.strategy} (${e.isCoW?"Copy-on-Write ✓":"full copy"})`)),console.log(chalk.dim(` Created in: ${e.elapsedMs}ms`))}catch(e){console.log(chalk.red(`[Error] ${e.message}`))}break}case"list":case"ls":{const e=n.list({name:s.name||s.filter});if(0===e.length)return console.log(chalk.yellow("\nNo snapshots found.")),void console.log(chalk.dim(" Try: thinknsh snapshot create mysnap"));const o=new Table({head:[chalk.cyan("ID"),chalk.cyan("Name"),chalk.cyan("Strategy"),chalk.cyan("CoW"),chalk.cyan("Size"),chalk.cyan("Created")],style:{head:[],border:[]}});for(const s of e)o.push([s.id.slice(-12),s.name,s.strategy,s.isCoW?chalk.green("✓"):chalk.dim("✗"),`${s.sizeMB}MB`,new Date(s.createdAt).toLocaleDateString()]);console.log(chalk.bold(`\nSnapshots (${e.length}):`)),console.log(o.toString());break}case"restore":{const o=e[1];if(!o)return void console.log(chalk.red("Usage: thinknsh snapshot restore <id> [target-path]"));const a=n.list().find(e=>e.id===o||e.id.endsWith(o)||e.name===o);if(!a)return void console.log(chalk.red(`Snapshot "${o}" not found.`));try{const o=await n.restore(a.id,e[2],{backup:s.backup});console.log(chalk.green(`[OK] Restored to: ${o.target}`))}catch(e){console.log(chalk.red(`[Error] ${e.message}`))}break}case"diff":{const s=e[1],o=e[2];if(!s||!o)return void console.log(chalk.red("Usage: thinknsh snapshot diff <id1> <id2>"));try{const e=n.diff(s,o);e.error||e.note?console.log(chalk.yellow(`[Diff] ${e.error||e.note}`)):(console.log(chalk.bold(`\nDiff: ${e.snapshot1} → ${e.snapshot2}`)),console.log(chalk.dim(` Changed files: ${e.changedFiles}`)),e.details?.length&&(console.log(chalk.dim("\n Changes:")),e.details.forEach(e=>console.log(chalk.dim(` ${e}`)))))}catch(e){console.log(chalk.red(`[Error] ${e.message}`))}break}case"delete":case"rm":case"remove":{const s=e[1];if(!s)return void console.log(chalk.red("Usage: thinknsh snapshot delete <id>"));const o=n.list().find(e=>e.id===s||e.id.endsWith(s)||e.name===s),a=o?o.id:s;try{n.delete(a),console.log(chalk.green(`[OK] Snapshot deleted: ${a}`))}catch(e){console.log(chalk.red(`[Error] ${e.message}`))}break}case"prune":{const o=parseInt(s.keep||e[1]||"3",10),a=n.prune(o);console.log(chalk.green(`[OK] Pruned ${a.pruned} old snapshots (keeping last ${o} per name)`));break}case"inspect":{const s=e[1],o=n.inspect(s);if(!o)return void console.log(chalk.red(`Snapshot "${s}" not found.`));console.log(JSON.stringify(o,null,2));break}case"info":case"status":case"system":{const e=n.systemInfo();console.log(chalk.bold("\nSnapshot System Info:")),console.log(chalk.dim(` Platform: ${e.platform}`)),console.log(chalk.dim(` Filesystem: ${e.fsType}`)),console.log(chalk.dim(` Strategy: ${e.strategy}`)),console.log(chalk.dim(` Copy-on-Write: ${e.isCoW?chalk.green("YES (instant, zero-disk)"):chalk.yellow("No (full copy)")}`)),console.log(chalk.dim(` Snapshot store: ${e.snapshotBase}`)),console.log(chalk.dim(` Total snapshots: ${e.totalSnapshots}`));break}default:console.log(chalk.yellow(`Unknown subcommand: ${a}`)),console.log(chalk.dim("Subcommands: create, list, restore, diff, delete, prune, info"))}}};
|
|
@@ -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
|
-
const os=require("os"),fs=require("fs-extra"),path=require("path"),{spawnSync}=require("child_process"),chalk=require("chalk"),Table=require("cli-table3");module.exports={name:"doctor",aliases:["health","check-system"],description:"Run comprehensive system diagnostic check (cgroups, CoW, runtimes, memory)",usage:"thinknsh doctor",examples:["thinknsh doctor"],async execute(e=[],
|
|
7
|
+
const os=require("os"),fs=require("fs-extra"),path=require("path"),{spawnSync}=require("child_process"),chalk=require("chalk"),Table=require("cli-table3");module.exports={name:"doctor",aliases:["health","check-system"],description:"Run comprehensive system diagnostic check (cgroups, CoW, runtimes, memory)",usage:"thinknsh doctor",examples:["thinknsh doctor"],async execute(e=[],t){console.log(chalk.bold.cyan("\n🔍 Running ThinkNCollab System Diagnostics...\n"));const s=[],o=process.platform,a=process.arch;s.push({category:"System Platform",item:"OS & Architecture",status:chalk.green("✓ PASS"),details:`${o} / ${a} (${os.release()})`});const n=(new(require("../../core/snapshot-manager"))).systemInfo(),i=n.isCoW;s.push({category:"Filesystem Storage",item:`CoW Support (${n.fsType.toUpperCase()})`,status:i?chalk.green("✓ PASS"):chalk.yellow("⚠ FALLBACK"),details:i?`CoW Active via ${n.strategy} (instant, 0-disk)`:`Standard copy fallback active on ${n.fsType}`});const c=(os.totalmem()/1024/1024/1024).toFixed(1),r=(os.freemem()/1024/1024/1024).toFixed(1),l=parseFloat(r)<.5;s.push({category:"Host Resources",item:"Memory Headroom",status:l?chalk.red("✗ LOW RAM"):chalk.green("✓ PASS"),details:`${r} GB free of ${c} GB total`});const{dockPaths:h}=require("../../dock/paths"),{ping:d}=require("../../dock/client"),u=await d(h().socket),m=fs.existsSync(path.join(os.homedir(),".tnc","dock","runtime.json"));s.push({category:"Container Engine",item:"dock runtime",status:m?chalk.green("✓ INSTALLED"):chalk.yellow("○ MISSING"),details:m?"Linux runtime for this machine is installed":'Run "thinknsh dock setup"'}),s.push({category:"Container Engine",item:"dock daemon",status:u?chalk.green("✓ RUNNING"):chalk.dim("○ STOPPED"),details:u?h().socket:"Starts automatically on the first container command"});const k=(new(require("../../core/policy-engine"))).getPolicy();s.push({category:"Security Policy",item:"Enterprise Policy Engine",status:k.enabled?chalk.green("✓ ENFORCING"):chalk.dim("○ DISABLED (Permissive)"),details:k.enabled?`Allowed Registries: ${k.allowedRegistries.join(", ")}`:"Permissive mode (all public registries allowed)"});const p=new Table({head:[chalk.cyan("Category"),chalk.cyan("Diagnostic Check"),chalk.cyan("Status"),chalk.cyan("Details")],style:{head:[],border:[]}});for(const e of s)p.push([e.category,e.item,e.status,e.details]);console.log(p.toString()),console.log(chalk.bold.green("\n[OK] ThinkNCollab is healthy and operational.")),console.log(chalk.dim(" Containers, network and builds end to end: thinknsh dock selftest (problems: thinknsh dock diagnose)\n"))},_cmdExists(e){try{return 0===spawnSync("which",[e],{stdio:"pipe"}).status}catch{return!1}}};
|
|
@@ -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
|
-
const chalk=require("chalk"),path=require("path"),fs=require("fs-extra"),{spawn}=require("child_process"),
|
|
7
|
+
const chalk=require("chalk"),path=require("path"),fs=require("fs-extra"),{spawn}=require("child_process"),{BUILD_FILE}=require("../../dock/api/wire");module.exports={name:"pipeline",description:"Build an image from a Containerfile (dock) or run a local CI/CD workflow",aliases:["run-ci"],requiresAuth:!1,requiresRoom:!1,async execute(e,s){const t=e.includes("-t")||e.includes("--tag"),o=e[0]&&(e[0].endsWith(".yml")||e[0].endsWith(".yaml")),n=["Containerfile",BUILD_FILE].some(e=>fs.existsSync(path.join(process.cwd(),e)));if((t||n||e.includes("."))&&!o){let s="latest";const t=-1!==e.indexOf("-t")?e.indexOf("-t"):e.indexOf("--tag");if(-1!==t&&e[t+1])s=e[t+1];else{const t=e.filter(e=>!e.startsWith("-")&&"."!==e)[0];t&&(s=t)}console.log(chalk.cyan(`\nBuilding image ${chalk.bold(s)} with dock...`));const{runCommand:o}=require("../../dock/cli/commands"),n=await o("build",["-t",s,"."]);return console.log(0===n?chalk.green(`\n[OK] Built image "${s}" (thinknsh images)\n`):chalk.red(`\n[FAIL] Build failed (exit ${n})\n`)),void(process.exitCode=n)}const r=e[0]||"tnc-ci.yml",l=process.cwd(),i=path.resolve(l,".tnc/workflows"),a=path.resolve(i,r);if(!a.startsWith(i+path.sep)&&a!==i)return void console.log(chalk.red("[FAIL] Security Error: Invalid workflow path outside .tnc/workflows"));if(!a.endsWith(".yml")&&!a.endsWith(".yaml"))return void console.log(chalk.red("[FAIL] Invalid workflow file format. Must be a .yml or .yaml file."));if(!fs.existsSync(a))return void console.log(chalk.red(`[FAIL] Workflow file not found: .tnc/workflows/${r}`));const c=path.basename(a);console.log(chalk.cyan(`\n Loading local CI/CD workflow: ${c}`));const h=fs.readFileSync(a,"utf8").split("\n");let u=c,d=[],f="";for(let e=0;e<h.length;e++){const s=h[e].trim();if(s.startsWith("name:"))u=s.substring(5).trim().replace(/['"]/g,"");else if(s.startsWith("-")){const t=s.substring(1).trim();if(t.startsWith("name:"))f=t.substring(5).trim().replace(/['"]/g,"");else if(t.startsWith("run:")){let s=t.substring(4).trim();if((s.startsWith('"')&&s.endsWith('"')||s.startsWith("'")&&s.endsWith("'"))&&(s=s.substring(1,s.length-1)),"|"===s){let t=[],o=e+1;for(;o<h.length;){const e=h[o],s=e.match(/^(\s+)(.*)/);if(!(s&&s[1].length>4))break;t.push(e.trim()),o++}s=t.join("\n"),e=o-1}d.push({name:f||`run: ${s.substring(0,30)}`,run:s}),f=""}}else if(s.startsWith("name:"))f=s.substring(5).trim().replace(/['"]/g,"");else if(s.startsWith("run:")){let t=s.substring(4).trim();if((t.startsWith('"')&&t.endsWith('"')||t.startsWith("'")&&t.endsWith("'"))&&(t=t.substring(1,t.length-1)),"|"===t){let s=[],o=e+1;for(;o<h.length;){const e=h[o],t=e.match(/^(\s+)(.*)/);if(!(t&&t[1].length>4))break;s.push(e.trim()),o++}t=s.join("\n"),e=o-1}d.push({name:f||`run: ${t.substring(0,30)}`,run:t}),f=""}}if(0===d.length)return void console.log(chalk.red("[FAIL] No build steps parsed from workflow"));const g=[];g.push(` [TNC Actions] Starting Local Workflow: "${u}" (${c})`),console.log(chalk.green(` [TNC Actions] Starting Local Workflow: "${u}" (${c})`));let p=!0;for(let e=0;e<d.length;e++){const s=d[e],t=`\n [Step ${e+1}/${d.length}] "${s.name}"\n$ ${s.run}`;g.push(t),console.log(chalk.yellow(t));const o=await new Promise(e=>{const t="win32"===process.platform?"cmd.exe":"/bin/sh",o="win32"===process.platform?["/s","/c",s.run]:["-c",s.run],n=spawn(t,o,{cwd:l,env:{...process.env,PATH:process.env.PATH,PUPPETEER_SKIP_DOWNLOAD:"true"}});n.stdout.on("data",e=>{const s=e.toString().trim();s&&s.split("\n").forEach(e=>{g.push(`[stdout] ${e}`),console.log(chalk.dim(` ${e}`))})}),n.stderr.on("data",e=>{const s=e.toString().trim();s&&s.split("\n").forEach(e=>{g.push(`[stderr] ${e}`),console.log(chalk.red(` ${e}`))})}),n.on("close",s=>{e(s)})});if(0!==o){p=!1;const e=`[FAIL] [Step Failed] Step "${s.name}" exited with status code: ${o}`;g.push(e),console.log(chalk.red(e));break}{const e=`[OK] [Step Passed] Step "${s.name}" completed successfully`;g.push(e),console.log(chalk.green(e))}}if(p){const e="\n Workflow ran successfully! status code: 0";g.push(e),console.log(chalk.green(e))}else{const e="\n[FAIL] Workflow failed!";g.push(e),console.log(chalk.red(e))}const m=s.ws.getCurrentRoom();if(m){process.stdout.write(chalk.dim(" Syncing stats to ThinkNCollab server...\r"));try{await s.api._request("POST",`/tnccicd/api/save-run/${m}`,{workflowFile:c,status:p?"passed":"failed",logs:g}),console.log(chalk.green(" [OK] Pipeline stats successfully synced to workspace!"))}catch(e){console.log(chalk.red(` [WARN] Failed to sync stats to server: ${e.message}`))}}}};
|
|
@@ -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
|
-
const chalk=require("chalk");async function cmdStart(e){if(e._shareActive)return void console.log(chalk.yellow('[Notice] Already sharing. Run "share stop" first.'));const s=e.ws.getCurrentRoom();if(s)return new Promise(r=>{const o=setTimeout(()=>{e.ws.socket.off("terminal:share:started",t),console.log(chalk.red("[Error] Share start timeout")),r()},6e3);function t(s){clearTimeout(o),e._shareActive=!0,e._shareSessionId=s.sessionId,console.log(chalk.green("\n[OK] Terminal sharing started")),console.log(chalk.dim(` Session : ${s.sessionId}`));const t=e.api&&e.api.apiUrl||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com";console.log(
|
|
7
|
+
const chalk=require("chalk"),crypto=require("crypto");function sealChunk(e,s,r){const o=crypto.randomBytes(12),t=crypto.createCipheriv("aes-256-gcm",e,o);t.setAAD(Buffer.from(s,"utf8"));const n=Buffer.concat([t.update(r,"utf8"),t.final()]);return{iv:o.toString("base64"),ciphertext:n.toString("base64"),authTag:t.getAuthTag().toString("base64")}}function openChunk(e,s,{iv:r,ciphertext:o,authTag:t}){const n=crypto.createDecipheriv("aes-256-gcm",e,Buffer.from(String(r),"base64"));return n.setAAD(Buffer.from(s,"utf8")),n.setAuthTag(Buffer.from(String(t),"base64")),Buffer.concat([n.update(Buffer.from(String(o),"base64")),n.final()]).toString("utf8")}async function cmdStart(e){if(e._shareActive)return void console.log(chalk.yellow('[Notice] Already sharing. Run "share stop" first.'));const s=e.ws.getCurrentRoom();if(s)return new Promise(r=>{const o=setTimeout(()=>{e.ws.socket.off("terminal:share:started",t),console.log(chalk.red("[Error] Share start timeout")),r()},6e3);function t(s){clearTimeout(o),e._shareActive=!0,e._shareSessionId=s.sessionId,e._shareKey=crypto.randomBytes(32),e._shareSeq=0,console.log(chalk.green("\n[OK] Terminal sharing started (end-to-end encrypted)")),console.log(chalk.dim(` Session : ${s.sessionId}`));const t=e.api&&e.api.apiUrl||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com";console.log(` Link : ${t}/terminal/${s.sessionId}#k=${e._shareKey.toString("base64url")}`),console.log(chalk.dim("\n The key is the part after #: it never reaches the server, so only people with")),console.log(chalk.dim(" this exact link can read your terminal. Send it to them yourself.")),console.log(chalk.dim(' Use "share grant <userId>" to give write access.\n')),_hookOutput(e,s.sessionId),r()}e.ws.socket.once("terminal:share:started",t),e.ws.socket.emit("terminal:share:start",{roomId:s})});console.log(chalk.red("[Error] Join a room first: join <room-id>"))}async function cmdStop(e){e._shareActive?(e.ws.socket.emit("terminal:share:stop"),_unhookOutput(e),e._shareActive=!1,e._shareSessionId=null,console.log(chalk.green("[OK] Terminal sharing stopped."))):console.log(chalk.yellow("[Notice] No active share session."))}async function cmdStatus(e){if(!e._shareActive||!e._shareSessionId)return void console.log(chalk.yellow("// No active share session"));const s=e._shareViewers||{},r=Object.values(s);console.log(chalk.cyan(`\n Terminal Share — ${e._shareSessionId}`)),console.log(chalk.dim(" "+"─".repeat(44))),r.length?r.forEach(e=>{const s=e.canWrite?chalk.blue("[write]"):chalk.dim("[read]"),r=e.requested?chalk.yellow(" ← requesting write"):"";console.log(` ${chalk.green("●")} ${chalk.white(e.name)} ${s}${r}`),console.log(chalk.dim(` userId: ${e.userId}`))}):console.log(chalk.dim(" No viewers yet.")),console.log()}async function cmdGrant(e,s){e?s._shareActive?(s.ws.socket.emit("terminal:grant",{sessionId:s._shareSessionId,userId:e}),s._shareViewers?.[e]&&(s._shareViewers[e].canWrite=!0,s._shareViewers[e].requested=!1),console.log(chalk.green(`[OK] Write access granted to ${e}`))):console.log(chalk.red("[Error] No active share session")):console.log(chalk.red("Usage: share grant <userId>"))}async function cmdRevoke(e,s){e?s._shareActive?(s.ws.socket.emit("terminal:revoke",{sessionId:s._shareSessionId,userId:e}),s._shareViewers?.[e]&&(s._shareViewers[e].canWrite=!1),console.log(chalk.green(`[OK] Write access revoked from ${e}`))):console.log(chalk.red("[Error] No active share session")):console.log(chalk.red("Usage: share revoke <userId>"))}async function cmdKick(e,s){e?s._shareActive?(s.ws.socket.emit("terminal:kick",{sessionId:s._shareSessionId,userId:e}),s._shareViewers&&delete s._shareViewers[e],console.log(chalk.green(`[OK] ${e} kicked from session`))):console.log(chalk.red("[Error] No active share session")):console.log(chalk.red("Usage: share kick <userId>"))}async function cmdLock(e){e._shareActive?(e._shareLocked=!0,e._shareViewers&&Object.values(e._shareViewers).forEach(e=>{e.canWrite=!1}),e.ws.socket.emit("terminal:lock",{sessionId:e._shareSessionId}),console.log(chalk.red.bold("\n[KILL-SWITCH ACTIVATED] All write access revoked and session locked immediately!")),console.log(chalk.dim(' No remote viewer can send input to your shell until you run "share unlock".\n'))):console.log(chalk.red("[Error] No active share session"))}async function cmdUnlock(e){e._shareActive?(e._shareLocked=!1,e.ws.socket.emit("terminal:unlock",{sessionId:e._shareSessionId}),console.log(chalk.green('\n[OK] Terminal share unlocked. Use "share grant <userId>" to grant write access.\n'))):console.log(chalk.red("[Error] No active share session"))}module.exports={_sealChunk:sealChunk,_openChunk:openChunk,name:"share",description:"Share your terminal with room members",aliases:["terminal-share","ts"],requiresAuth:!0,requiresRoom:!0,async execute(e,s){switch((e[0]||"").toLowerCase()){case"start":return cmdStart(s);case"stop":return cmdStop(s);case"status":return cmdStatus(s);case"grant":return cmdGrant(e[1],s);case"revoke":return cmdRevoke(e[1],s);case"kick":return cmdKick(e[1],s);case"lock":case"kill-switch":return cmdLock(s);case"unlock":return cmdUnlock(s);default:printHelp()}}};let _originalWrite=null;function _hookOutput(e,s){e._shareViewers={},e._shareWriterSeqs=new Map,e._shareLocked=!1,_originalWrite=process.stdout.write.bind(process.stdout),e.ws.socket.off("terminal:viewerJoined"),e.ws.socket.off("terminal:viewerLeft"),e.ws.socket.off("terminal:writeRequest"),e.ws.socket.off("terminal:input"),e.ws.socket.off("terminal:input:encrypted"),process.stdout.write=function(r,o,t){const n=r.toString();if(e.ws.isConnected()&&e._shareActive&&e._shareKey){const r=++e._shareSeq;e.ws.socket.emit("terminal:output:encrypted",{sessionId:s,seq:r,v:2,...sealChunk(e._shareKey,`term-output:${s}:${r}`,n)})}return _originalWrite(r,o,t)},e.ws.socket.on("terminal:viewerJoined",s=>{e._shareViewers||(e._shareViewers={}),e._shareViewers[s.userId]={userId:s.userId,name:s.name,canWrite:!1,requested:!1},console.log(chalk.dim(`\n [Viewer] ${s.name} is now viewing your terminal`))}),e.ws.socket.on("terminal:viewerLeft",s=>{e._shareViewers&&delete e._shareViewers[s.userId],console.log(chalk.dim(`\n [Viewer] ${s.name} stopped viewing`))}),e.ws.socket.on("terminal:writeRequest",s=>{e._shareViewers?.[s.userId]&&(e._shareViewers[s.userId].requested=!0),console.log(chalk.yellow(`\n [Request] ${s.name} is requesting write access`)),console.log(chalk.dim(` share grant ${s.userId} or share kick ${s.userId}\n`))}),e.ws.socket.on("terminal:input",e=>{console.log(chalk.red(`\n [Security] Ignored unencrypted input from ${e.fromName||e.from||"a viewer"} (their page has no key: send them the full link)`))}),e.ws.socket.on("terminal:input:encrypted",r=>{if(e._shareLocked)return void console.log(chalk.red("\n [Security] Dropped encrypted input: Session is locked (Kill-Switch Active)"));const o=r.from||r.userId,t=e._shareViewers?.[o];if(!t||!t.canWrite)return void console.log(chalk.red(`\n [Security] Rejected unauthorized encrypted input from ${r.fromName||o}`));const n="number"==typeof r.seq?r.seq:0;if(e._shareWriterSeqs.has(n)||Math.abs(n/1e3-Date.now())>12e4)console.log(chalk.red(`\n [Security Alert] Replayed or stale input rejected (seq ${n}) from ${r.fromName||o}`));else try{if(r.sessionId!==s||!e._shareKey)throw new Error("not for this share");const o=openChunk(e._shareKey,`term-input:${s}:${n}`,r);e._shareWriterSeqs.set(n,Date.now());for(const[s,r]of e._shareWriterSeqs)Date.now()-r>18e4&&e._shareWriterSeqs.delete(s);console.log(chalk.blue(`\n [${r.fromName||"Writer"}] (E2EE seq:${n}) → ${o}`)),e.execute(o.trim())}catch(e){console.log(chalk.red(`\n [Security Alert] Tampered or forged terminal input rejected: ${e.message}`))}})}function _unhookOutput(e){_originalWrite&&(process.stdout.write=_originalWrite,_originalWrite=null),e.ws.socket.off("terminal:viewerJoined"),e.ws.socket.off("terminal:viewerLeft"),e.ws.socket.off("terminal:writeRequest"),e.ws.socket.off("terminal:input"),e.ws.socket.off("terminal:input:encrypted"),e._shareKey=null}function printHelp(){console.log(chalk.cyan("\n Terminal Share Commands")),console.log(chalk.dim(" "+"─".repeat(40))),[["share start","Start sharing your terminal"],["share stop","Stop sharing"],["share status","Show viewers and write requests"],["share grant <userId>","Give write access to a viewer"],["share revoke <userId>","Revoke write access"],["share kick <userId>","Remove a viewer"],["share lock","Emergency kill-switch: lock session and drop all inputs"],["share unlock","Unlock terminal share"]].forEach(([e,s])=>{console.log(` ${chalk.green(e.padEnd(26))} ${chalk.dim(s)}`)}),console.log()}
|
|
@@ -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
|
-
const https=require("https"),http=require("http"),
|
|
7
|
+
const https=require("https"),http=require("http"),os=require("os"),store=require("./auth/session-store"),REQUEST_TIMEOUT_MS=15e3,LOOPBACK=new Set(["localhost","127.0.0.1","[::1]","::1"]);function assertSafeServer(s){const e=new URL(s);if(!("https:"===e.protocol||"http:"===e.protocol&&LOOPBACK.has(e.hostname)))throw new Error(`refusing to send credentials to ${e.origin}: use https://`)}function normaliseUser(s={}){const e=(s._id||s.userId||s.id||"").toString();return{_id:e,userId:e,email:s.email||"",name:s.name||"",userType:s.userType||s.model||"User"}}class ThinkNCollabAPI{constructor(s={}){this.apiUrl=(s.apiUrl||process.env.THINKNCOLLAB_API_URL||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com").replace(/\/$/,""),this.wsUrl=(s.wsUrl||process.env.THINKNCOLLAB_WS_URL||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com").replace(/\/$/,""),this.session=null,this.sessionPath=store.file(),this._loadSession()}_loadSession(){const s=(process.env.THINKNCOLLAB_TOKEN||"").trim();this.session=s?{token:s,fromEnv:!0,server:this.apiUrl,user:{}}:store.load(this.apiUrl)}_saveSession(s){const e={server:this.apiUrl,lastVerifiedAt:(new Date).toISOString(),...s};e.tokenIn=store.save(e),this.session=e}clearSession(){this.session?.fromEnv||store.clear(this.apiUrl),this.session=null}_clearSession(){this.clearSession()}isAuthenticated(){return!!this.session?.token}getToken(){return this.session?.token||null}tokenLocation(){return this.session?.fromEnv?"THINKNCOLLAB_TOKEN":store.where()}getUser(){const s=this.session?.user;return s?normaliseUser(s):null}_request(s,e,t=null,r={}){return new Promise((n,o)=>{const i=new URL(this.apiUrl+e),a="https:"===i.protocol,h=a?https:http,l=t?JSON.stringify(t):null,c=this.session?.token;try{assertSafeServer(i.href)}catch(s){return void o(s)}const u={hostname:i.hostname,port:i.port||(a?443:80),path:i.pathname+i.search,method:s,timeout:15e3,headers:{"Content-Type":"application/json",Accept:"application/json",...c?{"x-shell-token":c}:{},...r,...l?{"Content-Length":Buffer.byteLength(l)}:{}}},d=h.request(u,s=>{let e="";s.on("data",s=>e+=s),s.on("end",()=>{let t=null;try{t=JSON.parse(e)}catch{}if(s.statusCode>=400){const e=new Error(t?.error||t?.message||`HTTP ${s.statusCode}`);e.status=s.statusCode,401===s.statusCode&&c&&this.session?.token===c&&this.clearSession(),o(e)}else null===t?o(new Error(`Invalid response: ${e.slice(0,100)}`)):n(t)})});d.on("timeout",()=>d.destroy(new Error(`${this.apiUrl} did not answer within 15s`))),d.on("error",s=>{"ECONNREFUSED"===s.code?o(new Error(`Cannot connect to server at ${this.apiUrl} — is it running?`)):o(s)}),l&&d.write(l),d.end()})}_adopt(s,e){if(!s?.shellToken)throw new Error(s?.error||"the platform returned no shell token");return this._saveSession({token:s.shellToken,shellSessionId:s.shellSessionId||null,user:normaliseUser(s.user||s),expiresAt:s.expiresAt||null,method:e,timestamp:(new Date).toISOString()}),{token:s.shellToken,user:this.getUser()}}async login(s,e){const t=await this._request("POST","/thinknsh/login",null,{"x-email":s,"x-password":e,"x-machine-id":os.hostname()});return this._adopt(t,"password")}async exchangeCode({code:s,verifier:e,redirectUri:t}){const r=await this._request("POST","/thinknsh/auth/token",{grant_type:"authorization_code",client_id:"thinknsh",code:s,code_verifier:e,redirect_uri:t,machineId:os.hostname()}).catch(s=>{throw 404===s.status&&(s.message='this server does not support browser sign-in yet; use "login --password"'),s});return this._adopt(r,"browser")}async loginWithToken(s){const e=this.session;this.session={token:s,server:this.apiUrl,user:{}};try{const e=await this._request("GET","/thinknsh/auth/me");return this._adopt({...e,user:e.user||e,shellToken:s},"token")}catch(s){if(this.session=e,404===s.status)throw new Error('this platform does not accept shell tokens yet; use "login"');throw s}}async verify(){let s=null;try{s=await this._request("GET","/thinknsh/auth/me")}catch(s){if(404!==s.status)throw s;await this._request("GET","/thinknsh/session/history")}if(!this.session)return;const e=s?normaliseUser(s.user||s):this.session.user;this.session={...this.session,user:e,lastVerifiedAt:(new Date).toISOString()},this.session.fromEnv||store.touch({user:e,lastVerifiedAt:this.session.lastVerifiedAt})}async sessions(){return this._request("GET","/thinknsh/session/history")}async endSession(s){return this._request("POST","/thinknsh/session/end",{sessionId:s})}async logEvent(s,e=null,t={}){const r=this.session?.shellSessionId;r&&await this._request("POST","/thinknsh/session/event",{sessionId:r,type:s,roomId:e,meta:t}).catch(()=>{})}startHeartbeat(s=6e4){this._heartbeatTimer||(this._heartbeatTimer=setInterval(()=>{const s=this.session?.shellSessionId;s&&this._request("POST","/thinknsh/session/event",{sessionId:s,type:"heartbeat",meta:{ts:(new Date).toISOString()}}).catch(()=>{})},s),this._heartbeatTimer.unref?.())}stopHeartbeat(){this._heartbeatTimer&&(clearInterval(this._heartbeatTimer),this._heartbeatTimer=null)}async logout(){const s=this.session?.shellSessionId;return s&&await this.endSession(s).catch(()=>{}),this.stopHeartbeat(),this.clearSession(),{success:!0}}async whoami(){if(!this.isAuthenticated())throw new Error("Not logged in");return this.getUser()}async joinRoom(s,e){if(!s)throw new Error("roomId is required");const t=await this._request("POST",`/thinknsh/rooms/${s}/join`,e?{password:e}:null);return{room:t.room||t,participants:t.participants||t.members||[],recentMessages:t.recentMessages||t.history||[]}}async getRooms(){return this._request("GET","/thinknsh/rooms")}async mytasks(s,e){if(!s)throw new Error("roomId is required");const t=this.getUser();let r=`/thinknsh/tasks?roomId=${encodeURIComponent(s)}`;return t?._id&&(r+=`&userId=${encodeURIComponent(t._id)}`),e&&(r+=`&n=${e}`),this._request("GET",r)}async request(s,e,t={}){let r=e;if(t.params&&Object.keys(t.params).length){const s=new URLSearchParams(t.params).toString();r+=(r.includes("?")?"&":"?")+s}return this._request(s.toUpperCase(),r,t.body||null)}}module.exports=ThinkNCollabAPI;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license ThinkNCollab Enterprise & Open Core Ecosystem
|
|
3
|
+
* Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
|
|
4
|
+
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
|
+
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
|
+
*/
|
|
7
|
+
"use strict";const http=require("http"),os=require("os"),crypto=require("crypto"),{spawn}=require("child_process"),TIMEOUT_MS=3e5,b64url=e=>e.toString("base64url");function pkce(){const e=b64url(crypto.randomBytes(32));return{verifier:e,challenge:b64url(crypto.createHash("sha256").update(e).digest())}}function same(e,t){const r=Buffer.from(String(e||"")),n=Buffer.from(String(t||""));return r.length===n.length&&r.length>0&&crypto.timingSafeEqual(r,n)}function openUrl(e){if(!/^https?:\/\//i.test(e))return Promise.resolve(!1);const[t,r]="darwin"===process.platform?["open",[e]]:"win32"===process.platform?["rundll32",["url.dll,FileProtocolHandler",e]]:["xdg-open",[e]];return new Promise(e=>{try{const n=spawn(t,r,{stdio:"ignore",detached:!0,windowsHide:!0});n.on("error",()=>e(!1)),n.on("spawn",()=>{n.unref(),e(!0)})}catch(t){e(!1)}})}const PAGE=(e,t)=>`<!doctype html><meta charset="utf-8"><title>${e}</title>\n<body style="font:16px system-ui;margin:15vh auto;max-width:28em;text-align:center;color:#222">\n<h2>${e}</h2><p>${t}</p></body>`;function authorize({server:e,provider:t,open:r=openUrl,onUrl:n=()=>{},timeoutMs:o=3e5}={}){const i=b64url(crypto.randomBytes(32)),{verifier:a,challenge:s}=pkce();return new Promise((c,l)=>{let d=!1,h=0;const u=http.createServer((e,t)=>{const r=String(e.headers.host||"");if(r!==`127.0.0.1:${h}`&&r!==`localhost:${h}`)return t.writeHead(421),void t.end();const n=new URL(e.url,`http://127.0.0.1:${h}`);if("GET"!==e.method||"/callback"!==n.pathname||d)return t.writeHead(404),void t.end();if(!same(n.searchParams.get("state"),i))return t.writeHead(400,{"Content-Type":"text/html; charset=utf-8"}),void t.end(PAGE("Sign-in not accepted","This link did not come from the shell that is waiting. Run <b>login</b> again."));d=!0;const o=n.searchParams.get("error"),s=n.searchParams.get("code");t.writeHead(o||!s?400:200,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"}),t.end(o||!s?PAGE("Sign-in cancelled","Nothing was shared with the shell. You can close this tab."):PAGE("Signed in to ThinkNCollab Shell","You can close this tab and go back to your terminal.")),m(o||!s?new Error("access_denied"===o?"sign-in was declined in the browser":`sign-in failed: ${o||"no code returned"}`):null,{code:s,verifier:a,redirectUri:`http://127.0.0.1:${h}/callback`})}),p=setTimeout(()=>m(new Error("sign-in timed out after 5 minutes")),o);function m(e,t){clearTimeout(p),u.close(),u.closeIdleConnections?.(),e?l(e):c(t)}u.on("error",e=>m(e)),u.listen(0,"127.0.0.1",async()=>{h=u.address().port;const o=new URLSearchParams({response_type:"code",client_id:"thinknsh",redirect_uri:`http://127.0.0.1:${h}/callback`,state:i,code_challenge:s,code_challenge_method:"S256",machine:os.hostname(),...t?{provider:t}:{}}),a=`${e.replace(/\/$/,"")}/cli/authorize?${o}`;n(a,await r(a))})})}module.exports={authorize,openUrl,pkce,TIMEOUT_MS:3e5};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license ThinkNCollab Enterprise & Open Core Ecosystem
|
|
3
|
+
* Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
|
|
4
|
+
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
|
+
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
|
+
*/
|
|
7
|
+
"use strict";const chalk=require("chalk"),OPEN=new Set(["login","signin","logout","signout","register","signup","reg","whoami","me","user","auth","help","exit","quit","clear","cls","version","doctor"]),DOCK_LIFECYCLE=new Set(["setup","start","stop","restart","autostart","app","tray","status","vm-restart","uninstall","version"]),VERIFY_EVERY_MS=432e5,OFFLINE_GRACE_MS=6048e5,disabled=()=>["off","0","false"].includes(String(process.env.TNC_REQUIRE_LOGIN||"").toLowerCase());async function check(e,r=Date.now()){if(!e.isAuthenticated())return{ok:!1,reason:"not signed in"};const t=e.session;if(t.expiresAt&&Date.parse(t.expiresAt)<=r)return e.clearSession(),{ok:!1,reason:"your sign-in has expired"};const n=Date.parse(t.lastVerifiedAt||t.timestamp||0)||0;if(!t.fromEnv&&r-n<432e5)return{ok:!0};try{return await e.verify(),{ok:!0}}catch(o){return 401===o.status?{ok:!1,reason:"the platform ended this sign-in"}:!t.fromEnv&&r-n<6048e5?{ok:!0,offline:!0}:{ok:!1,reason:`cannot confirm your sign-in with ${e.apiUrl} (${o.message})`}}}function registryForm(e,r){if("login"!==e&&"logout"!==e)return!1;try{return require("../../commands/dock/containers").REGISTRY_FORM[e](r)}catch(e){return!1}}async function allow(e,r,t=[]){if(disabled())return!0;if(OPEN.has(e)&&!registryForm(e,t))return!0;if("dock"===e&&DOCK_LIFECYCLE.has(t[0]))return!0;const n=await check(r);return!!n.ok||(console.log(chalk.red(`[FAIL] ThinkNCollab sign-in required: ${n.reason}.`)),console.log(chalk.dim(' Run "login" to sign in through thinkncollab.com.')),process.exitCode=1,!1)}module.exports={allow,check,OPEN,DOCK_LIFECYCLE,VERIFY_EVERY_MS:432e5,OFFLINE_GRACE_MS:6048e5};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license ThinkNCollab Enterprise & Open Core Ecosystem
|
|
3
|
+
* Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
|
|
4
|
+
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
|
+
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
|
+
*/
|
|
7
|
+
"use strict";const fs=require("fs"),os=require("os"),path=require("path"),crypto=require("crypto"),dir=()=>path.join(os.homedir(),".thinkncollab"),file=()=>path.join(dir(),"session.json");function hostOf(e){try{return new URL(e).host}catch(n){return String(e||"")}}const account=e=>`thinkncollab-shell:${hostOf(e)}`;function keychain(){if("off"===process.env.THINKNCOLLAB_KEYCHAIN)return null;try{return require("../../dock/image/keychain").backend()}catch(e){return null}}function readFile(){try{return fs.lstatSync(file()).isFile()?JSON.parse(fs.readFileSync(file(),"utf8")):null}catch(e){return null}}function writeFile(e){fs.mkdirSync(dir(),{recursive:!0,mode:448});try{fs.chmodSync(dir(),448)}catch(e){}const n=path.join(dir(),`.session-${process.pid}-${crypto.randomBytes(4).toString("hex")}.json`);fs.writeFileSync(n,JSON.stringify(e,null,2),{mode:384,flag:"wx"}),fs.renameSync(n,file())}function load(e){const n=readFile();if(!n)return null;if(n.server&&hostOf(n.server)!==hostOf(e))return null;let t=null;const r="keychain"===n.tokenIn?keychain():null;if("keychain"===n.tokenIn)try{t=r?r.get(account(e)):null}catch(e){t=null}else if(t=n.token||null,t&&keychain())try{save({...n,token:t,server:e})}catch(e){}if(!t)return null;const{token:i,tokenIn:c,...o}=n;return{...o,server:n.server||e,token:t}}function save(e){const{token:n,...t}=e;if(!n)throw new Error("no token to save");const r=keychain();if(r)try{return r.set(account(t.server),n),writeFile({...t,tokenIn:"keychain"}),"keychain"}catch(e){}return writeFile({...t,tokenIn:"file",token:n}),"file"}function touch(e){const n=readFile();n&&writeFile({...n,...e})}function clear(e){const n=readFile(),t=keychain();if(t)try{t.remove(account(n?.server||e))}catch(e){}try{fs.rmSync(file(),{force:!0})}catch(e){}}function where(){const e=readFile();return e?"keychain"===e.tokenIn?keychain()?.name||"system keychain":file():null}module.exports={load,save,touch,clear,where,file,hostOf};
|
|
@@ -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 crypto=require("crypto"),PREFIX="tnc-sealed:v1:",b64u=e=>Buffer.from(e).toString("base64url"),unb64u=e=>Buffer.from(String(e),"base64url");function generateKeyPair(){const{publicKey:e,privateKey:r}=crypto.generateKeyPairSync("x25519");return{publicKey:e.export({format:"jwk"}).x,privateKey:r.export({format:"jwk"}).d}}const isPublicKey=e=>"string"==typeof e&&/^[A-Za-z0-9_-]{43}$/.test(e),pubObj=e=>crypto.createPublicKey({key:{kty:"OKP",crv:"X25519",x:e},format:"jwk"}),privObj=(e,r)=>crypto.createPrivateKey({key:{kty:"OKP",crv:"X25519",d:e,x:r},format:"jwk"});function publicFromPrivate(e){const r=Buffer.concat([Buffer.from("302e020100300506032b656e04220420","hex"),unb64u(e)]);return crypto.createPublicKey(crypto.createPrivateKey({key:r,format:"der",type:"pkcs8"})).export({format:"jwk"}).x}function fingerprint(e){return crypto.createHash("sha256").update(unb64u(e)).digest("hex").slice(0,16).match(/.{4}/g).join("-")}function kek(e,r,t,n){return Buffer.from(crypto.hkdfSync("sha256",e,Buffer.concat([unb64u(r),unb64u(t)]),Buffer.from(`tnc-seal-v1|${n}`),32))}function gcm(e,r,t,n){const i=crypto.createCipheriv("aes-256-gcm",e,r);return i.setAAD(Buffer.from(t)),Buffer.concat([i.update(n),i.final(),i.getAuthTag()])}function ungcm(e,r,t,n){const i=crypto.createDecipheriv("aes-256-gcm",e,r);return i.setAAD(Buffer.from(t)),i.setAuthTag(n.subarray(n.length-16)),Buffer.concat([i.update(n.subarray(0,n.length-16)),i.final()])}function seal(e,r,t){if(!r.length)throw new Error("nobody to seal for");const n=crypto.generateKeyPairSync("x25519"),i=n.publicKey.export({format:"jwk"}).x,o=crypto.randomBytes(32),c=crypto.randomBytes(12),u={};for(const{id:e,publicKey:c}of r){if(!isPublicKey(c))throw new Error(`recipient ${e} has no valid key`);const r=crypto.diffieHellman({privateKey:n.privateKey,publicKey:pubObj(c)}),a=crypto.randomBytes(12);u[e]=b64u(Buffer.concat([a,gcm(kek(r,i,c,e),a,`${t}|${e}`,o)]))}const a={e:i,n:b64u(c),c:b64u(gcm(o,c,t,Buffer.from(String(e),"utf8"))),r:u};return PREFIX+b64u(JSON.stringify(a))}const isSealed=e=>"string"==typeof e&&e.startsWith(PREFIX);function parse(e){if(!isSealed(e))throw new Error("not a sealed value");const r=JSON.parse(unb64u(e.slice(14)).toString("utf8"));if(!isPublicKey(r.e)||"string"!=typeof r.n||"string"!=typeof r.c||!r.r||"object"!=typeof r.r)throw new Error("malformed sealed value");return r}const recipientsOf=e=>Object.keys(parse(e).r);function open(e,r,t){const n=parse(e),i=n.r[r.id];if(!i)throw new Error("not sealed for this server");const o=r.publicKey||publicFromPrivate(r.privateKey),c=crypto.diffieHellman({privateKey:privObj(r.privateKey,o),publicKey:pubObj(n.e)}),u=unb64u(i),a=ungcm(kek(c,n.e,o,r.id),u.subarray(0,12),`${t}|${r.id}`,u.subarray(12));return ungcm(a,unb64u(n.n),t,unb64u(n.c)).toString("utf8")}module.exports={generateKeyPair,seal,open,isSealed,recipientsOf,fingerprint,isPublicKey,publicFromPrivate,PREFIX};
|
|
@@ -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
|
-
const path=require("path"),fs=require("fs-extra"),chalk=require("chalk"),MultiLanguageScanner=require("./multi-lang-scanner");let getDepStore;try{getDepStore=require("./dep-store").getDepStore}catch(e){getDepStore=null}class DependencyTracker{constructor(e=process.cwd()){this.cwd=e,this.ymlPath=path.join(this.cwd,"thinkncollab.yml"),this.scanner=new MultiLanguageScanner(this.cwd)}async interceptCommand(e,n=null){if(!e||"string"!=typeof e)return;const t=e.trim(),
|
|
7
|
+
const path=require("path"),fs=require("fs-extra"),chalk=require("chalk"),MultiLanguageScanner=require("./multi-lang-scanner");let getDepStore;try{getDepStore=require("./dep-store").getDepStore}catch(e){getDepStore=null}class DependencyTracker{constructor(e=process.cwd()){this.cwd=e,this.ymlPath=path.join(this.cwd,"thinkncollab.yml"),this.scanner=new MultiLanguageScanner(this.cwd)}async interceptCommand(e,n=null){if(!e||"string"!=typeof e)return;const t=e.trim(),s=/^(npm|pnpm|yarn|bun)\s+(i|install|add)\s+/i.test(t),i=/^(pip|pip3|poetry|pipenv|conda)\s+(install|add)\s+/i.test(t),r=/^(g\+\+|clang\+\+|cmake|make|conan|vcpkg|ninja)\s+/i.test(t),o=/^cargo\s+(add|install|build)\s+/i.test(t),c=/^go\s+(get|install|mod)\s+/i.test(t),a=/^(brew|apt|apt-get|apk|pacman)\s+(install|add|-S)\s+/i.test(t);(s||i||r||o||c||a)&&(console.log(chalk.dim(`\n [TNC:AutoDep] Intercepting multi-language dependency command: "${t}"`)),await this.scanAndRecord(n))}async scanAndRecord(e=null){try{const n=this.loadConfig(),t=await this.scanner.scan();if(n.runtime.primary=t.primaryRuntime,t.runtimeVersion&&(n.runtime.version=t.runtimeVersion),n.runtime.runtimes=Array.from(t.runtimes),n.dependencies={npm:t.dependencies.npm||[],python:t.dependencies.python||[],cpp:t.dependencies.cpp||[],rust:t.dependencies.rust||[],go:t.dependencies.go||[],system:t.dependencies.system||[]},t.scripts&&(n.scripts={...n.scripts,...t.scripts}),n.addons=Array.from(new Set([...n.addons||[],...Array.from(t.addons||[])])),t.runtimes.has("cpp")&&!n.scripts["build:cpp"]&&(fs.existsSync(path.join(this.cwd,"CMakeLists.txt"))?n.scripts["build:cpp"]="cmake -B build && cmake --build build":fs.existsSync(path.join(this.cwd,"Makefile"))&&(n.scripts["build:cpp"]="make")),this.saveConfig(n),console.log(chalk.green(`[OK] [TNC:AutoDep] Auto-recorded multi-language dependencies to ${chalk.bold("thinkncollab.yml")}`)),getDepStore)try{getDepStore().recordFromConfig(n,{sandboxId:e?.activeSandbox||null}),console.log(chalk.green("[OK] [TNC:AutoDep] Synced dependencies to persistent SQLite DB (~/.tnc/deps.db)"))}catch(e){}if(e?.ws?.isConnected())try{e.ws.socket.emit("sandbox:dependency-sync",{roomId:e.ws.roomId||e.ws.getCurrentRoom?.()||null,project:n.project,runtimes:n.runtime.runtimes,dependencies:n.dependencies,scripts:n.scripts,timestamp:(new Date).toISOString()})}catch(e){}return n}catch(e){console.warn(chalk.yellow(`[WARN] [TNC:AutoDep] Error recording dependencies: ${e.message}`))}}loadConfig(){if(fs.existsSync(this.ymlPath))try{const e=fs.readFileSync(this.ymlPath,"utf8");return this.parseSimpleYaml(e)}catch(e){}return{version:"1.0",project:path.basename(this.cwd).toLowerCase().replace(/[^a-z0-9_-]/g,"-"),runtime:{primary:"node",runtimes:["node"]},dependencies:{npm:[],python:[],cpp:[],rust:[],go:[],system:[]},addons:[],scripts:{dev:"npm run dev",start:"npm start",test:"npm test"},environment:{PORT:3e3,NODE_ENV:"development"},execution:{isolation:"auto",memory_limit:"2g"}}}saveConfig(e){const n=this.stringifySimpleYaml(e);fs.writeFileSync(this.ymlPath,n,"utf8")}parseSimpleYaml(e){const n={version:"1.0",project:path.basename(this.cwd),runtime:{primary:"node",runtimes:["node"]},dependencies:{npm:[],python:[],cpp:[],rust:[],go:[],system:[]},addons:[],scripts:{},environment:{},execution:{isolation:"auto",memory_limit:"2g"}};let t=null,s=null;const i=e.split("\n");for(const e of i){const i=e.trim();if(i&&!i.startsWith("#"))if("dependencies:"!==i)if("addons:"!==i)if("scripts:"!==i)if("environment:"!==i)if("runtime:"!==i)if("execution:"!==i){if("dependencies"===t)i.endsWith(":")?(s=i.replace(":",""),n.dependencies[s]||(n.dependencies[s]=[])):i.startsWith("-")&&s&&n.dependencies[s].push(i.replace(/^-\s*/,"").replace(/["']/g,""));else if("addons"===t)i.startsWith("-")&&n.addons.push(i.replace(/^-\s*/,"").replace(/["']/g,""));else if("scripts"===t){const e=i.split(":");if(e.length>=2){const t=e[0].trim(),s=e.slice(1).join(":").trim().replace(/["']/g,"");n.scripts[t]=s}}else if("environment"===t){const e=i.split(":");if(e.length>=2){const t=e[0].trim(),s=e.slice(1).join(":").trim().replace(/["']/g,"");n.environment[t]=s}}else if("runtime"===t){const e=i.split(":");if(e.length>=2){const t=e[0].trim(),s=e.slice(1).join(":").trim().replace(/["']/g,"");"primary"!==t&&"type"!==t||(n.runtime.primary=s),"version"===t&&(n.runtime.version=s)}}}else t="execution",s=null;else t="runtime",s=null;else t="environment",s=null;else t="scripts",s=null;else t="addons",s=null;else t="dependencies",s=null}return n}stringifySimpleYaml(e){let n=`# ThinkNCollab Unified Room Execution Blueprint\n# Auto-generated & synced in real-time\nversion: "${e.version||"1.0"}"\nproject: "${e.project}"\n\n`;n+=`runtime:\n primary: "${e.runtime?.primary||"node"}"\n`,e.runtime?.version&&(n+=` version: "${e.runtime.version}"\n`),e.runtime?.runtimes?.length>0&&(n+=" runtimes:\n",e.runtime.runtimes.forEach(e=>n+=` - "${e}"\n`)),n+="\n",e.addons?.length>0&&(n+="addons:\n",e.addons.forEach(e=>n+=` - "${e}"\n`),n+="\n"),n+="dependencies:\n";const t=["npm","python","cpp","rust","go","system"];for(const s of t)e.dependencies?.[s]?.length>0&&(n+=` ${s}:\n`,e.dependencies[s].forEach(e=>n+=` - "${e}"\n`));n+="\nscripts:\n";for(const[t,s]of Object.entries(e.scripts||{}))n+=` ${t}: "${s}"\n`;n+="\nenvironment:\n";for(const[t,s]of Object.entries(e.environment||{}))n+=` ${t}: "${s}"\n`;return n+=`\nexecution:\n isolation: "${e.execution?.isolation||"auto"}"\n memory_limit: "${e.execution?.memory_limit||"2g"}"\n`,n}}module.exports=DependencyTracker;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license ThinkNCollab Enterprise & Open Core Ecosystem
|
|
3
|
+
* Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
|
|
4
|
+
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
|
+
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
|
+
*/
|
|
7
|
+
"use strict";let depth=0;const queued=[],MAX_QUEUED=500;function whenFree(e){if(0===depth)return e();queued.length<500&&queued.push(e)}function enter(){depth++}function leave(){if(depth=Math.max(0,depth-1),depth>0)return;const e=queued.splice(0);e.length&&process.stdout.write("\n");for(const t of e)try{t()}catch(e){}}const busy=()=>depth>0;module.exports={whenFree,enter,leave,busy};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license ThinkNCollab Enterprise & Open Core Ecosystem
|
|
3
|
+
* Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
|
|
4
|
+
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
|
+
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
|
+
*/
|
|
7
|
+
"use strict";function connectPlatform({timeoutMs:e=15e3}={}){const r=new(require("./api-client")),n=r.getUser();if(!r.isAuthenticated()||!n?._id)return Promise.reject(new Error("not signed in to ThinkNCollab; run: thinknsh login"));const t=require("socket.io-client")(`${r.wsUrl}/thinknsh`,{transports:["websocket"],reconnection:!0,timeout:e});return new Promise((o,s)=>{const i=setTimeout(()=>{t.close(),s(new Error(`could not reach ${r.wsUrl}`))},e);t.on("connect",()=>t.emit("thinknsh:connected",{userId:n._id,userName:n.name,shellToken:r.getToken()})),t.once("thinknsh:ready",()=>{clearTimeout(i),o({socket:t,user:n,server:r.wsUrl})}),t.once("thinknsh:error",e=>{clearTimeout(i),t.close(),s(new Error(`platform: ${e?.message||"sign-in refused"}; run: thinknsh login`))})})}function request(e,r,n,t=2e4){return new Promise((o,s)=>{e.timeout(t).emit(r,n,(e,n)=>{e?s(new Error(`${r}: no answer from the platform`)):n&&!1===n.ok?s(new Error(n.error||`${r} refused`)):o(n)})})}module.exports={connectPlatform,request};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license ThinkNCollab Enterprise & Open Core Ecosystem
|
|
3
|
+
* Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
|
|
4
|
+
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
|
+
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
|
+
*/
|
|
7
|
+
const path=require("path"),os=require("os"),fs=require("fs-extra"),http=require("http"),https=require("https"),{spawnSync}=require("child_process"),crypto=require("crypto"),chalk=require("chalk"),e2eeVault=require("../crypto/e2ee-vault"),ROOMS_BASE=path.join(os.homedir(),".tnc","rooms"),DATA_DIR=path.join(os.homedir(),".tnc","data");global._tncP2pServers||(global._tncP2pServers=new Map);class RoomSyncManager{constructor(){fs.ensureDirSync(ROOMS_BASE)}getRoomDir(e){const t=e.replace(/[^a-zA-Z0-9_-]/g,"-"),o=path.join(ROOMS_BASE,t);return fs.ensureDirSync(o),o}_findBinary(e){try{const t=spawnSync("which",[e],{stdio:"pipe"});if(0===t.status)return t.stdout.toString().trim()}catch(e){}for(const t of["/opt/homebrew/bin","/usr/local/bin","/usr/bin"]){const o=path.join(t,e);if(fs.existsSync(o))return o}return e}_validateAndSanitizeExtracted(e,t){const o=fs.readdirSync(e,{withFileTypes:!0});for(const n of o){const o=path.join(e,n.name);if(n.isSymbolicLink())try{const e=fs.realpathSync(o),s=path.relative(t,e);if(s.startsWith("..")||path.isAbsolute(s)&&!e.startsWith(t))throw new Error(`Path traversal / symlink escape detected: "${n.name}" targets "${e}" outside snapshot root`)}catch(e){if(e.message.includes("Path traversal"))throw e;try{fs.removeSync(o)}catch(e){}continue}try{const e=fs.lstatSync(o);e.isSymbolicLink()||(3072&e.mode&&fs.chmodSync(o,-3073&e.mode),n.isDirectory()&&this._validateAndSanitizeExtracted(o,t))}catch(e){}}}_getIgnoreRules(e){const t=new Set(["node_modules",".git",".tnc",".DS_Store","logs","scratch","tmp","coverage",".next",".nuxt",".turbo",".cache"]);for(const o of[".ignoretnc",".gitignore"]){const n=path.join(e,o);if(fs.existsSync(n))try{const e=fs.readFileSync(n,"utf8").split(/\r?\n/);for(let o of e)o=o.trim(),o&&!o.startsWith("#")&&(o=o.replace(/\/+$/,""),o&&t.add(o))}catch(e){}}return t}_isBinaryFile(e){try{const t=fs.openSync(e,"r"),o=Buffer.alloc(4096),n=fs.readSync(t,o,0,4096,0);fs.closeSync(t);for(let e=0;e<n;e++)if(0===o[e])return!0;return!1}catch(e){return!1}}_mergePackageJson(e,t){try{const o=fs.readJsonSync(e),n=fs.readJsonSync(t);if(n.dependencies){o.dependencies=o.dependencies||{};for(const[e,t]of Object.entries(n.dependencies))o.dependencies[e]||(o.dependencies[e]=t)}if(n.devDependencies){o.devDependencies=o.devDependencies||{};for(const[e,t]of Object.entries(n.devDependencies))o.devDependencies[e]||(o.devDependencies[e]=t)}if(n.scripts){o.scripts=o.scripts||{};for(const[e,t]of Object.entries(n.scripts))o.scripts[e]||(o.scripts[e]=t)}return fs.writeJsonSync(e,o,{spaces:2}),!0}catch(e){return!1}}_mergeEnvFile(e,t,o){try{const n=fs.readFileSync(e,"utf8"),s=fs.readFileSync(t,"utf8"),r=new Set;for(const e of n.split(/\r?\n/)){const t=e.trim();if(t&&!t.startsWith("#")){const e=t.indexOf("=");e>0&&r.add(t.substring(0,e).trim())}}const a=[];for(const e of s.split(/\r?\n/)){const t=e.trim();if(t&&!t.startsWith("#")){const e=t.indexOf("=");if(e>0){const o=t.substring(0,e).trim();r.has(o)||(a.push(t),r.add(o))}}}if(a.length>0){const t=`\n# ── Synced from ${o||"teammate"} (${(new Date).toLocaleDateString()}) ──\n`;return fs.appendFileSync(e,t+a.join("\n")+"\n","utf8"),{addedCount:a.length,keys:a.map(e=>e.split("=")[0])}}return{addedCount:0,keys:[]}}catch(e){return{addedCount:0,keys:[]}}}_mergeWorkspaceFiles(e,t,o={}){const n={added:[],cleanMerged:[],conflicts:[],unchanged:[],updated:[]},s=o.roomId,r=o.author||"teammate",a=o.localUser||"Local",c=o.overwrite||!1,i=o.baseDir&&fs.existsSync(o.baseDir)?o.baseDir:null,l=`backup-${Date.now().toString(36)}`,d=path.join(os.homedir(),".tnc","backups",`${s||"room"}-${l}`);let h=!1;const p=(t,o=[])=>{const n=fs.readdirSync(t,{withFileTypes:!0});for(const s of n){const n=path.join(t,s.name),r=path.relative(e,n),a=r.split(path.sep);a.includes(".git")||a.includes(".tnc")||a.includes("node_modules")||(s.isDirectory()?p(n,o):s.isFile()&&o.push({full:n,rel:r}))}return o},y=p(e),u=path.join(os.tmpdir(),`tnc_empty_base_${Date.now()}`);fs.writeFileSync(u,"");try{for(const{full:e,rel:o}of y){const s=path.join(t,o);if(!fs.existsSync(s)){fs.ensureDirSync(path.dirname(s)),fs.copyFileSync(e,s),n.added.push(o);continue}const l=fs.readFileSync(e),p=fs.readFileSync(s);if(l.equals(p)){n.unchanged.push(o);continue}try{const e=path.join(d,o);fs.ensureDirSync(path.dirname(e)),fs.copyFileSync(s,e),h=!0}catch(e){}if(c){fs.copyFileSync(e,s),n.updated.push(o);continue}if("package.json"===o&&this._mergePackageJson(s,e)){n.cleanMerged.push(o);continue}if(this._isBinaryFile(e)||this._isBinaryFile(s)){const s=path.join(t,`${o}.incoming`);fs.copyFileSync(e,s),n.updated.push(`${o} (saved as .incoming)`);continue}let y=u;if(i){const e=path.join(i,o);fs.existsSync(e)&&(y=e)}0===spawnSync("git",["merge-file","-L",`Local (${a})`,"-L","Base","-L",`Incoming (${r})`,s,y,e],{stdio:"pipe"}).status?n.cleanMerged.push(o):n.conflicts.push(o)}}finally{try{fs.unlinkSync(u)}catch(e){}}if(h){n.backupDir=d;try{const e=this.getRoomDir(s);fs.writeJsonSync(path.join(e,"last_backup.json"),{backupDir:d,createdAt:(new Date).toISOString(),roomId:s})}catch(e){}}return n}rollbackLastMerge(e,t){const o=this.getRoomDir(e),n=path.join(o,"last_backup.json");if(!fs.existsSync(n))throw new Error("No previous merge backup found for this room.");const{backupDir:s}=fs.readJsonSync(n);if(!s||!fs.existsSync(s))throw new Error(`Backup directory no longer exists: ${s}`);return console.log(chalk.cyan(`\n[RoomSync] Rolling back to pre-merge snapshot: ${s}...`)),fs.copySync(s,t,{overwrite:!0}),console.log(chalk.green("[OK] Workspace successfully rolled back to pre-merge state!\n")),{success:!0}}async _captureMongoSnapshot(e,t){let o=process.env.MONGODB_URI||process.env.MONGO_URI;if(!o){const e=path.join(t,".env");if(fs.existsSync(e))try{const t=fs.readFileSync(e,"utf8").match(/^(?:MONGODB_URI|MONGO_URI)\s*=\s*["']?([^"'\r\n]+)["']?/m);t&&t[1]&&(o=t[1].trim())}catch(e){}}if(!o)return null;let n=null;try{const e=path.join(t,"node_modules","mongodb");fs.existsSync(e)&&(n=require(e).MongoClient)}catch(e){}if(!n)try{n=require("mongodb").MongoClient}catch(e){}if(!n)return null;console.log(chalk.dim(" Capturing MongoDB database snapshot (Atlas / Local)..."));let s=null;try{s=new n(o,{serverSelectionTimeoutMS:5e3,connectTimeoutMS:5e3}),await s.connect();const t=s.db(),r=new Set(["sessions","pagepathhits","pageviews","adminauditlogs","webhooklogs"]),a=(await t.listCollections().toArray()).filter(e=>!r.has(e.name)),c=path.join(e,"data","mongodb");fs.ensureDirSync(c);let i=0,l=0;const d=10;for(let e=0;e<a.length;e+=d){const o=a.slice(e,e+d);await Promise.all(o.map(async e=>{try{const o=await t.collection(e.name).find({}).limit(5e3).toArray();o.length>0&&(fs.writeJsonSync(path.join(c,`${e.name}.json`),o),i+=o.length,l++)}catch(e){}}))}if(l>0)return console.log(chalk.green(` ✓ Captured MongoDB snapshot (${l} collections, ${i} documents)!`)),{collections:l,documents:i}}catch(e){console.log(chalk.yellow(` [Notice] Could not capture MongoDB snapshot: ${e.message}`))}finally{if(s)try{await s.close()}catch(e){}}return null}async _restoreMongoSnapshot(e,t){let o=process.env.MONGODB_URI||process.env.MONGO_URI;if(!o){const e=path.join(t,".env");if(fs.existsSync(e))try{const t=fs.readFileSync(e,"utf8").match(/^(?:MONGODB_URI|MONGO_URI)\s*=\s*["']?([^"'\r\n]+)["']?/m);t&&t[1]&&(o=t[1].trim())}catch(e){}}const n=o||"mongodb://127.0.0.1:27017/thinkncollab_dev";let s=null;try{const e=path.join(t,"node_modules","mongodb");fs.existsSync(e)&&(s=require(e).MongoClient)}catch(e){}if(!s)try{s=require("mongodb").MongoClient}catch(e){}const r=fs.readdirSync(e).filter(e=>e.endsWith(".json"));if(0===r.length)return;let a=!1;if(s){let t=null;try{t=new s(n,{serverSelectionTimeoutMS:3e3,connectTimeoutMS:3e3}),await t.connect();const o=t.db();let c=0;for(const t of r){const n=path.basename(t,".json"),s=fs.readJsonSync(path.join(e,t));if(Array.isArray(s)&&s.length>0)try{await o.collection(n).deleteMany({}),await o.collection(n).insertMany(s),c+=s.length}catch(e){}}console.log(chalk.green(` ✓ Restored MongoDB database (${r.length} collections, ${c} documents)!`)),a=!0}catch(e){}finally{if(t)try{await t.close()}catch(e){}}}if(!a){const o=path.join(t,".tnc-mongo-dump");fs.copySync(e,o,{overwrite:!0}),console.log(chalk.cyan(` ✓ Saved MongoDB snapshot to .tnc-mongo-dump/ (${r.length} collections ready for import)`))}}async _captureRedisSnapshot(e,t){const o=path.join(e,"data","redis"),n=path.join(DATA_DIR,"redis"),s=path.join(n,"dump.rdb");if(fs.existsSync(s)||fs.existsSync(n)){try{const e=this._findBinary("redis-cli");spawnSync(e,["bgsave"],{stdio:"pipe",timeout:5e3})}catch(e){}if(fs.existsSync(s))return console.log(chalk.dim(" Capturing native Redis database snapshot (dump.rdb)...")),fs.ensureDirSync(o),fs.copyFileSync(s,path.join(o,"dump.rdb")),!0}let r=process.env.REDIS_URL;if(!r){const e=path.join(t,".env");if(fs.existsSync(e))try{const t=fs.readFileSync(e,"utf8").match(/^(?:REDIS_URL)\s*=\s*["']?([^"'\r\n]+)["']?/m);t&&t[1]&&(r=t[1].trim())}catch(e){}}if(r){let e=null;try{const o=path.join(t,"node_modules","ioredis");fs.existsSync(o)&&(e=require(o))}catch(e){}if(!e)try{e=require("ioredis")}catch(e){}if(e){let t=null;try{t=new e(r,{connectTimeout:3e3,lazyConnect:!0}),await t.connect();try{await t.bgsave()}catch(e){}const n=await t.keys("*");if(n.length>0){console.log(chalk.dim(` Capturing Redis in-memory cache (${n.length} keys)...`)),fs.ensureDirSync(o);const e=[];for(const o of n.slice(0,1e3))try{const n=await t.pttl(o),s=await t.dump(o);s&&e.push({k:o,ttl:n>0?n:0,v:s})}catch(e){}return fs.writeJsonSync(path.join(o,"keys.json"),e),console.log(chalk.green(` ✓ Captured Redis cache (${e.length} keys)!`)),!0}}catch(e){}finally{if(t)try{t.disconnect()}catch(e){}}}}return!1}_getLocalIp(){const e=os.networkInterfaces();for(const t of Object.keys(e))for(const o of e[t])if("IPv4"===o.family&&!o.internal)return o.address;return"127.0.0.1"}_startEphemeralP2PServer(e,t){return new Promise((o,n)=>{try{const s=crypto.randomBytes(16).toString("hex"),r=this._getLocalIp(),a=http.createServer((o,n)=>{try{const r=new URL(o.url,`http://${o.headers.host||"localhost"}`);if(r.pathname===`/p2p/state/${e}`){const e=o.headers["x-tnc-sync-token"]||r.searchParams.get("token"),a=Buffer.from(String(e||"")),c=Buffer.from(String(s));if(a.length!==c.length||!crypto.timingSafeEqual(a,c))return n.writeHead(403,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:"Invalid or expired P2P sync token"}));if(!fs.existsSync(t))return n.writeHead(404,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:"Bundle file no longer exists"}));const i=fs.statSync(t);n.writeHead(200,{"Content-Type":"application/octet-stream","Content-Length":i.size,"X-ThinkNCollab-Encrypted":"true","X-ThinkNCollab-P2P":"true","Cache-Control":"no-cache, no-store"}),console.log(chalk.dim(`\n[RoomSync:P2P] Teammate connected! Streaming AES-256-GCM encrypted state bundle (${(i.size/1024/1024).toFixed(2)} MB)...`));const l=fs.createReadStream(t);return l.pipe(n),void l.on("end",()=>{console.log(chalk.green("[RoomSync:P2P] ✓ Direct peer-to-peer encrypted transfer completed successfully!"))})}n.writeHead(404),n.end()}catch(e){console.error("[RoomSync:P2P] Server error:",e.message),n.writeHead(500),n.end()}}),c=()=>{const t=a.address().port,n={localIp:r,port:t,token:s,server:a};global._tncP2pServers.set(e,n),setTimeout(()=>{try{a.close()}catch(e){}global._tncP2pServers.delete(e)},72e5),o(n)};let i=!1;a.on("error",e=>{i||"EADDRNOTAVAIL"!==e.code&&"EINVAL"!==e.code?n(e):(i=!0,a.listen(0,"0.0.0.0",c))}),a.listen(0,r,c)}catch(e){n(e)}})}_uploadToRelay(e,t,o,n){return new Promise((s,r)=>{try{if(!fs.existsSync(o))return r(new Error(`Bundle file not found: ${o}`));const a=(n?.api?.apiUrl||process.env.THINKNCOLLAB_API_URL||"https://thinkncollab.com").replace(/\/$/,""),c=new URL(`${a}/thinknsh/${e}/cli/relay/${t}`),i="https:"===c.protocol,l=i?https:http,d=n?.api?.getToken?.()||"",h=fs.statSync(o);console.log(chalk.cyan(`\n[RoomSync:Relay] Off-LAN teammate requested state. Streaming AES-256-GCM bundle to in-memory relay (${(h.size/1024/1024).toFixed(2)} MB)...`));const p=l.request({hostname:c.hostname,port:c.port||(i?443:80),path:c.pathname+c.search,method:"POST",headers:{"Content-Type":"application/octet-stream","Content-Length":h.size,"X-ThinkNCollab-Encrypted":"true","x-shell-token":d}},e=>{let t="";e.on("data",e=>t+=e),e.on("end",()=>{if(e.statusCode>=400){let o=t;try{o=JSON.parse(t).error||t}catch(e){}413===e.statusCode?o="State bundle exceeds server relay capacity of 500 MB. Use Direct LAN P2P.":429===e.statusCode&&(o="Room relay limit reached (Max 2 concurrent active relays per room)."),console.error(chalk.red(`[RoomSync:Relay] Upload failed with HTTP ${e.statusCode}: ${o}`)),r(new Error(`Relay upload failed: ${o}`))}else console.log(chalk.green("[RoomSync:Relay] ✓ Encrypted relay stream completed for teammate!")),s()})});p.on("error",e=>{console.error(chalk.red(`[RoomSync:Relay] Network error: ${e.message}`)),r(e)}),fs.createReadStream(o).pipe(p)}catch(e){r(e)}})}async publishState(e,t,o={}){const n=this.getRoomDir(e),s=path.join(n,"states");fs.ensureDirSync(s);const r=t?.api?.getUser?.()||{name:os.userInfo().username,id:"local-user"},a=o.name||`state-${Date.now().toString(36)}`,c=`${a}-${crypto.randomBytes(3).toString("hex")}`,i=path.join(n,"staging",c);fs.ensureDirSync(i),console.log(chalk.cyan(`\n[RoomSync] Preparing encrypted environment state bundle for Room ${chalk.bold(e)}...`));const l=o.cwd||process.cwd(),d={id:c,name:a,roomId:e,author:r.name||r.username||"teammate",authorId:r.id||r._id||"unknown",createdAt:(new Date).toISOString(),description:o.description||"Live development state",services:[],files:[],encrypted:!0,cipher:"aes-256-gcm"};for(const e of["thinkncollab.yml","thinkncollab.yaml","package.json"]){const t=path.join(l,e);fs.existsSync(t)&&(fs.copySync(t,path.join(i,e)),d.files.push(e))}const h=path.join(l,".env");if(fs.existsSync(h)){const e=fs.readFileSync(h,"utf8");fs.writeFileSync(path.join(i,".env.shared"),e,"utf8"),d.files.push(".env.shared")}if(!1!==o.includeCode&&!o.noCode){console.log(chalk.dim(" Packaging project workspace files (skipping node_modules, git, and logs)..."));const e=path.join(i,"workspace");fs.ensureDirSync(e);const t=this._getIgnoreRules(l);let o=0;try{fs.copySync(l,e,{filter:e=>{const n=path.relative(l,e);if(!n)return!0;const s=n.split(path.sep);for(const e of s)if(t.has(e))return!1;const r=path.basename(e);return!(r.endsWith(".tar.gz")||r.endsWith(".tar.gz.enc")||r.endsWith(".log")||".env"===r||".env.room"===r||(o++,0))}}),d.files.push("workspace"),d.hasCode=!0,console.log(chalk.dim(` ✓ Packaged workspace project tree (${o} items)`))}catch(e){console.log(chalk.yellow(` [Notice] Non-fatal error copying workspace files: ${e.message}`))}}let p=[];try{p=await require("../../dock/room-data").captureServices(i,{log:e=>console.log(chalk.dim(` ${e}`))}),p.length&&console.log(chalk.dim(` ✓ Captured dock services: ${p.join(", ")}`))}catch(e){console.log(chalk.yellow(` [Notice] dock services not captured: ${e.message}`))}d.services.push(...p.map(e=>`dock:${e}`));try{!p.includes("redis")&&await this._captureRedisSnapshot(i,l)&&d.services.push("redis")}catch(e){}try{!p.includes("mongo")&&await this._captureMongoSnapshot(i,l)&&d.services.push("mongodb")}catch(e){}fs.writeJsonSync(path.join(i,"manifest.json"),d,{spaces:2});const y=path.join(s,`${c}.tar.gz`);console.log(chalk.dim(` Packaging bundle: ${path.basename(y)}...`)),spawnSync("tar",["-czf",y,"-C",i,"."],{stdio:"pipe"});try{fs.removeSync(i)}catch(e){}const u=path.join(s,`${c}.tar.gz.enc`);console.log(chalk.dim(" Encrypting state bundle with Room AES-256-GCM key...")),await e2eeVault.encryptFile(y,u,e);const m=((fs.statSync(u).size||0)/1024/1024).toFixed(2);d.bundleSizeMB=m,d.bundlePath=u;let f=null;try{f=await this._startEphemeralP2PServer(c,u),d.p2p={localIp:f.localIp,port:f.port,token:f.token},console.log(chalk.dim(` Direct LAN P2P endpoint active: http://${f.localIp}:${f.port}`))}catch(e){console.log(chalk.yellow(` P2P server setup skipped: ${e.message}`))}const g=path.join(n,"index.json"),S=fs.existsSync(g)?fs.readJsonSync(g):[];if(S.unshift(d),fs.writeJsonSync(g,S,{spaces:2}),t?.api?.isAuthenticated?.())try{await t.api._request("POST",`/thinknsh/${e}/cli/states`,{stateId:c,name:a,author:d.author,services:d.services,sizeMB:m,description:d.description,p2p:d.p2p,encrypted:!0,cipher:"aes-256-gcm"}),console.log(chalk.dim(" Registered state metadata with ThinkNCollab room cloud"))}catch(e){console.log(chalk.yellow(` Server state registration note: ${e.message}`))}if(t?.ws?.isConnected?.())try{t.ws.socket.emit("thinknsh:room-state-published",{roomId:e,stateId:c,name:a,author:d.author,sizeMB:m,services:d.services,p2p:d.p2p,encrypted:!0}),t.ws.socket.off("thinknsh:relay-upload-start"),t.ws.socket.on("thinknsh:relay-upload-start",o=>{o?.stateId===c&&this._uploadToRelay(e,o.transferId,u,t).catch(e=>{console.error("[RoomSync:Relay] Upload handler error:",e.message)})})}catch(e){}return console.log(chalk.green(`\n[OK] State published to Room [${e}] successfully!`)),console.log(chalk.dim(` State ID: ${c}`)),console.log(chalk.dim(" Cipher: AES-256-GCM (Zero plaintext over LAN/Relay)")),console.log(chalk.dim(` Bundle Size: ${m} MB (Zero Docker Hub needed)`)),console.log(chalk.dim(` Services: ${d.services.join(", ")||"Node runtime"}`)),console.log(chalk.cyan("\nTeammates in this room can pull this state instantly:")),console.log(chalk.bold(` thinknsh room pull ${c}\n`)),d}_downloadStream(e,t,o={}){return new Promise((n,s)=>{try{const r=new URL(e),a="https:"===r.protocol,c=a?https:http,i={hostname:r.hostname,port:r.port||(a?443:80),path:r.pathname+r.search,method:"GET",headers:{...o},timeout:5e3},l=c.request(i,e=>{if(e.statusCode>=400)return 413===e.statusCode?s(new Error("HTTP 413: State bundle exceeds maximum server relay limit (500 MB)")):429===e.statusCode?s(new Error("HTTP 429: Room relay limit reached (Max 2 concurrent active relays per room)")):s(new Error(`HTTP ${e.statusCode}: Failed to fetch stream`));const o=fs.createWriteStream(t);e.pipe(o),o.on("finish",()=>{o.close(),n()}),o.on("error",e=>{try{fs.removeSync(t)}catch(e){}s(e)})});l.on("timeout",()=>{l.destroy(new Error("Connection timed out"))}),l.on("error",e=>{s(e)}),l.end()}catch(e){s(e)}})}async pullState(e,t,o,n={}){const s=n.cwd||process.cwd(),r=this.getRoomDir(e),a=path.join(r,"states");fs.ensureDirSync(a),console.log(chalk.cyan(`\n[RoomSync] Resolving environment state in Room ${chalk.bold(e)}...`));let c=[];if(o?.api?.isAuthenticated?.())try{const t=await o.api._request("GET",`/thinknsh/${e}/cli/states`);t&&Array.isArray(t.states)&&(c=t.states)}catch(e){}const i=path.join(r,"index.json");if(fs.existsSync(i))try{const e=fs.readJsonSync(i);for(const t of e)c.some(e=>e.id===t.id)||c.push(t)}catch(e){}if(0===c.length)throw new Error(`No shared states found for room "${e}". Ask your teammate to publish one with: thinknsh room share`);const l=(t||"").toLowerCase().trim();let d=null;if(l){if(d=c.find(e=>e.id&&e.id.toLowerCase()===l||e.stateId&&e.stateId.toLowerCase()===l||e.name&&e.name.toLowerCase()===l||e.id&&e.id.toLowerCase().includes(l)||e.name&&e.name.toLowerCase().includes(l)||e.author&&e.author.toLowerCase()===l),!d)throw new Error(`State matching "${t}" not found in room ${e}. Run "thinknsh room states" to see available states.`)}else d=c[0];const h=d.id||d.stateId;console.log(chalk.cyan(` Selected state: "${chalk.bold(d.name)}" (Published by: ${chalk.bold(d.author)})`)),console.log(chalk.dim(` Size: ${d.sizeMB||d.bundleSizeMB||"0"} MB`)),console.log(chalk.dim(` Services: ${(d.services||[]).join(", ")||"Node runtime"}`));const p=path.join(a,`${h}.tar.gz.enc`),y=path.join(a,`${h}.tar.gz`);if(fs.existsSync(y)||fs.existsSync(p))console.log(chalk.dim(" Found cached local state bundle. Verifying..."));else{let t=!1;if(d.p2p&&d.p2p.localIp&&d.p2p.port&&d.p2p.token){const e=`http://${d.p2p.localIp}:${d.p2p.port}/p2p/state/${h}`;console.log(chalk.dim(` Attempting direct LAN P2P connection to teammate (${d.p2p.localIp}:${d.p2p.port})...`));try{await this._downloadStream(e,p,{"X-TNC-Sync-Token":d.p2p.token}),console.log(chalk.green(" ✓ Connected via Direct LAN P2P! Downloaded encrypted bundle at maximum speed.")),t=!0}catch(e){console.log(chalk.yellow(` Direct LAN connection unreachable (${e.message}). Switching to ThinkNCollab Cloud Relay...`))}}if(!t){if(!o?.api?.isAuthenticated?.())throw new Error("State author is not on local LAN, and you are not authenticated to request server relay. Run 'thinknsh login' first.");console.log(chalk.dim(" Requesting server-assisted in-memory stream relay..."));const n=await o.api._request("POST",`/thinknsh/${e}/cli/relay-request`,{stateId:h});if(!n?.relayUrl)throw new Error("Server could not initialize relay session: "+(n?.error||"Unknown error"));const s=`${o.api.apiUrl.replace(/\/$/,"")}${n.relayUrl}`,r=o.api.getToken();await this._downloadStream(s,p,{"x-shell-token":r}),console.log(chalk.green(" ✓ Streamed encrypted bundle via ThinkNCollab In-Memory Relay (0 customer disk bytes on server)!")),t=!0}}if(!fs.existsSync(y)){if(!fs.existsSync(p))throw new Error(`Missing encrypted bundle file: ${p}`);console.log(chalk.dim(" Decrypting AES-256-GCM bundle with Room Key & verifying authentication tag..."));try{await e2eeVault.decryptFile(p,y,e),console.log(chalk.green(" ✓ Cryptographic integrity verified! Decryption completed."))}catch(e){try{fs.removeSync(p)}catch(e){}try{fs.removeSync(y)}catch(e){}throw new Error(`SECURITY ALERT: Bundle integrity check failed or wrong room key! (${e.message})`)}}const u=path.join(r,"unpack",h);fs.ensureDirSync(u),console.log(chalk.dim(" Extracting archive with security constraints (--no-same-owner, --no-same-permissions)..."));const m=spawnSync("tar",["-xzf",y,"-C",u,"--no-same-owner","--no-same-permissions"],{stdio:"pipe"});if(0!==m.status)throw new Error(`Tar extraction failed: ${m.stderr?.toString()||"Unknown error"}`);try{this._validateAndSanitizeExtracted(u,u)}catch(e){try{fs.removeSync(u)}catch(e){}try{fs.removeSync(y)}catch(e){}throw new Error(`SECURITY VIOLATION in received snapshot: ${e.message}. Extraction terminated.`)}const f=d.services||[];if(f.some(e=>e.startsWith("dock:"))){console.log(chalk.dim(" Restoring database state into dock services..."));const e=await require("../../dock/room-data").restoreServices(u,{log:e=>console.log(chalk.dim(` ${e}`))});e.length&&console.log(chalk.green(` ✓ Restored ${e.join(", ")} with teammate database state (thinknsh service status)`))}if(f.includes("postgres")&&console.log(chalk.yellow(" [Notice] This share has a PostgreSQL data folder from an older thinknsh; ask the teammate to share again with thinknsh service postgres.")),f.includes("redis")){const e=path.join(u,"data","redis","keys.json");fs.existsSync(e)&&console.log(chalk.dim(` Redis keys saved at ${e}`))}if(f.includes("mongodb")){const e=path.join(u,"data","mongodb");if(fs.existsSync(e)){console.log(chalk.dim(" Restoring MongoDB database state..."));try{await this._restoreMongoSnapshot(e,s)}catch(e){console.log(chalk.yellow(` [Notice] MongoDB restore skipped: ${e.message}`))}}}const g=path.join(u,"workspace");if(fs.existsSync(g)){console.log(chalk.cyan(`\n Smart Merging project workspace with ${chalk.bold(d.author)}'s state...`));const t=path.join(r,"base",d.id),a=this._mergeWorkspaceFiles(g,s,{roomId:e,author:d.author,localUser:o?.api?.getUser?.()?.name||os.userInfo().username,overwrite:n.overwrite||!1,baseDir:fs.existsSync(t)?t:null});try{const e=path.join(r,"base",d.id);fs.ensureDirSync(e),fs.copySync(g,e,{overwrite:!0})}catch(e){}a.added.length>0&&console.log(chalk.green(` ✓ ${a.added.length} new files added from teammate`)),a.cleanMerged.length>0&&console.log(chalk.green(` ✓ ${a.cleanMerged.length} files cleanly merged (zero conflicts)`)),a.updated.length>0&&console.log(chalk.cyan(` ✓ ${a.updated.length} files updated`)),a.conflicts.length>0&&(console.log(chalk.yellow.bold(`\n ⚠ [MERGE CONFLICTS DETECTED] in ${a.conflicts.length} files:`)),a.conflicts.slice(0,10).forEach(e=>{console.log(chalk.yellow(` • ${e}`))}),a.conflicts.length>10&&console.log(chalk.dim(` ... and ${a.conflicts.length-10} more`)),console.log(chalk.dim(" Standard <<<<<<< LOCAL | >>>>>>> markers inserted.")),console.log(chalk.cyan(" Open in VS Code to review and click 'Accept Change' buttons.\n"))),a.backupDir&&console.log(chalk.dim(` (Safety backup saved at ${a.backupDir}. To undo: thinknsh room undo)`))}for(const e of["package.json","thinkncollab.yml","thinkncollab.yaml"]){const t=path.join(u,e),o=path.join(s,e);fs.existsSync(t)&&!fs.existsSync(o)&&(fs.copySync(t,o),console.log(chalk.green(` ✓ Restored ${e} into workspace`)))}const S=path.join(u,".env.shared");if(fs.existsSync(S)){const e=path.join(s,".env");if(fs.existsSync(e)){const t=this._mergeEnvFile(e,S,d.author);t.addedCount>0?console.log(chalk.green(` ✓ Smart merged .env: added ${t.addedCount} new keys from teammate (${t.keys.join(", ")})!`)):console.log(chalk.dim(" ✓ .env already up-to-date with teammate configuration.")),fs.copySync(S,path.join(s,".env.room")),console.log(chalk.dim(" ✓ Full teammate environment reference saved to .env.room"))}else fs.copySync(S,e),console.log(chalk.green(" ✓ Created .env from teammate shared configuration!"))}try{fs.removeSync(u)}catch(e){}return console.log(chalk.green("\n[OK] Environment Mirrored & Merged Successfully!")),console.log(chalk.dim(` Your local environment is now in exact sync with ${d.author}.`)),console.log(chalk.dim(" Zero Docker Hub registry downloads were used.")),fs.existsSync(path.join(s,"package.json"))&&!fs.existsSync(path.join(s,"node_modules"))?console.log(chalk.cyan(' Tip: Run "npm install" to install dependencies for this project.\n')):console.log(),{success:!0,state:d}}async listStates(e,t){let o=[];if(t?.api?.isAuthenticated?.())try{const n=await t.api._request("GET",`/thinknsh/${e}/cli/states`);n&&Array.isArray(n.states)&&(o=n.states)}catch(e){}const n=this.getRoomDir(e),s=path.join(n,"index.json");if(fs.existsSync(s))try{const e=fs.readJsonSync(s);for(const t of e)o.some(e=>e.id===t.id)||o.push(t)}catch(e){}return o}}module.exports=RoomSyncManager;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license ThinkNCollab Enterprise & Open Core Ecosystem
|
|
3
|
+
* Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
|
|
4
|
+
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
|
+
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
|
+
*/
|
|
7
|
+
"use strict";const fs=require("fs"),os=require("os"),path=require("path"),crypto=require("crypto"),chalk=require("chalk"),linksFile=()=>path.join(os.homedir(),".thinkncollab","projects.json");function projectRoot(e=process.cwd()){let o=path.resolve(e);const t=[path.parse(o).root,os.homedir()];for(let e=o;;e=path.dirname(e)){if(fs.existsSync(path.join(e,"tncproject"))||fs.existsSync(path.join(e,".tncproject"))||fs.existsSync(path.join(e,".git")))return e;if(t.includes(e))break}return o}function readLinks(){try{return fs.lstatSync(linksFile()).isFile()&&JSON.parse(fs.readFileSync(linksFile(),"utf8"))||{}}catch(e){return{}}}function writeLinks(e){const o=path.dirname(linksFile());fs.mkdirSync(o,{recursive:!0,mode:448});const t=path.join(o,`.projects-${process.pid}-${crypto.randomBytes(4).toString("hex")}.json`);fs.writeFileSync(t,JSON.stringify(e,null,2),{mode:384,flag:"wx"}),fs.renameSync(t,linksFile())}const hostOf=e=>{try{return new URL(e).host}catch(o){return String(e||"")}};function remember(e,o,t){const r=readLinks();r[projectRoot(t)]={roomId:o.id,roomName:o.name||"",server:hostOf(e),at:(new Date).toISOString()},writeLinks(r)}function forget(e){const o=readLinks();delete o[projectRoot(e)],writeLinks(o)}function knownRoom(e,o=process.cwd()){try{const{hasProjectConfig:e,loadProjectConfig:t}=require("./project-config");if(e(o))return{id:t(o).roomId,name:"",source:"tncproject"}}catch(e){}const t=readLinks()[projectRoot(o)];return t&&t.server===hostOf(e)?{id:t.roomId,name:t.roomName,source:"remembered"}:null}const normRooms=e=>(Array.isArray(e)?e:e?.rooms||[]).map(e=>({id:String(e.id||e._id||""),name:e.name||"Room",role:e.role||"",isMember:!1!==e.isMember})).filter(e=>e.id&&e.isMember);async function chooseRoom(e,{interactive:o,wanted:t}){const r=normRooms(await e.api.getRooms());if(t){const e=t.toLowerCase(),o=r.find(e=>e.id===t)||r.find(o=>o.name.toLowerCase()===e)||r.filter(o=>o.name.toLowerCase().includes(e)).sort((e,o)=>e.name.length-o.name.length)[0];if(!o)throw new Error(`no room of yours matches "${t}" (see "rooms")`);return o}if(!r.length)return null;if(1===r.length)return r[0];if(!o)return null;const{select:n}=require("@inquirer/prompts"),i=await n({message:`Which room is ${chalk.bold(path.basename(projectRoot()))} for?`,choices:r.map(e=>({name:e.role?`${e.name} ${chalk.dim(e.role)}`:e.name,value:e.id})),pageSize:12});return r.find(e=>e.id===i)}async function connect(e,{interactive:o=!1,wanted:t=null,quiet:r=!1}={}){if(!e.api.isAuthenticated())throw new Error("not signed in");const n=e.api.apiUrl;let i=t?await chooseRoom(e,{interactive:o,wanted:t}):knownRoom(n);if(i||(i=await chooseRoom(e,{interactive:o})),!i)return null;if(e.ws.setUser(e.api.getUser(),e.api.getToken()),e.ws.isConnected()||await e.ws.connect(e.api.getToken()),e.ws.getCurrentRoom()!==i.id){const o=await e.ws.joinRoom(i.id);i.history=o?.history||[]}if(!i.name)try{i.name=normRooms(await e.api.getRooms()).find(e=>e.id===i.id)?.name||""}catch(e){}if("tncproject"!==i.source)try{remember(n,i)}catch(e){}return r||console.log(`${chalk.green("✔")} Connected to room ${chalk.bold(i.name||i.id)}${chalk.dim("tncproject"===i.source?" (from tncproject)":"")}`),{room:i,history:i.history||[]}}module.exports={connect,knownRoom,remember,forget,projectRoot,linksFile};
|