thinkncollab-shell 2.0.0-beta.4 → 2.0.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/Readme.md +35 -18
  2. package/dist/native/brand/tnc-256.png +0 -0
  3. package/dist/src/commands/dock/cluster.js +1 -1
  4. package/dist/src/commands/dock/containers.js +1 -1
  5. package/dist/src/commands/room/sync.js +1 -1
  6. package/dist/src/commands/sandbox/common.js +7 -0
  7. package/dist/src/commands/sandbox/console.js +1 -1
  8. package/dist/src/commands/sandbox/dep.js +1 -1
  9. package/dist/src/commands/sandbox/deploy.js +1 -1
  10. package/dist/src/commands/sandbox/dev.js +1 -1
  11. package/dist/src/commands/sandbox/exec.js +1 -1
  12. package/dist/src/commands/sandbox/push.js +1 -1
  13. package/dist/src/commands/sandbox/sandbox.js +1 -1
  14. package/dist/src/commands/sandbox/share.js +1 -1
  15. package/dist/src/commands/sandbox/snapshot.js +1 -1
  16. package/dist/src/commands/shell/doctor.js +1 -1
  17. package/dist/src/commands/tasks/build.js +1 -1
  18. package/dist/src/commands/terminal/share.js +1 -1
  19. package/dist/src/core/crypto/seal.js +7 -0
  20. package/dist/src/core/dependency-tracker.js +1 -1
  21. package/dist/src/core/foreground.js +7 -0
  22. package/dist/src/core/platform-socket.js +7 -0
  23. package/dist/src/core/room/sync-manager.js +7 -0
  24. package/dist/src/core/shell.js +1 -1
  25. package/dist/src/core/{sandbox/snapshot-manager.js → snapshot-manager.js} +1 -1
  26. package/dist/src/core/tunnel/client.js +7 -0
  27. package/dist/src/core/tunnel/daemon.js +7 -0
  28. package/dist/src/core/tunnel/index.js +7 -0
  29. package/dist/src/core/websocket.js +1 -1
  30. package/dist/src/core/window-launcher.js +7 -0
  31. package/dist/src/dock/bench/memory.js +1 -1
  32. package/dist/src/dock/cli/commands.js +1 -1
  33. package/dist/src/dock/cli/k8s.js +7 -0
  34. package/dist/src/dock/cli/services.js +1 -1
  35. package/dist/src/dock/cli/session.js +1 -1
  36. package/dist/src/dock/cli/workspace.js +1 -1
  37. package/dist/src/dock/engine.js +1 -1
  38. package/dist/src/dock/image/certs.js +7 -0
  39. package/dist/src/dock/image/registry.js +1 -1
  40. package/dist/src/dock/linux/host.js +1 -1
  41. package/dist/src/dock/room-data.js +7 -0
  42. package/dist/src/dock/runtime/network.js +1 -1
  43. package/dist/src/dock/sandbox.js +7 -0
  44. package/dist/src/dock/selftest.js +1 -1
  45. package/dist/src/dock/setup/autostart.js +1 -1
  46. package/dist/src/dock/setup/index.js +1 -1
  47. package/dist/src/dock/tray/install.js +1 -1
  48. package/dist/src/dock/workspace/share.js +7 -0
  49. package/dist/src/dock/workspace/workspace.js +1 -1
  50. package/package.json +1 -1
  51. package/dist/src/core/sandbox/arch-shim.js +0 -7
  52. package/dist/src/core/sandbox/capsule-builder.js +0 -7
  53. package/dist/src/core/sandbox/cgroups.js +0 -7
  54. package/dist/src/core/sandbox/container-monitor.js +0 -7
  55. package/dist/src/core/sandbox/crun-manager.js +0 -7
  56. package/dist/src/core/sandbox/engine.js +0 -7
  57. package/dist/src/core/sandbox/image-manager.js +0 -7
  58. package/dist/src/core/sandbox/image-store.js +0 -7
  59. package/dist/src/core/sandbox/index.js +0 -7
  60. package/dist/src/core/sandbox/macos-sandbox.js +0 -7
  61. package/dist/src/core/sandbox/microvm-manager.js +0 -7
  62. package/dist/src/core/sandbox/native-service-manager.js +0 -7
  63. package/dist/src/core/sandbox/network.js +0 -7
  64. package/dist/src/core/sandbox/oci-exporter.js +0 -7
  65. package/dist/src/core/sandbox/oci-puller.js +0 -7
  66. package/dist/src/core/sandbox/oci-runtime-spec.js +0 -7
  67. package/dist/src/core/sandbox/port-proxy-daemon.js +0 -7
  68. package/dist/src/core/sandbox/room-sync-manager.js +0 -7
  69. package/dist/src/core/sandbox/runtime-manager.js +0 -7
  70. package/dist/src/core/sandbox/storage.js +0 -7
  71. package/dist/src/core/sandbox/supervisor.js +0 -7
  72. package/dist/src/core/sandbox/tunnel-daemon.js +0 -7
  73. package/dist/src/core/sandbox/tunnel.js +0 -7
  74. package/dist/src/core/sandbox/window-launcher.js +0 -7
  75. package/dist/src/core/sandbox/windows-wsl2.js +0 -7
  76. package/dist/src/core/sandbox-window.js +0 -8
  77. /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 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=[],s){console.log(chalk.bold.cyan("\n🔍 Running ThinkNCollab System Diagnostics...\n"));const t=[],o=process.platform,a=process.arch;t.push({category:"System Platform",item:"OS & Architecture",status:chalk.green("✓ PASS"),details:`${o} / ${a} (${os.release()})`});const i=(new(require("../../core/sandbox/snapshot-manager"))).systemInfo(),n=i.isCoW;t.push({category:"Filesystem Storage",item:`CoW Support (${i.fsType.toUpperCase()})`,status:n?chalk.green("✓ PASS"):chalk.yellow("⚠ FALLBACK"),details:n?`CoW Active via ${i.strategy} (instant, 0-disk)`:`Standard copy fallback active on ${i.fsType}`});const c=this._cmdExists("crun"),r=(this._cmdExists("runc"),this._cmdExists("skopeo")),l=this._cmdExists("zstd");t.push({category:"OCI Runtime",item:"crun (Fast C-based OCI runtime)",status:c?chalk.green("✓ INSTALLED"):chalk.dim("○ OPTIONAL"),details:c?"Available for native Linux execution":"crun not found (uses native process sandboxing)"}),t.push({category:"OCI Tools",item:"skopeo & zstd support",status:r||l?chalk.green("✓ READY"):chalk.yellow("○ PARTIAL"),details:`skopeo: ${r?"yes":"no"}, zstd: ${l?"yes":"no"}`});const h=(os.totalmem()/1024/1024/1024).toFixed(1),d=(os.freemem()/1024/1024/1024).toFixed(1),u=parseFloat(d)<.5;t.push({category:"Host Resources",item:"Memory Headroom",status:u?chalk.red("✗ LOW RAM"):chalk.green("✓ PASS"),details:`${d} GB free of ${h} GB total`});const{dockPaths:m}=require("../../dock/paths"),{ping:p}=require("../../dock/client"),y=await p(m().socket),k=fs.existsSync(path.join(os.homedir(),".tnc","dock","runtime.json"));t.push({category:"Container Engine",item:"dock runtime",status:k?chalk.green("✓ INSTALLED"):chalk.yellow("○ MISSING"),details:k?"Linux runtime for this machine is installed":'Run "thinknsh dock setup"'}),t.push({category:"Container Engine",item:"dock daemon",status:y?chalk.green("✓ RUNNING"):chalk.dim("○ STOPPED"),details:y?m().socket:"Starts automatically on the first container command"});const g=(new(require("../../core/sandbox/policy-engine"))).getPolicy();t.push({category:"Security Policy",item:"Enterprise Policy Engine",status:g.enabled?chalk.green("✓ ENFORCING"):chalk.dim("○ DISABLED (Permissive)"),details:g.enabled?`Allowed Registries: ${g.allowedRegistries.join(", ")}`:"Permissive mode (all public registries allowed)"});const S=new Table({head:[chalk.cyan("Category"),chalk.cyan("Diagnostic Check"),chalk.cyan("Status"),chalk.cyan("Details")],style:{head:[],border:[]}});for(const e of t)S.push([e.category,e.item,e.status,e.details]);console.log(S.toString()),console.log(chalk.bold.green("\n[OK] ThinkNCollab is healthy and operational.\n"))},_cmdExists(e){try{return 0===spawnSync("which",[e],{stdio:"pipe"}).status}catch{return!1}}};
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"),CapsuleBuilder=require("../../core/sandbox/capsule-builder"),{BUILD_FILE}=require("../../dock/api/wire");module.exports={name:"pipeline",description:"Build native container image from a Containerfile or run 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 native container image with tag: ${chalk.bold(s)}...`));try{const e=new CapsuleBuilder(process.cwd()),t=await e.build(s);console.log(chalk.green(`\n[OK] Successfully built image "${s}" (ID: ${t.imageId})\n`))}catch(e){console.log(chalk.red(`\n[FAIL] Build error: ${e.message}\n`))}return}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 u=fs.readFileSync(a,"utf8").split("\n");let h=c,d=[],f="";for(let e=0;e<u.length;e++){const s=u[e].trim();if(s.startsWith("name:"))h=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<u.length;){const e=u[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<u.length;){const e=u[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: "${h}" (${c})`),console.log(chalk.green(` [TNC Actions] Starting Local Workflow: "${h}" (${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}`))}}}};
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(chalk.dim(` URL : ${t}/terminal/${s.sessionId}`)),console.log(chalk.dim("\n Room members can now view your terminal.")),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={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)if(e._e2eeEnabled&&e._aesKey){const r=require("../../core/crypto/e2ee-vault")._generateStreamNonce(s,e.ws.socket?.id||"host"),o=require("../../utils/terminalCrypto").encryptPayload(n,e._aesKey,{nonce:r.nonce,seq:r.seq});e.ws.socket.emit("terminal:output:encrypted",{sessionId:s,...o})}else e.ws.socket.emit("terminal:output",{sessionId:s,data: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",s=>{if(e._shareLocked)return void console.log(chalk.red("\n [Warning] Dropped input: Session is locked (Kill-Switch Active)"));const r=s.userId||s.fromUserId||s.from,o=e._shareViewers?.[r];o&&o.canWrite?(console.log(chalk.blue(`\n [${s.fromName||"Writer"}] → ${s.input}`)),e.execute(s.input.trim())):console.log(chalk.red(`\n [Warning] Rejected unauthorized input from ${s.fromName||r} (No write permission)`))}),e.ws.socket.on("terminal:input:encrypted",s=>{if(e._shareLocked)return void console.log(chalk.red("\n [Security] Dropped encrypted input: Session is locked (Kill-Switch Active)"));const r=s.from||s.userId,o=e._shareViewers?.[r];if(!o||!o.canWrite)return void console.log(chalk.red(`\n [Security] Rejected unauthorized encrypted input from ${s.fromName||r}`));const t="number"==typeof s.seq?s.seq:0,n=e._shareWriterSeqs.get(r)||0;if(t<=n)return void console.log(chalk.red(`\n [Security Alert] Replay or reorder packet rejected! (seq ${t} <= lastSeq ${n}) from ${s.fromName||r}`));const i=s.roomId||e.ws.getCurrentRoom(),a=`term-input:${s.sessionId}:${r}:${i}:${t}`;try{const o=require("crypto"),n=require("../../core/crypto/e2ee-vault"),c=e._aesKey||n.getRoomKey(i),l=Buffer.from(s.iv,"hex"),h=Buffer.from(s.authTag,"hex"),d=Buffer.from(s.ciphertext,"hex"),u=o.createDecipheriv("aes-256-gcm",c,l);u.setAuthTag(h),u.setAAD(Buffer.from(a,"utf8"));const m=Buffer.concat([u.update(d),u.final()]).toString("utf8");e._shareWriterSeqs.set(r,t),console.log(chalk.blue(`\n [${s.fromName||"Writer"}] (E2EE seq:${t}) → ${m}`)),e.execute(m.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")}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()}
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()}
@@ -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(),i=/^(npm|pnpm|yarn|bun)\s+(i|install|add)\s+/i.test(t),s=/^(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);(i||s||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?.id||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",{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,i=null;const s=e.split("\n");for(const e of s){const s=e.trim();if(s&&!s.startsWith("#"))if("dependencies:"!==s)if("addons:"!==s)if("scripts:"!==s)if("environment:"!==s)if("runtime:"!==s)if("execution:"!==s){if("dependencies"===t)s.endsWith(":")?(i=s.replace(":",""),n.dependencies[i]||(n.dependencies[i]=[])):s.startsWith("-")&&i&&n.dependencies[i].push(s.replace(/^-\s*/,"").replace(/["']/g,""));else if("addons"===t)s.startsWith("-")&&n.addons.push(s.replace(/^-\s*/,"").replace(/["']/g,""));else if("scripts"===t){const e=s.split(":");if(e.length>=2){const t=e[0].trim(),i=e.slice(1).join(":").trim().replace(/["']/g,"");n.scripts[t]=i}}else if("environment"===t){const e=s.split(":");if(e.length>=2){const t=e[0].trim(),i=e.slice(1).join(":").trim().replace(/["']/g,"");n.environment[t]=i}}else if("runtime"===t){const e=s.split(":");if(e.length>=2){const t=e[0].trim(),i=e.slice(1).join(":").trim().replace(/["']/g,"");"primary"!==t&&"type"!==t||(n.runtime.primary=i),"version"===t&&(n.runtime.version=i)}}}else t="execution",i=null;else t="runtime",i=null;else t="environment",i=null;else t="scripts",i=null;else t="addons",i=null;else t="dependencies",i=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 i of t)e.dependencies?.[i]?.length>0&&(n+=` ${i}:\n`,e.dependencies[i].forEach(e=>n+=` - "${e}"\n`));n+="\nscripts:\n";for(const[t,i]of Object.entries(e.scripts||{}))n+=` ${t}: "${i}"\n`;n+="\nenvironment:\n";for(const[t,i]of Object.entries(e.environment||{}))n+=` ${t}: "${i}"\n`;return n+=`\nexecution:\n isolation: "${e.execution?.isolation||"auto"}"\n memory_limit: "${e.execution?.memory_limit||"2g"}"\n`,n}}module.exports=DependencyTracker;
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;
@@ -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"),os=require("os"),chalk=require("chalk"),net=require("net"),{spawn}=require("child_process"),EventEmitter=require("events"),WebSocketManager=require("./websocket"),CommandRegistry=require("../commands"),ThinkNCollabAPI=require("./api-client"),{loadProjectConfig,hasProjectConfig}=require("./project-config"),{SandboxEngine}=require("./sandbox");function getAvailablePort(e){return new Promise(t=>{const o=net.createServer();o.listen(e,"127.0.0.1",()=>{const e=o.address().port;o.close(()=>t(e))}),o.on("error",()=>t(getAvailablePort(e+1)))})}class ThinkNCollabShell extends EventEmitter{constructor(e={}){super(),this.config={historyFile:path.join(os.homedir(),".thinknsh_history"),configDir:path.join(os.homedir(),".thinkncollab"),maxHistory:1e3,websocket:{serverUrl:e.serverUrl||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com",autoConnect:e.autoConnect||!1},...e},fs.ensureDirSync(this.config.configDir),this.ws=new WebSocketManager({...this.config.websocket,_shellRef:this}),this.currentDir=process.cwd(),this.username=os.userInfo().username,this.history=[],this.running=!1,this.notifyClients=new Set,this.tcpServer=null,this.api=new ThinkNCollabAPI({apiUrl:e.apiUrl||process.env.THINKNCOLLAB_API_URL||e.serverUrl||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com",wsUrl:e.wsUrl||process.env.THINKNCOLLAB_WS_URL||e.serverUrl||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com"}),this.api.getCurrentRoom=()=>{const e=this.ws.getCurrentRoom();return e?{id:e,name:e}:null},this.commands=new Map,this.aliases=new Map,this.variables=new Map,this.sandboxEngine=new SandboxEngine,this.activeSandbox=null,this.api.isAuthenticated()&&this.ws.setUser(this.api.getUser(),this.api.getToken()),this.loadHistory(),this.registerBuiltinCommands(),this.commandRegistry=new CommandRegistry(this),this.setupWebSocketHandlers()}async startTCPServer(){this.notifyPort=await getAvailablePort(7379);const e=require("./notify-auth");try{this.notifyToken=e.writeToken(this.notifyPort)}catch(e){this.notifyToken=null}return new Promise(t=>{this.tcpServer=net.createServer(t=>{t.on("error",()=>this.notifyClients.delete(t)),this.notifyToken?e.guard(t,this.notifyToken,()=>{this.notifyClients.add(t),t.on("close",()=>this.notifyClients.delete(t))}):t.destroy()}),this.tcpServer.listen(this.notifyPort,"127.0.0.1",()=>t()),this.tcpServer.on("error",e=>{console.log(chalk.yellow(`[Notice] Notification server: ${e.message}`)),t()})})}pushNotification(e){if(0===this.notifyClients.size)return;const t=JSON.stringify(e)+"\n";for(const e of this.notifyClients)try{e.write(t)}catch{}if("darwin"===process.platform)try{let t="";if("message"===e.type?t=`${e.from}: ${e.text}`:"task"===e.type?t=e.message||e.title||"Task update":"task_assigned"===e.type?t=`Task assigned: ${e.taskTitle||""}`:"verdict"===e.type?t=`Verdict [${e.passed?"PASSED":"FAILED"}]: ${e.title||""}`:"notification"===e.type&&(t=`${e.title||""}: ${e.message||""}`),t){const e=t.replace(/["\\]/g," ").slice(0,150);spawn("osascript",["-e",`display notification "${e}" with title "ThinkNCollab"`],{detached:!0,stdio:"ignore"}).unref()}}catch(e){}(this.isVSCode||0===this.notifyClients.size)&&this.printInlineNotification(e)}printInlineNotification(e){const t=chalk.dim(`[${(new Date).toLocaleTimeString()}]`);switch(e.type){case"message":console.log(`\n${t} ${chalk.cyan("[Chat]")} ${chalk.bold(e.from)}: ${e.text}`);break;case"task_assigned":console.log(`\n${t} ${chalk.yellow("[Task]")} Assigned: ${chalk.white(e.taskTitle)} by ${chalk.cyan(e.assignedBy||"Admin")}`);break;case"task_completed":console.log(`\n${t} ${chalk.green("[Task Done]")} ${chalk.white(e.message||e.taskTitle)}`);break;case"verdict":break;case"notification":{const o="error"===e.level?chalk.red:"warn"===e.level?chalk.yellow:chalk.blue;console.log(`\n${t} ${o(`[Alert: ${e.title||"Info"}]`)} ${e.message}`);break}case"userJoined":console.log(`\n${t} ${chalk.dim(`[User+] ${e.username} joined`)}`);break;case"userLeft":console.log(`\n${t} ${chalk.dim(`[User-] ${e.username} left`)}`)}}spawnNotificationWindow(){const e=path.join(__dirname,"notification-window.js"),t=this.notifyPort,o={...process.env,THINKNSH_NOTIFY_PORT:String(t)},s="vscode"===process.env.TERM_PROGRAM||"1"===process.env.VSCODE_INJECTION||Boolean(process.env.VSCODE_PID)||Boolean(process.env.VSCODE_GIT_IPC_HANDLE)||Boolean(process.env.VSCODE_IPC_HOOK_CLI);if(this.isVSCode=s,s)return console.log(chalk.cyan(` [VS Code Integrated Mode] Live notification server active on port ${t}`)),console.log(chalk.dim(" - Inline notifications: ENABLED (alerts & chat display directly in VS Code)")),void console.log(chalk.dim(' - Split panel view: Run "thinknsh notifications" in a split terminal tab (Cmd+\\)\n'));const n="linux"===process.platform&&(()=>{try{return fs.readFileSync("/proc/version","utf8").toLowerCase().includes("microsoft")}catch{return!1}})(),i=()=>{console.log(chalk.yellow("\n [Notice] Could not open notification window.")),console.log(chalk.dim(" Open a new terminal and run:")),console.log(chalk.cyan(` THINKNSH_NOTIFY_PORT=${t} node "${e}"\n`))};try{if(n){const s=e.replace(/^\/mnt\/([a-z])/,(e,t)=>`${t.toUpperCase()}:`).replace(/\//g,"\\"),n=e=>{const t=spawn(`start cmd.exe /k ""${e}" "${s}""`,[],{detached:!0,stdio:"ignore",env:o,shell:!0});t.on("error",i),t.unref()},a=["/mnt/c/Program Files/nodejs/node.exe","/mnt/c/Program Files (x86)/nodejs/node.exe"].find(e=>{try{return fs.existsSync(e)}catch{return!1}});if(a){const e=a.replace(/^\/mnt\/([a-z])/,(e,t)=>`${t.toUpperCase()}:`).replace(/\//g,"\\");n(e)}else{const o=spawn("cmd.exe",["/c","where","node"],{stdio:["ignore","pipe","ignore"]});let s="";o.stdout.on("data",e=>s+=e),o.on("close",()=>{const o=s.trim().split("\n")[0]?.trim();if(o?.endsWith(".exe"))n(o);else{const o=spawn(`start cmd.exe /k "wsl.exe -- bash -c 'THINKNSH_NOTIFY_PORT=${t} node \\"${e}\\"; read'"`,[],{detached:!0,stdio:"ignore",shell:!0});o.on("error",i),o.unref()}})}}else if("win32"===process.platform){const s=process.execPath;if("vscode"===process.env.TERM_PROGRAM)console.log(chalk.yellow("\n VS Code detected — open a new terminal tab and run:")),console.log(chalk.cyan(` $env:THINKNSH_NOTIFY_PORT=${t}; node "${e}"\n`));else{const t=()=>{const t=spawn(`start cmd.exe /k ""${s}" "${e}""`,[],{detached:!0,stdio:"ignore",env:o,shell:!0});t.on("error",i),t.unref()},n=spawn("wt.exe",["cmd.exe","/k",`"${s}" "${e}"`],{detached:!0,stdio:"ignore",env:o});n.on("error",t),n.unref()}}else if("darwin"===process.platform){const o=process.execPath,s=`\n tell application "Terminal"\n launch\n do script "THINKNSH_NOTIFY_PORT=${t} '${o}' '${e}'"\n activate\n end tell\n `;try{spawn("osascript",["-e",s],{detached:!0,stdio:"ignore"}).unref()}catch{const s=`\n tell application "iTerm2"\n tell current window\n create tab with default profile\n tell current session of current tab\n write text "THINKNSH_NOTIFY_PORT=${t} '${o}' '${e}'"\n end tell\n end tell\n end tell\n `;try{spawn("osascript",["-e",s],{detached:!0,stdio:"ignore"}).unref()}catch{console.log(chalk.yellow(" [Notice] Could not auto-open notification window.")),console.log(chalk.dim(` Open Terminal.app and run: THINKNSH_NOTIFY_PORT=${t} node "${e}"`))}}}else{const s=process.execPath,n=`THINKNSH_NOTIFY_PORT=${t} '${s}' '${e}'; read`,a=[["gnome-terminal",["--","bash","-c",n]],["konsole",["--noclose","-e","bash","-c",n]],["xfce4-terminal",["--hold","-e",`bash -c "${n}"`]],["tilix",["-e",`bash -c "${n}"`]],["alacritty",["-e","bash","-c",n]],["kitty",["bash","-c",n]],["xterm",["-hold","-e",`bash -c "${n}"`]]];let c=!1;for(const[e,t]of a)try{spawn(e,t,{detached:!0,stdio:"ignore",env:o}).unref(),c=!0;break}catch{}c||i()}}catch(e){i()}}setupWebSocketHandlers(){this.ws.on("connected",e=>{this.api.logEvent("connected",null,{socketId:e?.socketId}),this.api.startHeartbeat(),this.pushNotification({type:"connected",socketId:e?.socketId})}),this.ws.on("disconnected",e=>{this.api.logEvent("disconnected",null,{reason:e?.reason}),this.api.stopHeartbeat(),this.pushNotification({type:"disconnected",reason:e?.reason})}),this.ws.on("reconnected",()=>{this.api.logEvent("reconnected"),this.api.startHeartbeat(),this.pushNotification({type:"connected",socketId:"reconnected"})}),this.ws.on("task:verdict",e=>{const t=e.passed,o=t?chalk.green("─".repeat(52)):chalk.red("─".repeat(52)),s=t?chalk.bgGreen.black(" PASS "):chalk.bgRed.white(" FAIL ");console.log("\n"+o),console.log(` ${s} ${chalk.bold(e.title||"Task")} ${t?chalk.green("PASSED"):chalk.red("FAILED")}`),console.log(` ${chalk.dim("Reason :")} ${t?chalk.green(e.reason):chalk.red(e.reason)}`),e.diff&&(console.log(` ${chalk.dim("Expected:")} ${chalk.cyan(JSON.stringify(e.diff.expected??e.diff.conditions))}`),console.log(` ${chalk.dim("Actual :")} ${chalk.yellow(JSON.stringify(e.diff.actual))}`)),console.log(` ${chalk.dim("At :")} ${chalk.dim(new Date(e.at||Date.now()).toLocaleTimeString())}`),console.log(o+"\n"),this.pushNotification({type:"verdict",passed:t,title:e.title,reason:e.reason,taskId:e.taskId,at:e.at})}),this.ws.on("message",e=>{const t=this.api.getUser()?._id;if(t&&e.userId===t)return;const o=e.name||e.username||e.sender?.name||"Someone",s=e.message||e.content||e.text||"",n=chalk.dim(`[${(new Date).toLocaleTimeString()}]`),i=e.isE2EE?chalk.green(" [🔒 E2EE]"):"";console.log(`\n${n}${i} ${chalk.cyan(o+":")} ${chalk.white(s)}`),this.pushNotification({type:"message",from:o,text:s})}),this.ws.on("messageHistory",e=>{e?.length&&(console.log(chalk.dim("\n ─── Room History ───────────────────────────────")),e.forEach(e=>{const t=e.name||e.username||e.sender?.name||"Unknown",o=e.message||e.content||e.text||"",s=e.timestamp||e.createdAt,n=s?chalk.dim(`[${new Date(s).toLocaleTimeString()}]`):"",i=e.isE2EE?chalk.green(" [🔒 E2EE]"):"";console.log(` ${n}${i} ${chalk.cyan(t+":")} ${chalk.white(o)}`)}),console.log(chalk.dim(" ────────────────────────────────────────────────\n")))}),this.ws.on("userJoined",e=>{console.log(chalk.dim(`\n [User+] ${e.username} joined the room`)),this.pushNotification({type:"userJoined",username:e.username})}),this.ws.on("userLeft",e=>{console.log(chalk.dim(`\n [User-] ${e.username} left the room`)),this.pushNotification({type:"userLeft",username:e.username})}),this.ws.on("joined",e=>this.api.logEvent("room_join",e)),this.ws.on("left",e=>this.api.logEvent("room_leave",e)),this.ws.on("notification",e=>{const t=chalk.dim(`[${(new Date).toLocaleTimeString()}]`),o={success:chalk.green,warning:chalk.yellow,error:chalk.red,info:chalk.blue}[e.level]||chalk.blue;console.log(`\n${t} [ALERT] ${o(`${e.title||"Notification"}: ${e.message||""}`)}`),this.pushNotification({type:e.type||"notification",level:e.level,title:e.title,message:e.message,taskTitle:e.taskTitle,assignedBy:e.assignedBy,meta:e.meta})});let e=0;this.ws.on("error",t=>{const o=Date.now();o-e>3e4&&(e=o,console.log(chalk.yellow(`[Notice] WebSocket reconnecting: ${t.message}`))),this.pushNotification({type:"notification",level:"error",title:"WebSocket Error",message:t.message})}),this.ws.onSandboxAction(this.sandboxEngine,(e,t)=>{t?.success?console.log(chalk.cyan(`\n[Web Dashboard Action] ${e.toUpperCase()} completed: ${t.message}`)):console.log(chalk.yellow(`\n[Web Dashboard Action] ${e.toUpperCase()} failed: ${t?.message}`))})}async start(){this.showWelcome(),await this.startTCPServer(),console.log(chalk.cyan(` Notification server active on port ${chalk.bold(this.notifyPort)}`)),console.log(chalk.dim(" To open live notification panel in another tab/terminal:")),console.log(chalk.white(` thinknsh notify ${this.notifyPort}\n`)),this.running=!0,process.on("SIGINT",()=>console.log(chalk.yellow('\nUse "exit" to quit'))),!await this._bootFromProjectConfig()&&this.config.websocket.autoConnect&&await this.ws.connect().catch(()=>{});const{input:e}=require("@inquirer/prompts");for(;this.running;)try{const t=await e({message:this.getPromptText(),required:!1}).catch(()=>null);if(null===t){console.log(chalk.yellow('Use "exit" to quit'));continue}const o=t.trim();if(!o)continue;this.history.push(o),this.saveHistory(),await this.execute(o)}catch(e){"ExitPromptError"===e.name||e.message?.includes("force closed")?console.log(chalk.yellow('Use "exit" to quit')):console.log(chalk.red(`Shell error: ${e.message}`))}}async _bootFromProjectConfig(){if(!hasProjectConfig())return!1;let e,t;try{e=loadProjectConfig()}catch(e){return console.log(chalk.yellow(`[Notice] tncproject: ${e.message}\n`)),!1}console.log(chalk.dim(" [Project] tncproject found")),console.log(chalk.dim(` Room : ${e.roomId}`)),console.log(chalk.dim(` File : ${e.filePath}\n`));try{t=await this.api._request("POST","/config/verify-config",{fileBase64:e.fileBase64,roomId:e.roomId,machineId:os.hostname()})}catch(e){return console.log(chalk.red(`[Error] tncproject auth failed: ${e.message}`)),console.log(chalk.dim(" Falling back to manual login.\n")),!1}this.api._saveSession({token:t.shellToken,shellSessionId:t.shellSessionId||null,user:{_id:t._id.toString(),userId:t._id.toString(),email:t.email,name:t.name,userType:"User"},timestamp:(new Date).toISOString()}),this.ws.setUser(this.api.getUser(),t.shellToken);try{await this.ws.connect(t.shellToken)}catch(e){return console.log(chalk.yellow(`[Notice] WebSocket: ${e.message}`)),console.log(chalk.green(`[Auth] ${t.name} logged in (offline mode)\n`)),!0}try{await this.ws.joinRoom(t.roomId.toString()),console.log(chalk.green(`[Room] ${t.name} -> room "${t.roomName||t.roomId}"\n`))}catch(e){console.log(chalk.yellow(`[Notice] Could not auto-join room: ${e.message}`)),console.log(chalk.dim(' Use "join <room-id>" manually.\n'))}return!0}async execute(e,t=null){if(!(e.trim()||t&&0!==t.length))return;const o=t&&t.length>0?t:this.parseCommand(e),s=o[0].toLowerCase(),n=o.slice(1);if(["login","logout","set","unset"].includes(s)||this.api.logEvent("command",null,{cmd:s}).catch(()=>{}),this.commands.has(s)){try{await this.commands.get(s).handler(n,this)}catch(e){console.log(chalk.red(`Error: ${e.message}`))}process.stdout.write("\r\n")}else{if(!this.commandRegistry||!await this.commandRegistry.execute(e,this,t))return"win32"===process.platform&&["open","which","grep","touch","cat"].includes(s)?(console.log(chalk.red(`'${s}' is not a thinknsh command.`)),console.log(chalk.dim(" Type 'help' to see available commands.")),void process.stdout.write("\r\n")):void await this.executeSystemCommand(e);this.commandRegistry.getCommand(s)?.selfExiting||process.stdout.write("\r\n")}}getPromptText(){let e="";const t=this.ws.getCurrentRoom?.();t&&(e+=`[${t}] `),this.ws.isConnected?.()&&(e+="● "),this.activeSandbox&&(e+=`[Sandbox: ${this.activeSandbox}] `);let o=this.currentDir;const s=os.homedir();return o="win32"===process.platform?o.toLowerCase()===s.toLowerCase()?"~":o.toLowerCase().startsWith(s.toLowerCase())?"~"+o.slice(s.length).replace(/\\/g,"/"):o.replace(/\\/g,"/"):o.replace(s,"~"),`${e}${this.username}@thinknsh:${o} $`}getPrompt(){return this.getPromptText()}printMessage(e){console.log(e)}registerBuiltinCommands(){const e=(e,t,o)=>this.commands.set(e,{handler:t,description:o});e("help",this.helpCommand.bind(this),"Show available commands"),e("exit",()=>{this.cleanup(),process.exit(0)},"Exit the shell"),e("quit",()=>{this.cleanup(),process.exit(0)},"Exit the shell"),e("clear",()=>console.clear(),"Clear screen"),e("history",this.historyCommand.bind(this),"Show command history"),e("cd",this.cdCommand.bind(this),"Change directory"),e("pwd",()=>console.log(process.cwd()),"Print working directory"),e("ls",this.lsCommand.bind(this),"List files"),e("use",e=>{const t=e[0];t?(this.activeSandbox=t,console.log(chalk.green(`\n[Sandbox] Switched active execution context to: [${t}]`)),console.log(chalk.dim(" All system commands will now execute within this isolated sandbox.\n"))):console.log(chalk.red("Usage: use <sandbox-id>"))},"Switch active shell context to a TNC sandbox"),e("leave-sandbox",()=>{this.activeSandbox?(console.log(chalk.cyan(`\nExited sandbox [${this.activeSandbox}]. Back to host shell.\n`)),this.activeSandbox=null):console.log(chalk.yellow("Not currently inside any sandbox context."))},"Exit active sandbox context"),e("connect",this.connectCommand.bind(this),"Connect to WebSocket"),e("disconnect",()=>this.ws.disconnect(),"Disconnect WebSocket"),e("status",this.statusCommand.bind(this),"Show connection status"),e("join",this.joinCommand.bind(this),"Join a room"),e("leave",()=>this.ws.leaveRoom?.(),"Leave current room"),e("rooms",this.roomsCommand.bind(this),"List rooms"),e("say",this.sayCommand.bind(this),"Send a message"),e("msg",this.sayCommand.bind(this),"Send a message"),e("set",this.setCommand.bind(this),"Set a variable"),e("unset",this.unsetCommand.bind(this),"Unset a variable"),this.addAlias("ll","ls -la"),this.addAlias("..","cd .."),this.addAlias("~","cd ~"),this.addAlias("unuse","leave-sandbox")}registerCommand(e,t,o=""){this.commands.set(e,{handler:t,description:o})}addAlias(e,t){this.aliases.set(e,t)}async helpCommand(){this.commandRegistry&&console.log(this.commandRegistry.getHelpText()),console.log(chalk.dim(" Any unrecognised command runs in your system shell\n"))}historyCommand(){this.history.forEach((e,t)=>console.log(`${(t+1).toString().padStart(4)} ${e}`))}async cdCommand(e){const t=e.join(" ").trim()||os.homedir();try{let e;if(e="~"===t?os.homedir():".."===t?path.dirname(this.currentDir):path.resolve(this.currentDir,t),e=path.normalize(e),!(await fs.stat(e)).isDirectory())return void console.log(chalk.red(`cd: not a directory: ${t}`));this.previousDir=this.currentDir,process.chdir(e),this.currentDir=process.cwd()}catch{console.log(chalk.red(`cd: no such directory: ${t}`))}}async lsCommand(e){try{const t=await fs.readdir(this.currentDir),o=e.includes("-a")?t:t.filter(e=>!e.startsWith("."));(await Promise.all(o.map(async e=>({name:e,isDir:(await fs.stat(path.join(this.currentDir,e))).isDirectory()})))).forEach(e=>e.isDir?console.log(chalk.blue(e.name+"/")):console.log(e.name))}catch(e){console.log(chalk.red(`ls: ${e.message}`))}}async connectCommand(){console.log(chalk.cyan("Connecting..."));try{await this.ws.connect()}catch(e){console.log(chalk.red(`Error: ${e.message}`))}}async statusCommand(){const e=this.ws.isConnected();console.log(chalk.cyan("\nStatus:")),console.log(` WebSocket : ${e?chalk.green("Connected"):chalk.red("Disconnected")}`),e&&console.log(` Socket ID : ${chalk.dim(this.ws.getSocketId())}`);const t=this.ws.getCurrentRoom();console.log(` Room : ${t?chalk.green(t):chalk.yellow("None")}`),console.log(` Auth : ${this.api.isAuthenticated()?chalk.green("Logged in"):chalk.yellow("Not logged in")}`);const o=this.api.getUser();o&&console.log(` User : ${chalk.dim(o.name||o.email)}`),console.log("")}async joinCommand(e){if(e[0]){this.ws.isConnected()||(console.log(chalk.yellow("Connecting...")),await this.ws.connect());try{const t=await this.ws.joinRoom(e[0]);console.log(chalk.green(`Joined: ${t.roomName||e[0]}`))}catch(e){console.log(chalk.red(`Error: ${e.message}`))}}else console.log(chalk.red("Usage: join <room-id>"))}async roomsCommand(){this.ws.isConnected()?(this.ws.socket.emit("rooms:list"),this.ws.socket.once("rooms:list",e=>{e.rooms?.length?e.rooms.forEach(e=>console.log(` ${chalk.cyan(e.name||e.id)} - Members: ${e.participantCount||0}`)):console.log(" No rooms")})):console.log(chalk.yellow("Not connected"))}async sayCommand(e){const t=e.join(" ");if(t)try{await this.ws.sendMessage(t);const e=this.api.getUser()?.name||this.username;console.log(chalk.green(`You: ${t}`)),this.pushNotification({type:"message",from:`You (${e})`,text:t})}catch(e){console.log(chalk.red(`Failed: ${e.message}`))}else console.log(chalk.red("Usage: say <message>"))}async setCommand(e){if(e.length)this.variables.set(e[0],e.slice(1).join(" ")),console.log(chalk.green(`${e[0]} = ${e.slice(1).join(" ")}`));else for(const[e,t]of this.variables)console.log(` ${chalk.green(e.padEnd(12))} = ${t}`)}async unsetCommand(e){e[0]&&this.variables.delete(e[0])}getSessionPath(){return path.join(this.config.configDir,"session.json")}parseCommand(e){const t=[];let o="",s=!1,n="",i=!1;for(const a of e)"\\"!==a||i||s?i?(o+=a,i=!1):'"'!==a&&"'"!==a||s?a===n&&s?(s=!1,n=""):" "!==a||s?o+=a:o&&(t.push(o),o=""):(s=!0,n=a):i=!0;return o&&t.push(o),t}executeSystemCommand(e){return new Promise(t=>{let o=e,s={...process.env},n=this.currentDir;if(this.activeSandbox){const t=this.sandboxEngine?.loadMeta(this.activeSandbox);t&&(s.TNC_SANDBOX_ID=this.activeSandbox,t.containerPort&&(s.PORT=String(t.containerPort))),console.log(chalk.cyan(`[TNC Sandbox: ${this.activeSandbox}] ${e}`))}const i=spawn(o,[],{cwd:n,shell:!0,stdio:"inherit",env:s});i.on("close",async o=>{try{this.currentDir=process.cwd()}catch{}if(0===o)try{const t=new(require("./dependency-tracker"))(n);await t.interceptCommand(e,this)}catch(e){}process.stdout.write("\r\n"),t()}),i.on("error",()=>{console.log(chalk.red(`Command not found: ${e}`)),t()})})}showWelcome(){try{const e=require("figlet");console.log(chalk.cyan(e.textSync("ThinkNCollab",{font:"Standard"})))}catch{}console.log(chalk.cyan("\n╔══════════════════════════════════════════════════════╗\n║ ThinkNCollab Shell v0.0.20 ║\n║ Type 'help' for commands ║\n╚══════════════════════════════════════════════════════╝"))}loadHistory(){try{fs.existsSync(this.config.historyFile)&&(this.history=fs.readFileSync(this.config.historyFile,"utf8").split("\n").filter(e=>e.trim()))}catch{}}saveHistory(){try{fs.writeFileSync(this.config.historyFile,this.history.slice(-this.config.maxHistory).join("\n"))}catch{}}cleanup(){if(this.activeSandbox)try{this.sandboxEngine.freeze(this.activeSandbox),console.log(chalk.cyan(`\n[TNC Smart Freeze] Auto-froze active sandbox [${this.activeSandbox}]. 0 RAM used!`))}catch(e){}if(this.api.logout(),this.tcpServer)try{this.tcpServer.close()}catch{}this.notifyPort&&require("./notify-auth").removeToken(this.notifyPort),this.ws&&this.ws.disconnect(),this.saveHistory()}}module.exports=ThinkNCollabShell;
7
+ const path=require("path"),fs=require("fs-extra"),os=require("os"),chalk=require("chalk"),net=require("net"),{spawn}=require("child_process"),EventEmitter=require("events"),WebSocketManager=require("./websocket"),CommandRegistry=require("../commands"),ThinkNCollabAPI=require("./api-client"),{loadProjectConfig,hasProjectConfig}=require("./project-config"),{DockSandbox}=require("../dock/sandbox"),foreground=require("./foreground");function getAvailablePort(e){return new Promise(o=>{const t=net.createServer();t.listen(e,"127.0.0.1",()=>{const e=t.address().port;t.close(()=>o(e))}),t.on("error",()=>o(getAvailablePort(e+1)))})}class ThinkNCollabShell extends EventEmitter{constructor(e={}){super(),this.config={historyFile:path.join(os.homedir(),".thinknsh_history"),configDir:path.join(os.homedir(),".thinkncollab"),maxHistory:1e3,websocket:{serverUrl:e.serverUrl||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com",autoConnect:e.autoConnect||!1},...e},fs.ensureDirSync(this.config.configDir),this.ws=new WebSocketManager({...this.config.websocket,_shellRef:this}),this.currentDir=process.cwd(),this.username=os.userInfo().username,this.history=[],this.running=!1,this.notifyClients=new Set,this.tcpServer=null,this.api=new ThinkNCollabAPI({apiUrl:e.apiUrl||process.env.THINKNCOLLAB_API_URL||e.serverUrl||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com",wsUrl:e.wsUrl||process.env.THINKNCOLLAB_WS_URL||e.serverUrl||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com"}),this.api.getCurrentRoom=()=>{const e=this.ws.getCurrentRoom();return e?{id:e,name:e}:null},this.commands=new Map,this.aliases=new Map,this.variables=new Map,this.sandboxEngine=new DockSandbox,this.activeSandbox=null,this.api.isAuthenticated()&&this.ws.setUser(this.api.getUser(),this.api.getToken()),this.loadHistory(),this.registerBuiltinCommands(),this.commandRegistry=new CommandRegistry(this),this.setupWebSocketHandlers()}async startTCPServer(){this.notifyPort=await getAvailablePort(7379);const e=require("./notify-auth");try{this.notifyToken=e.writeToken(this.notifyPort)}catch(e){this.notifyToken=null}return new Promise(o=>{this.tcpServer=net.createServer(o=>{o.on("error",()=>this.notifyClients.delete(o)),this.notifyToken?e.guard(o,this.notifyToken,()=>{this.notifyClients.add(o),o.on("close",()=>this.notifyClients.delete(o))}):o.destroy()}),this.tcpServer.listen(this.notifyPort,"127.0.0.1",()=>o()),this.tcpServer.on("error",e=>{console.log(chalk.yellow(`[Notice] Notification server: ${e.message}`)),o()})})}pushNotification(e){if(0===this.notifyClients.size)return;const o=JSON.stringify(e)+"\n";for(const e of this.notifyClients)try{e.write(o)}catch{}if("darwin"===process.platform)try{let o="";if("message"===e.type?o=`${e.from}: ${e.text}`:"task"===e.type?o=e.message||e.title||"Task update":"task_assigned"===e.type?o=`Task assigned: ${e.taskTitle||""}`:"verdict"===e.type?o=`Verdict [${e.passed?"PASSED":"FAILED"}]: ${e.title||""}`:"notification"===e.type&&(o=`${e.title||""}: ${e.message||""}`),o){const e=o.replace(/["\\]/g," ").slice(0,150);spawn("osascript",["-e",`display notification "${e}" with title "ThinkNCollab"`],{detached:!0,stdio:"ignore"}).unref()}}catch(e){}(this.isVSCode||0===this.notifyClients.size)&&this.printInlineNotification(e)}printInlineNotification(e){const o=chalk.dim(`[${(new Date).toLocaleTimeString()}]`);switch(e.type){case"message":console.log(`\n${o} ${chalk.cyan("[Chat]")} ${chalk.bold(e.from)}: ${e.text}`);break;case"task_assigned":console.log(`\n${o} ${chalk.yellow("[Task]")} Assigned: ${chalk.white(e.taskTitle)} by ${chalk.cyan(e.assignedBy||"Admin")}`);break;case"task_completed":console.log(`\n${o} ${chalk.green("[Task Done]")} ${chalk.white(e.message||e.taskTitle)}`);break;case"verdict":break;case"notification":{const t="error"===e.level?chalk.red:"warn"===e.level?chalk.yellow:chalk.blue;console.log(`\n${o} ${t(`[Alert: ${e.title||"Info"}]`)} ${e.message}`);break}case"userJoined":console.log(`\n${o} ${chalk.dim(`[User+] ${e.username} joined`)}`);break;case"userLeft":console.log(`\n${o} ${chalk.dim(`[User-] ${e.username} left`)}`)}}spawnNotificationWindow(){const e=path.join(__dirname,"notification-window.js"),o=this.notifyPort,t={...process.env,THINKNSH_NOTIFY_PORT:String(o)},s="vscode"===process.env.TERM_PROGRAM||"1"===process.env.VSCODE_INJECTION||Boolean(process.env.VSCODE_PID)||Boolean(process.env.VSCODE_GIT_IPC_HANDLE)||Boolean(process.env.VSCODE_IPC_HOOK_CLI);if(this.isVSCode=s,s)return console.log(chalk.cyan(` [VS Code Integrated Mode] Live notification server active on port ${o}`)),console.log(chalk.dim(" - Inline notifications: ENABLED (alerts & chat display directly in VS Code)")),void console.log(chalk.dim(' - Split panel view: Run "thinknsh notifications" in a split terminal tab (Cmd+\\)\n'));const n="linux"===process.platform&&(()=>{try{return fs.readFileSync("/proc/version","utf8").toLowerCase().includes("microsoft")}catch{return!1}})(),i=()=>{console.log(chalk.yellow("\n [Notice] Could not open notification window.")),console.log(chalk.dim(" Open a new terminal and run:")),console.log(chalk.cyan(` THINKNSH_NOTIFY_PORT=${o} node "${e}"\n`))};try{if(n){const s=e.replace(/^\/mnt\/([a-z])/,(e,o)=>`${o.toUpperCase()}:`).replace(/\//g,"\\"),n=e=>{const o=spawn(`start cmd.exe /k ""${e}" "${s}""`,[],{detached:!0,stdio:"ignore",env:t,shell:!0});o.on("error",i),o.unref()},a=["/mnt/c/Program Files/nodejs/node.exe","/mnt/c/Program Files (x86)/nodejs/node.exe"].find(e=>{try{return fs.existsSync(e)}catch{return!1}});if(a){const e=a.replace(/^\/mnt\/([a-z])/,(e,o)=>`${o.toUpperCase()}:`).replace(/\//g,"\\");n(e)}else{const t=spawn("cmd.exe",["/c","where","node"],{stdio:["ignore","pipe","ignore"]});let s="";t.stdout.on("data",e=>s+=e),t.on("close",()=>{const t=s.trim().split("\n")[0]?.trim();if(t?.endsWith(".exe"))n(t);else{const t=spawn(`start cmd.exe /k "wsl.exe -- bash -c 'THINKNSH_NOTIFY_PORT=${o} node \\"${e}\\"; read'"`,[],{detached:!0,stdio:"ignore",shell:!0});t.on("error",i),t.unref()}})}}else if("win32"===process.platform){const s=process.execPath;if("vscode"===process.env.TERM_PROGRAM)console.log(chalk.yellow("\n VS Code detected — open a new terminal tab and run:")),console.log(chalk.cyan(` $env:THINKNSH_NOTIFY_PORT=${o}; node "${e}"\n`));else{const o=()=>{const o=spawn(`start cmd.exe /k ""${s}" "${e}""`,[],{detached:!0,stdio:"ignore",env:t,shell:!0});o.on("error",i),o.unref()},n=spawn("wt.exe",["cmd.exe","/k",`"${s}" "${e}"`],{detached:!0,stdio:"ignore",env:t});n.on("error",o),n.unref()}}else if("darwin"===process.platform){const t=process.execPath,s=`\n tell application "Terminal"\n launch\n do script "THINKNSH_NOTIFY_PORT=${o} '${t}' '${e}'"\n activate\n end tell\n `;try{spawn("osascript",["-e",s],{detached:!0,stdio:"ignore"}).unref()}catch{const s=`\n tell application "iTerm2"\n tell current window\n create tab with default profile\n tell current session of current tab\n write text "THINKNSH_NOTIFY_PORT=${o} '${t}' '${e}'"\n end tell\n end tell\n end tell\n `;try{spawn("osascript",["-e",s],{detached:!0,stdio:"ignore"}).unref()}catch{console.log(chalk.yellow(" [Notice] Could not auto-open notification window.")),console.log(chalk.dim(` Open Terminal.app and run: THINKNSH_NOTIFY_PORT=${o} node "${e}"`))}}}else{const s=process.execPath,n=`THINKNSH_NOTIFY_PORT=${o} '${s}' '${e}'; read`,a=[["gnome-terminal",["--","bash","-c",n]],["konsole",["--noclose","-e","bash","-c",n]],["xfce4-terminal",["--hold","-e",`bash -c "${n}"`]],["tilix",["-e",`bash -c "${n}"`]],["alacritty",["-e","bash","-c",n]],["kitty",["bash","-c",n]],["xterm",["-hold","-e",`bash -c "${n}"`]]];let c=!1;for(const[e,o]of a)try{spawn(e,o,{detached:!0,stdio:"ignore",env:t}).unref(),c=!0;break}catch{}c||i()}}catch(e){i()}}setupWebSocketHandlers(){this.ws.on("connected",e=>{this.api.logEvent("connected",null,{socketId:e?.socketId}),this.api.startHeartbeat(),this.pushNotification({type:"connected",socketId:e?.socketId})}),this.ws.on("disconnected",e=>{this.api.logEvent("disconnected",null,{reason:e?.reason}),this.api.stopHeartbeat(),this.pushNotification({type:"disconnected",reason:e?.reason})}),this.ws.on("reconnected",()=>{this.api.logEvent("reconnected"),this.api.startHeartbeat(),this.pushNotification({type:"connected",socketId:"reconnected"})}),this.ws.on("task:verdict",e=>foreground.whenFree(()=>{const o=e.passed,t=o?chalk.green("─".repeat(52)):chalk.red("─".repeat(52)),s=o?chalk.bgGreen.black(" PASS "):chalk.bgRed.white(" FAIL ");console.log("\n"+t),console.log(` ${s} ${chalk.bold(e.title||"Task")} ${o?chalk.green("PASSED"):chalk.red("FAILED")}`),console.log(` ${chalk.dim("Reason :")} ${o?chalk.green(e.reason):chalk.red(e.reason)}`),e.diff&&(console.log(` ${chalk.dim("Expected:")} ${chalk.cyan(JSON.stringify(e.diff.expected??e.diff.conditions))}`),console.log(` ${chalk.dim("Actual :")} ${chalk.yellow(JSON.stringify(e.diff.actual))}`)),console.log(` ${chalk.dim("At :")} ${chalk.dim(new Date(e.at||Date.now()).toLocaleTimeString())}`),console.log(t+"\n"),this.pushNotification({type:"verdict",passed:o,title:e.title,reason:e.reason,taskId:e.taskId,at:e.at})})),this.ws.on("message",e=>foreground.whenFree(()=>{const o=this.api.getUser()?._id;if(o&&e.userId===o)return;const t=e.name||e.username||e.sender?.name||"Someone",s=e.message||e.content||e.text||"",n=chalk.dim(`[${(new Date).toLocaleTimeString()}]`),i=e.isE2EE?chalk.green(" [🔒 E2EE]"):"";console.log(`\n${n}${i} ${chalk.cyan(t+":")} ${chalk.white(s)}`),this.pushNotification({type:"message",from:t,text:s})})),this.ws.on("messageHistory",e=>foreground.whenFree(()=>{e?.length&&(console.log(chalk.dim("\n ─── Room History ───────────────────────────────")),e.forEach(e=>{const o=e.name||e.username||e.sender?.name||"Unknown",t=e.message||e.content||e.text||"",s=e.timestamp||e.createdAt,n=s?chalk.dim(`[${new Date(s).toLocaleTimeString()}]`):"",i=e.isE2EE?chalk.green(" [🔒 E2EE]"):"";console.log(` ${n}${i} ${chalk.cyan(o+":")} ${chalk.white(t)}`)}),console.log(chalk.dim(" ────────────────────────────────────────────────\n")))})),this.ws.on("userJoined",e=>foreground.whenFree(()=>{console.log(chalk.dim(`\n [User+] ${e.username} joined the room`)),this.pushNotification({type:"userJoined",username:e.username})})),this.ws.on("userLeft",e=>foreground.whenFree(()=>{console.log(chalk.dim(`\n [User-] ${e.username} left the room`)),this.pushNotification({type:"userLeft",username:e.username})})),this.ws.on("joined",e=>this.api.logEvent("room_join",e)),this.ws.on("left",e=>this.api.logEvent("room_leave",e)),this.ws.on("notification",e=>foreground.whenFree(()=>{const o=chalk.dim(`[${(new Date).toLocaleTimeString()}]`),t={success:chalk.green,warning:chalk.yellow,error:chalk.red,info:chalk.blue}[e.level]||chalk.blue;console.log(`\n${o} [ALERT] ${t(`${e.title||"Notification"}: ${e.message||""}`)}`),this.pushNotification({type:e.type||"notification",level:e.level,title:e.title,message:e.message,taskTitle:e.taskTitle,assignedBy:e.assignedBy,meta:e.meta})}));let e=0;this.ws.on("error",o=>{const t=Date.now();t-e>3e4&&(e=t,console.log(chalk.yellow(`[Notice] WebSocket reconnecting: ${o.message}`))),this.pushNotification({type:"notification",level:"error",title:"WebSocket Error",message:o.message})}),this.ws.onSandboxAction(this.sandboxEngine,(e,o)=>{o?.success?console.log(chalk.cyan(`\n[Web Dashboard Action] ${e.toUpperCase()} completed: ${o.message}`)):console.log(chalk.yellow(`\n[Web Dashboard Action] ${e.toUpperCase()} failed: ${o?.message}`))})}async start(){this.showWelcome(),await this.startTCPServer(),console.log(chalk.cyan(` Notification server active on port ${chalk.bold(this.notifyPort)}`)),console.log(chalk.dim(" To open live notification panel in another tab/terminal:")),console.log(chalk.white(` thinknsh notify ${this.notifyPort}\n`)),this.running=!0,process.on("SIGINT",()=>console.log(chalk.yellow('\nUse "exit" to quit'))),!await this._bootFromProjectConfig()&&this.config.websocket.autoConnect&&await this.ws.connect().catch(()=>{});const{input:e}=require("@inquirer/prompts");for(;this.running;)try{const o=await e({message:this.getPromptText(),required:!1}).catch(()=>null);if(null===o){console.log(chalk.yellow('Use "exit" to quit'));continue}const t=o.trim();if(!t)continue;this.history.push(t),this.saveHistory(),await this.execute(t)}catch(e){"ExitPromptError"===e.name||e.message?.includes("force closed")?console.log(chalk.yellow('Use "exit" to quit')):console.log(chalk.red(`Shell error: ${e.message}`))}}async _bootFromProjectConfig(){if(!hasProjectConfig())return!1;let e,o;try{e=loadProjectConfig()}catch(e){return console.log(chalk.yellow(`[Notice] tncproject: ${e.message}\n`)),!1}console.log(chalk.dim(" [Project] tncproject found")),console.log(chalk.dim(` Room : ${e.roomId}`)),console.log(chalk.dim(` File : ${e.filePath}\n`));try{o=await this.api._request("POST","/config/verify-config",{fileBase64:e.fileBase64,roomId:e.roomId,machineId:os.hostname()})}catch(e){return console.log(chalk.red(`[Error] tncproject auth failed: ${e.message}`)),console.log(chalk.dim(" Falling back to manual login.\n")),!1}this.api._saveSession({token:o.shellToken,shellSessionId:o.shellSessionId||null,user:{_id:o._id.toString(),userId:o._id.toString(),email:o.email,name:o.name,userType:"User"},timestamp:(new Date).toISOString()}),this.ws.setUser(this.api.getUser(),o.shellToken);try{await this.ws.connect(o.shellToken)}catch(e){return console.log(chalk.yellow(`[Notice] WebSocket: ${e.message}`)),console.log(chalk.green(`[Auth] ${o.name} logged in (offline mode)\n`)),!0}try{await this.ws.joinRoom(o.roomId.toString()),console.log(chalk.green(`[Room] ${o.name} -> room "${o.roomName||o.roomId}"\n`))}catch(e){console.log(chalk.yellow(`[Notice] Could not auto-join room: ${e.message}`)),console.log(chalk.dim(' Use "join <room-id>" manually.\n'))}return!0}async execute(e,o=null){if(!(e.trim()||o&&0!==o.length))return;const t=o&&o.length>0?o:this.parseCommand(e),s=t[0].toLowerCase(),n=t.slice(1);if(["login","logout","set","unset"].includes(s)||this.api.logEvent("command",null,{cmd:s}).catch(()=>{}),this.commands.has(s)){try{await this.commands.get(s).handler(n,this)}catch(e){console.log(chalk.red(`Error: ${e.message}`))}process.stdout.write("\r\n")}else{if(!this.commandRegistry||!await this.commandRegistry.execute(e,this,o))return"win32"===process.platform&&["open","which","grep","touch","cat"].includes(s)?(console.log(chalk.red(`'${s}' is not a thinknsh command.`)),console.log(chalk.dim(" Type 'help' to see available commands.")),void process.stdout.write("\r\n")):void await this.executeSystemCommand(e);this.commandRegistry.getCommand(s)?.selfExiting||process.stdout.write("\r\n")}}getPromptText(){let e="";const o=this.ws.getCurrentRoom?.();o&&(e+=`[${o}] `),this.ws.isConnected?.()&&(e+="● "),this.activeSandbox&&(e+=`[Sandbox: ${this.activeSandbox}] `);let t=this.currentDir;const s=os.homedir();return t="win32"===process.platform?t.toLowerCase()===s.toLowerCase()?"~":t.toLowerCase().startsWith(s.toLowerCase())?"~"+t.slice(s.length).replace(/\\/g,"/"):t.replace(/\\/g,"/"):t.replace(s,"~"),`${e}${this.username}@thinknsh:${t} $`}getPrompt(){return this.getPromptText()}printMessage(e){console.log(e)}registerBuiltinCommands(){const e=(e,o,t)=>this.commands.set(e,{handler:o,description:t});e("help",this.helpCommand.bind(this),"Show available commands"),e("exit",()=>{this.cleanup(),process.exit(0)},"Exit the shell"),e("quit",()=>{this.cleanup(),process.exit(0)},"Exit the shell"),e("clear",()=>console.clear(),"Clear screen"),e("history",this.historyCommand.bind(this),"Show command history"),e("cd",this.cdCommand.bind(this),"Change directory"),e("pwd",()=>console.log(process.cwd()),"Print working directory"),e("ls",this.lsCommand.bind(this),"List files"),e("use",e=>{const o=e[0];o?(this.activeSandbox=o,console.log(chalk.green(`\n[Sandbox] Switched active execution context to: [${o}]`)),console.log(chalk.dim(" System commands now run inside its container (leave-sandbox to go back).\n"))):console.log(chalk.red("Usage: use <sandbox-id>"))},"Switch active shell context to a TNC sandbox"),e("leave-sandbox",()=>{this.activeSandbox?(console.log(chalk.cyan(`\nExited sandbox [${this.activeSandbox}]. Back to host shell.\n`)),this.activeSandbox=null):console.log(chalk.yellow("Not currently inside any sandbox context."))},"Exit active sandbox context"),e("connect",this.connectCommand.bind(this),"Connect to WebSocket"),e("disconnect",()=>this.ws.disconnect(),"Disconnect WebSocket"),e("status",this.statusCommand.bind(this),"Show connection status"),e("join",this.joinCommand.bind(this),"Join a room"),e("leave",()=>this.ws.leaveRoom?.(),"Leave current room"),e("rooms",this.roomsCommand.bind(this),"List rooms"),e("say",this.sayCommand.bind(this),"Send a message"),e("msg",this.sayCommand.bind(this),"Send a message"),e("set",this.setCommand.bind(this),"Set a variable"),e("unset",this.unsetCommand.bind(this),"Unset a variable"),this.addAlias("ll","ls -la"),this.addAlias("..","cd .."),this.addAlias("~","cd ~"),this.addAlias("unuse","leave-sandbox")}registerCommand(e,o,t=""){this.commands.set(e,{handler:o,description:t})}addAlias(e,o){this.aliases.set(e,o)}async helpCommand(){this.commandRegistry&&console.log(this.commandRegistry.getHelpText()),console.log(chalk.dim(" Any unrecognised command runs in your system shell\n"))}historyCommand(){this.history.forEach((e,o)=>console.log(`${(o+1).toString().padStart(4)} ${e}`))}async cdCommand(e){const o=e.join(" ").trim()||os.homedir();try{let e;if(e="~"===o?os.homedir():".."===o?path.dirname(this.currentDir):path.resolve(this.currentDir,o),e=path.normalize(e),!(await fs.stat(e)).isDirectory())return void console.log(chalk.red(`cd: not a directory: ${o}`));this.previousDir=this.currentDir,process.chdir(e),this.currentDir=process.cwd()}catch{console.log(chalk.red(`cd: no such directory: ${o}`))}}async lsCommand(e){try{const o=await fs.readdir(this.currentDir),t=e.includes("-a")?o:o.filter(e=>!e.startsWith("."));(await Promise.all(t.map(async e=>({name:e,isDir:(await fs.stat(path.join(this.currentDir,e))).isDirectory()})))).forEach(e=>e.isDir?console.log(chalk.blue(e.name+"/")):console.log(e.name))}catch(e){console.log(chalk.red(`ls: ${e.message}`))}}async connectCommand(){console.log(chalk.cyan("Connecting..."));try{await this.ws.connect()}catch(e){console.log(chalk.red(`Error: ${e.message}`))}}async statusCommand(){const e=this.ws.isConnected();console.log(chalk.cyan("\nStatus:")),console.log(` WebSocket : ${e?chalk.green("Connected"):chalk.red("Disconnected")}`),e&&console.log(` Socket ID : ${chalk.dim(this.ws.getSocketId())}`);const o=this.ws.getCurrentRoom();console.log(` Room : ${o?chalk.green(o):chalk.yellow("None")}`),console.log(` Auth : ${this.api.isAuthenticated()?chalk.green("Logged in"):chalk.yellow("Not logged in")}`);const t=this.api.getUser();t&&console.log(` User : ${chalk.dim(t.name||t.email)}`),console.log("")}async joinCommand(e){if(e[0]){this.ws.isConnected()||(console.log(chalk.yellow("Connecting...")),await this.ws.connect());try{const o=await this.ws.joinRoom(e[0]);console.log(chalk.green(`Joined: ${o.roomName||e[0]}`))}catch(e){console.log(chalk.red(`Error: ${e.message}`))}}else console.log(chalk.red("Usage: join <room-id>"))}async roomsCommand(){this.ws.isConnected()?(this.ws.socket.emit("rooms:list"),this.ws.socket.once("rooms:list",e=>{e.rooms?.length?e.rooms.forEach(e=>console.log(` ${chalk.cyan(e.name||e.id)} - Members: ${e.participantCount||0}`)):console.log(" No rooms")})):console.log(chalk.yellow("Not connected"))}async sayCommand(e){const o=e.join(" ");if(o)try{await this.ws.sendMessage(o);const e=this.api.getUser()?.name||this.username;console.log(chalk.green(`You: ${o}`)),this.pushNotification({type:"message",from:`You (${e})`,text:o})}catch(e){console.log(chalk.red(`Failed: ${e.message}`))}else console.log(chalk.red("Usage: say <message>"))}async setCommand(e){if(e.length)this.variables.set(e[0],e.slice(1).join(" ")),console.log(chalk.green(`${e[0]} = ${e.slice(1).join(" ")}`));else for(const[e,o]of this.variables)console.log(` ${chalk.green(e.padEnd(12))} = ${o}`)}async unsetCommand(e){e[0]&&this.variables.delete(e[0])}getSessionPath(){return path.join(this.config.configDir,"session.json")}parseCommand(e){const o=[];let t="",s=!1,n="",i=!1;for(const a of e)"\\"!==a||i||s?i?(t+=a,i=!1):'"'!==a&&"'"!==a||s?a===n&&s?(s=!1,n=""):" "!==a||s?t+=a:t&&(o.push(t),t=""):(s=!0,n=a):i=!0;return t&&o.push(t),o}executeSystemCommand(e){return new Promise(o=>{let t=e,s={...process.env},n=this.currentDir,i=[],a=!0;this.activeSandbox&&(console.log(chalk.cyan(`[TNC Sandbox: ${this.activeSandbox}] ${e}`)),i=[process.argv[1],"exec",process.stdin.isTTY?"-it":"-i",this.sandboxEngine.name(this.activeSandbox),"sh","-c",e],t=process.execPath,a=!1);const c=spawn(t,i,{cwd:n,shell:a,stdio:"inherit",env:s});c.on("close",async t=>{try{this.currentDir=process.cwd()}catch{}if(0===t)try{const o=new(require("./dependency-tracker"))(n);await o.interceptCommand(e,this)}catch(e){}process.stdout.write("\r\n"),o()}),c.on("error",()=>{console.log(chalk.red(`Command not found: ${e}`)),o()})})}showWelcome(){try{const e=require("figlet");console.log(chalk.cyan(e.textSync("ThinkNCollab",{font:"Standard"})))}catch{}console.log(chalk.cyan("\n╔══════════════════════════════════════════════════════╗\n║ ThinkNCollab Shell v0.0.20 ║\n║ Type 'help' for commands ║\n╚══════════════════════════════════════════════════════╝"))}loadHistory(){try{fs.existsSync(this.config.historyFile)&&(this.history=fs.readFileSync(this.config.historyFile,"utf8").split("\n").filter(e=>e.trim()))}catch{}}saveHistory(){try{fs.writeFileSync(this.config.historyFile,this.history.slice(-this.config.maxHistory).join("\n"))}catch{}}cleanup(){if(this.activeSandbox)try{this.sandboxEngine.freeze(this.activeSandbox).catch(()=>{}),console.log(chalk.cyan(`\n[TNC Smart Freeze] Auto-froze active sandbox [${this.activeSandbox}]. 0 RAM used!`))}catch(e){}if(this.api.logout(),this.tcpServer)try{this.tcpServer.close()}catch{}this.notifyPort&&require("./notify-auth").removeToken(this.notifyPort),this.ws&&this.ws.disconnect(),this.saveHistory()}}module.exports=ThinkNCollabShell;
@@ -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{execSync,spawnSync}=require("child_process"),fs=require("fs-extra"),path=require("path"),os=require("os"),chalk=require("chalk"),SNAPSHOT_BASE=path.join(os.homedir(),".tnc","snapshots"),SNAPSHOT_META=path.join(SNAPSHOT_BASE,"snapshots.json");class SnapshotManager{constructor(){fs.ensureDirSync(SNAPSHOT_BASE),this.meta=this._loadMeta(),this.platform=process.platform,this.fsType=this._detectFS()}_loadMeta(){try{return fs.readJsonSync(SNAPSHOT_META)}catch{return{}}}_saveMeta(){fs.writeJsonSync(SNAPSHOT_META,this.meta,{spaces:2})}_detectFS(){if("darwin"===this.platform){try{if(execSync('diskutil info / 2>/dev/null | grep "File System"',{encoding:"utf8",stdio:"pipe"}).toLowerCase().includes("apfs"))return"apfs"}catch(e){}return"hfs+"}if("linux"===this.platform){try{const e=execSync("df -T ~/.tnc 2>/dev/null | tail -1",{encoding:"utf8",stdio:"pipe"});if(e.includes("btrfs"))return"btrfs";if(e.includes("xfs"))return"xfs";if(e.includes("ext4"))return"ext4"}catch(e){}return"ext4"}return"win32"===this.platform?"ntfs":"generic"}_getStrategy(){switch(this.fsType){case"apfs":return"apfs-cow";case"btrfs":return"btrfs-snap";case"xfs":return"reflink";case"ext4":return"reflink-auto";case"ntfs":return"robocopy";default:return"tar"}}async snapshot(e,t,s={}){const n=path.resolve(e);if(!fs.existsSync(n))throw new Error(`Source path not found: ${n}`);const a=t.replace(/[^a-zA-Z0-9_-]/g,"-"),r=`${a}-${Date.now().toString(36)}`,o=path.join(SNAPSHOT_BASE,r),i=this._getStrategy(),c=Date.now();console.log(chalk.cyan(`\n[Snapshot] Creating "${a}" using ${i}...`)),console.log(chalk.dim(` Source: ${n}`));try{switch(i){case"apfs-cow":spawnSync("cp",["-cR",n,o],{stdio:"pipe"});break;case"btrfs-snap":spawnSync("btrfs",["subvolume","snapshot",n,o],{stdio:"pipe"});break;case"reflink":fs.ensureDirSync(o),spawnSync("cp",["--reflink=always","-a",`${n}/.`,`${o}/`],{stdio:"pipe"});break;case"reflink-auto":fs.ensureDirSync(o),spawnSync("cp",["--reflink=auto","-a",`${n}/.`,`${o}/`],{stdio:"pipe"});break;case"robocopy":fs.ensureDirSync(o),spawnSync("robocopy",[n,o,"/E","/DCOPY:T"],{stdio:"pipe"});break;default:spawnSync("tar",["-czf",`${o}.tar.gz`,"-C",path.dirname(n),path.basename(n)],{stdio:"pipe"})}const e=Date.now()-c,t=this._dirSize("tar"===i?void 0:o),p=["apfs-cow","btrfs-snap","reflink"].includes(i),f={id:r,name:a,source:n,snapDir:"tar"===i?`${o}.tar.gz`:o,strategy:i,fsType:this.fsType,isCoW:p,sizeMB:t,createdAt:(new Date).toISOString(),elapsedMs:e,tags:s.tags||{},description:s.description||""};return this.meta[r]=f,this._saveMeta(),console.log(chalk.green(`[OK] Snapshot "${a}" created in ${e}ms`)),p?console.log(chalk.dim(" CoW: Hardware/Filesystem instant clone active (zero extra disk until modified) ✓")):console.log(chalk.yellow(` Notice: ${this.fsType} does not support instant CoW reflinks; standard copy performed.`)),console.log(chalk.dim(` ID: ${r}`)),f}catch(e){throw new Error(`Snapshot failed (${i}): ${e.message}`)}}async snapshotSafe(e,t,s=null,n=null,a={}){let r=!1;if(s&&n){const e=s.loadMeta(n);e&&"RUNNING"===e.state&&(r=!0,console.log(chalk.yellow(`[SnapshotSafe] Flushing & pausing container "${n}" for crash-free snapshot consistency...`)),await s.freeze(n))}try{return await this.snapshot(e,t,a)}finally{r&&s&&n&&(console.log(chalk.cyan(`[SnapshotSafe] Resuming container "${n}"...`)),await s.resume(n))}}async restore(e,t,s={}){const n=this.meta[e];if(!n)throw new Error(`Snapshot "${e}" not found.`);const a=path.resolve(t||n.source);return console.log(chalk.cyan(`\n[Snapshot] Restoring "${n.name}" → ${a}...`)),s.backup&&await this.snapshot(a,`pre-restore-${n.name}`),"tar"===n.strategy?(fs.ensureDirSync(a),spawnSync("tar",["-xzf",n.snapDir,"-C",path.dirname(a)],{stdio:"pipe"})):"btrfs-snap"===n.strategy?(fs.existsSync(a)&&spawnSync("btrfs",["subvolume","delete",a],{stdio:"pipe"}),spawnSync("btrfs",["subvolume","snapshot",n.snapDir,a],{stdio:"pipe"})):(fs.existsSync(a)&&fs.removeSync(a),fs.ensureDirSync(a),"apfs-cow"===n.strategy?spawnSync("cp",["-cR",n.snapDir,a],{stdio:"pipe"}):spawnSync("cp",["--reflink=auto","-a",`${n.snapDir}/.`,`${a}/`],{stdio:"pipe"})),console.log(chalk.green(`[OK] Restored to ${a}`)),{success:!0,target:a}}diff(e,t){const s=this.meta[e],n=this.meta[t];if(!s||!n)throw new Error("One or both snapshots not found.");if("tar"===s.strategy||"tar"===n.strategy)return{method:"tar",note:"Diff not available for tar snapshots"};try{const e=spawnSync("diff",["-rq","--brief",s.snapDir,n.snapDir],{encoding:"utf8",stdio:"pipe"}).stdout||"",t=e.trim().split("\n").filter(Boolean).length;return{method:"diff",snapshot1:s.name,snapshot2:n.name,changedFiles:t,details:e.trim().split("\n").filter(Boolean).slice(0,50)}}catch{return{method:"diff",error:"diff command failed"}}}delete(e){const t=this.meta[e];if(!t)throw new Error(`Snapshot "${e}" not found.`);if("btrfs-snap"===t.strategy)try{execSync(`btrfs subvolume delete "${t.snapDir}"`,{stdio:"pipe"})}catch(e){fs.removeSync(t.snapDir)}else fs.existsSync(t.snapDir)&&fs.removeSync(t.snapDir);return delete this.meta[e],this._saveMeta(),{success:!0,deleted:e}}list(e={}){let t=Object.values(this.meta);return e.name&&(t=t.filter(t=>t.name.includes(e.name))),t.sort((e,t)=>new Date(t.createdAt)-new Date(e.createdAt))}inspect(e){return this.meta[e]||null}prune(e=3){const t={};for(const e of Object.values(this.meta))t[e.name]||(t[e.name]=[]),t[e.name].push(e);let s=0;for(const[n,a]of Object.entries(t)){const t=a.sort((e,t)=>new Date(t.createdAt)-new Date(e.createdAt)).slice(e);for(const e of t)try{this.delete(e.id),s++}catch(e){}}return{pruned:s}}_dirSize(e){if(!e||!fs.existsSync(e))return 0;try{const t=spawnSync("du",["-sm",e],{encoding:"utf8",stdio:"pipe"});return parseInt((t.stdout||"").split(/\s+/)[0],10)||0}catch{return 0}}systemInfo(){return{platform:this.platform,fsType:this.fsType,strategy:this._getStrategy(),isCoW:["apfs","btrfs","xfs"].includes(this.fsType),snapshotBase:SNAPSHOT_BASE,totalSnapshots:Object.keys(this.meta).length}}}module.exports=SnapshotManager;
7
+ const{execSync,spawnSync}=require("child_process"),fs=require("fs-extra"),path=require("path"),os=require("os"),chalk=require("chalk"),SNAPSHOT_BASE=path.join(os.homedir(),".tnc","snapshots"),SNAPSHOT_META=path.join(SNAPSHOT_BASE,"snapshots.json");class SnapshotManager{constructor(){fs.ensureDirSync(SNAPSHOT_BASE),this.meta=this._loadMeta(),this.platform=process.platform,this.fsType=this._detectFS()}_loadMeta(){try{return fs.readJsonSync(SNAPSHOT_META)}catch{return{}}}_saveMeta(){fs.writeJsonSync(SNAPSHOT_META,this.meta,{spaces:2})}_detectFS(){if("darwin"===this.platform){try{if(execSync('diskutil info / 2>/dev/null | grep "File System"',{encoding:"utf8",stdio:"pipe"}).toLowerCase().includes("apfs"))return"apfs"}catch(e){}return"hfs+"}if("linux"===this.platform){try{const e=execSync("df -T ~/.tnc 2>/dev/null | tail -1",{encoding:"utf8",stdio:"pipe"});if(e.includes("btrfs"))return"btrfs";if(e.includes("xfs"))return"xfs";if(e.includes("ext4"))return"ext4"}catch(e){}return"ext4"}return"win32"===this.platform?"ntfs":"generic"}_getStrategy(){switch(this.fsType){case"apfs":return"apfs-cow";case"btrfs":return"btrfs-snap";case"xfs":return"reflink";case"ext4":return"reflink-auto";case"ntfs":return"robocopy";default:return"tar"}}async snapshot(e,t,s={}){const n=path.resolve(e);if(!fs.existsSync(n))throw new Error(`Source path not found: ${n}`);const a=t.replace(/[^a-zA-Z0-9_-]/g,"-"),r=`${a}-${Date.now().toString(36)}`,o=path.join(SNAPSHOT_BASE,r),i=this._getStrategy(),c=Date.now();console.log(chalk.cyan(`\n[Snapshot] Creating "${a}" using ${i}...`)),console.log(chalk.dim(` Source: ${n}`));try{switch(i){case"apfs-cow":spawnSync("cp",["-cR",n,o],{stdio:"pipe"});break;case"btrfs-snap":spawnSync("btrfs",["subvolume","snapshot",n,o],{stdio:"pipe"});break;case"reflink":fs.ensureDirSync(o),spawnSync("cp",["--reflink=always","-a",`${n}/.`,`${o}/`],{stdio:"pipe"});break;case"reflink-auto":fs.ensureDirSync(o),spawnSync("cp",["--reflink=auto","-a",`${n}/.`,`${o}/`],{stdio:"pipe"});break;case"robocopy":fs.ensureDirSync(o),spawnSync("robocopy",[n,o,"/E","/DCOPY:T"],{stdio:"pipe"});break;default:spawnSync("tar",["-czf",`${o}.tar.gz`,"-C",path.dirname(n),path.basename(n)],{stdio:"pipe"})}const e=Date.now()-c,t=this._dirSize("tar"===i?void 0:o),p=["apfs-cow","btrfs-snap","reflink"].includes(i),f={id:r,name:a,source:n,snapDir:"tar"===i?`${o}.tar.gz`:o,strategy:i,fsType:this.fsType,isCoW:p,sizeMB:t,createdAt:(new Date).toISOString(),elapsedMs:e,tags:s.tags||{},description:s.description||""};return this.meta[r]=f,this._saveMeta(),console.log(chalk.green(`[OK] Snapshot "${a}" created in ${e}ms`)),p?console.log(chalk.dim(" CoW: Hardware/Filesystem instant clone active (zero extra disk until modified) ✓")):console.log(chalk.yellow(` Notice: ${this.fsType} does not support instant CoW reflinks; standard copy performed.`)),console.log(chalk.dim(` ID: ${r}`)),f}catch(e){throw new Error(`Snapshot failed (${i}): ${e.message}`)}}async snapshotSafe(e,t,s=null,n=null,a={}){let r=!1;if(s&&n){const e=await s.loadMeta(n);e&&"RUNNING"===e.state&&(r=!0,console.log(chalk.yellow(`[SnapshotSafe] Flushing & pausing container "${n}" for crash-free snapshot consistency...`)),await s.freeze(n))}try{return await this.snapshot(e,t,a)}finally{r&&s&&n&&(console.log(chalk.cyan(`[SnapshotSafe] Resuming container "${n}"...`)),await s.resume(n))}}async restore(e,t,s={}){const n=this.meta[e];if(!n)throw new Error(`Snapshot "${e}" not found.`);const a=path.resolve(t||n.source);return console.log(chalk.cyan(`\n[Snapshot] Restoring "${n.name}" → ${a}...`)),s.backup&&await this.snapshot(a,`pre-restore-${n.name}`),"tar"===n.strategy?(fs.ensureDirSync(a),spawnSync("tar",["-xzf",n.snapDir,"-C",path.dirname(a)],{stdio:"pipe"})):"btrfs-snap"===n.strategy?(fs.existsSync(a)&&spawnSync("btrfs",["subvolume","delete",a],{stdio:"pipe"}),spawnSync("btrfs",["subvolume","snapshot",n.snapDir,a],{stdio:"pipe"})):(fs.existsSync(a)&&fs.removeSync(a),fs.ensureDirSync(a),"apfs-cow"===n.strategy?spawnSync("cp",["-cR",n.snapDir,a],{stdio:"pipe"}):spawnSync("cp",["--reflink=auto","-a",`${n.snapDir}/.`,`${a}/`],{stdio:"pipe"})),console.log(chalk.green(`[OK] Restored to ${a}`)),{success:!0,target:a}}diff(e,t){const s=this.meta[e],n=this.meta[t];if(!s||!n)throw new Error("One or both snapshots not found.");if("tar"===s.strategy||"tar"===n.strategy)return{method:"tar",note:"Diff not available for tar snapshots"};try{const e=spawnSync("diff",["-rq","--brief",s.snapDir,n.snapDir],{encoding:"utf8",stdio:"pipe"}).stdout||"",t=e.trim().split("\n").filter(Boolean).length;return{method:"diff",snapshot1:s.name,snapshot2:n.name,changedFiles:t,details:e.trim().split("\n").filter(Boolean).slice(0,50)}}catch{return{method:"diff",error:"diff command failed"}}}delete(e){const t=this.meta[e];if(!t)throw new Error(`Snapshot "${e}" not found.`);if("btrfs-snap"===t.strategy)try{execSync(`btrfs subvolume delete "${t.snapDir}"`,{stdio:"pipe"})}catch(e){fs.removeSync(t.snapDir)}else fs.existsSync(t.snapDir)&&fs.removeSync(t.snapDir);return delete this.meta[e],this._saveMeta(),{success:!0,deleted:e}}list(e={}){let t=Object.values(this.meta);return e.name&&(t=t.filter(t=>t.name.includes(e.name))),t.sort((e,t)=>new Date(t.createdAt)-new Date(e.createdAt))}inspect(e){return this.meta[e]||null}prune(e=3){const t={};for(const e of Object.values(this.meta))t[e.name]||(t[e.name]=[]),t[e.name].push(e);let s=0;for(const[n,a]of Object.entries(t)){const t=a.sort((e,t)=>new Date(t.createdAt)-new Date(e.createdAt)).slice(e);for(const e of t)try{this.delete(e.id),s++}catch(e){}}return{pruned:s}}_dirSize(e){if(!e||!fs.existsSync(e))return 0;try{const t=spawnSync("du",["-sm",e],{encoding:"utf8",stdio:"pipe"});return parseInt((t.stdout||"").split(/\s+/)[0],10)||0}catch{return 0}}systemInfo(){return{platform:this.platform,fsType:this.fsType,strategy:this._getStrategy(),isCoW:["apfs","btrfs","xfs"].includes(this.fsType),snapshotBase:SNAPSHOT_BASE,totalSnapshots:Object.keys(this.meta).length}}}module.exports=SnapshotManager;
@@ -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 http=require("http");class SandboxTunnel{constructor(){this.socket=null,this.sandboxId=null,this.localPort=null,this.active=!1,this._heartbeatInterval=null,this._reconnectTimer=null,this._stopping=!1,this._targetServer=null,this._userId=null,this._shellToken=null}connect(t,e,s){if(this.sandboxId=t,this.localPort=e,this._stopping=!1,this._targetServer=(s?.ws?.config?.serverUrl||s?.api?.apiUrl||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com").replace(/\/$/,""),this._userId=s?.api?.getUser?.()?._id||s?.api?.getUser?.()?.id||s?.api?.userId||s?.config?.user?._id||process.env.THINKNCOLLAB_USER_ID||null,this._shellToken=s?.config?.shellToken||s?.api?.shellToken||s?.api?.getToken?.()||process.env.THINKNCOLLAB_TOKEN||null,!this._shellToken||!this._userId)try{const t=new(require("../api-client"));this._shellToken=this._shellToken||t.getToken(),this._userId=this._userId||t.getUser()?._id||null,this._userName=t.getUser()?.name||null}catch(t){}this._doConnect()}_doConnect(){if(this._stopping)return;let t;try{t=require("socket.io-client")}catch(t){return}const e=t(this._targetServer+"/thinknsh",{transports:["websocket","polling"],timeout:5e3,reconnection:!1,auth:{userId:this._userId,shellToken:this._shellToken}});this.socket=e,e.on("connect",()=>{e.emit("thinknsh:connected",{userId:this._userId,userName:this._userName||null,shellToken:this._shellToken})}),e.on("thinknsh:ready",()=>{this.active=!0,e.emit("sandbox:tunnel-register",{sandboxId:this.sandboxId,localPort:this.localPort}),this._startHeartbeat()}),e.on("thinknsh:error",()=>{this._stopping=!0,this.active=!1}),e.on("tunnel:request",t=>{this._handleRequest(t,e)}),e.on("disconnect",()=>{this.active=!1,this._stopHeartbeat(),this._stopping||(this._reconnectTimer=setTimeout(()=>this._doConnect(),3e3))}),e.on("connect_error",()=>{this.active=!1,this._stopHeartbeat(),this._stopping||(this._reconnectTimer=setTimeout(()=>this._doConnect(),5e3))})}_handleRequest(t,e){if(!t||!t.requestId)return;const{requestId:s,method:n,path:o,headers:i,body:r}=t,h=this.localPort,a={...i||{}};delete a.host,delete a.connection,delete a["transfer-encoding"],a.host=`127.0.0.1:${h}`;const l=r?Buffer.from(r,"base64"):null;l&&l.length>0&&(a["content-length"]=l.length);const c={hostname:"127.0.0.1",port:h,path:o||"/",method:n||"GET",headers:a,timeout:3e4},u=http.request(c,t=>{const n=[];t.on("data",t=>n.push(t)),t.on("end",()=>{const o=Buffer.concat(n);e.emit("tunnel:response",{requestId:s,statusCode:t.statusCode,headers:t.headers,body:o.toString("base64")})})});u.on("error",t=>{e.emit("tunnel:response",{requestId:s,statusCode:502,headers:{"content-type":"text/plain"},body:Buffer.from(`Tunnel Error: ${t.message}`).toString("base64")})}),u.on("timeout",()=>{u.destroy(),e.emit("tunnel:response",{requestId:s,statusCode:504,headers:{"content-type":"text/plain"},body:Buffer.from("Tunnel Gateway Timeout").toString("base64")})}),l&&l.length>0&&u.write(l),u.end()}_startHeartbeat(){this._stopHeartbeat(),this._heartbeatInterval=setInterval(()=>{this.socket&&this.active&&this.socket.emit("sandbox:tunnel-ping",{sandboxId:this.sandboxId})},15e3)}_stopHeartbeat(){this._heartbeatInterval&&(clearInterval(this._heartbeatInterval),this._heartbeatInterval=null)}stop(){if(this._stopping=!0,this.active=!1,this._stopHeartbeat(),this._reconnectTimer&&clearTimeout(this._reconnectTimer),this.socket){try{this.socket.disconnect()}catch(t){}this.socket=null}}}module.exports=SandboxTunnel;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ const path=require("path"),fs=require("fs-extra"),os=require("os"),SandboxTunnel=require("./client"),sandboxId=process.argv[2],localPort=parseInt(process.argv[3],10),targetServer=process.argv[4]||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com",userId=process.argv[5]||process.env.THINKNCOLLAB_USER_ID||null;sandboxId&&localPort||(console.error("Usage: node daemon.js <sandboxId> <localPort> [serverUrl] [userId]"),process.exit(1));const sandboxDir=path.join(os.homedir(),".tnc","sandboxes",sandboxId);fs.ensureDirSync(sandboxDir);const pidFile=path.join(sandboxDir,"tunnel.pid");fs.writeFileSync(pidFile,String(process.pid));const tunnel=new SandboxTunnel,standaloneShellContext={ws:{config:{serverUrl:targetServer},getCurrentRoom:()=>null},api:{apiUrl:targetServer,getUser:()=>({_id:userId}),userId},config:{serverUrl:targetServer,userId}};function cleanup(){try{fs.existsSync(pidFile)&&fs.removeSync(pidFile)}catch(e){}tunnel.stop(),process.exit(0)}tunnel.connect(sandboxId,localPort,standaloneShellContext),process.on("SIGINT",cleanup),process.on("SIGTERM",cleanup),process.on("SIGHUP",cleanup),process.on("exit",()=>{try{fs.existsSync(pidFile)&&fs.removeSync(pidFile)}catch(e){}});
@@ -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"),{spawn}=require("child_process"),SandboxTunnel=require("./client"),pidFile=n=>path.join(os.homedir(),".tnc","sandboxes",n,"tunnel.pid");function startDaemon(n,e,i=null,o=null){stopDaemon(n),fs.mkdirSync(path.dirname(pidFile(n)),{recursive:!0});const s=spawn(process.execPath,[path.join(__dirname,"daemon.js"),n,String(e),i||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com",o||process.env.THINKNCOLLAB_USER_ID||""],{detached:!0,stdio:"ignore",windowsHide:!0});return s.unref(),fs.writeFileSync(pidFile(n),String(s.pid)),s.pid}function isTunnelDaemon(n,e){const{spawnSync:i}=require("child_process"),o=("win32"===process.platform?i("wmic",["process","where",`ProcessId=${n}`,"get","CommandLine","/value"],{encoding:"utf8",windowsHide:!0}):i("ps",["-p",String(n),"-o","command="],{encoding:"utf8"})).stdout||"";return/tunnel[/\\]daemon\.js/.test(o)&&o.includes(` ${e} `)}function readPid(n){const e=parseInt(fs.readFileSync(pidFile(n),"utf8"),10);return Number.isInteger(e)&&e>0?e:null}function stopDaemon(n){try{const e=readPid(n);if(e&&isTunnelDaemon(e,n))try{process.kill(e,"SIGTERM")}catch(n){}fs.rmSync(pidFile(n),{force:!0})}catch(n){}}function daemonRunning(n){try{const e=readPid(n);return e&&isTunnelDaemon(e,n)?e:null}catch(n){return null}}module.exports={SandboxTunnel,startDaemon,stopDaemon,daemonRunning};
@@ -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{io}=require("socket.io-client"),EventEmitter=require("events"),e2eeVault=require("./crypto/e2ee-vault");class WebSocketManager extends EventEmitter{constructor(e={}){super(),this.config={serverUrl:e.serverUrl||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com",reconnectAttempts:e.reconnectAttempts||5,reconnectDelay:e.reconnectDelay||1e3,...e},this.socket=null,this.connected=!1,this.currentUser=null,this.roomId=null,this.reconnectCount=0,this.messageQueue=[],this._authToken=null,this._handlersSetup=!1,this._serverReady=!1,this._shellRef=e._shellRef||null}connect(e=null){return new Promise((t,s)=>{try{e&&(this._authToken=e),this.socket=io(this.config.serverUrl+"/thinknsh",{transports:["websocket","polling"],reconnection:!0,reconnectionAttempts:this.config.reconnectAttempts,reconnectionDelay:this.config.reconnectDelay,auth:this._authToken?{token:this._authToken}:{}});const o=setTimeout(()=>{s(new Error("Connection timeout"))},1e4);this.socket.on("connect",()=>{clearTimeout(o),this.connected=!0,this.reconnectCount=0,this.currentUser&&this._announceUser(),this.processPendingMessages(),this.emit("connected",{socketId:this.socket.id}),t({success:!0,socketId:this.socket.id})}),this.socket.on("connect_error",e=>{clearTimeout(o),0===this.reconnectCount&&s(new Error(`Cannot connect to ${this.config.serverUrl}`)),this.reconnectCount++,this.emit("error",{message:e.message})}),this.socket.on("disconnect",e=>{this.connected=!1,this.emit("disconnected",{reason:e})}),this.socket.on("task:verdict",e=>{this.emit("task:verdict",e)}),this.socket.on("task:run-local",async e=>{try{const t=require("chalk"),{runLocal:s}=require("./local-runner"),o=e.taskId,r=this._shellRef,n=r?r.api:null;if(!n)return void console.log(t.red("\n[Error] CLI not authenticated. Run: login\n"));let i;try{i=await n._request("GET",`/thinknsh/${o}/cli`)}catch(e){return void console.log(t.red(`\n[Error] Could not fetch task info: ${e.message}\n`))}if(!i||!i.webhookSecret)return void console.log(t.red("\n[Error] Task has no webhookSecret - cannot submit result.\n"));await s({taskId:o,webhookSecret:i.webhookSecret,apiUrl:this.config.serverUrl||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com",repoUrl:i.repoUrl||i.githubUrl||i.repo_url||null,repoBranch:i.branch||i.repoBranch||"main",workflowFile:i.workflowFile||i.workflow||null,chalk:t,shell:r,onLog:e=>{this.socket.emit("task:log",{taskId:o,boardId:i.boardId,...e})}})}catch(e){console.error("task:run-local error:",e.message)}}),this.socket.on("reconnect",()=>{this.connected=!0,this.emit("reconnected",{}),this.currentUser&&this._announceUser(),this.roomId&&this.currentUser&&this._emitJoin(this.roomId)}),this._handlersSetup||(this.setupEventHandlers(),this._handlersSetup=!0)}catch(e){s(e)}})}_announceUser(){if(this.socket&&this.currentUser&&(this.socket.emit("thinknsh:connected",{userId:this.currentUser.userId,userName:this.currentUser.name,shellToken:this._authToken}),this._shellRef?.sandboxEngine))try{this._shellRef.sandboxEngine.list().forEach(e=>{this.emitSandboxSync(e,"sync")})}catch(e){}}setUser(e,t=null){if(this._user=e,!e)return;t&&(this._authToken=t);const s=(e.userId||e._id||"").toString();this.currentUser={userId:s,name:e.name||e.email||"Unknown",userType:e.userType||e.model||e.role||"User"},this.connected&&this.socket&&this._announceUser()}setupEventHandlers(){this.socket.on("thinknsh:ready",e=>{this._serverReady=!0,this.emit("ready",e)}),this.socket.on("thinknsh:joined",e=>{if(this.roomId=e.roomId,e.history?.length){const t=e.history.map(e=>{let t=e.message||e.content||e.text||"",s=!1;if("string"==typeof t&&t.startsWith("e2ee:")){const e=e2eeVault.decrypt(t,this.roomId);e!==t&&(t=e,s=!0)}return{...e,message:t,isE2EE:s}});this.emit("messageHistory",t)}this.emit("roomJoined",e)}),this.socket.on("thinknsh:message",e=>{let t=e.message||"",s=e.isE2EE||!1;if("string"==typeof t&&t.startsWith("e2ee:")){const e=e2eeVault.decrypt(t,this.roomId);e!==t&&(t=e,s=!0)}this.emit("message",{username:e.name,userId:e.userId,message:t,isE2EE:s,source:"shell",timestamp:e.timestamp})}),this.socket.on("chat-message",e=>{let t=e.message||"",s=e.isE2EE||!1;if("string"==typeof t&&t.startsWith("e2ee:")){const e=e2eeVault.decrypt(t,this.roomId);e!==t&&(t=e,s=!0)}this.emit("message",{username:e.name,userId:e.userId,message:t,isE2EE:s,source:e.source||"web",timestamp:e.timestamp})}),this.socket.on("e2ee:key-sync",e=>{!e?.roomId||this.roomId&&e.roomId!==this.roomId||this.emit("e2ee:key-sync",e)}),this.socket.on("e2ee:key-delivered",e=>{e?.packet?.rawKey&&(e2eeVault.setRawRoomKey(e.roomId,e.packet.rawKey),this.emit("e2ee:key-updated",e))}),this.socket.on("e2ee:packet",e=>{this.emit("e2ee:packet",e)}),this.socket.on("thinknsh:userJoined",e=>{this.emit("userJoined",{username:e.name,source:"shell"})}),this.socket.on("thinknsh:userLeft",e=>{this.emit("userLeft",{username:e.name,source:"shell"})}),this.socket.on("user-joined",e=>{this.emit("userJoined",{username:e,source:"web"})}),this.socket.on("user-left",e=>{this.emit("userLeft",{username:e,source:"web"})}),this.socket.on("user-list",e=>{this.emit("userList",{users:e})}),this.socket.on("thinknsh:history",e=>{this.emit("messageHistory",e.messages?.length?e.messages:[])}),this.socket.on("new-notification",e=>{this.emit("notification",{type:e.type||"info",level:e.type||"info",title:e.title||"Notification",message:e.message||"",meta:e.meta||{},taskTitle:e.taskTitle||e.meta?.taskTitle||"",assignedBy:e.assignedBy||e.meta?.assignedBy||""})}),this.socket.on("room:activity",e=>{this.emit("roomActivity",e)}),this.socket.on("thinknsh:status",e=>{this.emit("shellStatus",e)}),this.socket.on("thinknsh:notifications",e=>{this.emit("notificationList",e.notifications)}),this.socket.on("thinknsh:error",e=>{this.emit("serverError",e)}),this.socket.on("error",e=>{this.emit("serverError",e)});const e=new Set;this.socket.on("cicd:run-dispatch",async t=>{if(!t||!t.runId)return;if(e.has(t.runId))return;const s=this.roomId||this._joiningRoomId;if(!s||t.roomId&&String(t.roomId)!==String(s)){const e=require("chalk");return void console.log(e.yellow(`\n[CI/CD] Ignored run ${t.runId}: not for the room this shell joined.`))}e.add(t.runId);try{const e=require("chalk"),{runCicdLocal:s}=require("./local-runner"),o=this._shellRef,r=o?o.api:null;console.log(e.cyan(`\n[CI/CD] Execution request received for room: ${t.roomId||this.roomId}`)),await s({runId:t.runId,roomId:t.roomId||this.roomId,workflowFile:t.workflowFile||"universal-polyglot-ci.yml",workflowYaml:t.workflowYaml||"",localPath:t.localPath||"",apiUrl:r?r.baseURL:this.config.serverUrl,chalk:e,api:r,socket:this.socket})}catch(e){console.error("\n[Error] Error in CI/CD local execution:",e)}finally{setTimeout(()=>e.delete(t.runId),3e4)}});const t=new Set;this.socket.on("thinknsh:test-dispatch",async e=>{if(e&&e.runId&&e.taskId&&!t.has(e.runId)){t.add(e.runId);try{const t=require("chalk"),{spawn:s}=require("child_process"),o=require("http"),r=require("https"),n=require("crypto"),i=(e,t)=>{if(!t)return null;const s=n.createHash("sha256").update(t).digest(),o=n.randomBytes(12),r=n.createCipheriv("aes-256-gcm",s,o),i=JSON.stringify(e||{}),c=Buffer.concat([r.update(i,"utf8"),r.final()]),a=r.getAuthTag();return{iv:o.toString("hex"),tag:a.toString("hex"),cipherText:c.toString("hex")}},c=(e,t)=>{if(!(e&&e.cipherText&&e.iv&&e.tag))return null;try{const s=n.createHash("sha256").update(t).digest(),o=Buffer.from(e.iv,"hex"),r=Buffer.from(e.tag,"hex"),i=Buffer.from(e.cipherText,"hex"),c=n.createDecipheriv("aes-256-gcm",s,o);c.setAuthTag(r);const a=Buffer.concat([c.update(i),c.final()]);return JSON.parse(a.toString("utf8"))}catch(e){return null}},{runId:a,taskId:h,runResultToken:l,encryptedPayload:u}=e;let d=e.testConfig,m=e.commit,k=e.taskTitle;if(u&&l){const e=c(u,l);e&&(d=e.testConfig||d,m=e.commit||m,k=e.taskTitle||k)}console.log(t.cyan(`\n[TNC TestRunner - AES-256-GCM Secure] Task: "${k}"`)),console.log(t.gray(` Run ID : ${a}`)),console.log(t.gray(` Type : ${d?.type}`)),console.log(t.gray(` Commit : ${m?.sha?.slice(0,7)||"N/A"} — ${m?.message||""}`));let g={exitCode:1,stdout:"",stderr:"",status:null,body:null,url:null};if("exitCode"===d.type||"stdout"===d.type||"regex"===d.type){const e=d.command||d.script;e?(console.log(t.yellow(` Running : ${e}`)),g=await new Promise(o=>{const r=s(e,[],{shell:!0,cwd:process.cwd(),timeout:6e4});let n="",i="";r.stdout.on("data",e=>{n+=e.toString(),process.stdout.write(t.gray(" │ ")+e.toString())}),r.stderr.on("data",e=>{i+=e.toString(),process.stderr.write(t.red(" │ ")+e.toString())}),r.on("close",e=>{o({exitCode:e??1,stdout:n.trim(),stderr:i.trim(),status:null,body:null,url:null})}),r.on("error",e=>{o({exitCode:1,stdout:"",stderr:e.message,status:null,body:null,url:null})})}),console.log(0===g.exitCode?t.green(" [OK] Exit 0"):t.red(` [FAIL] Exit ${g.exitCode} - ${g.stderr.slice(0,100)}`))):g.stderr="No command specified in testConfig"}else if("http"===d.type){const e=d.endpoint||d.url,s=(d.method||"GET").toUpperCase();console.log(t.yellow(` Hitting : ${s} ${e}`)),g=await new Promise(n=>{const i=e?.startsWith("https")?r:o,c=new URL(e),a={hostname:c.hostname,port:c.port||("https:"===c.protocol?443:80),path:c.pathname+c.search,method:s},h=i.request(a,s=>{let o="";s.on("data",e=>o+=e.toString()),s.on("end",()=>{let r=o;try{r=JSON.parse(o)}catch(e){}console.log(t.green(` [OK] HTTP ${s.statusCode}`)),n({exitCode:0,stdout:o.slice(0,500),stderr:"",status:s.statusCode,body:r,url:e})})});h.on("error",s=>{console.log(t.red(` [Error] HTTP error: ${s.message}`)),n({exitCode:1,stdout:"",stderr:s.message,status:null,body:null,url:e})}),h.setTimeout(15e3,()=>{h.destroy(),n({exitCode:1,stdout:"",stderr:"Request timeout (15s)",status:null,body:null,url:e})})})}else if("browser"===d.type){console.log(t.yellow(` Browser Flow : ${d.url}`));try{const{runBrowserTest:e}=require("../commands/tasks/complete"),s=await e(d,null,{});g={exitCode:s.elementExists?0:1,stdout:s.bodyText?s.bodyText.slice(0,500):"",stderr:s.elementExists?"":`Element "${d.expect?.element}" not found`,elementExists:s.elementExists,elementText:s.elementText,url:s.url,status:200,body:null},console.log(t.green(` [OK] Browser check completed: elementExists=${s.elementExists}`))}catch(e){console.log(t.red(` [Error] Browser check failed: ${e.message}`)),g={exitCode:1,stdout:"",stderr:e.message,elementExists:!1,elementText:"",url:d.url,status:null,body:null}}}else{const e=d.command||d.script;e?g=await new Promise(t=>{const o=s(e,[],{shell:!0,cwd:process.cwd(),timeout:6e4});let r="",n="";o.stdout.on("data",e=>r+=e.toString()),o.stderr.on("data",e=>n+=e.toString()),o.on("close",e=>t({exitCode:e??1,stdout:r.trim(),stderr:n.trim(),status:null,body:null,url:null})),o.on("error",e=>t({exitCode:1,stdout:"",stderr:e.message,status:null,body:null,url:null}))}):g.stderr=`Unsupported testConfig type: ${d.type}`}const p={runId:a,taskId:h,runResultToken:l,encryptedResult:l?i(g,l):null,...g};this.socket.emit("thinknsh:test-result",p),console.log(t.cyan(" [AES-256-GCM] Encrypted result dispatched to TNC server.\n"))}catch(e){console.error("thinknsh:test-dispatch error:",e.message)}finally{setTimeout(()=>t.delete(e.runId),6e4)}}})}joinRoom(e){return new Promise((t,s)=>{if(!this.connected||!this.socket)return s(new Error("Not connected to server"));if(!this.currentUser)return s(new Error("Not logged in — run: login"));const o=setTimeout(()=>{this.socket.off("thinknsh:joined",r),this.socket.off("thinknsh:error",i),this.socket.off("thinknsh:ready",n),s(new Error("Join timeout"))},8e3),r=e=>{clearTimeout(o),this.socket.off("thinknsh:error",i),this.roomId=e.roomId,t(e)};let n=null;const i=e=>{clearTimeout(o),this.socket.off("thinknsh:joined",r),n&&this.socket.off("thinknsh:ready",n),s(new Error(e.message||"Join error"))},c=()=>{this.socket.once("thinknsh:joined",r),this.socket.once("thinknsh:error",i),this._emitJoin(e)};this._serverReady?c():(n=()=>{this._serverReady=!0,c()},this.socket.once("thinknsh:ready",n))})}_emitJoin(e){this.socket&&this.currentUser&&(this._joiningRoomId=e,this.socket.emit("thinknsh:join",{roomId:e,userId:this.currentUser.userId,name:this.currentUser.name,userType:this.currentUser.userType}))}leaveRoom(){return this.socket&&this.roomId&&(this.socket.emit("leave-room",{roomId:this.roomId}),this.roomId=null,this._joiningRoomId=null),Promise.resolve({success:!0})}sendMessage(e,t=null){return new Promise((s,o)=>{if(!this.socket?.connected)return o(new Error("Not connected"));const r=this.roomId;if(!r)return o(new Error("Not in any room. Use: join <room-id>"));if(!this.currentUser)return o(new Error("Not authenticated. Run: login"));const n=e?e.trim():"",i=e2eeVault.encrypt(n,r);this.socket.emit("thinknsh:message",{roomId:r,userId:this.currentUser.userId,name:this.currentUser.name,userType:this.currentUser.userType||"User",message:i,isE2EE:!0,replyTo:t||null}),s({success:!0,message:i,plaintext:n})})}getHistory(e=30,t=0){this.connected&&this.socket&&this.roomId&&this.socket.emit("thinknsh:getHistory",{roomId:this.roomId,limit:e,skip:t})}getNotifications(){this.connected&&this.currentUser&&this.socket.emit("thinknsh:getNotifications",{userId:this.currentUser.userId})}sendNotification(e,t="room"){return this.connected&&this.socket?(this.socket.emit("notification:send",{type:e.type||"info",title:e.title||"Notification",message:e.message,data:e.data||{},target:"room"===t?this.roomId:t,timestamp:(new Date).toISOString()}),Promise.resolve({success:!0})):Promise.reject(new Error("Not connected"))}markNotificationsRead(e=[]){this.connected&&this.currentUser&&this.socket.emit("thinknsh:markRead",{userId:this.currentUser.userId,notificationIds:e})}processPendingMessages(){this.messageQueue.length&&this.roomId&&this.currentUser&&(this.messageQueue.forEach(e=>this.sendMessage(e)),this.messageQueue=[])}disconnect(){this.socket&&(this.socket.removeAllListeners(),this.socket.disconnect(),this.socket=null,this.connected=!1,this.currentUser=null,this.roomId=null,this._handlersSetup=!1,this._serverReady=!1,this._serverReady=!1)}emitSandboxSync(e,t){this.connected&&this.socket&&e&&this.socket.emit("sandbox:sync",{userId:this.currentUser?.userId,roomId:this.roomId,event:t,sandbox:{id:e.id,state:e.state,pid:e.pid||null,cmd:e.cmd,hostPort:e.hostPort,containerPort:e.containerPort,isolationLevel:e.isolationLevel,memoryLimitMB:e.memoryLimitMB,startedAt:e.startedAt,frozenAt:e.frozenAt||null,platform:process.platform},timestamp:(new Date).toISOString()})}onSandboxAction(e,t){this.socket&&this.socket.on("sandbox:action",async s=>{const{sandboxId:o,action:r,requestedBy:n}=s||{};if(!o||!r)return;let i=null;try{switch(r){case"freeze":i=await e.freeze(o),i.success&&this.emitSandboxSync(i.meta,"frozen");break;case"resume":i=await e.resume(o),i.success&&this.emitSandboxSync(i.meta,"resumed");break;case"destroy":i=await e.destroy(o),this.emitSandboxSync({id:o,state:"DESTROYED"},"destroyed");break;default:i={success:!1,message:`Unknown action: ${r}`}}}catch(e){i={success:!1,message:e.message}}this.socket.emit("sandbox:action-ack",{sandboxId:o,action:r,requestedBy:n,success:i?.success,message:i?.message,timestamp:(new Date).toISOString()}),t&&t(r,i)})}isConnected(){return this.connected&&!!this.socket?.connected}getCurrentRoom(){return this.roomId}getSocketId(){return this.socket?.id||null}getParticipants(){return Promise.resolve([])}sendTyping(){}ping(){this.connected&&this.socket&&this.socket.emit("ping")}}module.exports=WebSocketManager;
7
+ const{io}=require("socket.io-client"),EventEmitter=require("events"),e2eeVault=require("./crypto/e2ee-vault");class WebSocketManager extends EventEmitter{constructor(e={}){super(),this.config={serverUrl:e.serverUrl||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com",reconnectAttempts:e.reconnectAttempts||5,reconnectDelay:e.reconnectDelay||1e3,...e},this.socket=null,this.connected=!1,this.currentUser=null,this.roomId=null,this.reconnectCount=0,this.messageQueue=[],this._authToken=null,this._handlersSetup=!1,this._serverReady=!1,this._shellRef=e._shellRef||null}connect(e=null){return new Promise((t,s)=>{try{e&&(this._authToken=e),this.socket=io(this.config.serverUrl+"/thinknsh",{transports:["websocket","polling"],reconnection:!0,reconnectionAttempts:this.config.reconnectAttempts,reconnectionDelay:this.config.reconnectDelay,auth:this._authToken?{token:this._authToken}:{}});const o=setTimeout(()=>{s(new Error("Connection timeout"))},1e4);this.socket.on("connect",()=>{clearTimeout(o),this.connected=!0,this.reconnectCount=0,this.currentUser&&this._announceUser(),this.processPendingMessages(),this.emit("connected",{socketId:this.socket.id}),t({success:!0,socketId:this.socket.id})}),this.socket.on("connect_error",e=>{clearTimeout(o),0===this.reconnectCount&&s(new Error(`Cannot connect to ${this.config.serverUrl}`)),this.reconnectCount++,this.emit("error",{message:e.message})}),this.socket.on("disconnect",e=>{this.connected=!1,this.emit("disconnected",{reason:e})}),this.socket.on("task:verdict",e=>{this.emit("task:verdict",e)}),this.socket.on("task:run-local",async e=>{try{const t=require("chalk"),{runLocal:s}=require("./local-runner"),o=e.taskId,r=this._shellRef,n=r?r.api:null;if(!n)return void console.log(t.red("\n[Error] CLI not authenticated. Run: login\n"));let i;try{i=await n._request("GET",`/thinknsh/${o}/cli`)}catch(e){return void console.log(t.red(`\n[Error] Could not fetch task info: ${e.message}\n`))}if(!i||!i.webhookSecret)return void console.log(t.red("\n[Error] Task has no webhookSecret - cannot submit result.\n"));await s({taskId:o,webhookSecret:i.webhookSecret,apiUrl:this.config.serverUrl||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com",repoUrl:i.repoUrl||i.githubUrl||i.repo_url||null,repoBranch:i.branch||i.repoBranch||"main",workflowFile:i.workflowFile||i.workflow||null,chalk:t,shell:r,onLog:e=>{this.socket.emit("task:log",{taskId:o,boardId:i.boardId,...e})}})}catch(e){console.error("task:run-local error:",e.message)}}),this.socket.on("reconnect",()=>{this.connected=!0,this.emit("reconnected",{}),this.currentUser&&this._announceUser(),this.roomId&&this.currentUser&&this._emitJoin(this.roomId)}),this._handlersSetup||(this.setupEventHandlers(),this._handlersSetup=!0)}catch(e){s(e)}})}_announceUser(){if(this.socket&&this.currentUser&&(this.socket.emit("thinknsh:connected",{userId:this.currentUser.userId,userName:this.currentUser.name,shellToken:this._authToken}),this._shellRef?.sandboxEngine))try{this._shellRef.sandboxEngine.list({start:!1}).then(e=>e.forEach(e=>this.emitSandboxSync(e,"sync"))).catch(()=>{})}catch(e){}}setUser(e,t=null){if(this._user=e,!e)return;t&&(this._authToken=t);const s=(e.userId||e._id||"").toString();this.currentUser={userId:s,name:e.name||e.email||"Unknown",userType:e.userType||e.model||e.role||"User"},this.connected&&this.socket&&this._announceUser()}setupEventHandlers(){this.socket.on("thinknsh:ready",e=>{this._serverReady=!0,this.emit("ready",e)}),this.socket.on("thinknsh:joined",e=>{if(this.roomId=e.roomId,e.history?.length){const t=e.history.map(e=>{let t=e.message||e.content||e.text||"",s=!1;if("string"==typeof t&&t.startsWith("e2ee:")){const e=e2eeVault.decrypt(t,this.roomId);e!==t&&(t=e,s=!0)}return{...e,message:t,isE2EE:s}});this.emit("messageHistory",t)}this.emit("roomJoined",e)}),this.socket.on("thinknsh:message",e=>{let t=e.message||"",s=e.isE2EE||!1;if("string"==typeof t&&t.startsWith("e2ee:")){const e=e2eeVault.decrypt(t,this.roomId);e!==t&&(t=e,s=!0)}this.emit("message",{username:e.name,userId:e.userId,message:t,isE2EE:s,source:"shell",timestamp:e.timestamp})}),this.socket.on("chat-message",e=>{let t=e.message||"",s=e.isE2EE||!1;if("string"==typeof t&&t.startsWith("e2ee:")){const e=e2eeVault.decrypt(t,this.roomId);e!==t&&(t=e,s=!0)}this.emit("message",{username:e.name,userId:e.userId,message:t,isE2EE:s,source:e.source||"web",timestamp:e.timestamp})}),this.socket.on("e2ee:key-sync",e=>{!e?.roomId||this.roomId&&e.roomId!==this.roomId||this.emit("e2ee:key-sync",e)}),this.socket.on("e2ee:key-delivered",e=>{e?.packet?.rawKey&&(e2eeVault.setRawRoomKey(e.roomId,e.packet.rawKey),this.emit("e2ee:key-updated",e))}),this.socket.on("e2ee:packet",e=>{this.emit("e2ee:packet",e)}),this.socket.on("thinknsh:userJoined",e=>{this.emit("userJoined",{username:e.name,source:"shell"})}),this.socket.on("thinknsh:userLeft",e=>{this.emit("userLeft",{username:e.name,source:"shell"})}),this.socket.on("user-joined",e=>{this.emit("userJoined",{username:e,source:"web"})}),this.socket.on("user-left",e=>{this.emit("userLeft",{username:e,source:"web"})}),this.socket.on("user-list",e=>{this.emit("userList",{users:e})}),this.socket.on("thinknsh:history",e=>{this.emit("messageHistory",e.messages?.length?e.messages:[])}),this.socket.on("new-notification",e=>{this.emit("notification",{type:e.type||"info",level:e.type||"info",title:e.title||"Notification",message:e.message||"",meta:e.meta||{},taskTitle:e.taskTitle||e.meta?.taskTitle||"",assignedBy:e.assignedBy||e.meta?.assignedBy||""})}),this.socket.on("room:activity",e=>{this.emit("roomActivity",e)}),this.socket.on("thinknsh:status",e=>{this.emit("shellStatus",e)}),this.socket.on("thinknsh:notifications",e=>{this.emit("notificationList",e.notifications)}),this.socket.on("thinknsh:error",e=>{this.emit("serverError",e)}),this.socket.on("error",e=>{this.emit("serverError",e)});const e=new Set;this.socket.on("cicd:run-dispatch",async t=>{if(!t||!t.runId)return;if(e.has(t.runId))return;const s=this.roomId||this._joiningRoomId;if(!s||t.roomId&&String(t.roomId)!==String(s)){const e=require("chalk");return void console.log(e.yellow(`\n[CI/CD] Ignored run ${t.runId}: not for the room this shell joined.`))}e.add(t.runId);try{const e=require("chalk"),{runCicdLocal:s}=require("./local-runner"),o=this._shellRef,r=o?o.api:null;console.log(e.cyan(`\n[CI/CD] Execution request received for room: ${t.roomId||this.roomId}`)),await s({runId:t.runId,roomId:t.roomId||this.roomId,workflowFile:t.workflowFile||"universal-polyglot-ci.yml",workflowYaml:t.workflowYaml||"",localPath:t.localPath||"",apiUrl:r?r.baseURL:this.config.serverUrl,chalk:e,api:r,socket:this.socket})}catch(e){console.error("\n[Error] Error in CI/CD local execution:",e)}finally{setTimeout(()=>e.delete(t.runId),3e4)}});const t=new Set;this.socket.on("thinknsh:test-dispatch",async e=>{if(e&&e.runId&&e.taskId&&!t.has(e.runId)){t.add(e.runId);try{const t=require("chalk"),{spawn:s}=require("child_process"),o=require("http"),r=require("https"),n=require("crypto"),i=(e,t)=>{if(!t)return null;const s=n.createHash("sha256").update(t).digest(),o=n.randomBytes(12),r=n.createCipheriv("aes-256-gcm",s,o),i=JSON.stringify(e||{}),c=Buffer.concat([r.update(i,"utf8"),r.final()]),a=r.getAuthTag();return{iv:o.toString("hex"),tag:a.toString("hex"),cipherText:c.toString("hex")}},c=(e,t)=>{if(!(e&&e.cipherText&&e.iv&&e.tag))return null;try{const s=n.createHash("sha256").update(t).digest(),o=Buffer.from(e.iv,"hex"),r=Buffer.from(e.tag,"hex"),i=Buffer.from(e.cipherText,"hex"),c=n.createDecipheriv("aes-256-gcm",s,o);c.setAuthTag(r);const a=Buffer.concat([c.update(i),c.final()]);return JSON.parse(a.toString("utf8"))}catch(e){return null}},{runId:a,taskId:h,runResultToken:l,encryptedPayload:u}=e;let d=e.testConfig,m=e.commit,k=e.taskTitle;if(u&&l){const e=c(u,l);e&&(d=e.testConfig||d,m=e.commit||m,k=e.taskTitle||k)}console.log(t.cyan(`\n[TNC TestRunner - AES-256-GCM Secure] Task: "${k}"`)),console.log(t.gray(` Run ID : ${a}`)),console.log(t.gray(` Type : ${d?.type}`)),console.log(t.gray(` Commit : ${m?.sha?.slice(0,7)||"N/A"} — ${m?.message||""}`));let g={exitCode:1,stdout:"",stderr:"",status:null,body:null,url:null};if("exitCode"===d.type||"stdout"===d.type||"regex"===d.type){const e=d.command||d.script;e?(console.log(t.yellow(` Running : ${e}`)),g=await new Promise(o=>{const r=s(e,[],{shell:!0,cwd:process.cwd(),timeout:6e4});let n="",i="";r.stdout.on("data",e=>{n+=e.toString(),process.stdout.write(t.gray(" │ ")+e.toString())}),r.stderr.on("data",e=>{i+=e.toString(),process.stderr.write(t.red(" │ ")+e.toString())}),r.on("close",e=>{o({exitCode:e??1,stdout:n.trim(),stderr:i.trim(),status:null,body:null,url:null})}),r.on("error",e=>{o({exitCode:1,stdout:"",stderr:e.message,status:null,body:null,url:null})})}),console.log(0===g.exitCode?t.green(" [OK] Exit 0"):t.red(` [FAIL] Exit ${g.exitCode} - ${g.stderr.slice(0,100)}`))):g.stderr="No command specified in testConfig"}else if("http"===d.type){const e=d.endpoint||d.url,s=(d.method||"GET").toUpperCase();console.log(t.yellow(` Hitting : ${s} ${e}`)),g=await new Promise(n=>{const i=e?.startsWith("https")?r:o,c=new URL(e),a={hostname:c.hostname,port:c.port||("https:"===c.protocol?443:80),path:c.pathname+c.search,method:s},h=i.request(a,s=>{let o="";s.on("data",e=>o+=e.toString()),s.on("end",()=>{let r=o;try{r=JSON.parse(o)}catch(e){}console.log(t.green(` [OK] HTTP ${s.statusCode}`)),n({exitCode:0,stdout:o.slice(0,500),stderr:"",status:s.statusCode,body:r,url:e})})});h.on("error",s=>{console.log(t.red(` [Error] HTTP error: ${s.message}`)),n({exitCode:1,stdout:"",stderr:s.message,status:null,body:null,url:e})}),h.setTimeout(15e3,()=>{h.destroy(),n({exitCode:1,stdout:"",stderr:"Request timeout (15s)",status:null,body:null,url:e})})})}else if("browser"===d.type){console.log(t.yellow(` Browser Flow : ${d.url}`));try{const{runBrowserTest:e}=require("../commands/tasks/complete"),s=await e(d,null,{});g={exitCode:s.elementExists?0:1,stdout:s.bodyText?s.bodyText.slice(0,500):"",stderr:s.elementExists?"":`Element "${d.expect?.element}" not found`,elementExists:s.elementExists,elementText:s.elementText,url:s.url,status:200,body:null},console.log(t.green(` [OK] Browser check completed: elementExists=${s.elementExists}`))}catch(e){console.log(t.red(` [Error] Browser check failed: ${e.message}`)),g={exitCode:1,stdout:"",stderr:e.message,elementExists:!1,elementText:"",url:d.url,status:null,body:null}}}else{const e=d.command||d.script;e?g=await new Promise(t=>{const o=s(e,[],{shell:!0,cwd:process.cwd(),timeout:6e4});let r="",n="";o.stdout.on("data",e=>r+=e.toString()),o.stderr.on("data",e=>n+=e.toString()),o.on("close",e=>t({exitCode:e??1,stdout:r.trim(),stderr:n.trim(),status:null,body:null,url:null})),o.on("error",e=>t({exitCode:1,stdout:"",stderr:e.message,status:null,body:null,url:null}))}):g.stderr=`Unsupported testConfig type: ${d.type}`}const y={runId:a,taskId:h,runResultToken:l,encryptedResult:l?i(g,l):null,...g};this.socket.emit("thinknsh:test-result",y),console.log(t.cyan(" [AES-256-GCM] Encrypted result dispatched to TNC server.\n"))}catch(e){console.error("thinknsh:test-dispatch error:",e.message)}finally{setTimeout(()=>t.delete(e.runId),6e4)}}})}joinRoom(e){return new Promise((t,s)=>{if(!this.connected||!this.socket)return s(new Error("Not connected to server"));if(!this.currentUser)return s(new Error("Not logged in — run: login"));const o=setTimeout(()=>{this.socket.off("thinknsh:joined",r),this.socket.off("thinknsh:error",i),this.socket.off("thinknsh:ready",n),s(new Error("Join timeout"))},8e3),r=e=>{clearTimeout(o),this.socket.off("thinknsh:error",i),this.roomId=e.roomId,t(e)};let n=null;const i=e=>{clearTimeout(o),this.socket.off("thinknsh:joined",r),n&&this.socket.off("thinknsh:ready",n),s(new Error(e.message||"Join error"))},c=()=>{this.socket.once("thinknsh:joined",r),this.socket.once("thinknsh:error",i),this._emitJoin(e)};this._serverReady?c():(n=()=>{this._serverReady=!0,c()},this.socket.once("thinknsh:ready",n))})}_emitJoin(e){this.socket&&this.currentUser&&(this._joiningRoomId=e,this.socket.emit("thinknsh:join",{roomId:e,userId:this.currentUser.userId,name:this.currentUser.name,userType:this.currentUser.userType}))}leaveRoom(){return this.socket&&this.roomId&&(this.socket.emit("leave-room",{roomId:this.roomId}),this.roomId=null,this._joiningRoomId=null),Promise.resolve({success:!0})}sendMessage(e,t=null){return new Promise((s,o)=>{if(!this.socket?.connected)return o(new Error("Not connected"));const r=this.roomId;if(!r)return o(new Error("Not in any room. Use: join <room-id>"));if(!this.currentUser)return o(new Error("Not authenticated. Run: login"));const n=e?e.trim():"",i=e2eeVault.encrypt(n,r);this.socket.emit("thinknsh:message",{roomId:r,userId:this.currentUser.userId,name:this.currentUser.name,userType:this.currentUser.userType||"User",message:i,isE2EE:!0,replyTo:t||null}),s({success:!0,message:i,plaintext:n})})}getHistory(e=30,t=0){this.connected&&this.socket&&this.roomId&&this.socket.emit("thinknsh:getHistory",{roomId:this.roomId,limit:e,skip:t})}getNotifications(){this.connected&&this.currentUser&&this.socket.emit("thinknsh:getNotifications",{userId:this.currentUser.userId})}sendNotification(e,t="room"){return this.connected&&this.socket?(this.socket.emit("notification:send",{type:e.type||"info",title:e.title||"Notification",message:e.message,data:e.data||{},target:"room"===t?this.roomId:t,timestamp:(new Date).toISOString()}),Promise.resolve({success:!0})):Promise.reject(new Error("Not connected"))}markNotificationsRead(e=[]){this.connected&&this.currentUser&&this.socket.emit("thinknsh:markRead",{userId:this.currentUser.userId,notificationIds:e})}processPendingMessages(){this.messageQueue.length&&this.roomId&&this.currentUser&&(this.messageQueue.forEach(e=>this.sendMessage(e)),this.messageQueue=[])}disconnect(){this.socket&&(this.socket.removeAllListeners(),this.socket.disconnect(),this.socket=null,this.connected=!1,this.currentUser=null,this.roomId=null,this._handlersSetup=!1,this._serverReady=!1,this._serverReady=!1)}emitSandboxSync(e,t){this.connected&&this.socket&&e&&this.socket.emit("sandbox:sync",{userId:this.currentUser?.userId,roomId:this.roomId,event:t,sandbox:{id:e.id,state:e.state,pid:e.pid||null,cmd:e.cmd,hostPort:e.hostPort,containerPort:e.containerPort,isolationLevel:e.isolationLevel,memoryLimitMB:e.memoryLimitMB,image:e.image||null,container:e.container||null,runtime:e.runtime||null,roomName:e.roomName||null,userName:e.userName||null,startedAt:e.startedAt,frozenAt:e.frozenAt||null,platform:process.platform},timestamp:(new Date).toISOString()})}onSandboxAction(e,t){this.socket&&(this.socket.on("sandbox:action",async s=>{const{sandboxId:o,action:r,requestedBy:n}=s||{};if(!o||!r)return;const i=this.currentUser?.userId;if(!/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,62}$/.test(String(o))||n&&i&&String(n)!==String(i))return void this.socket.emit("sandbox:action-ack",{sandboxId:o,action:r,requestedBy:n,success:!1,message:"Only the sandbox owner can do that",timestamp:(new Date).toISOString()});let c=null;try{switch(r){case"freeze":c=await e.freeze(o),c.success&&this.emitSandboxSync(c.meta,"frozen");break;case"resume":c=await e.resume(o),c.success&&this.emitSandboxSync(c.meta,"resumed");break;case"destroy":c=await e.destroy(o),this.emitSandboxSync({id:o,state:"DESTROYED"},"destroyed");break;case"restart":await e.freeze(o),c=await e.resume(o),c.success&&this.emitSandboxSync(c.meta,"resumed");break;default:c={success:!1,message:`Unknown action: ${r}`}}}catch(e){c={success:!1,message:e.message}}this.socket.emit("sandbox:action-ack",{sandboxId:o,action:r,requestedBy:n,success:c?.success,message:c?.message,timestamp:(new Date).toISOString()}),t&&t(r,c)}),this.socket.on("sandbox:logs-request",async({requestId:t,sandboxId:s,requestedBy:o}={})=>{if(!t||!/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,62}$/.test(String(s||"")))return;const r=this.currentUser?.userId;if(o&&r&&String(o)!==String(r))return;let n;try{n=await e.logs(s,{tail:300})}catch(e){n=`[thinknsh] ${e.message}`}this.socket.emit("sandbox:logs-reply",{requestId:t,sandboxId:s,logs:n})}))}isConnected(){return this.connected&&!!this.socket?.connected}getCurrentRoom(){return this.roomId}getSocketId(){return this.socket?.id||null}getParticipants(){return Promise.resolve([])}sendTyping(){}ping(){this.connected&&this.socket&&this.socket.emit("ping")}}module.exports=WebSocketManager;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ const{spawn}=require("child_process"),chalk=require("chalk");class SandboxWindowLauncher{static spawn(e){const n=process.argv[1],o=process.execPath,s=`logs -f tnc-sb-${e}`;if("darwin"===process.platform){"vscode"===process.env.TERM_PROGRAM||process.env.VSCODE_INJECTION;const a=`\n tell application "Terminal"\n launch\n do script "'${o}' '${n}' ${s}"\n activate\n end tell\n `;try{return spawn("osascript",["-e",a],{detached:!0,stdio:"ignore"}).unref(),console.log(chalk.green("[TNC] Dedicated Sandbox Console opened in separate Terminal window!")),!0}catch(n){return console.log(chalk.yellow(`[Notice] Could not auto-launch Terminal window: ${n.message}`)),console.log(chalk.dim(`You can manually view it with: thinknsh logs -f tnc-sb-${e}`)),!1}}if("win32"===process.platform)try{return spawn(`start cmd.exe /k "title TNC Sandbox: ${e} && "${o}" "${n}" ${s}""`,[],{detached:!0,stdio:"ignore",shell:!0}).unref(),console.log(chalk.green("[TNC] Sandbox Console opened in separate Command Prompt window!")),!0}catch(e){return!1}const a=["x-terminal-emulator","gnome-terminal","konsole","xterm"];for(const e of a)try{return spawn(e,["-e",`'${o}' '${n}' ${s}`],{detached:!0,stdio:"ignore"}).unref(),console.log(chalk.green(`[TNC] Sandbox Console opened in ${e}!`)),!0}catch(e){}return console.log(chalk.dim(`Tip: Run "thinknsh logs -f tnc-sb-${e}" to view live logs.`)),!1}}module.exports=SandboxWindowLauncher;