thinkncollab-shell 2.0.0-beta.6 β†’ 2.0.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Readme.md CHANGED
@@ -19,6 +19,35 @@ Instead of running a heavy 4GB+ RAM virtual machine daemon just to boot a local
19
19
 
20
20
  ---
21
21
 
22
+ ## πŸ” Sign in with ThinkNCollab
23
+
24
+ Every `thinknsh` command needs a ThinkNCollab account. One command signs you in and connects this project to its room:
25
+
26
+ ```bash
27
+ thinknsh login # asks: "Continue with Google" or "Email and password", then connects the room
28
+ thinknsh connect # choose this project's room again (or: connect <room name>)
29
+ thinknsh login --google # Google in the browser; already signed in there? just press Allow
30
+ thinknsh login --no-browser # prints the Google sign-in link instead (open it on this computer)
31
+ thinknsh login --password # email + password at a hidden prompt
32
+ thinknsh auth # who is signed in, where the token is kept, when it was last confirmed
33
+ thinknsh auth sessions # this account's shell sessions on every machine
34
+ thinknsh auth revoke <ID> # end one (a lost laptop)
35
+ thinknsh logout # revoke this machine's token
36
+ ```
37
+
38
+ - Which room: a `tncproject` file in the folder wins; otherwise the room you picked for this project before (kept per project folder in `~/.thinkncollab/projects.json`); with one room it is that one; otherwise `login` asks once. Commands that need a room (`tasks`, `say`, `issues`, …) join it by themselves.
39
+ - With Google, a browser that is already signed in to thinkncollab.com only asks you to allow this computer; otherwise it goes straight to Google's account picker.
40
+ - The shell never sees your password in browser sign-in: the site sends a one-time code back to `127.0.0.1` (state + PKCE), and the shell trades it for a token.
41
+ - The token lives in the system keychain (macOS Keychain, Windows Credential Manager, Secret Service); `~/.thinkncollab/session.json` (0600) keeps only who you are. A token is only ever sent to the server that issued it, over HTTPS.
42
+ - The shell re-confirms the sign-in with the platform every 12 hours, and any 401 from the platform signs you out at once, so a revoked token stops the shell on its next platform call or check. Offline, the last confirmation counts for 7 days.
43
+ - Passwords and tokens are never taken as arguments, and old history lines that had them are scrubbed from `~/.thinknsh_history` (now 0600).
44
+ - CI and scripts: set `THINKNCOLLAB_TOKEN` to a token from thinkncollab.com (never written to disk).
45
+ - Without signing in you can still run `login`, `register`, `help` and the engine's own setup (`dock setup / start / stop / status / autostart / app`), which the installers use.
46
+
47
+ How it works and what the platform has to serve: [docs/PLATFORM-AUTH.md](docs/PLATFORM-AUTH.md).
48
+
49
+ ---
50
+
22
51
  ## πŸ“¦ Containers: `thinknsh run`, `build`, `compose`
23
52
 
24
53
  thinknsh has its own container engine, **dock**. You run, build and compose OCI containers with thinknsh commands. No other tool is needed. Images come from any OCI registry (the public one that hosts alpine, postgres and nginx works out of the box), containers run with `crun` on a real Linux kernel, and all state lives in one SQLite file. The daemon starts by itself on the first command.
@@ -38,7 +67,7 @@ thinknsh tag myapp ghcr.io/me/myapp:1.0 && thinknsh push ghcr.io/me/myapp:1.0
38
67
  thinknsh logout ghcr.io
39
68
  ```
40
69
 
41
- Registry logins are checked with the registry, then saved per registry host in `~/.tnc/dock/auth.json` (mode 0600, read only by the dock daemon, never shown by any command). `thinknsh login` with an email, or with no arguments, still signs you in to ThinkNCollab; with `-u`, `--password-stdin` or a registry host it logs in to a container registry. `thinknsh push ecr://…` (also `gar://`, `gcr://`, `ghcr://`) gets the registry and a short-lived login from that cloud's CLI and pushes through dock; `push --export IMAGE` writes it to an archive (`thinknsh save`). Registries on `localhost`/`127.0.0.1` use plain HTTP. A registry for the whole LAN (`thinknsh registry start --lan`) is TLS with its own certificate: other machines pin it with `thinknsh registry trust HOST:PORT --fingerprint …` and then pull and push over verified TLS. For another plain-HTTP registry, list its host:port in `TNC_DOCK_INSECURE_REGISTRIES`.
70
+ Registry logins are checked with the registry, then saved per registry host in `~/.tnc/dock/auth.json` (mode 0600, read only by the dock daemon, never shown by any command). `thinknsh login` with no arguments, `--password`, `--token` or an email signs you in to ThinkNCollab; with `-u`, `--password-stdin` or a registry host it logs in to a container registry. `thinknsh push ecr://…` (also `gar://`, `gcr://`, `ghcr://`) gets the registry and a short-lived login from that cloud's CLI and pushes through dock; `push --export IMAGE` writes it to an archive (`thinknsh save`). Registries on `localhost`/`127.0.0.1` use plain HTTP. A registry for the whole LAN (`thinknsh registry start --lan`) is TLS with its own certificate: other machines pin it with `thinknsh registry trust HOST:PORT --fingerprint …` and then pull and push over verified TLS. For another plain-HTTP registry, list its host:port in `TNC_DOCK_INSECURE_REGISTRIES`.
42
71
 
43
72
  Every command has its own help, for example `thinknsh run --help`. The full list:
44
73
 
