thinkncollab-shell 2.0.0-beta.5 → 2.0.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +64 -17
- package/dist/bin/thinknsh.js +1 -1
- package/dist/src/commands/auth/auth.js +7 -0
- package/dist/src/commands/auth/login.js +1 -1
- package/dist/src/commands/auth/logout.js +1 -1
- package/dist/src/commands/auth/register.js +1 -1
- package/dist/src/commands/auth/whoami.js +1 -1
- package/dist/src/commands/dock/cluster.js +1 -1
- package/dist/src/commands/dock/containers.js +1 -1
- package/dist/src/commands/index.js +1 -1
- package/dist/src/commands/room/connect.js +7 -0
- package/dist/src/commands/room/join.js +1 -1
- package/dist/src/commands/room/sync.js +1 -1
- package/dist/src/commands/sandbox/common.js +7 -0
- package/dist/src/commands/sandbox/console.js +1 -1
- package/dist/src/commands/sandbox/dep.js +1 -1
- package/dist/src/commands/sandbox/deploy.js +1 -1
- package/dist/src/commands/sandbox/dev.js +1 -1
- package/dist/src/commands/sandbox/exec.js +1 -1
- package/dist/src/commands/sandbox/push.js +1 -1
- package/dist/src/commands/sandbox/sandbox.js +1 -1
- package/dist/src/commands/sandbox/share.js +1 -1
- package/dist/src/commands/sandbox/snapshot.js +1 -1
- package/dist/src/commands/shell/doctor.js +1 -1
- package/dist/src/commands/tasks/build.js +1 -1
- package/dist/src/commands/terminal/share.js +1 -1
- package/dist/src/core/api-client.js +1 -1
- package/dist/src/core/auth/browser-login.js +7 -0
- package/dist/src/core/auth/gate.js +7 -0
- package/dist/src/core/auth/session-store.js +7 -0
- package/dist/src/core/crypto/seal.js +7 -0
- package/dist/src/core/dependency-tracker.js +1 -1
- package/dist/src/core/foreground.js +7 -0
- package/dist/src/core/platform-socket.js +7 -0
- package/dist/src/core/room/sync-manager.js +7 -0
- package/dist/src/core/room-link.js +7 -0
- package/dist/src/core/shell.js +1 -1
- package/dist/src/core/{sandbox/snapshot-manager.js → snapshot-manager.js} +1 -1
- package/dist/src/core/tunnel/client.js +7 -0
- package/dist/src/core/tunnel/daemon.js +7 -0
- package/dist/src/core/tunnel/index.js +7 -0
- package/dist/src/core/websocket.js +1 -1
- package/dist/src/core/window-launcher.js +7 -0
- package/dist/src/dock/bench/memory.js +1 -1
- package/dist/src/dock/cli/commands.js +1 -1
- package/dist/src/dock/cli/k8s.js +7 -0
- package/dist/src/dock/cli/services.js +1 -1
- package/dist/src/dock/cli/session.js +1 -1
- package/dist/src/dock/cli/workspace.js +1 -1
- package/dist/src/dock/engine.js +1 -1
- package/dist/src/dock/image/certs.js +7 -0
- package/dist/src/dock/image/registry.js +1 -1
- package/dist/src/dock/room-data.js +7 -0
- package/dist/src/dock/runtime/network.js +1 -1
- package/dist/src/dock/sandbox.js +7 -0
- package/dist/src/dock/setup/index.js +1 -1
- package/dist/src/dock/workspace/share.js +7 -0
- package/dist/src/dock/workspace/workspace.js +1 -1
- package/package.json +1 -1
- package/dist/src/core/sandbox/arch-shim.js +0 -7
- package/dist/src/core/sandbox/capsule-builder.js +0 -7
- package/dist/src/core/sandbox/cgroups.js +0 -7
- package/dist/src/core/sandbox/container-monitor.js +0 -7
- package/dist/src/core/sandbox/crun-manager.js +0 -7
- package/dist/src/core/sandbox/engine.js +0 -7
- package/dist/src/core/sandbox/image-manager.js +0 -7
- package/dist/src/core/sandbox/image-store.js +0 -7
- package/dist/src/core/sandbox/index.js +0 -7
- package/dist/src/core/sandbox/macos-sandbox.js +0 -7
- package/dist/src/core/sandbox/microvm-manager.js +0 -7
- package/dist/src/core/sandbox/native-service-manager.js +0 -7
- package/dist/src/core/sandbox/network.js +0 -7
- package/dist/src/core/sandbox/oci-exporter.js +0 -7
- package/dist/src/core/sandbox/oci-puller.js +0 -7
- package/dist/src/core/sandbox/oci-runtime-spec.js +0 -7
- package/dist/src/core/sandbox/port-proxy-daemon.js +0 -7
- package/dist/src/core/sandbox/room-sync-manager.js +0 -7
- package/dist/src/core/sandbox/runtime-manager.js +0 -7
- package/dist/src/core/sandbox/storage.js +0 -7
- package/dist/src/core/sandbox/supervisor.js +0 -7
- package/dist/src/core/sandbox/tunnel-daemon.js +0 -7
- package/dist/src/core/sandbox/tunnel.js +0 -7
- package/dist/src/core/sandbox/window-launcher.js +0 -7
- package/dist/src/core/sandbox/windows-wsl2.js +0 -7
- package/dist/src/core/sandbox-window.js +0 -8
- /package/dist/src/core/{sandbox/policy-engine.js → policy-engine.js} +0 -0
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
|
|
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
|
|
|
@@ -47,9 +76,11 @@ Every command has its own help, for example `thinknsh run --help`. The full list
|
|
|
47
76
|
| Containers | `run`, `create`, `start`, `stop`, `restart`, `kill`, `rm`, `ps`, `logs`, `exec`, `wait`, `inspect` |
|
|
48
77
|
| Images | `images`, `pull`, `push`, `build` (`--platform a,b --push`), `tag`, `rmi`, `image prune`, `search`, `manifest create / inspect / push / rm` |
|
|
49
78
|
| Registries | `login [-u USER] [--password-stdin] [REGISTRY]`, `logout [REGISTRY]` |
|
|
50
|
-
| Agent workspaces | `workspace up / open / apps / ls / shell / exec / ssh-config / stop / rm` |
|
|
51
|
-
|
|
|
52
|
-
|
|
|
79
|
+
| Agent workspaces | `workspace up / open / apps / ls / shell / exec / ssh-config / stop / rm`, `workspace share / join / joined / leave` (a teammate's VS Code on your workspace) |
|
|
80
|
+
| Sandboxes | `dev`, `sandbox start / freeze / resume / ps / logs / stats / exec / share / destroy`, `freeze`, `resume`, `deploy` — project folders as dock containers |
|
|
81
|
+
| Dev services | `service start postgres\|redis\|mysql\|mongo [-p PORT]`, `service stop / restart / status / logs / rm [-v]` |
|
|
82
|
+
| Local registry | `registry start [--lan] [PORT] / stop / status / push IMAGE / pull IMAGE / trust HOST:PORT` |
|
|
83
|
+
| Kubernetes | `k8s up / status / down / rm / kubeconfig / load IMAGE / kubectl …` (a local cluster in one command), `cluster k8s up / status / kubeconfig / down` (across your servers) |
|
|
53
84
|
| Apps | `compose up / down / ps / logs / exec / run / create / build / pull / push / start / stop / restart / kill / pause / unpause / rm / top / port / images / cp / scale / wait / events / ls / watch / config` |
|
|
54
85
|
| Storage and networks | `volume ls / create / rm / inspect / prune`, `network ls / create / rm / connect / disconnect / prune` |
|
|
55
86
|
| Files and state | `cp`, `diff`, `export`, `commit`, `save`, `load`, `import`, `history`, `stats`, `top`, `pause` / `unpause`, `rename`, `update`, `port` |
|
|
@@ -116,7 +147,9 @@ New here? Read the step-by-step guide: [`docs/HOW-TO-USE.md`](docs/HOW-TO-USE.md
|
|
|
116
147
|
| Image signatures: `trust key generate`, `trust sign`, `trust verify` (the format cosign reads); only signed pulls with `dock config --trust-key`; keyless with `--keyless` | ✅ keyless needs cosign installed |
|
|
117
148
|
| Vulnerability scan and SBOM: `image scan` (apk, deb, rpm, npm, Python packages against OSV; `--exit-code` for CI; CycloneDX / SPDX) | ✅ OS advisories for Alpine, Debian, Ubuntu, AlmaLinux, Rocky, openSUSE; Fedora and Amazon Linux: packages listed only |
|
|
118
149
|
| userns-remap: the root of containers (and of build steps) is an unprivileged id range on the host (`dock config --userns-remap default`) | ✅ Linux as root; needs runtime bundle 0.2.3+ |
|
|
119
|
-
| Overlay networks across machines (`network create -d overlay`, VXLAN), names of every machine's containers | ✅ Linux as root
|
|
150
|
+
| Overlay networks across machines (`network create -d overlay`, VXLAN), names of every machine's containers | ✅ Linux as root |
|
|
151
|
+
| Encrypted overlays (`-o encrypted -o key=…`: IPsec ESP, AES-256-GCM, per-direction keys; plain VXLAN from a peer dropped) | ✅ Linux as root |
|
|
152
|
+
| Kubernetes on this machine (`thinknsh k8s up`: k3s, kubectl context `thinknsh`, local images with `k8s load`, Secrets encrypted at rest) | ✅ on Linux; macOS and Windows VM: needs runtime bundle 0.2.6 or newer |
|
|
120
153
|
| BuildKit: buildx and BuildKit-mode builds through the engine CLI (`--secret`, cache mounts, and SSH-agent forwarding in BuildKit mode) | ✅ |
|
|
121
154
|
| buildx multi-platform: `buildx build --platform linux/amd64,linux/arm64 --push` (one OCI index, pushed with dock's logins), `--load` | ✅ |
|
|
122
155
|
| Compose: build, depends_on with `service_healthy`, healthchecks, profiles, `.env`, `extends`, `secrets`, `configs`, recreate on change | ✅ |
|
|
@@ -139,6 +172,7 @@ New here? Read the step-by-step guide: [`docs/HOW-TO-USE.md`](docs/HOW-TO-USE.md
|
|
|
139
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 |
|
|
140
173
|
| Corporate proxy and CA certificates, registry mirrors, insecure registries (`dock config`) | ✅ |
|
|
141
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 |
|
|
142
176
|
| Diagnostics bundle, factory reset (`dock diagnose`, `dock reset`) | ✅ |
|
|
143
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 |
|
|
144
178
|
| Registry logins from credential helpers (ECR, GCR, OS keychains: `credHelpers` / `credsStore` of the engine CLI config, or `dock config --credential-helper`) | ✅ |
|
|
@@ -148,6 +182,8 @@ New here? Read the step-by-step guide: [`docs/HOW-TO-USE.md`](docs/HOW-TO-USE.md
|
|
|
148
182
|
| Agent workspaces over SSH (key-only, loopback, no capabilities) | ✅ |
|
|
149
183
|
| Rootless mode on Linux (a normal user, no sudo: user namespaces, pasta / slirp4netns; limits and pause through the cgroup systemd delegates to the user) | ✅ |
|
|
150
184
|
| Deploy compose apps to your own servers | ✅ `thinknsh cluster deploy` |
|
|
185
|
+
| Between your servers: pod traffic over TLS keyed by each server's own key (mesh), secrets sealed to the servers (the platform cannot read them) | ✅ needs the matching thinknagent |
|
|
186
|
+
| Kubernetes across your servers (`thinknsh cluster k8s up`: k3s set up by thinknagent, WireGuard between nodes, join token and kubeconfig sealed end to end) | ✅ needs the matching thinknagent |
|
|
151
187
|
|
|
152
188
|
### Platforms
|
|
153
189
|
|
|
@@ -229,14 +265,15 @@ A step-by-step guide for users is in [`docs/HOW-TO-USE.md`](docs/HOW-TO-USE.md).
|
|
|
229
265
|
|
|
230
266
|
### 1. One container engine for everything
|
|
231
267
|
* **Containers, apps and services through dock:** `run`, `build`, `compose`, `workspace`, `service` and `registry` all run on the same engine (see the containers section above). On macOS that is one small Linux VM (`tnc-vz`) that sleeps when idle; on Windows one WSL2 distro; on Linux the host kernel.
|
|
232
|
-
* **Dev databases in one command:** `thinknsh service start postgres` (also `redis`, `mysql`) runs it as a container on `127.0.0.1`, waits until it is healthy, prints the connection URL, and keeps its data in a volume across restarts and upgrades.
|
|
268
|
+
* **Dev databases in one command:** `thinknsh service start postgres` (also `redis`, `mysql`, `mongo`) runs it as a container on `127.0.0.1`, waits until it is healthy, prints the connection URL, and keeps its data in a volume across restarts and upgrades.
|
|
233
269
|
* **Bind mounts from your folders:** `-v ./src:/app` shares files both ways (VirtioFS on macOS, `/mnt/c` on Windows).
|
|
234
270
|
* **Port conflict protection:** publishing a port that is already in use fails with a message instead of taking over the running program.
|
|
235
|
-
* **Older names still work:** `capsule`, `vault`, `mesh`, `stack` and `vm` run `build`/`images`, `volume`, `network`, `compose` and `dock`, and print the new command each time. `vault backup` / `restore` copy a volume to or from a `.tar.gz` file.
|
|
271
|
+
* **Older names still work:** `capsule`, `vault`, `mesh`, `stack` and `vm` run `build`/`images`, `volume`, `network`, `compose` and `dock`, and print the new command each time. `vault backup` / `restore` copy a volume to or from a `.tar.gz` file.
|
|
272
|
+
* **Sandboxes are dock containers too:** `thinknsh dev` detects the project (Node, Python, Go, Rust, C/C++ or `thinkncollab.yml`), runs it in the container `tnc-sb-<name>` with the folder at `/work` and the dependencies in a volume of their own, installed inside the first time and again only when `package.json` / `requirements.txt` change. `freeze` stops it (0 CPU, 0 RAM, files and dependencies kept) and `resume` starts it in about a second. The older separate sandbox engine is gone; one engine runs everything.
|
|
236
273
|
|
|
237
274
|
### 2. Multi-Layer Collaborative State Sync (`room share` / `room pull`)
|
|
238
275
|
* Share entire development environments between teammates in seconds without third-party registries.
|
|
239
|
-
* **Encrypted Multi-Layer Bundle:** Packages workspace code,
|
|
276
|
+
* **Encrypted Multi-Layer Bundle:** Packages workspace code, dumps of your running dock services (`thinknsh service`: Postgres, MySQL, Mongo, Redis), and sanitized `.env` secrets into a single AES-256-GCM encrypted bundle. `room pull` starts the same services on the teammate's machine and loads the dumps into them.
|
|
240
277
|
* **Hybrid Transport:** Direct Gigabit Wi-Fi LAN P2P mesh (zero cloud bandwidth) with automatic fallback to ephemeral in-memory cloud socket relays.
|
|
241
278
|
|
|
242
279
|
### 3. Smart 3-Way Code & Secret Merge Engine
|
|
@@ -248,7 +285,8 @@ A step-by-step guide for users is in [`docs/HOW-TO-USE.md`](docs/HOW-TO-USE.md).
|
|
|
248
285
|
### 4. Low-Bandwidth Terminal Multiplexing
|
|
249
286
|
* **Semantic ANSI Text vs Video Pixels:** Streams UTF-8 terminal text chunks over WebSockets instead of rendering video frames, eliminating screen sharing latency and blur.
|
|
250
287
|
* **Sub-Millisecond Dynamic RBAC:** Grant and revoke peer keyboard input access in real-time.
|
|
251
|
-
* **
|
|
288
|
+
* **End to end, always:** every share has its own random key, carried only in the link's `#k=…` part, which browsers never send to the server. Output and input are AES-256-GCM under that key; the platform relays ciphertext only, and unencrypted input is refused.
|
|
289
|
+
* **AAD Replay Defense:** each chunk is bound to the session and a sequence number (`term-input:${sessionId}:${seq}`); an input runs once, and only within two minutes of being typed.
|
|
252
290
|
* **Emergency Host Lock:** Instantly drop all remote input with one command.
|
|
253
291
|
|
|
254
292
|
### 5. Signal-Grade E2EE Security
|
|
@@ -264,7 +302,7 @@ ThinkNCollab terminal pair programming operates on semantic text streams rather
|
|
|
264
302
|
|
|
265
303
|
* **Text Streams Over WebSocket:** Raw PTY output (ANSI escape sequences and UTF-8 characters) is encrypted and streamed directly to connected peers, rendering natively in their local terminal client.
|
|
266
304
|
* **Zero Video Overhead:** Avoids high-bandwidth video encoding/decoding, delivering sharp text reproduction and minimal latency even over constrained networks.
|
|
267
|
-
* **Cryptographic Session Integrity:** Every
|
|
305
|
+
* **Cryptographic Session Integrity:** Every packet carries a sequence number and an AES-256-GCM authentication tag bound to the session, under a key only the people with the share link have.
|
|
268
306
|
|
|
269
307
|
---
|
|
270
308
|
|
|
@@ -313,7 +351,7 @@ thinknsh compose up -d
|
|
|
313
351
|
|
|
314
352
|
### Room State & Database Sync
|
|
315
353
|
```bash
|
|
316
|
-
thinknsh room share [name] # Publish code
|
|
354
|
+
thinknsh room share [name] # Publish code + the dock services' data (Postgres, MySQL, Mongo, Redis)
|
|
317
355
|
thinknsh room share --no-code # Publish database & environment only
|
|
318
356
|
thinknsh room pull [state-id] # Smart-merge teammate state into workspace
|
|
319
357
|
thinknsh room merge [state-id] # Explicit 3-way merge
|
|
@@ -323,18 +361,27 @@ thinknsh room verify # Display 60-digit E2EE safety number & TOFU
|
|
|
323
361
|
thinknsh room rotate-key # Rekey room for forward secrecy
|
|
324
362
|
```
|
|
325
363
|
|
|
364
|
+
### Open a teammate's workspace in your own VS Code
|
|
365
|
+
```bash
|
|
366
|
+
thinknsh workspace share main # owner: prints a join code, asks before letting anyone in
|
|
367
|
+
thinknsh workspace join CODE # teammate: adds host tnc-join-… and opens VS Code on it
|
|
368
|
+
```
|
|
369
|
+
The teammate gets a guest container with your folders and services but its own home (none of your Claude / Codex logins). SSH end to end, checked against the key fingerprint in the code; the code's secret never reaches ThinkNCollab. Directly over a private local network when both are on one, otherwise relayed by ThinkNCollab. Ctrl-C on the owner's side removes the guest container.
|
|
370
|
+
|
|
326
371
|
### Real-Time Terminal Pair Programming
|
|
327
372
|
```bash
|
|
328
373
|
thinknsh share # Start real-time PTY terminal sharing session
|
|
329
374
|
thinknsh share --readonly # Start in read-only broadcast mode
|
|
330
375
|
```
|
|
331
376
|
|
|
332
|
-
###
|
|
377
|
+
### Sandboxes (dock containers)
|
|
333
378
|
```bash
|
|
334
|
-
thinknsh dev --port 3000 #
|
|
335
|
-
thinknsh ps
|
|
336
|
-
thinknsh freeze <name> #
|
|
337
|
-
thinknsh resume <name> #
|
|
379
|
+
thinknsh dev --port 3000 # Detect the project and run it in container tnc-sb-<folder>
|
|
380
|
+
thinknsh sandbox ps # Sandboxes, ports, images
|
|
381
|
+
thinknsh freeze <name> # Stop it: 0% CPU/RAM, files and dependencies kept
|
|
382
|
+
thinknsh resume <name> # Start it again, nothing to reinstall
|
|
383
|
+
thinknsh sandbox exec <name> # Shell inside (or: use <name> in the thinknsh shell)
|
|
384
|
+
thinknsh deploy # Production sandbox here (servers: thinknsh cluster deploy)
|
|
338
385
|
thinknsh snapshot create <name> # Create sub-second CoW volume snapshot
|
|
339
386
|
thinknsh snapshot restore <name> # Restore volume state
|
|
340
387
|
```
|
|
@@ -351,4 +398,4 @@ thinknsh dep sync # Reconcile environment from thinkncollab.yml
|
|
|
351
398
|
## 📄 License
|
|
352
399
|
|
|
353
400
|
Apache License 2.0. See [LICENSE](LICENSE) for details.
|
|
354
|
-
Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
|
|
401
|
+
Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
|
package/dist/bin/thinknsh.js
CHANGED
|
@@ -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");
|
|
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")
|
|
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")
|
|
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(
|
|
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
|
-
"use strict";const fs=require("fs"),path=require("path"),chalk=require("chalk"),{composeToStack}=require("../../dock/deploy/compose"),{COMPOSE_LEGACY_FILES}=require("../../dock/api/wire");function flag(e,o){const s=e.indexOf(o);return-1===s?null:e[s+1]}function roomOf(e,o){const s=flag(e,"--room")||o?.ws?.getCurrentRoom?.()||o?.config?.roomId||process.env.THINKNCOLLAB_ROOM_ID;if(!s)throw new Error("no room selected: join a room first or pass --room <id>");return"object"==typeof s?s._id||s.id:s}function findComposeFile(e){const o=flag(e,"-f")||flag(e,"--file");if(o)return o;for(const e of["compose.yaml","compose.yml",...COMPOSE_LEGACY_FILES])if(fs.existsSync(e))return e;throw new Error("no compose file found (compose.yaml / compose.yml); pass -f <file>")}async function api(e,o,s,r){if(!e?.api?.isAuthenticated?.())throw new Error("please login first: thinknsh login");return e.api._request(o,`/dock/v2${s}`,r)}function printStatus({deployments:e=[],pods:o=[],nodes:s=[]}){const r=(e,o)=>String(e??"").padEnd(o);console.log(chalk.bold(`\n${r("DEPLOYMENT",28)}${r("READY",8)}${r("REV",5)}IMAGE`));for(const o of e){const e=o.status||{},s=`${e.ready||0}/${o.spec.replicas}`,t=(e.ready||0)===o.spec.replicas?chalk.green:chalk.yellow;console.log(`${r(o.name,28)}${t(r(s,8))}${r(o.revision,5)}${o.spec.image}`)}e.length||console.log(chalk.dim(" (no deployments)")),console.log(chalk.bold(`\n${r("POD",36)}${r("NODE",18)}${r("PHASE",12)}MESSAGE`));const t=e=>s.find(o=>String(o.agentId)===String(e))?.name||(e?String(e).slice(-8):"-");for(const e of o){const o=e.terminating?"Terminating":e.phase,s=e.ready?chalk.green:"Failed"===o?chalk.red:chalk.yellow;console.log(`${r(e.name,36)}${r(t(e.nodeAgentId),18)}${s(r(o,12))}${chalk.dim(e.message||"")}`)}console.log(chalk.bold(`\n${r("NODE",24)}${r("STATUS",10)}${r("CPU",6)}${r("MEMORY",10)}DOCK`));for(const e of s)console.log(`${r(e.name,24)}${(e.ready?chalk.green:chalk.red)(r(e.ready?"Ready":"NotReady",10))}${r(e.capacity?.cpu,6)}${r(`${e.capacity?.memoryMB} MB`,10)}${e.dockVersion||chalk.dim(e.message||"-")}`);console.log("")}module.exports={name:"cluster",description:"Deploy compose stacks to your servers and manage them (dock cluster)",usage:"thinknsh cluster <deploy|status|scale|rollback|rm|nodes|secret>",requiresAuth:!1,examples:["thinknsh cluster deploy -f compose.yaml","thinknsh cluster status","thinknsh cluster scale shop-web 3","thinknsh cluster rollback shop-web"],async execute(e=[],o){const s=(e[0]||"status").toLowerCase();try{switch(s){case"deploy":{const s=findComposeFile(e),r=composeToStack(s,{stackName:flag(e,"--stack")});for(const e of r.warnings)console.log(chalk.yellow(` ! ${e}`));console.log(chalk.bold(`\nstack ${r.stack}: ${Object.keys(r.services).length} service(s) from ${path.basename(s)}`));for(const[e,o]of Object.entries(r.services)){const s=o.ports.map(e=>e.hostPort?`${e.hostPort}→${e.containerPort}`:e.containerPort).join(", ");console.log(` ${chalk.cyan(`${r.stack}-${e}`.padEnd(28))} ${o.image} ×${o.replicas}${s?` ports ${s}`:""}`)}if(e.includes("--dry-run"))return;const t=await api(o,"PUT",`/rooms/${roomOf(e,o)}/stacks/${r.stack}`,{services:r.services});return console.log(chalk.green(`\n✓ applied ${t.applied.length} deployment(s)`)+(t.removed.length?chalk.dim(`, removed ${t.removed.join(", ")}`):"")),void console.log(chalk.dim(" watch the rollout: thinknsh cluster status"))}case"status":case"nodes":return void printStatus(await api(o,"GET",`/rooms/${roomOf(e,o)}/cluster`));case"scale":{const[,s,r]=e;if(!s||void 0===r)throw new Error("usage: thinknsh cluster scale <deployment> <replicas>");const{deployment:t}=await api(o,"POST",`/rooms/${roomOf(e,o)}/deployments/${s}/scale`,{replicas:Number(r)});return void console.log(chalk.green(`✓ ${t.name} scaled to ${t.spec.replicas}`))}case"rollback":{const[,s,r]=e;if(!s)throw new Error("usage: thinknsh cluster rollback <deployment> [revision]");const{deployment:t}=await api(o,"POST",`/rooms/${roomOf(e,o)}/deployments/${s}/rollback`,r&&!r.startsWith("--")?{revision:Number(r)}:{});return void console.log(chalk.green(`✓ ${t.name} rolled back: now revision ${t.revision} (${t.spec.image})`))}case"secret":{const[,s="ls",r,t]=e,n=roomOf(e,o);if("ls"===s){const e=await api(o,"GET",`/rooms/${n}/secrets`);e.length||console.log(chalk.dim(" (no secrets)"));for(const o of e)console.log(` ${o.name.padEnd(32)} ${chalk.dim(new Date(o.updatedAt).toLocaleString())}`);return}if(!r)throw new Error(`usage: thinknsh cluster secret ${s} <NAME>`);if("rm"===s)return await api(o,"DELETE",`/rooms/${n}/secrets/${encodeURIComponent(r)}`),void console.log(chalk.green(`✓ secret ${r} removed`));if("set"!==s)throw new Error("usage: thinknsh cluster secret <set|ls|rm> [NAME] [value|-]");let c=t&&"-"!==t&&!t.startsWith("--")?t:null;if(null!==c||process.stdin.isTTY&&"-"!==t||(c=await new Promise(e=>{let o="";process.stdin.on("data",e=>{o+=e}),process.stdin.on("end",()=>e(o.replace(/\r?\n$/,"")))})),null===c){const{password:e}=require("@inquirer/prompts");c=await e({message:`value for ${r}:`,mask:"*"})}return await api(o,"PUT",`/rooms/${n}/secrets/${encodeURIComponent(r)}`,{value:c}),void console.log(chalk.green(`✓ secret ${r} saved`)+chalk.dim(" (pods using it are recreated with the new value)"))}case"rm":{const s=e[1];if(!s)throw new Error("usage: thinknsh cluster rm <stack>");const r=await api(o,"PUT",`/rooms/${roomOf(e,o)}/stacks/${s}`,{services:{}});return void console.log(chalk.green(`✓ removed ${r.removed.length} deployment(s) of stack ${s}`))}default:console.log(chalk.yellow(`unknown subcommand "${s}". Usage: ${module.exports.usage}`)),process.exitCode=1}}catch(e){console.error(chalk.red(`cluster ${s}: ${e.message}`)),process.exitCode=1}}};
|
|
7
|
+
"use strict";const fs=require("fs"),path=require("path"),chalk=require("chalk"),{composeToStack}=require("../../dock/deploy/compose"),{COMPOSE_LEGACY_FILES}=require("../../dock/api/wire"),seal=require("../../core/crypto/seal"),KEYS_FILE=()=>path.join(require("os").homedir(),".thinkncollab","cluster-keys.json"),keyId=e=>seal.fingerprint(e).replace(/-/g,"");function sealTargets(e,o,{accept:s=!1,file:n=KEYS_FILE()}={}){let r={};try{r=JSON.parse(fs.readFileSync(n,"utf8"))}catch(e){}const t=r[e]||{},a=[],l=[];for(const e of o){const o=e.mesh?.key;if(!seal.isPublicKey(o))continue;const n=String(e.agentId);!t[n]||t[n]===o||s?(t[n]=o,l.push({id:keyId(o),publicKey:o,name:e.name||n})):a.push(`${e.name||n} (now ${seal.fingerprint(o)}, pinned ${seal.fingerprint(t[n])})`)}if(a.length)throw new Error(`the key of ${a.join(", ")} changed since it was pinned. If the server was reinstalled, check its key (thinknagent prints it at start) and run again with --accept-new-keys; otherwise someone may be impersonating it`);return r[e]=t,fs.mkdirSync(path.dirname(n),{recursive:!0,mode:448}),fs.writeFileSync(n,JSON.stringify(r,null,2),{mode:384}),l}function flag(e,o){const s=e.indexOf(o);return-1===s?null:e[s+1]}function roomOf(e,o){const s=flag(e,"--room")||o?.ws?.getCurrentRoom?.()||o?.config?.roomId||process.env.THINKNCOLLAB_ROOM_ID;if(!s)throw new Error("no room selected: join a room first or pass --room <id>");return"object"==typeof s?s._id||s.id:s}function findComposeFile(e){const o=flag(e,"-f")||flag(e,"--file");if(o)return o;for(const e of["compose.yaml","compose.yml",...COMPOSE_LEGACY_FILES])if(fs.existsSync(e))return e;throw new Error("no compose file found (compose.yaml / compose.yml); pass -f <file>")}async function api(e,o,s,n){if(!e?.api?.isAuthenticated?.())throw new Error("please login first: thinknsh login");return e.api._request(o,`/dock/v2${s}`,n)}function printStatus({deployments:e=[],pods:o=[],nodes:s=[]}){const n=(e,o)=>String(e??"").padEnd(o);console.log(chalk.bold(`\n${n("DEPLOYMENT",28)}${n("READY",8)}${n("REV",5)}IMAGE`));for(const o of e){const e=o.status||{},s=`${e.ready||0}/${o.spec.replicas}`,r=(e.ready||0)===o.spec.replicas?chalk.green:chalk.yellow;console.log(`${n(o.name,28)}${r(n(s,8))}${n(o.revision,5)}${o.spec.image}`)}e.length||console.log(chalk.dim(" (no deployments)")),console.log(chalk.bold(`\n${n("POD",36)}${n("NODE",18)}${n("PHASE",12)}MESSAGE`));const r=e=>s.find(o=>String(o.agentId)===String(e))?.name||(e?String(e).slice(-8):"-");for(const e of o){const o=e.terminating?"Terminating":e.phase,s=e.ready?chalk.green:"Failed"===o?chalk.red:chalk.yellow;console.log(`${n(e.name,36)}${n(r(e.nodeAgentId),18)}${s(n(o,12))}${chalk.dim(e.message||"")}`)}console.log(chalk.bold(`\n${n("NODE",24)}${n("STATUS",10)}${n("CPU",6)}${n("MEMORY",10)}${n("KEY",21)}DOCK`));for(const e of s){const o=seal.isPublicKey(e.mesh?.key)?seal.fingerprint(e.mesh.key):chalk.yellow("none (old agent)");console.log(`${n(e.name,24)}${(e.ready?chalk.green:chalk.red)(n(e.ready?"Ready":"NotReady",10))}${n(e.capacity?.cpu,6)}${n(`${e.capacity?.memoryMB} MB`,10)}${n(o,21)}${e.dockVersion||chalk.dim(e.message||"-")}`)}console.log("")}module.exports={sealTargets,name:"cluster",description:"Deploy compose stacks to your servers and manage them (dock cluster)",usage:"thinknsh cluster <deploy|status|scale|rollback|rm|nodes|secret|k8s>",requiresAuth:!1,examples:["thinknsh cluster deploy -f compose.yaml","thinknsh cluster status","thinknsh cluster scale shop-web 3","thinknsh cluster rollback shop-web"],async execute(e=[],o){const s=(e[0]||"status").toLowerCase();try{switch(s){case"deploy":{const s=findComposeFile(e),n=composeToStack(s,{stackName:flag(e,"--stack")});for(const e of n.warnings)console.log(chalk.yellow(` ! ${e}`));console.log(chalk.bold(`\nstack ${n.stack}: ${Object.keys(n.services).length} service(s) from ${path.basename(s)}`));for(const[e,o]of Object.entries(n.services)){const s=o.ports.map(e=>e.hostPort?`${e.hostPort}→${e.containerPort}`:e.containerPort).join(", ");console.log(` ${chalk.cyan(`${n.stack}-${e}`.padEnd(28))} ${o.image} ×${o.replicas}${s?` ports ${s}`:""}`)}if(e.includes("--dry-run"))return;const r=await api(o,"PUT",`/rooms/${roomOf(e,o)}/stacks/${n.stack}`,{services:n.services});return console.log(chalk.green(`\n✓ applied ${r.applied.length} deployment(s)`)+(r.removed.length?chalk.dim(`, removed ${r.removed.join(", ")}`):"")),void console.log(chalk.dim(" watch the rollout: thinknsh cluster status"))}case"status":case"nodes":return void printStatus(await api(o,"GET",`/rooms/${roomOf(e,o)}/cluster`));case"scale":{const[,s,n]=e;if(!s||void 0===n)throw new Error("usage: thinknsh cluster scale <deployment> <replicas>");const{deployment:r}=await api(o,"POST",`/rooms/${roomOf(e,o)}/deployments/${s}/scale`,{replicas:Number(n)});return void console.log(chalk.green(`✓ ${r.name} scaled to ${r.spec.replicas}`))}case"rollback":{const[,s,n]=e;if(!s)throw new Error("usage: thinknsh cluster rollback <deployment> [revision]");const{deployment:r}=await api(o,"POST",`/rooms/${roomOf(e,o)}/deployments/${s}/rollback`,n&&!n.startsWith("--")?{revision:Number(n)}:{});return void console.log(chalk.green(`✓ ${r.name} rolled back: now revision ${r.revision} (${r.spec.image})`))}case"secret":{const[,s="ls",n,r]=e,t=roomOf(e,o);if("ls"===s){const[e,{nodes:s=[]}]=await Promise.all([api(o,"GET",`/rooms/${t}/secrets`),api(o,"GET",`/rooms/${t}/cluster`)]);e.length||console.log(chalk.dim(" (no secrets)"));for(const o of e){const e=s.filter(e=>seal.isPublicKey(e.mesh?.key)&&!(o.sealedFor||[]).includes(keyId(e.mesh.key))).map(e=>e.name),n=o.sealed?e.length?chalk.yellow(`sealed; not for ${e.join(", ")}: set it again`):chalk.green("sealed"):chalk.yellow("readable by the platform: set it again to seal it");console.log(` ${o.name.padEnd(32)} ${n} ${chalk.dim(new Date(o.updatedAt).toLocaleString())}`)}return}if(!n)throw new Error(`usage: thinknsh cluster secret ${s} <NAME>`);if("rm"===s)return await api(o,"DELETE",`/rooms/${t}/secrets/${encodeURIComponent(n)}`),void console.log(chalk.green(`✓ secret ${n} removed`));if("set"!==s)throw new Error("usage: thinknsh cluster secret <set|ls|rm> [NAME] [value|-]");let a=r&&"-"!==r&&!r.startsWith("--")?r:null;if(null!==a||process.stdin.isTTY&&"-"!==r||(a=await new Promise(e=>{let o="";process.stdin.on("data",e=>{o+=e}),process.stdin.on("end",()=>e(o.replace(/\r?\n$/,"")))})),null===a){const{password:e}=require("@inquirer/prompts");a=await e({message:`value for ${n}:`,mask:"*"})}const{nodes:l=[]}=await api(o,"GET",`/rooms/${t}/cluster`),c=sealTargets(String(t),l,{accept:e.includes("--accept-new-keys")});if(!c.length)throw new Error("no server in this room can open sealed secrets yet: approve a server running thinknagent 1.x with serverSystem (cluster nodes lists their keys)");const i=seal.seal(a,c,`room/${t}/secret/${n}`);await api(o,"PUT",`/rooms/${t}/secrets/${encodeURIComponent(n)}`,{value:i}),console.log(chalk.green(`✓ secret ${n} sealed for ${c.map(e=>e.name).join(", ")}`)+chalk.dim(" (the platform cannot read it; pods using it are recreated)"));const d=l.filter(e=>!seal.isPublicKey(e.mesh?.key)).map(e=>e.name);return void(d.length&&console.log(chalk.yellow(` ${d.join(", ")} cannot open it (older thinknagent): pods needing it will not run there`)))}case"k8s":case"kubernetes":{const[,s="status"]=e,n=String(roomOf(e,o));if("up"===s){let s;console.log(chalk.dim(" setting up Kubernetes on the room's servers (pulling k3s can take a few minutes)…")),await api(o,"POST",`/rooms/${n}/k8s`,{server:flag(e,"--server")||void 0,version:flag(e,"--version")||void 0});for(const e=Date.now()+12e5;await new Promise(e=>setTimeout(e,3e3)),({job:s}=await api(o,"GET",`/rooms/${n}/k8s`)),s&&"running"===s.state;)if(Date.now()>e)throw new Error("still setting up after 20 minutes; check with: thinknsh cluster k8s status");if(!s)throw new Error("the platform lost track of the setup (restarted?); run thinknsh cluster k8s up again");if(!s.result)throw new Error(s.error||"setup failed");const r=s.result;for(const e of r.nodes)console.log(` ${e.ok?chalk.green("✓"):chalk.red("✗")} ${e.node.padEnd(24)} ${e.role}${e.ok?"":chalk.red(` ${e.error}`)}`);return void console.log(chalk.green(`\n✓ Kubernetes ${r.version}, API ${r.url}`)+chalk.dim("\n kubectl access: thinknsh cluster k8s kubeconfig"))}if("kubeconfig"===s){const e=seal.generateKeyPair(),s=await api(o,"POST",`/rooms/${n}/k8s/kubeconfig`,{publicKey:e.publicKey}),r=seal.open(s.kubeconfig,{id:"cli",...e},`room/${n}/k8s/kubeconfig`),t=require("js-yaml").load(r).clusters[0].cluster.server,a=`thinknsh-${n.slice(-6)}`,{file:l}=require("../../dock/cli/k8s").mergeKubeconfig(r,null,{name:a,server:t});return void console.log(chalk.green(`✓ kubectl context ${a} (${t}, on ${s.server}) in ${l}`)+chalk.dim(`\n kubectl --context ${a} get nodes`))}if("down"===s){const s=await api(o,"DELETE",`/rooms/${n}/k8s${e.includes("--purge")?"?purge=1":""}`);for(const e of s.nodes)console.log(` ${e.ok?chalk.green("✓"):chalk.red("✗")} ${e.node}${e.ok?"":chalk.red(` ${e.error}`)}`);return void(s.nodes.length||console.log(chalk.dim(" (no node runs Kubernetes)")))}const{nodes:r,job:t}=await api(o,"GET",`/rooms/${n}/k8s`);"running"===t?.state&&console.log(chalk.dim(` setting up (started ${new Date(t.startedAt).toLocaleTimeString()})…`)),"failed"===t?.state&&console.log(chalk.red(` last setup failed: ${t.error||t.result?.nodes.filter(e=>!e.ok).map(e=>`${e.node}: ${e.error}`).join("; ")}`));for(const e of r){const o=e.k8s?`${e.k8s.role||"?"} ${e.k8s.running?chalk.green("running"):chalk.yellow("stopped")} ${e.k8s.version||""}`:chalk.dim("-");console.log(` ${String(e.name).padEnd(24)} ${o}`)}return void(r.length||console.log(chalk.dim(" (no nodes)")))}case"rm":{const s=e[1];if(!s)throw new Error("usage: thinknsh cluster rm <stack>");const n=await api(o,"PUT",`/rooms/${roomOf(e,o)}/stacks/${s}`,{services:{}});return void console.log(chalk.green(`✓ removed ${n.removed.length} deployment(s) of stack ${s}`))}default:console.log(chalk.yellow(`unknown subcommand "${s}". Usage: ${module.exports.usage}`)),process.exitCode=1}}catch(e){console.error(chalk.red(`cluster ${s}: ${e.message}`)),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
|
-
"use strict";const{runCommand}=require("../../dock/cli/commands"),DESCRIPTIONS={run:"Run a container (pulls the image if needed)",create:"Create a container without starting it",start:"Start stopped containers",stop:"Stop running containers",restart:"Restart containers",kill:"Send a signal to containers",rm:"Remove containers",wait:"Wait for containers to stop and print exit codes",ps:"List containers",logs:"Show container output",exec:"Run a command in a running container",inspect:"Show details of containers, images, volumes or networks",images:"List images",pull:"Download an image",push:"Upload an image to a registry (also: sandbox push ecr://… / --export)",login:"Log in to a container registry (-u USER [REGISTRY]); with an email, to ThinkNCollab",logout:"Log out of a container registry (REGISTRY); without one, of ThinkNCollab",rmi:"Remove images",tag:"Give an image another name",manifest:"Multi-platform images from local images (create, inspect, push, rm)",search:"Search the public image registry",events:"Stream engine events (container, image, volume, network)",build:"Build an image from a Containerfile",trust:"Sign images and check their signatures (key generate, sign, verify)",scan:"Packages and known vulnerabilities of an image (also: image scan), or its SBOM",cp:"Copy files between a container and this machine (CONTAINER:PATH)",stats:"Live CPU, memory, network and disk use of containers",top:"Show the processes of a container",pause:"Freeze every process of containers",unpause:"Thaw paused containers",rename:"Rename a container",update:"Change memory/CPU limits or the restart policy of containers",diff:"Show files changed in a container",port:"Show the published ports of a container",commit:"Make an image from a container's changes",save:"Write images to a tar file (loads in any engine)",load:"Read images from a tar file",import:"Make an image from a root filesystem tar",compose:"Run multi-container apps from a compose file",workspace:"One secure container per project for all coding agents and editors (Claude, Codex, Antigravity, Cursor)",service:"Dev databases as containers: postgres, redis, mysql (start, stop, status, logs, rm)",registry:"A local image registry as a container (start, stop, status, push, pull)",capsule:"Older name for image commands (build, images, run, rmi)",vault:"Older name for volume commands, plus backup and restore",mesh:"Older name for network commands",stack:"Older name for compose",vm:"Older name for dock setup/start/stop/status",volume:"Manage volumes",network:"Manage networks",system:"Disk usage, cleanup and engine info",container:"Manage containers (ls, rm, prune, …)",image:"Manage images (ls, rm, prune, …)"},NAMES=Object.keys(DESCRIPTIONS),looksLikeRegistry=e=>!!e&&!e.startsWith("-")&&!e.includes("@")&&(e.includes(".")||e.includes(":")||"localhost"===e),REGISTRY_FORM={login:e=>e.some(e=>/^(-u|--username|--password-stdin)(=|$)/.test(e)||/^-u./.test(e))||1===e.length&&looksLikeRegistry(e[0])||["--help","-h"].includes(e[0]),logout:e=>1===e.length&&(looksLikeRegistry(e[0])||["--help","-h"].includes(e[0])),push:e=>!e.some(e=>/^[a-z][a-z0-9+.-]*:\/\//.test(e)||/^--export/.test(e))&&"export"!==e[0]},LEGACY={login:"../auth/login",logout:"../auth/logout",push:"../sandbox/push"},ALIASES={volume:["vol","volumes"],network:["net","networks"],images:["img"],service:["svc","services","db"],capsule:["cap"],vm:["microvm","vz"]};async function runLegacy(e,a,s,t){const n=require(LEGACY[e]);if(!n.requiresAuth||s?.api?.isAuthenticated())return n.execute(a,s,t);console.log(require("chalk").red("[FAIL] Please login first: login"))}module.exports=NAMES.map(e=>({name:e,description:DESCRIPTIONS[e],usage:`thinknsh ${e} --help`,requiresAuth:!1,selfExiting:!0,...LEGACY[e]?{aliases:require(LEGACY[e]).aliases}:ALIASES[e]?{aliases:ALIASES[e]}:{},async execute(a=[],s,t=e){if(LEGACY[e]&&(t!==e||!REGISTRY_FORM[e](a)))return runLegacy(e,a,s,t);process.exitCode=await runCommand(e,a)}})),module.exports.REGISTRY_FORM=REGISTRY_FORM,module.exports.NAMES=NAMES,module.exports.ALL_NAMES=module.exports.flatMap(e=>[e.name,...e.aliases||[]]);
|
|
7
|
+
"use strict";const{runCommand}=require("../../dock/cli/commands"),DESCRIPTIONS={run:"Run a container (pulls the image if needed)",create:"Create a container without starting it",start:"Start stopped containers",stop:"Stop running containers",restart:"Restart containers",kill:"Send a signal to containers",rm:"Remove containers",wait:"Wait for containers to stop and print exit codes",ps:"List containers",logs:"Show container output",exec:"Run a command in a running container",inspect:"Show details of containers, images, volumes or networks",images:"List images",pull:"Download an image",push:"Upload an image to a registry (also: sandbox push ecr://… / --export)",login:"Log in to a container registry (-u USER [REGISTRY]); with an email, to ThinkNCollab",logout:"Log out of a container registry (REGISTRY); without one, of ThinkNCollab",rmi:"Remove images",tag:"Give an image another name",manifest:"Multi-platform images from local images (create, inspect, push, rm)",search:"Search the public image registry",events:"Stream engine events (container, image, volume, network)",build:"Build an image from a Containerfile",trust:"Sign images and check their signatures (key generate, sign, verify)",scan:"Packages and known vulnerabilities of an image (also: image scan), or its SBOM",cp:"Copy files between a container and this machine (CONTAINER:PATH)",stats:"Live CPU, memory, network and disk use of containers",top:"Show the processes of a container",pause:"Freeze every process of containers",unpause:"Thaw paused containers",rename:"Rename a container",update:"Change memory/CPU limits or the restart policy of containers",diff:"Show files changed in a container",port:"Show the published ports of a container",commit:"Make an image from a container's changes",save:"Write images to a tar file (loads in any engine)",load:"Read images from a tar file",import:"Make an image from a root filesystem tar",compose:"Run multi-container apps from a compose file",workspace:"One secure container per project for all coding agents and editors (Claude, Codex, Antigravity, Cursor)",service:"Dev databases as containers: postgres, redis, mysql (start, stop, status, logs, rm)",registry:"A local image registry as a container (start [--lan], stop, status, push, pull, trust)",k8s:"A local Kubernetes cluster in one command (up, status, down, rm, load, kubectl)",capsule:"Older name for image commands (build, images, run, rmi)",vault:"Older name for volume commands, plus backup and restore",mesh:"Older name for network commands",stack:"Older name for compose",vm:"Older name for dock setup/start/stop/status",volume:"Manage volumes",network:"Manage networks",system:"Disk usage, cleanup and engine info",container:"Manage containers (ls, rm, prune, …)",image:"Manage images (ls, rm, prune, …)"},NAMES=Object.keys(DESCRIPTIONS),looksLikeRegistry=e=>!!e&&!e.startsWith("-")&&!e.includes("@")&&(e.includes(".")||e.includes(":")||"localhost"===e),REGISTRY_FORM={login:e=>e.some(e=>/^(-u|--username|--password-stdin)(=|$)/.test(e)||/^-u./.test(e))||1===e.length&&looksLikeRegistry(e[0])||["--help","-h"].includes(e[0]),logout:e=>1===e.length&&(looksLikeRegistry(e[0])||["--help","-h"].includes(e[0])),push:e=>!e.some(e=>/^[a-z][a-z0-9+.-]*:\/\//.test(e)||/^--export/.test(e))&&"export"!==e[0]},LEGACY={login:"../auth/login",logout:"../auth/logout",push:"../sandbox/push"},ALIASES={volume:["vol","volumes"],network:["net","networks"],images:["img"],service:["svc","services","db"],capsule:["cap"],vm:["microvm","vz"],k8s:["kube","kubernetes"]};async function runLegacy(e,a,s,t){const n=require(LEGACY[e]);if(!n.requiresAuth||s?.api?.isAuthenticated())return n.execute(a,s,t);console.log(require("chalk").red("[FAIL] Please login first: login"))}module.exports=NAMES.map(e=>({name:e,description:DESCRIPTIONS[e],usage:`thinknsh ${e} --help`,requiresAuth:!1,selfExiting:!0,...LEGACY[e]?{aliases:require(LEGACY[e]).aliases}:ALIASES[e]?{aliases:ALIASES[e]}:{},async execute(a=[],s,t=e){if(LEGACY[e]&&(t!==e||!REGISTRY_FORM[e](a)))return runLegacy(e,a,s,t);process.exitCode=await runCommand(e,a)}})),module.exports.REGISTRY_FORM=REGISTRY_FORM,module.exports.NAMES=NAMES,module.exports.ALL_NAMES=module.exports.flatMap(e=>[e.name,...e.aliases||[]]);
|
|
@@ -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,
|
|
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
|
-
|
|
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 chalk=require("chalk"),Table=require("cli-table3"),RoomSyncManager=require("../../core/
|
|
7
|
+
const chalk=require("chalk"),Table=require("cli-table3"),RoomSyncManager=require("../../core/room/sync-manager");module.exports={name:"room",description:"Manage room environments, live states, collaboration, and P2P sync",aliases:["room-sync","sync","mirror"],requiresAuth:!0,async execute(e,o){const n=e[0]?.toLowerCase()||"help";if("join"===n)return require("./join").execute(e.slice(1),o);if("leave"===n)return require("./leave").execute(e.slice(1),o);if("rooms"===n||"list"===n&&!e[1])return require("./rooms").execute(e.slice(1),o);const r=new RoomSyncManager,t=o?.ws?.getCurrentRoom?.()||o?.currentRoom||o?.api?.getCurrentRoom?.()||o?.config?.roomId||process.env.THINKNCOLLAB_ROOM_ID,a="object"==typeof t&&null!==t?t.id||t._id||t.roomId:t;if(!a)return console.log(chalk.red("\n[FAIL] You are not in an active room.")),void console.log(chalk.dim(" Join a room first using: join <room-id> or room join <room-id>\n"));if("publish"===n||"share"===n||"push"===n){const n=e.slice(1).find(e=>!e.startsWith("-"))||"dev-state",t=e.includes("--no-code")||e.includes("--db-only");try{await r.publishState(a,o,{name:n,cwd:process.cwd(),noCode:t})}catch(e){console.log(chalk.red(`\n[FAIL] Could not publish state to room: ${e.message}\n`))}return}if("states"===n||"list"===n||"ls"===n){const e=await r.listStates(a,o);if(console.log(chalk.cyan(`\n[RoomSync] Shared States in Room [${a}]:`)),0===e.length)return console.log(chalk.dim(" No shared states found in this room yet.")),void console.log(chalk.dim(" Publish your current state with: thinknsh room share\n"));const n=new Table({head:[chalk.cyan("STATE ID"),chalk.cyan("NAME"),chalk.cyan("AUTHOR"),chalk.cyan("SERVICES"),chalk.cyan("CODE"),chalk.cyan("SIZE"),chalk.cyan("CREATED")],style:{head:[],border:[]}});return e.forEach(e=>{n.push([chalk.green((e.id||e.stateId||"").slice(0,16)),chalk.bold(e.name||e.id),e.author||"teammate",(e.services||[]).join(", ")||"runtime",e.hasCode?chalk.green("✓ Yes"):chalk.dim("No"),`${e.sizeMB||e.bundleSizeMB||"0"} MB`,e.createdAt||e.publishedAt?new Date(e.createdAt||e.publishedAt).toLocaleTimeString():"now"])}),console.log(n.toString()),void console.log(chalk.dim("\nPull any state using: thinknsh room pull <state-id>\n"))}if("pull"===n||"sync"===n||"merge"===n){const n=e.slice(1).filter(e=>!e.startsWith("-")),t=n.length>0?n.join(" "):null,l=e.includes("--overwrite")||e.includes("--force");try{await r.pullState(a,t,o,{cwd:process.cwd(),overwrite:l})}catch(e){console.log(chalk.red(`\n[FAIL] Could not sync state from room: ${e.message}\n`))}return}if("undo"!==n&&"rollback"!==n&&"revert"!==n){if("verify"===n||"safety"===n||"safety-number"===n){const o=require("../../core/crypto/e2ee-vault"),n=e[1]?.toLowerCase();if("mark"===n&&e[2]){const n=e[2];return void(o.markPeerVerified(n)?console.log(chalk.green(`\n[OK] Peer "${n}" marked as MANUALLY VERIFIED.`)):console.log(chalk.yellow(`\n[Notice] Peer "${n}" not found in local known_peers.json registry.`)))}const r=o.getRoomKey(a),t=o.computeSafetyNumber(r),l=o._fingerprint(r),c=o.getKnownPeers();console.log(chalk.cyan.bold("\n ThinkNCollab End-to-End Encryption (E2EE) Verification")),console.log(chalk.dim(" "+"═".repeat(60))),console.log(chalk.white(" Room ID: ")+chalk.bold(a)),console.log(chalk.white(" Active Key Fingerprint: ")+chalk.green(l)),console.log(chalk.white("\n 60-DIGIT SAFETY NUMBER (Compare with teammates on call/Slack):")),console.log(chalk.yellow.bold(" ┌────────────────────────────────────────────────────────┐")),console.log(chalk.yellow.bold(` │ ${t} │`)),console.log(chalk.yellow.bold(" └────────────────────────────────────────────────────────┘")),console.log(chalk.dim(" If the digits match on both sides, your channel is guaranteed free")),console.log(chalk.dim(" from any Man-In-The-Middle (MITM) tampering or compromised relays.\n"));const s=Object.entries(c);if(s.length>0){console.log(chalk.cyan(" Known Peers in TOFU (Trust-On-First-Use) Registry:"));const e=new Table({head:[chalk.cyan("USER ID"),chalk.cyan("KEY FINGERPRINT"),chalk.cyan("FIRST SEEN"),chalk.cyan("STATUS")],style:{head:[],border:[]}});s.forEach(([o,n])=>{e.push([o,n.fingerprint||"unknown",n.firstSeen?new Date(n.firstSeen).toLocaleDateString():"recent",n.verified?chalk.green("✓ VERIFIED"):chalk.yellow("● TOFU (UNVERIFIED)")])}),console.log(e.toString()),console.log(chalk.dim("\n To mark a peer verified: thinknsh room verify mark <userId>\n"))}else console.log(chalk.dim(" No remote peer public keys recorded yet in this workspace.\n"));return}if("rotate-key"===n||"rekey"===n){const e=require("../../core/crypto/e2ee-vault");console.log(chalk.cyan(`\n[RoomKey] Rotating 256-bit encryption key for Room [${a}]...`));const n=e.rotateRoomKey(a),r=e.computeSafetyNumber(n),t=e._fingerprint(n);if(o?.ws?.isConnected?.())try{o.ws.socket.emit("thinknsh:room-rekeyed",{roomId:a,fingerprint:t,rotatedAt:(new Date).toISOString()})}catch(e){}return console.log(chalk.green("[OK] Room key rotated successfully (Forward Secrecy Activated)!")),console.log(chalk.dim(" Previous key has been securely moved to archive (~/.tnc/vault/rooms/archive).")),console.log(chalk.dim(" Evicted members can no longer decrypt newly published state bundles or terminal streams.")),console.log(chalk.white(" New Key Fingerprint: ")+chalk.green(t)),void console.log(chalk.white(" New Safety Number: ")+chalk.yellow(r)+"\n")}console.log(chalk.cyan("\n ThinkNCollab Room Commands")),console.log(chalk.dim(" "+"─".repeat(45))),[["room share [name]","Publish project code, database & env state to room"],["room pull [state]","Smart-merge teammate code, database & env into workspace"],["room merge [state]","Smart 3-way merge teammate changes into workspace"],["room undo","Rollback last merge from safety backup"],["room states","List shared state bundles in room"],["room verify","Show E2EE 60-digit safety number & TOFU peers"],["room rotate-key","Rotate room key (Forward Secrecy on eviction)"],["room join <id>","Join a collaborative room"],["room leave","Leave current room"],["room list","List available rooms"]].forEach(([e,o])=>{console.log(` ${chalk.green(e.padEnd(24))} ${chalk.dim(o)}`)}),console.log()}else try{r.rollbackLastMerge(a,process.cwd())}catch(e){console.log(chalk.red(`\n[FAIL] Could not undo merge: ${e.message}\n`))}}};
|
|
@@ -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{DockSandbox}=require("../../dock/sandbox");let engine=null;const getEngine=()=>engine=engine||new DockSandbox,serverOf=e=>(e?.ws?.config?.serverUrl||e?.api?.apiUrl||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com").replace(/\/$/,"");function owner(e){const n=e?.api?.getUser?.()||e?.config?.user||{};return{roomId:e?.ws?.getCurrentRoom?.()||e?.config?.roomId||process.env.THINKNCOLLAB_ROOM_ID||null,roomName:e?.config?.roomName||null,userId:n._id||n.id||e?.api?.userId||e?.ws?.userId||e?.config?.userId||process.env.THINKNCOLLAB_USER_ID||null,userName:n.name||e?.config?.userName||e?.config?.username||process.env.THINKNCOLLAB_USER_NAME||null}}function syncToDashboard(e,n,o){const t=owner(o),r=n=>t[n]||e?.[n]||null,s={roomId:r("roomId"),roomName:r("roomName"),userId:r("userId"),userName:r("userName")},c={event:n,sandbox:{...e,...s},...s,timestamp:(new Date).toISOString()};if(o?.ws?.isConnected?.())try{return o.ws.emitSandboxSync(c.sandbox,n),Promise.resolve()}catch(e){}const i=serverOf(o);return new Promise(e=>{let n=!1;const o=()=>{n||(n=!0,e())};setTimeout(o,1500);try{const e=require("socket.io-client")(`${i}/thinknsh`,{transports:["websocket","polling"],timeout:1200,reconnection:!1});e.on("connect",()=>{e.emit("sandbox:sync",c),setTimeout(()=>{e.disconnect(),o()},60)}),e.on("connect_error",()=>{e.disconnect(),postSync(i,c).then(o)})}catch(e){postSync(i,c).then(o)}})}function postSync(e,n){return new Promise(o=>{try{const t=new URL("/thinknsh/api/sandboxes/sync",e),r=JSON.stringify(n),s=require("https:"===t.protocol?"https":"http").request(t,{method:"POST",timeout:1e3,headers:{"Content-Type":"application/json","Content-Length":Buffer.byteLength(r)}},e=>{e.resume(),o()});s.on("error",o),s.on("timeout",()=>{s.destroy(),o()}),s.end(r)}catch(e){o()}})}module.exports={getEngine,owner,serverOf,syncToDashboard};
|
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
5
|
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
6
|
*/
|
|
7
|
-
const chalk=require("chalk"),{
|
|
7
|
+
const chalk=require("chalk"),{getEngine}=require("./common"),SandboxWindowLauncher=require("../../core/window-launcher");module.exports={name:"console",description:"Launch the dedicated 3rd Terminal Window for a sandbox",aliases:["terminal","sandbox-window"],requiresAuth:!1,async execute(n,e){let o=n[0];if(!o){const n=(await getEngine().list()).filter(n=>"RUNNING"===n.state);if(1!==n.length)return n.length>1?(console.log(chalk.yellow("Multiple sandboxes running. Please specify one:")),void n.forEach(n=>console.log(` • thinknsh console ${n.id}`))):void console.log(chalk.yellow("No running sandbox found. Start one first using: thinknsh dev"));o=n[0].id}console.log(chalk.cyan(`\nLaunching Sandbox Console Window for [${chalk.bold(o)}]...`)),SandboxWindowLauncher.spawn(o)}};
|
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
5
|
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
6
|
*/
|
|
7
|
-
const chalk=require("chalk"),Table=require("cli-table3"),path=require("path"),{execSync}=require("child_process"),{getDepStore}=require("../../core/dep-store"),DependencyTracker=require("../../core/dependency-tracker"),MANAGER_INSTALL_CMD={npm:e=>`npm install ${e}`,pip:e=>`pip3 install ${e}`,cargo:e=>`cargo add ${e}`,go:e=>`go get ${e}`,brew:e=>`brew install ${e}`,apt:e=>`apt-get install -y ${e}`,system:e=>null},MANAGER_REMOVE_CMD={npm:e=>`npm uninstall ${e}`,pip:e=>`pip3 uninstall -y ${e}`,cargo:e=>`cargo remove ${e}`,go:e=>`go mod edit -droprequire ${e}`,brew:e=>`brew uninstall ${e}`,apt:e=>`apt-get remove -y ${e}`,system:e=>null};function detectManager(e){const n=-1!==e.indexOf("--manager")?e.indexOf("--manager"):e.indexOf("-m");return-1!==n&&e[n+1]?e[n+1]:"npm"}module.exports={name:"dep",description:"Multi-language dependency manager (install, remove, list, status, sync, log)",aliases:["deps","dependencies","pkg"],requiresAuth:!1,async execute(e,n){const a=getDepStore(),o=e[0]?.toLowerCase(),l=process.cwd(),c=path.basename(l);if("install"===o||"add"===o||"i"===o){const t=e.find(e=>!e.startsWith("-")&&e!==o);if(!t)return void console.log(chalk.red("[FAIL] Usage: dep install <package>[@version] [--manager npm|pip|cargo|go]"));const s=t.match(/^([^@>=<^~]+)(?:[@>=<^~]+(.+))?$/),r=s?s[1]:t,i=s&&s[2]||null,d=detectManager(e),g=n.activeSandbox?.id||null,p=MANAGER_INSTALL_CMD[d]?.(t);if(!p)return console.log(chalk.yellow(`[INFO] Package "${r}" registered in dep store for system manager. Install manually.`)),a.recordDep(r,{version:i,manager:d,project:c,sandboxId:g,status:"pending"}),void a.logAction(r,{manager:d,action:"register",sandboxId:g,project:c});console.log(chalk.cyan(`\n⟳ Installing ${chalk.bold(t)} via ${chalk.yellow(d)}...`));try{execSync(p,{stdio:"inherit",cwd:l}),a.recordDep(r,{version:i,manager:d,project:c,sandboxId:g,status:"installed"}),a.logAction(r,{manager:d,action:"install",sandboxId:g,project:c});try{const e=new DependencyTracker(l);await e.scanAndRecord(n)}catch(e){}console.log(chalk.green(`\n[OK] ${r} installed & recorded in dep store (${chalk.dim("~/.tnc/deps.db")})\n`))}catch(e){a.recordDep(r,{version:i,manager:d,project:c,sandboxId:g,status:"failed"}),a.logAction(r,{manager:d,action:"fail",sandboxId:g,project:c,error:e.message}),console.log(chalk.red(`[FAIL] Install failed: ${e.message}`))}return}if("remove"===o||"rm"===o||"uninstall"===o){const t=e.find(e=>!e.startsWith("-")&&e!==o);if(!t)return void console.log(chalk.red("[FAIL] Usage: dep remove <package> [--manager npm|pip|cargo|go]"));const s=detectManager(e),r=n.activeSandbox?.id||null,i=MANAGER_REMOVE_CMD[s]?.(t);if(i){console.log(chalk.cyan(`\n⟳ Removing ${chalk.bold(t)} via ${chalk.yellow(s)}...`));try{execSync(i,{stdio:"inherit",cwd:l})}catch(e){console.log(chalk.yellow(`[WARN] Remove command had issues: ${e.message.split("\n")[0]}`))}}a.removeDep(t,{manager:s,project:c}),a.logAction(t,{manager:s,action:"remove",sandboxId:r,project:c});try{const e=new DependencyTracker(l);await e.scanAndRecord(n)}catch(e){}return void console.log(chalk.green(`\n[OK] ${t} removed from dep store.\n`))}if("list"===o||"ls"===o){const n=e.includes("--manager")?e[e.indexOf("--manager")+1]:null,o=e.includes("--sandbox")?e[e.indexOf("--sandbox")+1]:null,l=e.includes("--status")?e[e.indexOf("--status")+1]:null,c=e.includes("--project")?e[e.indexOf("--project")+1]:null,t=a.listDeps({manager:n,project:c,sandboxId:o,status:l});if(0===t.length)return void console.log(chalk.yellow('\n No recorded dependencies found. Run "dep install <pkg>" or "dep sync" to populate.\n'));const s=new Table({head:[chalk.cyan("Package"),chalk.cyan("Version"),chalk.cyan("Manager"),chalk.cyan("Project"),chalk.cyan("Status"),chalk.cyan("Installed At")],colWidths:[28,14,10,18,12,22],style:{head:[],border:[]}});for(const e of t){const n="installed"===e.status?chalk.green:"failed"===e.status?chalk.red:chalk.yellow;s.push([chalk.white(e.name),e.version||chalk.dim("latest"),chalk.yellow(e.manager),e.project||chalk.dim("-"),n(e.status||"unknown"),e.installed_at?chalk.dim(e.installed_at.split("T")[0]):chalk.dim("-")])}return console.log(chalk.cyan("\n TNC Dependency Registry — ~/.tnc/deps.db")),console.log(` Total: ${chalk.bold(t.length)} packages\n`),console.log(s.toString()),void console.log("")}if("status"===o){const e=a.summary({project:path.basename(l)}),n=a.summary();if(console.log(chalk.cyan("\n TNC Dependency Store Status")),console.log(chalk.dim(" DB: ~/.tnc/deps.db\n")),console.log(chalk.yellow(` Current Project: ${chalk.white(c)}`)),0===Object.keys(e).length)console.log(chalk.dim(" No dependencies recorded for this project."));else for(const[n,a]of Object.entries(e))console.log(` ${chalk.green("●")} ${chalk.yellow(n.padEnd(10))} ${chalk.white(a)} packages`);console.log(chalk.yellow("\n Global Store:"));for(const[e,a]of Object.entries(n))console.log(` ${chalk.dim("○")} ${chalk.dim(e.padEnd(10))} ${chalk.dim(a)} packages`);const o=a.getLog({limit:3});if(o.length>0){console.log(chalk.yellow("\n Recent Activity:"));for(const e of o){const n="install"===e.action?chalk.green("↑ install"):"remove"===e.action?chalk.red("↓ remove"):chalk.yellow(` ${e.action}`);console.log(` ${n} ${chalk.white(e.package_name)} [${chalk.dim(e.manager)}] ${chalk.dim(e.timestamp?.split("T")[0]||"")}`)}}return void console.log("")}if("log"===o){const n=e.indexOf("--limit"),o=-1!==n&&parseInt(e[n+1])||20,l=a.getLog({limit:o});if(0===l.length)return void console.log(chalk.yellow("\n No install history found.\n"));const c=new Table({head:[chalk.cyan("Action"),chalk.cyan("Package"),chalk.cyan("Manager"),chalk.cyan("Project"),chalk.cyan("Timestamp")],colWidths:[12,28,10,18,22],style:{head:[],border:[]}});for(const e of l){const n="install"===e.action?chalk.green(e.action):"remove"===e.action?chalk.red(e.action):chalk.yellow(e.action);c.push([n,e.package_name,chalk.yellow(e.manager),e.project||"-",chalk.dim(e.timestamp||"")])}return console.log(chalk.cyan(`\n TNC Dep Install Log (last ${o}):\n`)),console.log(c.toString()),void console.log("")}if("sync"!==o){if("purge"===o){const n=e[1]||c;return a.purgeProject(n),void console.log(chalk.green(`[OK] All dep records for project "${n}" purged from store.\n`))}console.log(`\n${chalk.cyan("TNC Dependency Manager
|
|
7
|
+
const chalk=require("chalk"),Table=require("cli-table3"),path=require("path"),{execSync}=require("child_process"),{getDepStore}=require("../../core/dep-store"),DependencyTracker=require("../../core/dependency-tracker"),MANAGER_INSTALL_CMD={npm:e=>`npm install ${e}`,pip:e=>`pip3 install ${e}`,cargo:e=>`cargo add ${e}`,go:e=>`go get ${e}`,brew:e=>`brew install ${e}`,apt:e=>`apt-get install -y ${e}`,system:e=>null},MANAGER_REMOVE_CMD={npm:e=>`npm uninstall ${e}`,pip:e=>`pip3 uninstall -y ${e}`,cargo:e=>`cargo remove ${e}`,go:e=>`go mod edit -droprequire ${e}`,brew:e=>`brew uninstall ${e}`,apt:e=>`apt-get remove -y ${e}`,system:e=>null};function detectManager(e){const n=-1!==e.indexOf("--manager")?e.indexOf("--manager"):e.indexOf("-m");return-1!==n&&e[n+1]?e[n+1]:"npm"}module.exports={name:"dep",description:"Multi-language dependency manager (install, remove, list, status, sync, log)",aliases:["deps","dependencies","pkg"],requiresAuth:!1,async execute(e,n){const a=getDepStore(),o=e[0]?.toLowerCase(),l=process.cwd(),c=path.basename(l);if("install"===o||"add"===o||"i"===o){const t=e.find(e=>!e.startsWith("-")&&e!==o);if(!t)return void console.log(chalk.red("[FAIL] Usage: dep install <package>[@version] [--manager npm|pip|cargo|go]"));const s=t.match(/^([^@>=<^~]+)(?:[@>=<^~]+(.+))?$/),r=s?s[1]:t,i=s&&s[2]||null,d=detectManager(e),g=n.activeSandbox?.id||null,p=MANAGER_INSTALL_CMD[d]?.(t);if(!p)return console.log(chalk.yellow(`[INFO] Package "${r}" registered in dep store for system manager. Install manually.`)),a.recordDep(r,{version:i,manager:d,project:c,sandboxId:g,status:"pending"}),void a.logAction(r,{manager:d,action:"register",sandboxId:g,project:c});console.log(chalk.cyan(`\n⟳ Installing ${chalk.bold(t)} via ${chalk.yellow(d)}...`));try{execSync(p,{stdio:"inherit",cwd:l}),a.recordDep(r,{version:i,manager:d,project:c,sandboxId:g,status:"installed"}),a.logAction(r,{manager:d,action:"install",sandboxId:g,project:c});try{const e=new DependencyTracker(l);await e.scanAndRecord(n)}catch(e){}console.log(chalk.green(`\n[OK] ${r} installed & recorded in dep store (${chalk.dim("~/.tnc/deps.db")})\n`))}catch(e){a.recordDep(r,{version:i,manager:d,project:c,sandboxId:g,status:"failed"}),a.logAction(r,{manager:d,action:"fail",sandboxId:g,project:c,error:e.message}),console.log(chalk.red(`[FAIL] Install failed: ${e.message}`))}return}if("remove"===o||"rm"===o||"uninstall"===o){const t=e.find(e=>!e.startsWith("-")&&e!==o);if(!t)return void console.log(chalk.red("[FAIL] Usage: dep remove <package> [--manager npm|pip|cargo|go]"));const s=detectManager(e),r=n.activeSandbox?.id||null,i=MANAGER_REMOVE_CMD[s]?.(t);if(i){console.log(chalk.cyan(`\n⟳ Removing ${chalk.bold(t)} via ${chalk.yellow(s)}...`));try{execSync(i,{stdio:"inherit",cwd:l})}catch(e){console.log(chalk.yellow(`[WARN] Remove command had issues: ${e.message.split("\n")[0]}`))}}a.removeDep(t,{manager:s,project:c}),a.logAction(t,{manager:s,action:"remove",sandboxId:r,project:c});try{const e=new DependencyTracker(l);await e.scanAndRecord(n)}catch(e){}return void console.log(chalk.green(`\n[OK] ${t} removed from dep store.\n`))}if("list"===o||"ls"===o){const n=e.includes("--manager")?e[e.indexOf("--manager")+1]:null,o=e.includes("--sandbox")?e[e.indexOf("--sandbox")+1]:null,l=e.includes("--status")?e[e.indexOf("--status")+1]:null,c=e.includes("--project")?e[e.indexOf("--project")+1]:null,t=a.listDeps({manager:n,project:c,sandboxId:o,status:l});if(0===t.length)return void console.log(chalk.yellow('\n No recorded dependencies found. Run "dep install <pkg>" or "dep sync" to populate.\n'));const s=new Table({head:[chalk.cyan("Package"),chalk.cyan("Version"),chalk.cyan("Manager"),chalk.cyan("Project"),chalk.cyan("Status"),chalk.cyan("Installed At")],colWidths:[28,14,10,18,12,22],style:{head:[],border:[]}});for(const e of t){const n="installed"===e.status?chalk.green:"failed"===e.status?chalk.red:chalk.yellow;s.push([chalk.white(e.name),e.version||chalk.dim("latest"),chalk.yellow(e.manager),e.project||chalk.dim("-"),n(e.status||"unknown"),e.installed_at?chalk.dim(e.installed_at.split("T")[0]):chalk.dim("-")])}return console.log(chalk.cyan("\n TNC Dependency Registry — ~/.tnc/deps.db")),console.log(` Total: ${chalk.bold(t.length)} packages\n`),console.log(s.toString()),void console.log("")}if("status"===o){const e=a.summary({project:path.basename(l)}),n=a.summary();if(console.log(chalk.cyan("\n TNC Dependency Store Status")),console.log(chalk.dim(" DB: ~/.tnc/deps.db\n")),console.log(chalk.yellow(` Current Project: ${chalk.white(c)}`)),0===Object.keys(e).length)console.log(chalk.dim(" No dependencies recorded for this project."));else for(const[n,a]of Object.entries(e))console.log(` ${chalk.green("●")} ${chalk.yellow(n.padEnd(10))} ${chalk.white(a)} packages`);console.log(chalk.yellow("\n Global Store:"));for(const[e,a]of Object.entries(n))console.log(` ${chalk.dim("○")} ${chalk.dim(e.padEnd(10))} ${chalk.dim(a)} packages`);const o=a.getLog({limit:3});if(o.length>0){console.log(chalk.yellow("\n Recent Activity:"));for(const e of o){const n="install"===e.action?chalk.green("↑ install"):"remove"===e.action?chalk.red("↓ remove"):chalk.yellow(` ${e.action}`);console.log(` ${n} ${chalk.white(e.package_name)} [${chalk.dim(e.manager)}] ${chalk.dim(e.timestamp?.split("T")[0]||"")}`)}}return void console.log("")}if("log"===o){const n=e.indexOf("--limit"),o=-1!==n&&parseInt(e[n+1])||20,l=a.getLog({limit:o});if(0===l.length)return void console.log(chalk.yellow("\n No install history found.\n"));const c=new Table({head:[chalk.cyan("Action"),chalk.cyan("Package"),chalk.cyan("Manager"),chalk.cyan("Project"),chalk.cyan("Timestamp")],colWidths:[12,28,10,18,22],style:{head:[],border:[]}});for(const e of l){const n="install"===e.action?chalk.green(e.action):"remove"===e.action?chalk.red(e.action):chalk.yellow(e.action);c.push([n,e.package_name,chalk.yellow(e.manager),e.project||"-",chalk.dim(e.timestamp||"")])}return console.log(chalk.cyan(`\n TNC Dep Install Log (last ${o}):\n`)),console.log(c.toString()),void console.log("")}if("sync"!==o){if("purge"===o){const n=e[1]||c;return a.purgeProject(n),void console.log(chalk.green(`[OK] All dep records for project "${n}" purged from store.\n`))}console.log(`\n${chalk.cyan("TNC Dependency Manager: container-style package control")}\n\n${chalk.yellow("Usage:")}\n dep install <pkg>[@version] [--manager npm|pip|cargo|go] Install & record\n dep remove <pkg> [--manager npm|pip|cargo|go] Uninstall & mark removed\n dep list [--manager X] [--sandbox X] [--status X] List dep registry\n dep status Show dep store summary\n dep log [--limit 20] Install history\n dep sync Sync from thinkncollab.yml\n dep purge <project> Wipe project records\n\n${chalk.yellow("Examples:")}\n dep install express@4.18.2\n dep install --manager pip numpy\n dep list --manager npm\n dep list --sandbox my-sandbox\n dep status\n dep sync\n\n${chalk.dim("Registry: ~/.tnc/deps.db (SQLite) or ~/.tnc/deps.json (fallback)")}\n`)}else{console.log(chalk.cyan("\n⟳ Syncing thinkncollab.yml → dep store..."));try{const e=new DependencyTracker(l).loadConfig();getDepStore().recordFromConfig(e,{sandboxId:n.activeSandbox?.id||null}),console.log(chalk.green("[OK] Dep store synchronized from thinkncollab.yml\n")),console.log(chalk.dim(' Run "dep list" to view recorded packages.\n'))}catch(e){console.log(chalk.red(`[FAIL] Sync error: ${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"),path=require("path"),fs=require("fs-extra"),
|
|
7
|
+
const chalk=require("chalk"),path=require("path"),fs=require("fs-extra"),net=require("net"),{SandboxTunnel}=require("../../core/tunnel"),{getEngine,owner,serverOf,syncToDashboard}=require("./common");function isPortInUse(e){return new Promise(n=>{const o=net.createServer();o.once("error",()=>n(!0)),o.once("listening",()=>o.close(()=>n(!1))),o.listen(e,"127.0.0.1")})}async function findFreePort(e=5e3){for(;e<65535;e++)if(!await isPortInUse(e))return e;return 5e3}function detect(e){const n=path.join(e,"package.json");if(fs.existsSync(n))try{const e=fs.readJsonSync(n);return{cmd:(e.scripts?.build?"npm run build && ":"")+(e.scripts?.start?"npm start":e.scripts?.dev?"npm run dev":"node server.js"),port:parseInt(e.config?.port,10)||3e3}}catch(e){}return fs.existsSync(path.join(e,"app.py"))?{cmd:"python app.py",port:5e3}:fs.existsSync(path.join(e,"main.py"))?{cmd:"python main.py",port:8e3}:fs.existsSync(path.join(e,"go.mod"))?{cmd:"go run .",port:8080}:{cmd:"npm start",port:3e3}}module.exports={name:"deploy",description:"Run the project as a production sandbox here (servers: thinknsh cluster deploy)",aliases:["prod-deploy","release"],requiresAuth:!1,async execute(e,n){const o=process.cwd(),t=e[0]&&!e[0].startsWith("-")?e[0]:"production",r=`prod-${path.basename(o).toLowerCase().replace(/[^a-z0-9_-]/g,"-").replace(/^-+|-+$/g,"")||"app"}`,s=(...n)=>{const o=e.findIndex(e=>n.includes(e));return-1!==o?e[o+1]:null},c=detect(o),a=parseInt(s("-p","--port"),10)||c.port,i=await isPortInUse(a)?await findFreePort(5e3):a;console.log(chalk.cyan(`\n[Deploy] ${chalk.bold(r)} (${t})`)),console.log(chalk.dim(` Source : ${o}`)),console.log(chalk.dim(` Command : ${c.cmd}`));const l=await getEngine().start(r,{cmd:c.cmd,cwd:o,hostPort:i,containerPort:a,memoryLimit:s("--mem","--memory","-m")||"2g",production:!0,restart:"unless-stopped",env:{TNC_ENV:t},...owner(n)});if(!(l.success||l.meta&&/already running/.test(l.message)))return void console.log(chalk.red(`\n[Error] Deployment failed: ${l.message}\n`));const p={...l.meta,environment:t,deployedAt:(new Date).toISOString()},d=serverOf(n);console.log(chalk.green(`\n[OK] ${t} is live`)),console.log(` Local : ${chalk.bold.cyan(`http://localhost:${p.hostPort}`)}`),console.log(` Public Gateway : ${chalk.cyan(`${d}/thinknsh/proxy/${encodeURIComponent(r)}/`)}`),console.log(` Dashboard : ${chalk.underline(`${d}/thinknsh/sandboxes?env=production${p.roomId?"&roomId="+p.roomId:""}`)}`),console.log(chalk.dim(` Container ${p.container}; restarts with dock. Logs: thinknsh sandbox logs -f ${r}`)),console.log(chalk.dim(" On your own servers: thinknsh cluster deploy\n")),await syncToDashboard(p,"deployed",n);const h=new SandboxTunnel;h.connect(r,p.hostPort,n),console.log(chalk.green(" Tunnel : ACTIVE (Ctrl-C stops the tunnel; the app keeps running)\n")),process.on("SIGINT",()=>{h.stop(),process.exit(0)}),process.on("SIGTERM",()=>{h.stop(),process.exit(0)})}};
|