@@ -143,6 +172,7 @@ New here? Read the step-by-step guide: [`docs/HOW-TO-USE.md`](docs/HOW-TO-USE.md
143
172
  | Installers with their own Node.js: `.pkg`/`.dmg`, Windows setup, Linux tarball + systemd | βœ… built in CI (`dock-bundle.yml`); signing when the certificates are set |
144
173
  | Corporate proxy and CA certificates, registry mirrors, insecure registries (`dock config`) | βœ… |
145
174
  | Registry passwords in the system keychain (macOS Keychain, Windows Credential Manager, Secret Service) | βœ… |
175
+ | ThinkNCollab sign-in for every command: browser login (PKCE), token in the system keychain, `auth status / sessions / revoke`, `THINKNCOLLAB_TOKEN` for CI | βœ… browser login and `--token` need the platform update in `docs/PLATFORM-AUTH.md` deployed; `--password` works today |
146
176
  | Diagnostics bundle, factory reset (`dock diagnose`, `dock reset`) | βœ… |
147
177
  | GPUs (`--gpus`, compose `deploy.resources.reservations.devices`, CDI names like `nvidia.com/gpu=all`) | βœ… Linux and WSL2 through CDI (NVIDIA Container Toolkit); ❌ not possible on macOS |
148
178
  | Registry logins from credential helpers (ECR, GCR, OS keychains: `credHelpers` / `credsStore` of the engine CLI config, or `dock config --credential-helper`) | βœ… |
@@ -5,4 +5,4 @@
5
5
  * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
6
6
  * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
7
7
  */
8
- const path=require("path"),chalk=require("chalk"),ThinkNCollabShell=require("../src/core/shell"),args=process.argv.slice(2);if(0!==args.length&&["--help","-h","help"].includes(args[0]))console.log(`\n${chalk.cyan("ThinkNCollab Shell V2 - Native Cloud & Container Engine")}\n\n${chalk.yellow("Usage:")}\n thinknsh Start interactive shell\n thinknsh <command> Run single command\n thinknsh --help Show this help\n thinknsh --version Show version\n\n${chalk.yellow("Containers:")}\n ${chalk.green("thinknsh run")} [options] IMAGE [CMD] Run a container (thinknsh run -it alpine sh)\n ${chalk.green("thinknsh ps")} [-a] List containers\n ${chalk.green("thinknsh logs")} [-f] NAME Show or follow output\n ${chalk.green("thinknsh exec")} -it NAME sh Shell into a running container\n ${chalk.green("thinknsh stop")} | start | restart | rm NAME…\n ${chalk.green("thinknsh build")} -t NAME . Build an image from a Containerfile\n ${chalk.green("thinknsh images")} | pull | rmi | tag Manage images\n ${chalk.green("thinknsh compose")} up -d | down | ps | logs Multi-container apps\n ${chalk.green("thinknsh volume")} | network | system Volumes, networks, disk usage\n ${chalk.green("thinknsh dock")} setup | status | selftest Engine setup and checks\n\n Every command has its own help: thinknsh run --help\n\n${chalk.yellow("Examples:")}\n thinknsh run -d --name db -e POSTGRES_PASSWORD=dev -p 5432:5432 postgres:16\n thinknsh run --rm -it alpine sh\n thinknsh build -t myapp .\n thinknsh compose up -d\n\n${chalk.dim('For more commands, type "help" in the interactive shell')}\n `),process.exit(0);else if(args.includes("--version")||1===args.length&&"-v"===args[0]){let n;try{n=require("../../package.json")}catch{n=require("../package.json")}console.log(n.version),process.exit(0)}else{const n=new ThinkNCollabShell({serverUrl:process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com",apiUrl:process.env.THINKNCOLLAB_API_URL||"https://thinkncollab.com",wsUrl:process.env.THINKNCOLLAB_WS_URL||"https://thinkncollab.com",autoConnect:"true"===process.env.THINKNCOLLAB_AUTO_CONNECT});if(args.length>0){const e=!require("../src/commands/dock/containers").ALL_NAMES.includes(args[0])&&args.some(n=>["dev","up","console","notifications","panel","notif-panel"].includes(n))||args.includes("-f")||args.includes("--follow")||"notify"===args[0]&&args.some(n=>/^\d{4,5}$/.test(n)),s=args.map(n=>n.includes(" ")||n.includes("\t")?`"${n.replace(/"/g,'\\"')}"`:n).join(" ");n.execute(s,args).then(()=>{e||process.exit(process.exitCode||0)}).catch(n=>{console.error(chalk.red(`Error: ${n.message}`)),process.exit(1)})}else n.start().catch(n=>{console.error(chalk.red(`Fatal error: ${n.message}`)),process.exit(1)})}
8
+ const path=require("path"),chalk=require("chalk"),ThinkNCollabShell=require("../src/core/shell"),args=process.argv.slice(2);if(0!==args.length&&["--help","-h","help"].includes(args[0]))console.log(`\n${chalk.cyan("ThinkNCollab Shell V2 - Native Cloud & Container Engine")}\n\n${chalk.yellow("Usage:")}\n thinknsh Start interactive shell\n thinknsh <command> Run single command\n thinknsh --help Show this help\n thinknsh --version Show version\n\n${chalk.yellow("Account (required for every command):")}\n ${chalk.green("thinknsh login")} Sign in (Google or email and password) and connect to this project's room\n ${chalk.green("thinknsh connect")} [ROOM] Choose or switch this project's room\n ${chalk.green("thinknsh login --google")} Google in the browser; just "Allow" if already signed in there\n ${chalk.green("thinknsh login --password")} Email and password at a hidden prompt\n ${chalk.green("thinknsh auth")} status | sessions | revoke ID Manage the sign-in\n ${chalk.green("thinknsh logout")} Sign out and revoke this machine's token\n CI and scripts: set THINKNCOLLAB_TOKEN to a token from thinkncollab.com\n\n${chalk.yellow("Containers:")}\n ${chalk.green("thinknsh run")} [options] IMAGE [CMD] Run a container (thinknsh run -it alpine sh)\n ${chalk.green("thinknsh ps")} [-a] List containers\n ${chalk.green("thinknsh logs")} [-f] NAME Show or follow output\n ${chalk.green("thinknsh exec")} -it NAME sh Shell into a running container\n ${chalk.green("thinknsh stop")} | start | restart | rm NAME…\n ${chalk.green("thinknsh build")} -t NAME . Build an image from a Containerfile\n ${chalk.green("thinknsh images")} | pull | rmi | tag Manage images\n ${chalk.green("thinknsh compose")} up -d | down | ps | logs Multi-container apps\n ${chalk.green("thinknsh volume")} | network | system Volumes, networks, disk usage\n ${chalk.green("thinknsh dock")} setup | status | selftest Engine setup and checks\n\n Every command has its own help: thinknsh run --help\n\n${chalk.yellow("Examples:")}\n thinknsh run -d --name db -e POSTGRES_PASSWORD=dev -p 5432:5432 postgres:16\n thinknsh run --rm -it alpine sh\n thinknsh build -t myapp .\n thinknsh compose up -d\n\n${chalk.dim('For more commands, type "help" in the interactive shell')}\n `),process.exit(0);else if(args.includes("--version")||1===args.length&&"-v"===args[0]){let n;try{n=require("../../package.json")}catch{n=require("../package.json")}console.log(n.version),process.exit(0)}else{const n=new ThinkNCollabShell({serverUrl:process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com",apiUrl:process.env.THINKNCOLLAB_API_URL||"https://thinkncollab.com",wsUrl:process.env.THINKNCOLLAB_WS_URL||"https://thinkncollab.com",autoConnect:"true"===process.env.THINKNCOLLAB_AUTO_CONNECT});if(args.length>0){const e=!require("../src/commands/dock/containers").ALL_NAMES.includes(args[0])&&args.some(n=>["dev","up","console","notifications","panel","notif-panel"].includes(n))||args.includes("-f")||args.includes("--follow")||"notify"===args[0]&&args.some(n=>/^\d{4,5}$/.test(n)),s=args.map(n=>n.includes(" ")||n.includes("\t")?`"${n.replace(/"/g,'\\"')}"`:n).join(" ");n.execute(s,args).then(()=>{e||process.exit(process.exitCode||0)}).catch(n=>{console.error(chalk.red(`Error: ${n.message}`)),process.exit(1)})}else n.start().catch(n=>{console.error(chalk.red(`Fatal error: ${n.message}`)),process.exit(1)})}
@@ -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 chalk=require("chalk"),when=e=>e?new Date(e).toLocaleString():"-";async function status(e){const o=e.api,{VERIFY_EVERY_MS:s}=require("../../core/auth/gate");if(console.log(chalk.cyan("\n ThinkNCollab sign-in")),console.log(chalk.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")),console.log(` ${chalk.yellow("Server:")} ${o.apiUrl}`),!o.isAuthenticated())return console.log(` ${chalk.yellow("Status:")} ${chalk.red("Not signed in")} ${chalk.dim('(run "login")')}`),void console.log();const n=o.session,t=o.getUser()||{};if(console.log(` ${chalk.yellow("Status:")} ${chalk.green("Signed in")}`),console.log(` ${chalk.yellow("Account:")} ${t.name||"-"} ${t.email?chalk.dim(`<${t.email}>`):""}`),console.log(` ${chalk.yellow("Method:")} ${n.fromEnv?"THINKNCOLLAB_TOKEN":n.method||"password"}`),console.log(` ${chalk.yellow("Token kept in:")} ${o.tokenLocation()}`),console.log(` ${chalk.yellow("Signed in at:")} ${when(n.timestamp)}`),!n.fromEnv){const e=(Date.parse(n.lastVerifiedAt||n.timestamp)||0)+s;console.log(` ${chalk.yellow("Confirmed:")} ${when(n.lastVerifiedAt)} ${chalk.dim(`(next check after ${when(e)})`)}`)}n.expiresAt&&console.log(` ${chalk.yellow("Expires:")} ${when(n.expiresAt)}`),console.log()}async function sessions(e){const o=await e.api.sessions(),s=Array.isArray(o)?o:o?.sessions||[];if(!s.length)return void console.log(chalk.dim("No shell sessions."));const n=new(require("cli-table3"))({head:["ID","Machine","Started","Last seen","Ended"].map(e=>chalk.cyan(e))}),t=e.api.session?.shellSessionId;for(const e of s){const o=String(e._id||e.id||"");n.push([o===t?`${o} ${chalk.green("(this)")}`:o,e.machineId||"-",when(e.createdAt||e.loginAt),when(e.lastSeenAt),e.logoutAt?when(e.logoutAt):chalk.green("active")])}console.log(n.toString()),console.log(chalk.dim(" End one with: auth revoke <ID>"))}async function revoke(e,o){return o?o===e.api.session?.shellSessionId?(console.log(chalk.dim("That is this session; signing out.")),e.commandRegistry.execute("logout",e,["logout"])):(await e.api.endSession(o),void console.log(chalk.green(`[OK] Session ${o} ended`))):(console.log(chalk.red('[FAIL] Usage: auth revoke <session-id> (see "auth sessions")')),void(process.exitCode=1))}module.exports={name:"auth",description:"Manage the ThinkNCollab sign-in: status, verify, sessions, revoke",requiresAuth:!1,async execute(e,o){const[s="status",...n]=e;try{switch(s){case"status":return await status(o);case"login":return await o.commandRegistry.execute("login",o,["login",...n]);case"logout":return await o.commandRegistry.execute("logout",o,["logout"]);case"verify":return o.api.isAuthenticated()?(await o.api.verify(),void console.log(chalk.green("[OK] The platform confirmed this sign-in"))):(console.log(chalk.red("[FAIL] Not signed in")),void(process.exitCode=1));case"sessions":case"ls":return await sessions(o);case"revoke":return await revoke(o,n[0]);default:console.log(chalk.red(`[FAIL] Unknown: auth ${s}`)),console.log(chalk.dim(" auth status | verify | sessions | revoke <id> | login | logout")),process.exitCode=1}}catch(e){process.exitCode=1,console.log(chalk.red(`[FAIL] ${401===e.status?'The platform ended this sign-in; run "login"':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");module.exports={name:"login",description:"Login to ThinkNCollab",aliases:["signin"],requiresAuth:!1,async execute(e,s){if(s.api.isAuthenticated()){const e=s.api.getUser();return console.log(chalk.green(`[OK] Already logged in as ${e.name} (${e.email})`)),void console.log(chalk.dim(' Run "logout" first to switch accounts.'))}let a=e[0],o=e[1];if(!a||!o){const{input:e,password:s}=require("@inquirer/prompts");a||(a=await e({message:"Email:",validate:e=>e.includes("@")||"Enter a valid email"})),o||(o=await s({message:"Password:",mask:"*",validate:e=>e.length>=1||"Password required"}))}console.log(chalk.blue(" Logging in..."));try{const e=await s.api.login(a,o),l=e.user;if(s.ws.setUser(l,e.token),s.ws.isConnected())s.ws._announceUser?.();else try{await s.ws.connect(e.token),console.log(chalk.dim(" Connected to ThinkNCollab server"))}catch(e){console.log(chalk.yellow(` [WARN] WebSocket unavailable: ${e.message}`))}console.log(chalk.green(`[OK] Welcome, ${l.name}!`)),console.log(chalk.dim(` Logged in as ${l.email}`))}catch(e){e.message.includes("401")||e.message.toLowerCase().includes("invalid")||e.message.toLowerCase().includes("unauthorized")?console.log(chalk.red("[FAIL] Invalid email or password")):e.message.includes("connect")||e.message.includes("ECONNREFUSED")?console.log(chalk.red("[FAIL] Cannot reach server β€” is it running?")):console.log(chalk.red(`[FAIL] Login failed: ${e.message}`))}}};
7
+ const chalk=require("chalk");async function readStdin(){let o="";for await(const e of process.stdin)o+=e;return o.trim()}const cancelled=o=>"ExitPromptError"===o?.name||/force closed/i.test(o?.message||"");async function connectRoom(o){try{await require("../../core/room-link").connect(o,{interactive:!!process.stdin.isTTY})||console.log(chalk.dim(' No room picked for this project yet. Choose one any time with "connect".'))}catch(o){if(cancelled(o))return void console.log(chalk.dim(' No room picked. Choose one any time with "connect".'));console.log(chalk.yellow(` Signed in, but could not connect to a room: ${o.message}`)),console.log(chalk.dim(' Try again with "connect".'))}}async function chooseMethod(){const{select:o}=require("@inquirer/prompts");return o({message:"How would you like to sign in to ThinkNCollab?",choices:[{name:"Continue with Google",value:"google",description:"Opens your browser. If you are already signed in there, you only confirm this computer."},{name:"Email and password",value:"password",description:"Typed here; the password stays hidden."}]})}async function viaGoogle(o,{open:e}){const{authorize:n,openUrl:i}=require("../../core/auth/browser-login"),s=await n({server:o.api.apiUrl,provider:"google",open:e?i:async()=>!1,onUrl:(o,e)=>{console.log(""),console.log(e?` ${chalk.cyan("β†’")} Your browser is open. Sign in with Google and press ${chalk.bold("Allow")}.`:` ${chalk.cyan("β†’")} Open this link on this computer, sign in with Google and press ${chalk.bold("Allow")}:`),e&&console.log(chalk.dim(" Didn't open? Use this link:")),console.log(` ${chalk.cyan(o)}`),console.log(chalk.dim(" Waiting for you in the browser… (Ctrl+C to cancel)"))}});return o.api.exchangeCode(s)}async function viaPassword(o,e){const{input:n,password:i}=require("@inquirer/prompts");let s=e.find(o=>!o.startsWith("-"));s||(s=await n({message:"Email:",validate:o=>o.includes("@")||"Please enter your email address"}));const a=process.stdin.isTTY?await i({message:"Password:",mask:"*",validate:o=>o.length>=1||"Please enter your password"}):await readStdin();return console.log(chalk.dim(" Signing you in…")),o.api.login(s,a)}async function viaToken(o){let e;if(process.stdin.isTTY){const{password:o}=require("@inquirer/prompts");e=await o({message:"ThinkNCollab token:",mask:"*",validate:o=>o.trim().length>=16||"Paste the token from thinkncollab.com"})}else e=await readStdin();return console.log(chalk.dim(" Checking the token…")),o.api.loginWithToken(e.trim())}module.exports={name:"login",description:"Sign in to ThinkNCollab (Google or email and password)",aliases:["signin"],requiresAuth:!1,async execute(o,e){if(e.api.isAuthenticated()&&!e.api.session?.fromEnv){const o=e.api.getUser();return console.log(`${chalk.green("βœ”")} You're already signed in as ${chalk.bold(o.name||o.email)}${o.name?chalk.dim(` (${o.email})`):""}.`),await connectRoom(e),void console.log(chalk.dim(' To use another account, run "logout" first.'))}const n=o.filter(o=>!o.startsWith("-"));if(n.length>1||n.length&&!n[0].includes("@"))return console.log(chalk.yellow("Please don't type a password or token on the command line; it would stay in your history.")),console.log(chalk.dim(' Run "login" and choose how to sign in.')),void(process.exitCode=1);try{let i=o.includes("--token")?"token":o.includes("--password")||n.length?"password":o.includes("--google")||o.includes("--no-browser")||!process.stdin.isTTY?"google":null;i||(i=await chooseMethod());const s="token"===i?await viaToken(e):"password"===i?await viaPassword(e,o):await viaGoogle(e,{open:!o.includes("--no-browser")});console.log(""),console.log(`${chalk.green("βœ”")} Welcome, ${chalk.bold(s.user.name||s.user.email)}! You're signed in${s.user.name?chalk.dim(` as ${s.user.email}`):""}.`),await connectRoom(e),console.log(chalk.dim(` Your sign-in is kept in ${e.api.tokenLocation()}. "auth" shows it, "logout" ends it.`))}catch(o){if(cancelled(o))return void console.log(chalk.dim("\n Sign-in cancelled."));process.exitCode=1;const n=o.message||"";401===o.status||/invalid|unauthorized/i.test(n)?console.log(chalk.red("βœ– That email and password didn't match. Please try again.")):/declined/.test(n)?console.log(chalk.yellow("βœ– Sign-in was cancelled in the browser. Nothing was shared.")):n.includes("ECONNREFUSED")||n.includes("Cannot connect")?console.log(chalk.red(`βœ– Couldn't reach ${e.api.apiUrl}. Check your connection and try again.`)):console.log(chalk.red(`βœ– Sign-in didn't work: ${n}`))}}};
@@ -4,4 +4,4 @@
4
4
  * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
5
  * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
6
  */
7
- const chalk=require("chalk"),fs=require("fs-extra");module.exports={name:"logout",description:"Logout from ThinkNCollab",aliases:["signout","exit"],requiresAuth:!0,async execute(e,s){s.printMessage(chalk.cyan(" Logging out..."));try{await s.api.logout();const e=s.getSessionPath();await fs.remove(e),s.printMessage(chalk.green("[OK] Logged out successfully"))}catch(e){s.printMessage(chalk.red(`[FAIL] Logout failed: ${e.message}`))}}};
7
+ const chalk=require("chalk");module.exports={name:"logout",description:"Sign out and revoke this machine's token",aliases:["signout"],requiresAuth:!1,async execute(e,s){if(s.api.session?.fromEnv)s.printMessage(chalk.yellow("[WARN] Signed in through THINKNCOLLAB_TOKEN; unset it to sign out."));else if(s.api.isAuthenticated()){s.printMessage(chalk.cyan(" Logging out..."));try{await s.api.logout(),s.ws?.disconnect?.(),s.printMessage(chalk.green("[OK] Logged out successfully"))}catch(e){s.printMessage(chalk.red(`[FAIL] Logout failed: ${e.message}`))}}else s.printMessage(chalk.dim("Not signed in."))}};
@@ -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"),{exec}=require("child_process");module.exports={name:"register",description:"Create a new ThinkNCollab account",aliases:["signup","reg"],requiresAuth:!1,async execute(e,o){try{console.log(chalk.cyan(" Opening registration in browser..."));const e=o.api.apiUrl||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com",l=`${e}/register?ref=cli&cliRef=${require("crypto").randomBytes(8).toString("hex")}`,c="win32"===process.platform?`start "" "${l}"`:"darwin"===process.platform?`open "${l}"`:`xdg-open "${l}"`;exec(c,e=>{e&&(console.log(chalk.yellow("[WARN] Could not open browser automatically.")),console.log(chalk.dim(" Open this URL manually:")),console.log(chalk.cyan(` ${l}`)))}),console.log(chalk.green("[OK] Registration page opened!")),console.log(chalk.dim(` URL: ${l}`)),console.log(""),console.log(chalk.dim(" After registering, come back and run:")),console.log(chalk.cyan(" login"))}catch(e){console.log(chalk.red(`[FAIL] Error: ${e.message}`))}}};
7
+ const chalk=require("chalk");module.exports={name:"register",description:"Create a new ThinkNCollab account",aliases:["signup","reg"],requiresAuth:!1,async execute(e,o){const{openUrl:l}=require("../../core/auth/browser-login"),r=`${o.api.apiUrl}/register?ref=cli`;console.log(chalk.cyan(" Opening registration in browser...")),await l(r)||console.log(chalk.yellow("[WARN] Could not open browser automatically.")),console.log(chalk.dim(` URL: ${r}`)),console.log(""),console.log(chalk.dim(" After registering, come back and run:")),console.log(chalk.cyan(" login"))}};
@@ -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");module.exports={name:"whoami",description:"Show current user information",aliases:["user","me"],requiresAuth:!1,async execute(e,l){const o=l.api.getUser(),a=l.ws.getCurrentRoom?.(),n=l.api.session;let c=null;try{const e=(await l.api._request("GET","/thinknsh/session/history")).find(e=>e._id!==n?.shellSessionId);e&&(c=e)}catch{}console.log(chalk.cyan("\n User Information:")),console.log(chalk.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")),console.log(` ${chalk.yellow("Name:")} ${o.name||"Not set"}`),console.log(` ${chalk.yellow("Email:")} ${o.email}`),console.log(` ${chalk.yellow("User ID:")} ${o._id||"N/A"}`),console.log(` ${chalk.yellow("Type:")} ${o.userType||"User"}`),console.log(` ${chalk.yellow("Status:")} ${l.ws.isConnected?.()?chalk.green("● Online"):chalk.gray("β—‹ Offline")}`),a&&console.log(` ${chalk.yellow("Room:")} ${chalk.cyan(a)}`),n?.timestamp&&console.log(` ${chalk.yellow("Login At:")} ${chalk.dim(new Date(n.timestamp).toLocaleString())}`),c?.logoutAt?console.log(` ${chalk.yellow("Last Logout:")} ${chalk.dim(new Date(c.logoutAt).toLocaleString())}`):c?.lastSeenAt&&console.log(` ${chalk.yellow("Last Seen:")} ${chalk.dim(new Date(c.lastSeenAt).toLocaleString())} ${chalk.red("(crash?)")}`),c?.machineId&&console.log(` ${chalk.yellow("Last Machine:")} ${chalk.dim(c.machineId)}`),console.log()}};
7
+ const chalk=require("chalk");module.exports={name:"whoami",description:"Show current user information",aliases:["user","me"],requiresAuth:!1,async execute(l,o){if(!o.api.isAuthenticated())return void console.log(chalk.yellow('Not signed in. Run "login" to sign in through thinkncollab.com.'));const e=o.api.getUser()||{},n=o.ws.getCurrentRoom?.(),a=o.api.session;let c=null;try{const l=await o.api._request("GET","/thinknsh/session/history"),e=Array.isArray(l)?l.find(l=>l._id!==a?.shellSessionId):null;e&&(c=e)}catch{}console.log(chalk.cyan("\n User Information:")),console.log(chalk.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")),console.log(` ${chalk.yellow("Name:")} ${e.name||"Not set"}`),console.log(` ${chalk.yellow("Email:")} ${e.email||"N/A"}`),console.log(` ${chalk.yellow("User ID:")} ${e._id||"N/A"}`),console.log(` ${chalk.yellow("Type:")} ${e.userType||"User"}`),console.log(` ${chalk.yellow("Status:")} ${o.ws.isConnected?.()?chalk.green("● Online"):chalk.gray("β—‹ Offline")}`),n&&console.log(` ${chalk.yellow("Room:")} ${chalk.cyan(n)}`),a?.timestamp&&console.log(` ${chalk.yellow("Login At:")} ${chalk.dim(new Date(a.timestamp).toLocaleString())}`),c?.logoutAt?console.log(` ${chalk.yellow("Last Logout:")} ${chalk.dim(new Date(c.logoutAt).toLocaleString())}`):c?.lastSeenAt&&console.log(` ${chalk.yellow("Last Seen:")} ${chalk.dim(new Date(c.lastSeenAt).toLocaleString())} ${chalk.red("(crash?)")}`),c?.machineId&&console.log(` ${chalk.yellow("Last Machine:")} ${chalk.dim(c.machineId)}`),console.log()}};
@@ -4,4 +4,4 @@
4
4
  * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
5
  * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
6
  */
7
- const chalk=require("chalk");class CommandRegistry{constructor(s){this.shell=s,this.commands=new Map,this.aliases=new Map,this.categories=new Map,this.loadCommands()}loadCommands(){const s=(s,a)=>{try{const e=require(s);if(Array.isArray(e))return void e.forEach(s=>this.register(s,a));if(!e||!e.name)return void console.error(chalk.red(`[FAIL] BAD COMMAND (no name): ${s}`));this.register(e,a)}catch(a){console.error(chalk.red(`[FAIL] Failed to load ${s}: ${a.message}`))}};s("./auth/login","auth"),s("./auth/logout","auth"),s("./auth/whoami","auth"),s("./auth/register","auth"),s("./team/myteam","team"),s("./team/teammates","team"),s("./team/invite","team"),s("./room/join","room"),s("./room/leave","room"),s("./room/rooms","room"),s("./room/sync","room"),s("./message/say","message"),s("./shell/status","shell"),s("./shell/notify","shell"),s("./shell/version","shell"),s("./shell/doctor","shell"),s("./tasks/tasks","tasks"),s("./tasks/task","tasks"),s("./tasks/task-status","tasks"),s("./tasks/complete","tasks"),s("./tasks/create-task","tasks"),s("./tasks/loadtest","tasks"),s("./tasks/build","tasks"),s("./terminal/share","terminal"),s("./github/statusgh","github"),s("./github/issues","github"),s("./github/pulls","github"),s("./github/commits","github"),s("./github/issue-create","github"),s("./sandbox/sandbox","sandbox"),s("./sandbox/dev","sandbox"),s("./sandbox/freeze","sandbox"),s("./sandbox/resume","sandbox"),s("./sandbox/share","sandbox"),s("./sandbox/deploy","sandbox"),s("./sandbox/sync-deps","sandbox"),s("./sandbox/dep","sandbox"),s("./sandbox/console","sandbox"),s("./sandbox/snapshot","sandbox"),s("./sandbox/push","sandbox"),s("./dock/containers","containers"),s("./dock/dock","dock"),s("./dock/bench","dock"),s("./dock/cluster","dock"),s("./dock/ci","dock")}register(s,a){s&&s.name?(this.commands.set(s.name,s),this.categories.has(a)||this.categories.set(a,[]),this.categories.get(a).push(s.name),s.aliases&&s.aliases.forEach(a=>this.aliases.set(a,s.name))):console.error(chalk.yellow(`[WARN] Skipping invalid command in category: ${a}`))}addAlias(s,a){this.aliases.set(s,a)}getCommand(s){return this.commands.has(s)||this.aliases.has(s)&&(s=this.aliases.get(s)),this.commands.get(s)}async execute(s,a,e=null){const o=e&&e.length>0?e:a.parseCommand(s),t=o[0].toLowerCase(),n=o.slice(1),r=this.getCommand(t);if(!r)return!1;try{if(r.requiresAuth&&!a.api?.isAuthenticated())return console.log(chalk.red("[FAIL] Please login first: login")),!0;if(r.requiresRoom&&!a.ws?.getCurrentRoom())return console.log(chalk.red("[FAIL] Please join a room first: join <room-id>")),!0;a._lastCommand=t,await r.execute(n,a,t)}catch(s){console.log(chalk.red(`[FAIL] Command error: ${s.message}`))}return!0}getHelpText(){let s=chalk.cyan("\n Available Commands:\n");s+=chalk.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n");for(const[a,e]of this.categories)s+=chalk.yellow(`${a.toUpperCase()}:\n`),e.forEach(a=>{const e=this.commands.get(a);if(!e||!a)return;const o=e.aliases?chalk.dim(` (${e.aliases.join(", ")})`):"";s+=` ${chalk.green(a.padEnd(14))}${o} - ${e.description||""}\n`}),s+="\n";return s}}module.exports=CommandRegistry;
7
+ const chalk=require("chalk");class CommandRegistry{constructor(s){this.shell=s,this.commands=new Map,this.aliases=new Map,this.categories=new Map,this.loadCommands()}loadCommands(){const s=(s,e)=>{try{const a=require(s);if(Array.isArray(a))return void a.forEach(s=>this.register(s,e));if(!a||!a.name)return void console.error(chalk.red(`[FAIL] BAD COMMAND (no name): ${s}`));this.register(a,e)}catch(e){console.error(chalk.red(`[FAIL] Failed to load ${s}: ${e.message}`))}};s("./auth/login","auth"),s("./auth/logout","auth"),s("./auth/whoami","auth"),s("./auth/register","auth"),s("./auth/auth","auth"),s("./team/myteam","team"),s("./team/teammates","team"),s("./team/invite","team"),s("./room/join","room"),s("./room/connect","room"),s("./room/leave","room"),s("./room/rooms","room"),s("./room/sync","room"),s("./message/say","message"),s("./shell/status","shell"),s("./shell/notify","shell"),s("./shell/version","shell"),s("./shell/doctor","shell"),s("./tasks/tasks","tasks"),s("./tasks/task","tasks"),s("./tasks/task-status","tasks"),s("./tasks/complete","tasks"),s("./tasks/create-task","tasks"),s("./tasks/loadtest","tasks"),s("./tasks/build","tasks"),s("./terminal/share","terminal"),s("./github/statusgh","github"),s("./github/issues","github"),s("./github/pulls","github"),s("./github/commits","github"),s("./github/issue-create","github"),s("./sandbox/sandbox","sandbox"),s("./sandbox/dev","sandbox"),s("./sandbox/freeze","sandbox"),s("./sandbox/resume","sandbox"),s("./sandbox/share","sandbox"),s("./sandbox/deploy","sandbox"),s("./sandbox/sync-deps","sandbox"),s("./sandbox/dep","sandbox"),s("./sandbox/console","sandbox"),s("./sandbox/snapshot","sandbox"),s("./sandbox/push","sandbox"),s("./dock/containers","containers"),s("./dock/dock","dock"),s("./dock/bench","dock"),s("./dock/cluster","dock"),s("./dock/ci","dock")}register(s,e){s&&s.name?(this.commands.set(s.name,s),this.categories.has(e)||this.categories.set(e,[]),this.categories.get(e).push(s.name),s.aliases&&s.aliases.forEach(e=>this.aliases.set(e,s.name))):console.error(chalk.yellow(`[WARN] Skipping invalid command in category: ${e}`))}addAlias(s,e){this.aliases.set(s,e)}getCommand(s){return this.commands.has(s)||this.aliases.has(s)&&(s=this.aliases.get(s)),this.commands.get(s)}async execute(s,e,a=null){const o=a&&a.length>0?a:e.parseCommand(s),t=o[0].toLowerCase(),n=o.slice(1),r=this.getCommand(t);if(!r)return!1;try{if(r.requiresAuth&&!e.api?.isAuthenticated())return console.log(chalk.red("[FAIL] Please login first: login")),!0;if(r.requiresRoom&&!e.ws?.getCurrentRoom()&&!await require("../core/room-link").connect(e,{quiet:!0}).catch(()=>null))return console.log(chalk.red('[FAIL] This project has no room yet. Run "connect" to choose one.')),process.exitCode=1,!0;e._lastCommand=t,await r.execute(n,e,t)}catch(s){console.log(chalk.red(`[FAIL] Command error: ${s.message}`))}return!0}getHelpText(){let s=chalk.cyan("\n Available Commands:\n");s+=chalk.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n");for(const[e,a]of this.categories)s+=chalk.yellow(`${e.toUpperCase()}:\n`),a.forEach(e=>{const a=this.commands.get(e);if(!a||!e)return;const o=a.aliases?chalk.dim(` (${a.aliases.join(", ")})`):"";s+=` ${chalk.green(e.padEnd(14))}${o} - ${a.description||""}\n`}),s+="\n";return s}}module.exports=CommandRegistry;
@@ -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 chalk=require("chalk"),roomLink=require("../../core/room-link");module.exports={name:"connect",description:"Connect this project to its ThinkNCollab room (or switch rooms)",requiresAuth:!0,async execute(o,e){if(o.includes("--forget"))return roomLink.forget(),void console.log(`${chalk.green("βœ”")} Forgot the room for ${roomLink.projectRoot()}.`);const n=o.find(o=>!o.startsWith("-"))||null;try{if(!n&&e.ws.getCurrentRoom()){const o=roomLink.knownRoom(e.api.apiUrl);if(o?.id===e.ws.getCurrentRoom())return void console.log(`${chalk.green("βœ”")} Already connected to ${chalk.bold(o.name||o.id)}. ${chalk.dim("Switch with: connect <room>")}`)}const o=await roomLink.connect(e,{interactive:!!process.stdin.isTTY,wanted:n});if(!o)return console.log(chalk.yellow('You are not a member of any room yet. Create or join one on thinkncollab.com, then run "connect".')),void(process.exitCode=1);const t=(o.history||[]).slice(-3);if(t.length){console.log(chalk.dim(" Recent messages:"));for(const o of t)console.log(chalk.dim(` ${o.name||"someone"}: ${String(o.message||"").slice(0,120)}`))}}catch(o){if("ExitPromptError"===o?.name)return void console.log(chalk.dim(" Cancelled."));process.exitCode=1,console.log(chalk.red(`βœ– Could not connect: ${o.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"),JOIN_TIMEOUT_MS=8e3;module.exports={name:"join",description:"Join a collaboration room",aliases:["enter","goto"],requiresAuth:!0,async execute(o,e){if(0===o.length)return void console.log(chalk.red("[FAIL] Usage: join <room-id> [password]"));const n=o[0],s=o[1];if(!e.ws.isConnected()){console.log(chalk.dim(" Connecting..."));try{await e.ws.connect()}catch(o){return void console.log(chalk.red(`[FAIL] WebSocket connect failed: ${o.message}`))}}let c;console.log(chalk.cyan(` Joining room ${chalk.bold(n)}...`));try{c=await e.api.joinRoom(n,s)}catch(o){return void(o.message?.toLowerCase().includes("password")?console.log(chalk.red(`[FAIL] Room requires a password: join ${n} <password>`)):o.message?.toLowerCase().includes("not found")?(console.log(chalk.red(`[FAIL] Room not found: ${n}`)),console.log(chalk.dim(' Type "rooms" to see available rooms'))):o.message?.toLowerCase().includes("not a member")||o.message?.toLowerCase().includes("unauthorized")?console.log(chalk.red("[FAIL] You are not a member of this room")):console.log(chalk.red(`[FAIL] Failed to join: ${o.message}`)))}const l=c.room||c,a=e.api.getUser?.()||{};await new Promise(o=>{const s=setTimeout(()=>{e.ws.socket?.off("thinknsh:joined",t),e.ws.socket?.off("thinknsh:error",i),console.log(chalk.red("[FAIL] Join timeout β€” server did not confirm. Try again.")),o()},8e3);function t(t){if(clearTimeout(s),e.ws.socket?.off("thinknsh:error",i),!t.success)return console.log(chalk.red(`[FAIL] Join rejected: ${t.message||"unknown error"}`)),void o();console.log(chalk.green(`[OK] Joined ${chalk.bold(l.name||n)}`)),l.description&&console.log(chalk.dim(` ${l.description}`));const r=c.participants||[];r.length&&console.log(chalk.dim(` ${r.length} participant(s)`));const h=t.history||[];h.length&&(console.log(chalk.cyan("\n Recent messages:")),h.slice(-5).forEach(o=>{const e=new Date(o.timestamp).toLocaleTimeString(),n=o.userId===a._id?.toString()||o.userId===a.id?.toString()?chalk.green("you"):chalk.cyan(o.name||"Unknown"),s="shell"===o.source?chalk.dim("[shell]"):"";console.log(` ${chalk.dim(`[${e}]`)} ${n}${s}: ${o.message}`)}));const d=r.filter(o=>o.online);d.length&&(console.log(chalk.cyan("\n Online:")),d.forEach(o=>{const e=o.email===a.email?chalk.green(" (you)"):"";console.log(` ${chalk.green("●")} ${o.name||o.email}${e}`)})),console.log(chalk.dim("\n say <message> to chat | leave to exit\n")),o()}function i(n){clearTimeout(s),e.ws.socket?.off("thinknsh:joined",t),console.log(chalk.red(`[FAIL] ${n.message||"Join error"}`)),o()}e.ws.socket.once("thinknsh:joined",t),e.ws.socket.once("thinknsh:error",i),e.ws.socket.emit("thinknsh:join",{roomId:n,userId:a._id?.toString()||a.id?.toString(),name:a.name||a.username||"Unknown",userType:a.model||a.role||"User"})})}};
7
+ module.exports={name:"join",description:"Join one of your rooms (and use it for this project)",aliases:["enter","goto"],requiresAuth:!0,execute:async(e,o)=>e.length?require("./connect").execute([e[0]],o):(console.log(require("chalk").red('[FAIL] Usage: join <room-id | room-name> (or "connect" to pick from a list)')),void(process.exitCode=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 https=require("https"),http=require("http"),path=require("path"),fs=require("fs-extra"),os=require("os");class ThinkNCollabAPI{constructor(s={}){this.apiUrl=(s.apiUrl||process.env.THINKNCOLLAB_API_URL||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com").replace(/\/$/,""),this.wsUrl=(s.wsUrl||process.env.THINKNCOLLAB_WS_URL||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com").replace(/\/$/,""),this.session=null,this.sessionPath=path.join(os.homedir(),".thinkncollab","session.json"),this._loadSession()}_loadSession(){try{fs.existsSync(this.sessionPath)&&(this.session=fs.readJsonSync(this.sessionPath))}catch{}}_saveSession(s){try{const e=path.dirname(this.sessionPath);fs.ensureDirSync(e,{mode:448}),fs.writeJsonSync(this.sessionPath,s,{spaces:2,mode:384});try{fs.chmodSync(this.sessionPath,384)}catch(s){}this.session=s}catch{}}_clearSession(){try{fs.removeSync(this.sessionPath)}catch{}this.session=null}isAuthenticated(){return!!this.session?.token}getToken(){return this.session?.token||null}getUser(){const s=this.session?.user;if(!s)return null;const e=(s._id||s.userId||"").toString();return{_id:e,userId:e,email:s.email||"",name:s.name||"",userType:s.userType||s.model||"User"}}_request(s,e,t=null,n={}){return new Promise((r,o)=>{const i=new URL(this.apiUrl+e),a="https:"===i.protocol,h=a?https:http,c=t?JSON.stringify(t):null,l={hostname:i.hostname,port:i.port||(a?443:80),path:i.pathname+i.search,method:s,headers:{"Content-Type":"application/json",Accept:"application/json",...this.session?.token?{"x-shell-token":this.session.token}:{},...n,...c?{"Content-Length":Buffer.byteLength(c)}:{}}},u=h.request(l,s=>{let e="";s.on("data",s=>e+=s),s.on("end",()=>{try{const t=JSON.parse(e);s.statusCode>=400?o(new Error(t.error||t.message||`HTTP ${s.statusCode}`)):r(t)}catch{o(new Error(`Invalid response: ${e.slice(0,100)}`))}})});u.on("error",s=>{"ECONNREFUSED"===s.code?o(new Error(`Cannot connect to server at ${this.apiUrl} β€” is it running?`)):o(s)}),c&&u.write(c),u.end()})}async login(s,e){const t={email:s,password:e,machineId:os.hostname()},n=await this._request("POST","/thinknsh/login",t,{"x-email":s,"x-password":e,"x-machine-id":os.hostname()});return this._saveSession({token:n.shellToken,shellSessionId:n.shellSessionId,user:{_id:n._id,userId:n._id,email:n.email,name:n.name,userType:n.userType||n.model||"User"},timestamp:(new Date).toISOString()}),{token:n.shellToken,user:this.getUser()}}async logEvent(s,e=null,t={}){const n=this.session?.shellSessionId;n&&await this._request("POST","/thinknsh/session/event",{sessionId:n,type:s,roomId:e,meta:t}).catch(()=>{})}startHeartbeat(s=6e4){this._heartbeatTimer||(this._heartbeatTimer=setInterval(()=>{const s=this.session?.shellSessionId;s&&this._request("POST","/thinknsh/session/event",{sessionId:s,type:"heartbeat",meta:{ts:(new Date).toISOString()}}).catch(()=>{})},s))}stopHeartbeat(){this._heartbeatTimer&&(clearInterval(this._heartbeatTimer),this._heartbeatTimer=null)}async logout(){const s=this.session?.shellSessionId;return s&&await this._request("POST","/thinknsh/session/end",{sessionId:s}).catch(()=>{}),this.stopHeartbeat(),this._clearSession(),{success:!0}}async whoami(){if(!this.isAuthenticated())throw new Error("Not logged in");return this.getUser()}async joinRoom(s,e){if(!s)throw new Error("roomId is required");const t=await this._request("POST",`/thinknsh/rooms/${s}/join`,e?{password:e}:null);return{room:t.room||t,participants:t.participants||t.members||[],recentMessages:t.recentMessages||t.history||[]}}async getRooms(){return this._request("GET","/thinknsh/rooms")}async mytasks(s,e){if(!s)throw new Error("roomId is required");const t=this.getUser();let n=`/thinknsh/tasks?roomId=${encodeURIComponent(s)}`;return t?._id&&(n+=`&userId=${encodeURIComponent(t._id)}`),e&&(n+=`&n=${e}`),this._request("GET",n)}async request(s,e,t={}){let n=e;if(t.params&&Object.keys(t.params).length){const s=new URLSearchParams(t.params).toString();n+=(n.includes("?")?"&":"?")+s}return this._request(s.toUpperCase(),n,t.body||null)}}module.exports=ThinkNCollabAPI;
7
+ const https=require("https"),http=require("http"),os=require("os"),store=require("./auth/session-store"),REQUEST_TIMEOUT_MS=15e3,LOOPBACK=new Set(["localhost","127.0.0.1","[::1]","::1"]);function assertSafeServer(s){const e=new URL(s);if(!("https:"===e.protocol||"http:"===e.protocol&&LOOPBACK.has(e.hostname)))throw new Error(`refusing to send credentials to ${e.origin}: use https://`)}function normaliseUser(s={}){const e=(s._id||s.userId||s.id||"").toString();return{_id:e,userId:e,email:s.email||"",name:s.name||"",userType:s.userType||s.model||"User"}}class ThinkNCollabAPI{constructor(s={}){this.apiUrl=(s.apiUrl||process.env.THINKNCOLLAB_API_URL||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com").replace(/\/$/,""),this.wsUrl=(s.wsUrl||process.env.THINKNCOLLAB_WS_URL||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com").replace(/\/$/,""),this.session=null,this.sessionPath=store.file(),this._loadSession()}_loadSession(){const s=(process.env.THINKNCOLLAB_TOKEN||"").trim();this.session=s?{token:s,fromEnv:!0,server:this.apiUrl,user:{}}:store.load(this.apiUrl)}_saveSession(s){const e={server:this.apiUrl,lastVerifiedAt:(new Date).toISOString(),...s};e.tokenIn=store.save(e),this.session=e}clearSession(){this.session?.fromEnv||store.clear(this.apiUrl),this.session=null}_clearSession(){this.clearSession()}isAuthenticated(){return!!this.session?.token}getToken(){return this.session?.token||null}tokenLocation(){return this.session?.fromEnv?"THINKNCOLLAB_TOKEN":store.where()}getUser(){const s=this.session?.user;return s?normaliseUser(s):null}_request(s,e,t=null,r={}){return new Promise((n,o)=>{const i=new URL(this.apiUrl+e),a="https:"===i.protocol,h=a?https:http,l=t?JSON.stringify(t):null,c=this.session?.token;try{assertSafeServer(i.href)}catch(s){return void o(s)}const u={hostname:i.hostname,port:i.port||(a?443:80),path:i.pathname+i.search,method:s,timeout:15e3,headers:{"Content-Type":"application/json",Accept:"application/json",...c?{"x-shell-token":c}:{},...r,...l?{"Content-Length":Buffer.byteLength(l)}:{}}},d=h.request(u,s=>{let e="";s.on("data",s=>e+=s),s.on("end",()=>{let t=null;try{t=JSON.parse(e)}catch{}if(s.statusCode>=400){const e=new Error(t?.error||t?.message||`HTTP ${s.statusCode}`);e.status=s.statusCode,401===s.statusCode&&c&&this.session?.token===c&&this.clearSession(),o(e)}else null===t?o(new Error(`Invalid response: ${e.slice(0,100)}`)):n(t)})});d.on("timeout",()=>d.destroy(new Error(`${this.apiUrl} did not answer within 15s`))),d.on("error",s=>{"ECONNREFUSED"===s.code?o(new Error(`Cannot connect to server at ${this.apiUrl} β€” is it running?`)):o(s)}),l&&d.write(l),d.end()})}_adopt(s,e){if(!s?.shellToken)throw new Error(s?.error||"the platform returned no shell token");return this._saveSession({token:s.shellToken,shellSessionId:s.shellSessionId||null,user:normaliseUser(s.user||s),expiresAt:s.expiresAt||null,method:e,timestamp:(new Date).toISOString()}),{token:s.shellToken,user:this.getUser()}}async login(s,e){const t=await this._request("POST","/thinknsh/login",null,{"x-email":s,"x-password":e,"x-machine-id":os.hostname()});return this._adopt(t,"password")}async exchangeCode({code:s,verifier:e,redirectUri:t}){const r=await this._request("POST","/thinknsh/auth/token",{grant_type:"authorization_code",client_id:"thinknsh",code:s,code_verifier:e,redirect_uri:t,machineId:os.hostname()}).catch(s=>{throw 404===s.status&&(s.message='this server does not support browser sign-in yet; use "login --password"'),s});return this._adopt(r,"browser")}async loginWithToken(s){const e=this.session;this.session={token:s,server:this.apiUrl,user:{}};try{const e=await this._request("GET","/thinknsh/auth/me");return this._adopt({...e,user:e.user||e,shellToken:s},"token")}catch(s){if(this.session=e,404===s.status)throw new Error('this platform does not accept shell tokens yet; use "login"');throw s}}async verify(){let s=null;try{s=await this._request("GET","/thinknsh/auth/me")}catch(s){if(404!==s.status)throw s;await this._request("GET","/thinknsh/session/history")}if(!this.session)return;const e=s?normaliseUser(s.user||s):this.session.user;this.session={...this.session,user:e,lastVerifiedAt:(new Date).toISOString()},this.session.fromEnv||store.touch({user:e,lastVerifiedAt:this.session.lastVerifiedAt})}async sessions(){return this._request("GET","/thinknsh/session/history")}async endSession(s){return this._request("POST","/thinknsh/session/end",{sessionId:s})}async logEvent(s,e=null,t={}){const r=this.session?.shellSessionId;r&&await this._request("POST","/thinknsh/session/event",{sessionId:r,type:s,roomId:e,meta:t}).catch(()=>{})}startHeartbeat(s=6e4){this._heartbeatTimer||(this._heartbeatTimer=setInterval(()=>{const s=this.session?.shellSessionId;s&&this._request("POST","/thinknsh/session/event",{sessionId:s,type:"heartbeat",meta:{ts:(new Date).toISOString()}}).catch(()=>{})},s),this._heartbeatTimer.unref?.())}stopHeartbeat(){this._heartbeatTimer&&(clearInterval(this._heartbeatTimer),this._heartbeatTimer=null)}async logout(){const s=this.session?.shellSessionId;return s&&await this.endSession(s).catch(()=>{}),this.stopHeartbeat(),this.clearSession(),{success:!0}}async whoami(){if(!this.isAuthenticated())throw new Error("Not logged in");return this.getUser()}async joinRoom(s,e){if(!s)throw new Error("roomId is required");const t=await this._request("POST",`/thinknsh/rooms/${s}/join`,e?{password:e}:null);return{room:t.room||t,participants:t.participants||t.members||[],recentMessages:t.recentMessages||t.history||[]}}async getRooms(){return this._request("GET","/thinknsh/rooms")}async mytasks(s,e){if(!s)throw new Error("roomId is required");const t=this.getUser();let r=`/thinknsh/tasks?roomId=${encodeURIComponent(s)}`;return t?._id&&(r+=`&userId=${encodeURIComponent(t._id)}`),e&&(r+=`&n=${e}`),this._request("GET",r)}async request(s,e,t={}){let r=e;if(t.params&&Object.keys(t.params).length){const s=new URLSearchParams(t.params).toString();r+=(r.includes("?")?"&":"?")+s}return this._request(s.toUpperCase(),r,t.body||null)}}module.exports=ThinkNCollabAPI;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const http=require("http"),os=require("os"),crypto=require("crypto"),{spawn}=require("child_process"),TIMEOUT_MS=3e5,b64url=e=>e.toString("base64url");function pkce(){const e=b64url(crypto.randomBytes(32));return{verifier:e,challenge:b64url(crypto.createHash("sha256").update(e).digest())}}function same(e,t){const r=Buffer.from(String(e||"")),n=Buffer.from(String(t||""));return r.length===n.length&&r.length>0&&crypto.timingSafeEqual(r,n)}function openUrl(e){if(!/^https?:\/\//i.test(e))return Promise.resolve(!1);const[t,r]="darwin"===process.platform?["open",[e]]:"win32"===process.platform?["rundll32",["url.dll,FileProtocolHandler",e]]:["xdg-open",[e]];return new Promise(e=>{try{const n=spawn(t,r,{stdio:"ignore",detached:!0,windowsHide:!0});n.on("error",()=>e(!1)),n.on("spawn",()=>{n.unref(),e(!0)})}catch(t){e(!1)}})}const PAGE=(e,t)=>`<!doctype html><meta charset="utf-8"><title>${e}</title>\n<body style="font:16px system-ui;margin:15vh auto;max-width:28em;text-align:center;color:#222">\n<h2>${e}</h2><p>${t}</p></body>`;function authorize({server:e,provider:t,open:r=openUrl,onUrl:n=()=>{},timeoutMs:o=3e5}={}){const i=b64url(crypto.randomBytes(32)),{verifier:a,challenge:s}=pkce();return new Promise((c,l)=>{let d=!1,h=0;const u=http.createServer((e,t)=>{const r=String(e.headers.host||"");if(r!==`127.0.0.1:${h}`&&r!==`localhost:${h}`)return t.writeHead(421),void t.end();const n=new URL(e.url,`http://127.0.0.1:${h}`);if("GET"!==e.method||"/callback"!==n.pathname||d)return t.writeHead(404),void t.end();if(!same(n.searchParams.get("state"),i))return t.writeHead(400,{"Content-Type":"text/html; charset=utf-8"}),void t.end(PAGE("Sign-in not accepted","This link did not come from the shell that is waiting. Run <b>login</b> again."));d=!0;const o=n.searchParams.get("error"),s=n.searchParams.get("code");t.writeHead(o||!s?400:200,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"}),t.end(o||!s?PAGE("Sign-in cancelled","Nothing was shared with the shell. You can close this tab."):PAGE("Signed in to ThinkNCollab Shell","You can close this tab and go back to your terminal.")),m(o||!s?new Error("access_denied"===o?"sign-in was declined in the browser":`sign-in failed: ${o||"no code returned"}`):null,{code:s,verifier:a,redirectUri:`http://127.0.0.1:${h}/callback`})}),p=setTimeout(()=>m(new Error("sign-in timed out after 5 minutes")),o);function m(e,t){clearTimeout(p),u.close(),u.closeIdleConnections?.(),e?l(e):c(t)}u.on("error",e=>m(e)),u.listen(0,"127.0.0.1",async()=>{h=u.address().port;const o=new URLSearchParams({response_type:"code",client_id:"thinknsh",redirect_uri:`http://127.0.0.1:${h}/callback`,state:i,code_challenge:s,code_challenge_method:"S256",machine:os.hostname(),...t?{provider:t}:{}}),a=`${e.replace(/\/$/,"")}/cli/authorize?${o}`;n(a,await r(a))})})}module.exports={authorize,openUrl,pkce,TIMEOUT_MS:3e5};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const chalk=require("chalk"),OPEN=new Set(["login","signin","logout","signout","register","signup","reg","whoami","me","user","auth","help","exit","quit","clear","cls","version","doctor"]),DOCK_LIFECYCLE=new Set(["setup","start","stop","restart","autostart","app","tray","status","vm-restart","uninstall","version"]),VERIFY_EVERY_MS=432e5,OFFLINE_GRACE_MS=6048e5,disabled=()=>["off","0","false"].includes(String(process.env.TNC_REQUIRE_LOGIN||"").toLowerCase());async function check(e,r=Date.now()){if(!e.isAuthenticated())return{ok:!1,reason:"not signed in"};const t=e.session;if(t.expiresAt&&Date.parse(t.expiresAt)<=r)return e.clearSession(),{ok:!1,reason:"your sign-in has expired"};const n=Date.parse(t.lastVerifiedAt||t.timestamp||0)||0;if(!t.fromEnv&&r-n<432e5)return{ok:!0};try{return await e.verify(),{ok:!0}}catch(o){return 401===o.status?{ok:!1,reason:"the platform ended this sign-in"}:!t.fromEnv&&r-n<6048e5?{ok:!0,offline:!0}:{ok:!1,reason:`cannot confirm your sign-in with ${e.apiUrl} (${o.message})`}}}function registryForm(e,r){if("login"!==e&&"logout"!==e)return!1;try{return require("../../commands/dock/containers").REGISTRY_FORM[e](r)}catch(e){return!1}}async function allow(e,r,t=[]){if(disabled())return!0;if(OPEN.has(e)&&!registryForm(e,t))return!0;if("dock"===e&&DOCK_LIFECYCLE.has(t[0]))return!0;const n=await check(r);return!!n.ok||(console.log(chalk.red(`[FAIL] ThinkNCollab sign-in required: ${n.reason}.`)),console.log(chalk.dim(' Run "login" to sign in through thinkncollab.com.')),process.exitCode=1,!1)}module.exports={allow,check,OPEN,DOCK_LIFECYCLE,VERIFY_EVERY_MS:432e5,OFFLINE_GRACE_MS:6048e5};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const fs=require("fs"),os=require("os"),path=require("path"),crypto=require("crypto"),dir=()=>path.join(os.homedir(),".thinkncollab"),file=()=>path.join(dir(),"session.json");function hostOf(e){try{return new URL(e).host}catch(n){return String(e||"")}}const account=e=>`thinkncollab-shell:${hostOf(e)}`;function keychain(){if("off"===process.env.THINKNCOLLAB_KEYCHAIN)return null;try{return require("../../dock/image/keychain").backend()}catch(e){return null}}function readFile(){try{return fs.lstatSync(file()).isFile()?JSON.parse(fs.readFileSync(file(),"utf8")):null}catch(e){return null}}function writeFile(e){fs.mkdirSync(dir(),{recursive:!0,mode:448});try{fs.chmodSync(dir(),448)}catch(e){}const n=path.join(dir(),`.session-${process.pid}-${crypto.randomBytes(4).toString("hex")}.json`);fs.writeFileSync(n,JSON.stringify(e,null,2),{mode:384,flag:"wx"}),fs.renameSync(n,file())}function load(e){const n=readFile();if(!n)return null;if(n.server&&hostOf(n.server)!==hostOf(e))return null;let t=null;const r="keychain"===n.tokenIn?keychain():null;if("keychain"===n.tokenIn)try{t=r?r.get(account(e)):null}catch(e){t=null}else if(t=n.token||null,t&&keychain())try{save({...n,token:t,server:e})}catch(e){}if(!t)return null;const{token:i,tokenIn:c,...o}=n;return{...o,server:n.server||e,token:t}}function save(e){const{token:n,...t}=e;if(!n)throw new Error("no token to save");const r=keychain();if(r)try{return r.set(account(t.server),n),writeFile({...t,tokenIn:"keychain"}),"keychain"}catch(e){}return writeFile({...t,tokenIn:"file",token:n}),"file"}function touch(e){const n=readFile();n&&writeFile({...n,...e})}function clear(e){const n=readFile(),t=keychain();if(t)try{t.remove(account(n?.server||e))}catch(e){}try{fs.rmSync(file(),{force:!0})}catch(e){}}function where(){const e=readFile();return e?"keychain"===e.tokenIn?keychain()?.name||"system keychain":file():null}module.exports={load,save,touch,clear,where,file,hostOf};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const fs=require("fs"),os=require("os"),path=require("path"),crypto=require("crypto"),chalk=require("chalk"),linksFile=()=>path.join(os.homedir(),".thinkncollab","projects.json");function projectRoot(e=process.cwd()){let o=path.resolve(e);const t=[path.parse(o).root,os.homedir()];for(let e=o;;e=path.dirname(e)){if(fs.existsSync(path.join(e,"tncproject"))||fs.existsSync(path.join(e,".tncproject"))||fs.existsSync(path.join(e,".git")))return e;if(t.includes(e))break}return o}function readLinks(){try{return fs.lstatSync(linksFile()).isFile()&&JSON.parse(fs.readFileSync(linksFile(),"utf8"))||{}}catch(e){return{}}}function writeLinks(e){const o=path.dirname(linksFile());fs.mkdirSync(o,{recursive:!0,mode:448});const t=path.join(o,`.projects-${process.pid}-${crypto.randomBytes(4).toString("hex")}.json`);fs.writeFileSync(t,JSON.stringify(e,null,2),{mode:384,flag:"wx"}),fs.renameSync(t,linksFile())}const hostOf=e=>{try{return new URL(e).host}catch(o){return String(e||"")}};function remember(e,o,t){const r=readLinks();r[projectRoot(t)]={roomId:o.id,roomName:o.name||"",server:hostOf(e),at:(new Date).toISOString()},writeLinks(r)}function forget(e){const o=readLinks();delete o[projectRoot(e)],writeLinks(o)}function knownRoom(e,o=process.cwd()){try{const{hasProjectConfig:e,loadProjectConfig:t}=require("./project-config");if(e(o))return{id:t(o).roomId,name:"",source:"tncproject"}}catch(e){}const t=readLinks()[projectRoot(o)];return t&&t.server===hostOf(e)?{id:t.roomId,name:t.roomName,source:"remembered"}:null}const normRooms=e=>(Array.isArray(e)?e:e?.rooms||[]).map(e=>({id:String(e.id||e._id||""),name:e.name||"Room",role:e.role||"",isMember:!1!==e.isMember})).filter(e=>e.id&&e.isMember);async function chooseRoom(e,{interactive:o,wanted:t}){const r=normRooms(await e.api.getRooms());if(t){const e=t.toLowerCase(),o=r.find(e=>e.id===t)||r.find(o=>o.name.toLowerCase()===e)||r.filter(o=>o.name.toLowerCase().includes(e)).sort((e,o)=>e.name.length-o.name.length)[0];if(!o)throw new Error(`no room of yours matches "${t}" (see "rooms")`);return o}if(!r.length)return null;if(1===r.length)return r[0];if(!o)return null;const{select:n}=require("@inquirer/prompts"),i=await n({message:`Which room is ${chalk.bold(path.basename(projectRoot()))} for?`,choices:r.map(e=>({name:e.role?`${e.name} ${chalk.dim(e.role)}`:e.name,value:e.id})),pageSize:12});return r.find(e=>e.id===i)}async function connect(e,{interactive:o=!1,wanted:t=null,quiet:r=!1}={}){if(!e.api.isAuthenticated())throw new Error("not signed in");const n=e.api.apiUrl;let i=t?await chooseRoom(e,{interactive:o,wanted:t}):knownRoom(n);if(i||(i=await chooseRoom(e,{interactive:o})),!i)return null;if(e.ws.setUser(e.api.getUser(),e.api.getToken()),e.ws.isConnected()||await e.ws.connect(e.api.getToken()),e.ws.getCurrentRoom()!==i.id){const o=await e.ws.joinRoom(i.id);i.history=o?.history||[]}if(!i.name)try{i.name=normRooms(await e.api.getRooms()).find(e=>e.id===i.id)?.name||""}catch(e){}if("tncproject"!==i.source)try{remember(n,i)}catch(e){}return r||console.log(`${chalk.green("βœ”")} Connected to room ${chalk.bold(i.name||i.id)}${chalk.dim("tncproject"===i.source?" (from tncproject)":"")}`),{room:i,history:i.history||[]}}module.exports={connect,knownRoom,remember,forget,projectRoot,linksFile};
@@ -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"),{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;
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"),SECRET_LINE=/^(login|signin|auth\s+login)\b|--password|--token|THINKNCOLLAB_TOKEN|x-password/i;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 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(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=>foreground.whenFree(()=>{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=>foreground.whenFree(()=>{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=>foreground.whenFree(()=>{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=>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 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')));const e=await this._bootFromProjectConfig();if(e||this.api.isAuthenticated()){if(!e)try{await require("./room-link").connect(this,{interactive:!0})}catch(e){console.log(chalk.dim(` Not connected to a room (${e.message}). Run "connect" when you want to.\n`))}}else console.log(chalk.cyan(" Welcome! Sign in to ThinkNCollab to get started.\n")),await this.execute("login");!e&&this.config.websocket.autoConnect&&await this.ws.connect().catch(()=>{});const{input:t}=require("@inquirer/prompts");for(;this.running;)try{const e=await t({message:this.getPromptText(),required:!1}).catch(()=>null);if(null===e){console.log(chalk.yellow('Use "exit" to quit'));continue}const o=e.trim();if(!o)continue;SECRET_LINE.test(o)||(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}try{this.api._adopt(t,"tncproject")}catch(e){return console.log(chalk.red(`[Error] tncproject auth failed: ${e.message}`)),!1}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(await require("./auth/gate").allow(s,this.api,n))if(["login","logout","auth","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(" 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("disconnect",()=>this.ws.disconnect(),"Disconnect WebSocket"),e("status",this.statusCommand.bind(this),"Show connection status"),e("leave",()=>this.ws.leaveRoom?.(),"Leave current room"),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 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 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,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],o=process.execPath,a=!1);const c=spawn(o,i,{cwd:n,shell:a,stdio:"inherit",env:s});c.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()}),c.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{if(!fs.existsSync(this.config.historyFile))return;const e=fs.readFileSync(this.config.historyFile,"utf8").split("\n").filter(e=>e.trim());this.history=e.filter(e=>!SECRET_LINE.test(e.trim())),this.history.length!==e.length&&this.saveHistory()}catch{}}saveHistory(){try{fs.writeFileSync(this.config.historyFile,this.history.slice(-this.config.maxHistory).join("\n"),{mode:384}),fs.chmodSync(this.config.historyFile,384)}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.logEvent("shell-exit").catch(()=>{}),this.api.stopHeartbeat(),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{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;
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{if(e&&(this._authToken=e),this._authToken){const e=new URL(this.config.serverUrl);if("https:"!==e.protocol&&!["localhost","127.0.0.1","[::1]"].includes(e.hostname))return s(new Error(`refusing to send the sign-in token to ${e.origin}: use https://`))}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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkncollab-shell",
3
- "version": "2.0.0-beta.6",
3
+ "version": "2.0.0-beta.7",
4
4
  "description": "Lightweight MicroVM Sandbox & Zero-Daemon Dev Runner: Fast MicroVM Containers, Native Microservices, Live Database Snapshots, and End-to-End Encrypted State Sync",
5
5
  "main": "dist/src/core/shell.js",
6
6
  "bin": {