thinkncollab-shell 2.0.0-beta.5 → 2.0.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +34 -17
- package/dist/src/commands/dock/cluster.js +1 -1
- package/dist/src/commands/dock/containers.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/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/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
|
@@ -38,7 +38,7 @@ thinknsh tag myapp ghcr.io/me/myapp:1.0 && thinknsh push ghcr.io/me/myapp:1.0
|
|
|
38
38
|
thinknsh logout ghcr.io
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
Registry logins are checked with the registry, then saved per registry host in `~/.tnc/dock/auth.json` (mode 0600, read only by the dock daemon, never shown by any command). `thinknsh login` with an email, or with no arguments, still signs you in to ThinkNCollab; with `-u`, `--password-stdin` or a registry host it logs in to a container registry. `thinknsh push ecr://…` and `push --export`
|
|
41
|
+
Registry logins are checked with the registry, then saved per registry host in `~/.tnc/dock/auth.json` (mode 0600, read only by the dock daemon, never shown by any command). `thinknsh login` with an email, or with no arguments, still signs you in to ThinkNCollab; with `-u`, `--password-stdin` or a registry host it logs in to a container registry. `thinknsh push ecr://…` (also `gar://`, `gcr://`, `ghcr://`) gets the registry and a short-lived login from that cloud's CLI and pushes through dock; `push --export IMAGE` writes it to an archive (`thinknsh save`). Registries on `localhost`/`127.0.0.1` use plain HTTP. A registry for the whole LAN (`thinknsh registry start --lan`) is TLS with its own certificate: other machines pin it with `thinknsh registry trust HOST:PORT --fingerprint …` and then pull and push over verified TLS. For another plain-HTTP registry, list its host:port in `TNC_DOCK_INSECURE_REGISTRIES`.
|
|
42
42
|
|
|
43
43
|
Every command has its own help, for example `thinknsh run --help`. The full list:
|
|
44
44
|
|
|
@@ -47,9 +47,11 @@ Every command has its own help, for example `thinknsh run --help`. The full list
|
|
|
47
47
|
| Containers | `run`, `create`, `start`, `stop`, `restart`, `kill`, `rm`, `ps`, `logs`, `exec`, `wait`, `inspect` |
|
|
48
48
|
| Images | `images`, `pull`, `push`, `build` (`--platform a,b --push`), `tag`, `rmi`, `image prune`, `search`, `manifest create / inspect / push / rm` |
|
|
49
49
|
| 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
|
-
|
|
|
50
|
+
| 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) |
|
|
51
|
+
| Sandboxes | `dev`, `sandbox start / freeze / resume / ps / logs / stats / exec / share / destroy`, `freeze`, `resume`, `deploy` — project folders as dock containers |
|
|
52
|
+
| Dev services | `service start postgres\|redis\|mysql\|mongo [-p PORT]`, `service stop / restart / status / logs / rm [-v]` |
|
|
53
|
+
| Local registry | `registry start [--lan] [PORT] / stop / status / push IMAGE / pull IMAGE / trust HOST:PORT` |
|
|
54
|
+
| 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
55
|
| 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
56
|
| Storage and networks | `volume ls / create / rm / inspect / prune`, `network ls / create / rm / connect / disconnect / prune` |
|
|
55
57
|
| Files and state | `cp`, `diff`, `export`, `commit`, `save`, `load`, `import`, `history`, `stats`, `top`, `pause` / `unpause`, `rename`, `update`, `port` |
|
|
@@ -116,7 +118,9 @@ New here? Read the step-by-step guide: [`docs/HOW-TO-USE.md`](docs/HOW-TO-USE.md
|
|
|
116
118
|
| 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
119
|
| 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
120
|
| 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
|
|
121
|
+
| Overlay networks across machines (`network create -d overlay`, VXLAN), names of every machine's containers | ✅ Linux as root |
|
|
122
|
+
| Encrypted overlays (`-o encrypted -o key=…`: IPsec ESP, AES-256-GCM, per-direction keys; plain VXLAN from a peer dropped) | ✅ Linux as root |
|
|
123
|
+
| 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
124
|
| BuildKit: buildx and BuildKit-mode builds through the engine CLI (`--secret`, cache mounts, and SSH-agent forwarding in BuildKit mode) | ✅ |
|
|
121
125
|
| buildx multi-platform: `buildx build --platform linux/amd64,linux/arm64 --push` (one OCI index, pushed with dock's logins), `--load` | ✅ |
|
|
122
126
|
| Compose: build, depends_on with `service_healthy`, healthchecks, profiles, `.env`, `extends`, `secrets`, `configs`, recreate on change | ✅ |
|
|
@@ -148,6 +152,8 @@ New here? Read the step-by-step guide: [`docs/HOW-TO-USE.md`](docs/HOW-TO-USE.md
|
|
|
148
152
|
| Agent workspaces over SSH (key-only, loopback, no capabilities) | ✅ |
|
|
149
153
|
| 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
154
|
| Deploy compose apps to your own servers | ✅ `thinknsh cluster deploy` |
|
|
155
|
+
| 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 |
|
|
156
|
+
| 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
157
|
|
|
152
158
|
### Platforms
|
|
153
159
|
|
|
@@ -229,14 +235,15 @@ A step-by-step guide for users is in [`docs/HOW-TO-USE.md`](docs/HOW-TO-USE.md).
|
|
|
229
235
|
|
|
230
236
|
### 1. One container engine for everything
|
|
231
237
|
* **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.
|
|
238
|
+
* **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
239
|
* **Bind mounts from your folders:** `-v ./src:/app` shares files both ways (VirtioFS on macOS, `/mnt/c` on Windows).
|
|
234
240
|
* **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.
|
|
241
|
+
* **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.
|
|
242
|
+
* **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
243
|
|
|
237
244
|
### 2. Multi-Layer Collaborative State Sync (`room share` / `room pull`)
|
|
238
245
|
* Share entire development environments between teammates in seconds without third-party registries.
|
|
239
|
-
* **Encrypted Multi-Layer Bundle:** Packages workspace code,
|
|
246
|
+
* **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
247
|
* **Hybrid Transport:** Direct Gigabit Wi-Fi LAN P2P mesh (zero cloud bandwidth) with automatic fallback to ephemeral in-memory cloud socket relays.
|
|
241
248
|
|
|
242
249
|
### 3. Smart 3-Way Code & Secret Merge Engine
|
|
@@ -248,7 +255,8 @@ A step-by-step guide for users is in [`docs/HOW-TO-USE.md`](docs/HOW-TO-USE.md).
|
|
|
248
255
|
### 4. Low-Bandwidth Terminal Multiplexing
|
|
249
256
|
* **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
257
|
* **Sub-Millisecond Dynamic RBAC:** Grant and revoke peer keyboard input access in real-time.
|
|
251
|
-
* **
|
|
258
|
+
* **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.
|
|
259
|
+
* **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
260
|
* **Emergency Host Lock:** Instantly drop all remote input with one command.
|
|
253
261
|
|
|
254
262
|
### 5. Signal-Grade E2EE Security
|
|
@@ -264,7 +272,7 @@ ThinkNCollab terminal pair programming operates on semantic text streams rather
|
|
|
264
272
|
|
|
265
273
|
* **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
274
|
* **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
|
|
275
|
+
* **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
276
|
|
|
269
277
|
---
|
|
270
278
|
|
|
@@ -313,7 +321,7 @@ thinknsh compose up -d
|
|
|
313
321
|
|
|
314
322
|
### Room State & Database Sync
|
|
315
323
|
```bash
|
|
316
|
-
thinknsh room share [name] # Publish code
|
|
324
|
+
thinknsh room share [name] # Publish code + the dock services' data (Postgres, MySQL, Mongo, Redis)
|
|
317
325
|
thinknsh room share --no-code # Publish database & environment only
|
|
318
326
|
thinknsh room pull [state-id] # Smart-merge teammate state into workspace
|
|
319
327
|
thinknsh room merge [state-id] # Explicit 3-way merge
|
|
@@ -323,18 +331,27 @@ thinknsh room verify # Display 60-digit E2EE safety number & TOFU
|
|
|
323
331
|
thinknsh room rotate-key # Rekey room for forward secrecy
|
|
324
332
|
```
|
|
325
333
|
|
|
334
|
+
### Open a teammate's workspace in your own VS Code
|
|
335
|
+
```bash
|
|
336
|
+
thinknsh workspace share main # owner: prints a join code, asks before letting anyone in
|
|
337
|
+
thinknsh workspace join CODE # teammate: adds host tnc-join-… and opens VS Code on it
|
|
338
|
+
```
|
|
339
|
+
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.
|
|
340
|
+
|
|
326
341
|
### Real-Time Terminal Pair Programming
|
|
327
342
|
```bash
|
|
328
343
|
thinknsh share # Start real-time PTY terminal sharing session
|
|
329
344
|
thinknsh share --readonly # Start in read-only broadcast mode
|
|
330
345
|
```
|
|
331
346
|
|
|
332
|
-
###
|
|
347
|
+
### Sandboxes (dock containers)
|
|
333
348
|
```bash
|
|
334
|
-
thinknsh dev --port 3000 #
|
|
335
|
-
thinknsh ps
|
|
336
|
-
thinknsh freeze <name> #
|
|
337
|
-
thinknsh resume <name> #
|
|
349
|
+
thinknsh dev --port 3000 # Detect the project and run it in container tnc-sb-<folder>
|
|
350
|
+
thinknsh sandbox ps # Sandboxes, ports, images
|
|
351
|
+
thinknsh freeze <name> # Stop it: 0% CPU/RAM, files and dependencies kept
|
|
352
|
+
thinknsh resume <name> # Start it again, nothing to reinstall
|
|
353
|
+
thinknsh sandbox exec <name> # Shell inside (or: use <name> in the thinknsh shell)
|
|
354
|
+
thinknsh deploy # Production sandbox here (servers: thinknsh cluster deploy)
|
|
338
355
|
thinknsh snapshot create <name> # Create sub-second CoW volume snapshot
|
|
339
356
|
thinknsh snapshot restore <name> # Restore volume state
|
|
340
357
|
```
|
|
@@ -351,4 +368,4 @@ thinknsh dep sync # Reconcile environment from thinkncollab.yml
|
|
|
351
368
|
## 📄 License
|
|
352
369
|
|
|
353
370
|
Apache License 2.0. See [LICENSE](LICENSE) for details.
|
|
354
|
-
Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
|
|
371
|
+
Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
|
|
@@ -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"),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)})}};
|
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
5
|
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
6
|
*/
|
|
7
|
-
const chalk=require("chalk"),path=require("path"),fs=require("fs-extra"),net=require("net"),{
|
|
7
|
+
const chalk=require("chalk"),path=require("path"),fs=require("fs-extra"),net=require("net"),{SandboxTunnel}=require("../../core/tunnel"),{getEngine,owner,serverOf,syncToDashboard}=require("./common");function isPortInUse(e){return new Promise(o=>{const n=net.createServer();n.once("error",e=>{"EADDRINUSE"===e.code?o(!0):o(!1)}),n.once("listening",()=>{n.close(),o(!1)}),n.listen(e)})}async function findFreePort(e=4e3){let o=e;for(;o<65535;){if(!await isPortInUse(o))return o;o++}return 4e3}function detectProjectConfig(e){const o=path.join(e,"package.json");if(fs.existsSync(o))try{const n=fs.readJsonSync(o);let t="node server.js",s=3e3;return n.scripts?.dev?t="npm run dev":n.scripts?.start?t="npm start":fs.existsSync(path.join(e,"server.js"))?t="node server.js":fs.existsSync(path.join(e,"app.js"))?t="node app.js":fs.existsSync(path.join(e,"index.js"))&&(t="node index.js"),n.config?.port&&(s=parseInt(n.config.port,10)),{type:"Node.js",cmd:t,containerPort:s}}catch(e){}return fs.existsSync(path.join(e,"CMakeLists.txt"))?{type:"C++",cmd:"cmake -B build && cmake --build build && (./build/app || ./build/main || ./build/server || true)",containerPort:8080}:fs.existsSync(path.join(e,"Makefile"))?{type:"C++",cmd:"make && (./app || ./main || ./server || true)",containerPort:8080}:fs.existsSync(path.join(e,"main.cpp"))?{type:"C++",cmd:"g++ -std=c++17 main.cpp -o app && ./app",containerPort:8080}:fs.existsSync(path.join(e,"app.py"))?{type:"Python",cmd:"python3 app.py",containerPort:5e3}:fs.existsSync(path.join(e,"main.py"))?{type:"Python",cmd:"python3 main.py",containerPort:8e3}:fs.existsSync(path.join(e,"main.go"))||fs.existsSync(path.join(e,"go.mod"))?{type:"Go",cmd:"go run .",containerPort:8080}:fs.existsSync(path.join(e,"Cargo.toml"))?{type:"Rust",cmd:"cargo run",containerPort:8080}:{type:"Generic",cmd:"node server.js",containerPort:3e3}}module.exports={name:"dev",description:"Zero-config smart auto-detection & sandbox launcher for current directory",aliases:["up","run-app"],requiresAuth:!1,async execute(e,o){const n=process.cwd(),t=path.basename(n).toLowerCase().replace(/[^a-z0-9_-]/g,"-").replace(/^-+|-+$/g,"")||"app";let s=null;const c=[];for(let o=0;o<e.length;o++)"--name"!==e[o]&&"-n"!==e[o]||!e[o+1]?e[o].startsWith("--name=")?s=e[o].slice(7):c.push(e[o]):(s=e[o+1],o++);const r=s||c[0]||t;console.log(chalk.cyan(`\n[Inspect] Project in: ${chalk.bold(n)}`));const a=detectProjectConfig(n);console.log(chalk.dim(` Type Detected : ${chalk.green(a.type)}`)),console.log(chalk.dim(` Launch Command: ${chalk.white(a.cmd)}`));let i=a.containerPort;for(let o=0;o<e.length;o++)if(("-p"===e[o]||"--port"===e[o])&&e[o+1]){const n=e[o+1].split(":");i=n[1]?parseInt(n[1],10):parseInt(n[0],10)}let l=i;await isPortInUse(l)&&(l=await findFreePort(4e3),console.log(chalk.yellow(` [Notice] Port ${i} is busy on host. Auto-routing to Host Port ${chalk.bold(l)}`))),console.log(chalk.dim(` Port Mapping : Host:${chalk.bold(l)} -> Container:${i}`)),console.log(chalk.cyan(`\n[Launch] Auto-Launching TNC Sandbox [${chalk.bold(r)}]...`));const d=e.indexOf("--image"),p=await getEngine().start(r,{cmd:a.cmd,cwd:n,hostPort:l,containerPort:i,...-1!==d&&e[d+1]?{image:e[d+1]}:{},...owner(o)});if(p.success||p.meta&&p.message?.includes("already running")){const n=p.meta,t=!p.success;t?console.log(chalk.green(`\n[OK] Sandbox "${r}" is ALREADY UP and running!`)):console.log(chalk.green(`\n[OK] Sandbox "${r}" is UP and running!`));const s=serverOf(o),c=`${s}/thinknsh/proxy/${encodeURIComponent(r)}/`,a=`${s}/thinknsh/preview/${encodeURIComponent(r)}`;console.log(` App URL : ${chalk.bold.cyan(c)}`),console.log(` Preview URL : ${chalk.cyan(a)}`),console.log(` Local URL : ${chalk.bold.cyan(`http://localhost:${n.hostPort}`)}`),console.log(` Web Dashboard : ${chalk.underline(`${s}/thinknsh/sandboxes${n.roomId?"?roomId="+n.roomId:""}`)}`),console.log(` Container : ${n.container} (${n.image})`),n.roomId&&console.log(` Workspace Room : ${n.roomName||n.roomId}`),console.log(chalk.dim("\n Quick Controls:")),console.log(chalk.dim(` Console window : thinknsh console ${r}`)),console.log(chalk.dim(` Stream Logs : thinknsh sandbox logs -f ${r}`)),console.log(chalk.dim(` Shell inside : thinknsh sandbox exec ${r}`)),console.log(chalk.dim(` Pause / Freeze : thinknsh sandbox freeze ${r}`)),console.log(chalk.dim(` Wakeup : thinknsh sandbox resume ${r}`)),console.log(chalk.dim(` Destroy : thinknsh sandbox destroy ${r}\n`)),e.includes("--no-window")||e.includes("--headless")||require("../../core/window-launcher").spawn(r),await syncToDashboard(n,t?"updated":"started",o);const i=new SandboxTunnel;i.connect(r,n.hostPort||l,o),console.log(chalk.green(" Tunnel : ACTIVE")),console.log(chalk.dim(` Remote preview streams via thinkncollab.com/thinknsh/proxy/${encodeURIComponent(r)}/`)),process.on("SIGINT",()=>{i.stop(),process.exit(0)}),process.on("SIGTERM",()=>{i.stop(),process.exit(0)})}else console.log(chalk.red(`\n[Error] Failed to start sandbox: ${p.message}\n`))}};
|
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
5
|
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
6
|
*/
|
|
7
|
-
const chalk=require("chalk"),{
|
|
7
|
+
const chalk=require("chalk"),{getEngine}=require("./common");module.exports={name:"sandbox-exec",description:"Run a command inside a sandbox (thinknsh sandbox exec ID [cmd])",requiresAuth:!1,async execute(e){const n=[];let o=0;for(;o<e.length;o++){const s=e[o];if("-it"!==s&&"-ti"!==s&&"-i"!==s&&"-t"!==s){if("-e"!==s&&"-w"!==s&&"-u"!==s||!e[o+1])break;n.push(s,e[++o])}}const s=e[o];if(!s)return void console.log(chalk.yellow("Usage: thinknsh sandbox exec [-e KEY=VAL] [-w DIR] <sandbox-id> [command...]"));const t=getEngine(),i=await t.loadMeta(s);if(!i||"RUNNING"!==i.state)return void console.log(chalk.red(`[FAIL] Sandbox "${s}" is not running (${i?i.state:"not found"}). Start it: thinknsh sandbox resume ${s}`));const a=e.slice(o+1),c=process.stdin.isTTY&&process.stdout.isTTY,{runCommand:d}=require("../../dock/cli/commands");process.exitCode=await d("exec",[c?"-it":"-i",...n,t.name(s),...a.length?a:["sh"]])}};
|
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
5
|
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
6
|
*/
|
|
7
|
-
const chalk=require("chalk"),
|
|
7
|
+
const chalk=require("chalk"),{execFileSync}=require("child_process"),sh=(e,s)=>execFileSync(e,s,{encoding:"utf8",stdio:["ignore","pipe","pipe"]}).trim();function cloudCli(e,s,t){try{return sh(e,s)}catch(s){const o="ENOENT"===s.code?`${e} is not installed`:(s.stderr||s.message).trim().split("\n").pop();throw new Error(`${o}\n Setup: ${t}`)}}function resolveTarget(e,s){const t=/^([a-z]+):\/\/(.+)$/.exec(e);if(!t)throw new Error(`"${e}" is not a scheme://name target (ecr://, gar://, gcr://, ghcr://)`);const[,o,r]=t,n=r.lastIndexOf(":")>r.lastIndexOf("/")?r.lastIndexOf(":"):-1,a=-1===n?r:r.slice(0,n),c=-1===n?"latest":r.slice(n+1);switch(o){case"ecr":{const e=process.env.AWS_REGION||process.env.AWS_DEFAULT_REGION||cloudCli("aws",["configure","get","region"],"aws configure");return{host:`${cloudCli("aws",["sts","get-caller-identity","--query","Account","--output","text"],"aws configure")}.dkr.ecr.${e}.amazonaws.com`,path:a,tag:c,username:"AWS",password:cloudCli("aws",["ecr","get-login-password","--region",e],"aws configure")}}case"gar":case"gcr":{const[e,...s]=a.split("/");return{host:e.includes(".")?e:"gcr.io",path:e.includes(".")?s.join("/"):a,tag:c,username:"oauth2accesstoken",password:cloudCli("gcloud",["auth","print-access-token"],"gcloud auth login")}}case"ghcr":{const e=s.token||process.env.GITHUB_TOKEN||null;return{host:"ghcr.io",path:a,tag:c,username:e?s.username||process.env.GITHUB_ACTOR||"token":null,password:e}}default:throw new Error(`unknown registry scheme "${o}://" (ecr, gar, gcr, ghcr)`)}}module.exports={name:"push",aliases:["image-push","export"],description:"Push an image to AWS ECR, Google Artifact Registry or GHCR by scheme (ecr:// gar:// ghcr://)",usage:"thinknsh push [LOCAL] <scheme://name:tag> | push --export IMAGE [out.tar]",examples:["thinknsh push ecr://my-app:latest","thinknsh push gar://us-docker.pkg.dev/my-project/apps/my-app:v1","thinknsh push ghcr://username/my-app:latest","thinknsh push --export postgres:16 ./postgres.tar"],async execute(e=[],s){const{runCommand:t}=require("../../dock/cli/commands"),{DockApi:o}=require("../../dock/cli/api"),r={},n=[];for(let s=0;s<e.length;s++){const t=e[s];"--export"===t||"export"===t?r.export=!0:/^--(username|token|output)$/.test(t)||"-o"===t?r[t.replace(/^-+/,"").replace(/^o$/,"output")]=e[++s]:n.push(t)}if(r.export){const e=n[0];if(!e)return void console.log(chalk.red("Usage: thinknsh push --export <image>:<tag> [output.tar]"));const s=r.output||n[1]||`${e.replace(/[/:@]+/g,"-")}.tar`,o=await t("save",["-o",s,e]);return 0===o&&(console.log(chalk.green(`[OK] ${e} → ${s}`)),console.log(chalk.dim(` Load it on another machine: thinknsh load -i ${s}`))),void(process.exitCode=o)}const a=n[n.length-1];if(a)try{const e=resolveTarget(a,r),s=n.length>=2?n[0]:`${e.path.split("/").pop()}:${e.tag}`,c=`${e.host}/${e.path}:${e.tag}`,i=new o;await i.ensureRunning({quiet:!0});try{await i.json("GET",`/images/${encodeURIComponent(s)}/json`)}catch(e){throw new Error(`no local image "${s}"; build it (thinknsh build -t ${s} .) or name it: thinknsh push IMAGE ${a}`)}if(e.username&&await i.json("POST","/auth",{username:e.username,password:e.password,serveraddress:e.host}),s!==c&&0!==await t("tag",[s,c]))return void(process.exitCode=1);console.log(chalk.cyan(`Pushing ${s} → ${c}`));const l=await t("push",[c]);0===l&&console.log(chalk.green(`\n[OK] Pushed: ${c}`)),process.exitCode=l}catch(e){console.log(chalk.red(`\n[Error] Push failed: ${e.message}`)),process.exitCode=1}else console.log(chalk.yellow(`Usage: ${module.exports.usage}`))}},module.exports.resolveTarget=resolveTarget;
|
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
5
|
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
6
|
*/
|
|
7
|
-
const chalk=require("chalk"),Table=require("cli-table3"),path=require("path"),fs=require("fs-extra"),{SandboxEngine}=require("../../core/sandbox"),SandboxTunnel=require("../../core/sandbox/tunnel"),CgroupsV2=require("../../core/sandbox/cgroups"),engine=new SandboxEngine,cgroups=new CgroupsV2;function syncToDashboard(e,o,n){if(n?.ws?.isConnected())try{n.ws.emitSandboxSync(e,o)}catch(e){}return new Promise(s=>{let a=!1;const l=()=>{a||(a=!0,s())},t=(n?.ws?.config?.serverUrl||n?.api?.apiUrl||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com").replace(/\/$/,""),r=n?.ws?.getCurrentRoom?.()||n?.config?.roomId||process.env.THINKNCOLLAB_ROOM_ID||e?.roomId||null,c=n?.api?.getUser?.()?._id||n?.api?.getUser?.()?.id||n?.api?.userId||n?.ws?.userId||n?.config?.user?._id||n?.config?.userId||process.env.THINKNCOLLAB_USER_ID||e?.userId||null,i=n?.config?.roomName||e?.roomName||null,d=n?.api?.getUser?.()?.name||n?.config?.user?.name||n?.config?.userName||n?.config?.username||e?.userName||process.env.THINKNCOLLAB_USER_NAME||null,m={event:o,sandbox:{...e,roomId:r||e?.roomId||null,roomName:i||e?.roomName||null,userId:c||e?.userId||null,userName:d||e?.userName||null},roomId:r||e?.roomId||null,roomName:i||e?.roomName||null,userId:c||e?.userId||null,userName:d||e?.userName||null,timestamp:(new Date).toISOString()};try{const e=require("socket.io-client")(t+"/thinknsh",{transports:["websocket","polling"],timeout:1200,reconnection:!1});e.on("connect",()=>{e.emit("sandbox:sync",m),setTimeout(()=>{e.disconnect(),l()},60)}),e.on("connect_error",()=>{e.disconnect(),l()})}catch(e){l()}try{const e=t.startsWith("https"),o=e?require("https"):require("http"),n=new URL(t),s=JSON.stringify(m),a=o.request({hostname:n.hostname,port:n.port||(e?443:80),path:"/thinknsh/api/sandboxes/sync",method:"POST",headers:{"Content-Type":"application/json","Content-Length":Buffer.byteLength(s)},timeout:1e3},()=>l());a.on("error",()=>l()),a.on("timeout",()=>{a.destroy(),l()}),a.write(s),a.end()}catch(e){l()}setTimeout(l,1500)})}module.exports={name:"sandbox",description:"Manage isolated lightweight sandboxes (Start, Freeze, Resume, Destroy)",aliases:["sb"],requiresAuth:!1,async execute(e,o){const n=(e[0]||"help").toLowerCase();switch(n){case"up":case"dev":{const n=require("./dev");return await n.execute(e.slice(1),o)}case"start":{const n=e[1]||`app-${Date.now().toString().slice(-4)}`;let s="node server.js",a=null,l=null,t=null;const r=new Set(["-p","--port","--memory","--mem","--ephemeral","--node","--node-version"]);for(let o=2;o<e.length;o++)if("--cmd"===e[o]){const n=[];for(o++;o<e.length&&!r.has(e[o]);)n.push(e[o]),o++;o--,n.length>0&&(s=n.join(" "))}else if("-p"!==e[o]&&"--port"!==e[o]||!e[o+1])"--node"!==e[o]&&"--node-version"!==e[o]||!e[o+1]||(t=e[++o]);else{const n=e[++o].split(":");a=parseInt(n[0],10),l=n[1]?parseInt(n[1],10):a}console.log(chalk.cyan(`\n[Start] Starting TNC Sandbox [${n}]...`)),console.log(chalk.dim(` Command : ${s}`)),a&&console.log(chalk.dim(` Port : Host:${a} -> Container:${l}`)),t&&console.log(chalk.dim(` Node : ${t}`));const c=o?.ws?.getCurrentRoom?.()||o?.config?.roomId||process.env.THINKNCOLLAB_ROOM_ID||null,i=o?.api?.getUser?.()?._id||o?.api?.getUser?.()?.id||o?.api?.userId||o?.ws?.userId||o?.config?.user?._id||o?.config?.userId||process.env.THINKNCOLLAB_USER_ID||null,d=o?.config?.roomName||null,m=o?.api?.getUser?.()?.name||o?.config?.user?.name||o?.config?.userName||o?.config?.username||process.env.THINKNCOLLAB_USER_NAME||null,g=await engine.start(n,{cmd:s,cwd:process.cwd(),hostPort:a,containerPort:l,nodeVersion:t,roomId:c,roomName:d,userId:i,userName:m});if(g.success){const e={"linux-unshare":chalk.green("linux-unshare (kernel)"),"macos-seatbelt":chalk.magenta("macos-seatbelt (syscall)"),"wsl2-kernel":chalk.blue("wsl2-kernel (linux)"),"host-process":chalk.gray("host-process (basic)")}[g.meta.isolationLevel]||chalk.gray(g.meta.isolationLevel);console.log(chalk.green(`\n[OK] ${g.message}`)),console.log(chalk.dim(` PID : ${g.meta.pid}`)),console.log(chalk.dim(` State : ${chalk.green(g.meta.state)}`)),console.log(` Isolation : ${e}`),g.meta.runtime&&g.meta.runtime.node&&console.log(chalk.dim(` Node.js : v${g.meta.runtime.node}${g.meta.runtime.isHermetic?" (hermetic runtime)":" (host)"}`)),g.meta.roomId&&console.log(` Room : ${g.meta.roomName||g.meta.roomId}`),g.meta.memoryLimitMB&&console.log(chalk.dim(` Mem Limit : ${g.meta.memoryLimitMB} MB`)),console.log(chalk.dim(` To freeze later: thinknsh sandbox freeze ${n}\n`)),await syncToDashboard(g.meta,"started",o),(new SandboxTunnel).connect(n,a||g.meta.hostPort,o)}else console.log(chalk.yellow(`\n[Notice] ${g.message}\n`));break}case"freeze":case"stop":{const n=e[1];if(!n)return void console.log(chalk.red("[Error] Usage: thinknsh sandbox freeze <sandbox-id>"));console.log(chalk.cyan(`\n[Freeze] Freezing Sandbox [${n}]...`));const s=await engine.freeze(n);s.success?(console.log(chalk.green(`[OK] ${s.message}`)),console.log(chalk.dim(" RAM released to 0 MB. All project files and node_modules preserved!")),console.log(chalk.dim(` To wake up: thinknsh sandbox resume ${n}\n`)),await syncToDashboard(s.meta,"frozen",o)):console.log(chalk.red(`[Error] ${s.message}\n`));break}case"resume":case"unfreeze":{const n=e[1];if(!n)return void console.log(chalk.red("[Error] Usage: thinknsh sandbox resume <sandbox-id>"));console.log(chalk.cyan(`\n[Resume] Resuming Sandbox [${n}]...`));const s=await engine.resume(n);s.success?(console.log(chalk.green(`[OK] ${s.message}`)),console.log(chalk.dim(` PID: ${s.meta.pid} | State: ${chalk.green(s.meta.state)}\n`)),await syncToDashboard(s.meta,"resumed",o)):console.log(chalk.red(`[Error] ${s.message}\n`));break}case"ps":case"list":case"ls":{const e=engine.list();if(0===e.length)return console.log(chalk.yellow("\nNo active or frozen sandboxes found.")),void console.log(chalk.dim('Start one with: thinknsh sandbox start <name> --cmd "<command>"\n'));const o=new Table({head:[chalk.cyan("ID"),chalk.cyan("ROOM"),chalk.cyan("STATE"),chalk.cyan("PID"),chalk.cyan("PORTS"),chalk.cyan("ISOLATION"),chalk.cyan("COMMAND"),chalk.cyan("STARTED")]}),n={"linux-unshare":chalk.green("unshare"),"macos-seatbelt":chalk.magenta("seatbelt"),"wsl2-kernel":chalk.blue("wsl2"),"host-process":chalk.gray("host"),"macos-microvm-vz":chalk.cyan("microvm-vz"),"linux-microvm-vz":chalk.cyan("microvm-vz"),"linux-crun-oci":chalk.green("crun-oci")};for(const s of e){const e="RUNNING"===s.state?chalk.green(s.state):"FROZEN"===s.state?chalk.blue(s.state):chalk.gray(s.state),a=s.hostPort?`${s.hostPort}:${s.containerPort||s.hostPort}`:"-",l=n[s.isolationLevel]||chalk.gray(s.isolationLevel||"?"),t=s.roomName||(s.roomId?`Room ${s.roomId.slice(-6)}`:"-");o.push([chalk.bold(s.id),t,e,s.pid||chalk.dim("frozen"),a,l,s.cmd||"-",s.startedAt?new Date(s.startedAt).toLocaleTimeString():"-"])}console.log(chalk.cyan("\nTNC Sandboxes:")),console.log(o.toString()),console.log("");break}case"logs":{const o=e[1];if(!o)return void console.log(chalk.red("[Error] Usage: thinknsh sandbox logs <sandbox-id>"));const n=path.join(engine.baseDir,o,"output.log");if(fs.existsSync(n)){const e=fs.readFileSync(n,"utf8");console.log(chalk.dim(`\n--- Logs for ${o} (last 50 lines) ---`)),console.log(e.split("\n").slice(-50).join("\n"))}else console.log(chalk.yellow(`No log file found for sandbox "${o}".`));break}case"destroy":case"rm":case"prune":{const s=e[1];if("--all"===s||"-a"===s||"prune"===n){const e=engine.list();if(0===e.length)return void console.log(chalk.yellow("\nNo active or frozen sandboxes to destroy.\n"));console.log(chalk.yellow(`\n[Destroy] Destroying all ${e.length} sandboxes...`));for(const n of e)await engine.destroy(n.id),await syncToDashboard({id:n.id,state:"DESTROYED"},"destroyed",o),console.log(chalk.green(`[OK] Destroyed "${n.id}"`));return void console.log(chalk.green("\n[OK] All sandboxes cleaned up successfully.\n"))}if(!s)return void console.log(chalk.red("[Error] Usage: thinknsh sandbox destroy <sandbox-id> (or --all)"));console.log(chalk.yellow(`\n[Destroy] Destroying Sandbox [${s}]...`));const a=await engine.destroy(s);console.log(chalk.green(`[OK] ${a.message}\n`)),await syncToDashboard({id:s,state:"DESTROYED"},"destroyed",o);break}case"stats":case"top":{const o=e[1];if(!o)return void console.log(chalk.red("[Error] Usage: thinknsh sandbox stats <sandbox-id>"));const n=engine.loadMeta(o);if(!n)return void console.log(chalk.red(`[Error] Sandbox "${o}" not found.`));if(console.log(chalk.cyan(`\nStats: ${chalk.bold(o)}`)),console.log(chalk.dim("─────────────────────────────────")),n.pid){const e=(new(require("../../core/sandbox/macos-sandbox"))).getMemoryUsage(n.pid);console.log(` PID : ${chalk.yellow(n.pid)}`),console.log(` RAM (RSS) : ${chalk.yellow(e+" MB")} (live)`)}else console.log(` State : ${chalk.blue("FROZEN")} — RAM = 0 MB`);if("linux"===process.platform&&cgroups.isAvailable()){const e=cgroups.getStats(o);e&&(console.log(` cgroup RAM : ${chalk.green(e.memoryMB+" MB")} / ${e.memoryLimitMB?e.memoryLimitMB+" MB":"unlimited"}`),console.log(` CPU time : ${chalk.green(e.cpuUsageMs+" ms")}`),console.log(` PIDs alive : ${chalk.green(e.pids)} / ${e.pidsLimit||"∞"}`),e.oomKills>0?console.log(` OOM kills : ${chalk.red("[Notice] "+e.oomKills)}`):console.log(` OOM kills : ${chalk.dim("0 (healthy)")}`))}else"linux"!==process.platform&&console.log(chalk.dim(` cgroups v2 : Linux only (you're on ${process.platform})`));console.log(` Isolation : ${chalk.magenta(n.isolationLevel||"?")}`),n.roomId&&console.log(` Room : ${n.roomName||n.roomId}`),console.log(` Started : ${n.startedAt?new Date(n.startedAt).toLocaleString():"-"}`),n.frozenAt&&console.log(` Frozen at : ${chalk.blue(new Date(n.frozenAt).toLocaleString())}`),console.log("");break}case"exec":{const n=require("./exec");return await n.execute(e.slice(1),o)}default:console.log(chalk.cyan("\nThinkNCollab Sandbox Manager")),console.log(chalk.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")),console.log(` ${chalk.green('thinknsh sandbox start <id> [--cmd "..."] [-p host:cont]')}`),console.log(` ${chalk.green("thinknsh sandbox freeze <id>")}`),console.log(` ${chalk.green("thinknsh sandbox resume <id>")}`),console.log(` ${chalk.green("thinknsh sandbox ps")}`),console.log(` ${chalk.green("thinknsh sandbox stats <id>")} ${chalk.dim("<- live memory/CPU/PID stats")}`),console.log(` ${chalk.green("thinknsh sandbox logs <id>")}`),console.log(` ${chalk.green("thinknsh sandbox destroy <id>")}\n`)}}};
|
|
7
|
+
const chalk=require("chalk"),Table=require("cli-table3"),{SandboxTunnel}=require("../../core/tunnel"),{getEngine,owner,syncToDashboard}=require("./common"),dockCommand=(e,o)=>require("../../dock/cli/commands").runCommand(e,o),STATE_COLOR={RUNNING:chalk.green,FROZEN:chalk.blue};function parseStart(e){const o={cmd:null,hostPort:null,containerPort:null,nodeVersion:null,image:null,memoryLimit:null},n=new Set(["-p","--port","--memory","--mem","-m","--node","--node-version","--image"]);for(let s=0;s<e.length;s++){const t=e[s];if("--cmd"===t){const t=[];for(;s+1<e.length&&!n.has(e[s+1]);)t.push(e[++s]);t.length&&(o.cmd=t.join(" "))}else if("-p"!==t&&"--port"!==t||!e[s+1])"--node"!==t&&"--node-version"!==t||!e[s+1]?"--image"===t&&e[s+1]?o.image=e[++s]:"--memory"!==t&&"--mem"!==t&&"-m"!==t||!e[s+1]||(o.memoryLimit=e[++s]):o.nodeVersion=e[++s];else{const[n,t]=e[++s].split(":");o.hostPort=parseInt(n,10),o.containerPort=t?parseInt(t,10):o.hostPort}}return o}async function list(){const e=await getEngine().list();if(!e.length)return console.log(chalk.yellow("\nNo sandboxes yet.")),void console.log(chalk.dim("Start one in a project folder with: thinknsh dev\n"));const o=new Table({head:["ID","ROOM","STATE","PORTS","IMAGE","COMMAND","STARTED"].map(e=>chalk.cyan(e))});for(const n of e)o.push([chalk.bold(n.id),n.roomName||(n.roomId?`Room ${n.roomId.slice(-6)}`:"-"),(STATE_COLOR[n.state]||chalk.gray)(n.state),n.hostPort?`${n.hostPort}:${n.containerPort||n.hostPort}`:"-",n.image,n.cmd||"-",n.startedAt?new Date(n.startedAt).toLocaleTimeString():"-"]);console.log(chalk.cyan("\nTNC Sandboxes:")),console.log(o.toString()),console.log(chalk.dim(" containers tnc-sb-<id>: thinknsh ps, logs, exec and stats work on them too\n"))}module.exports={name:"sandbox",description:"Project folders as dock containers (start, freeze, resume, destroy)",aliases:["sb"],requiresAuth:!1,async execute(e,o){try{return await this.run(e,o)}catch(e){console.log(chalk.red(`[Error] ${e.message}`)),process.exitCode=1}},async run(e,o){const n=(e[0]||"help").toLowerCase(),s=getEngine(),t=e[1],a=e=>(t||console.log(chalk.red(`[Error] Usage: thinknsh sandbox ${e}`)),!t);switch(n){case"up":case"dev":return require("./dev").execute(e.slice(1),o);case"start":{const n=t||`app-${Date.now().toString().slice(-4)}`,a=parseStart(e.slice(2));console.log(chalk.cyan(`\n[Start] Sandbox [${n}]...`));const r=await s.start(n,{...a,cwd:process.cwd(),...owner(o)});return r.success?(console.log(chalk.green(`\n[OK] ${r.message}`)),console.log(chalk.dim(` Container : ${r.meta.container}`)),r.meta.hostPort&&console.log(chalk.dim(` URL : http://localhost:${r.meta.hostPort}`)),r.meta.memoryLimitMB&&console.log(chalk.dim(` Mem Limit : ${r.meta.memoryLimitMB} MB`)),console.log(chalk.dim(` Freeze later: thinknsh sandbox freeze ${n}\n`)),await syncToDashboard(r.meta,"started",o),void(r.meta.hostPort&&(new SandboxTunnel).connect(n,r.meta.hostPort,o))):void console.log(chalk.yellow(`\n[Notice] ${r.message}\n`))}case"freeze":case"stop":{if(a("freeze <sandbox-id>"))return;const e=await s.freeze(t);return e.success?(s.stopTunnelDaemon(t),console.log(chalk.green(`[OK] ${e.message}`)),console.log(chalk.dim(` Wake it up: thinknsh sandbox resume ${t}\n`)),void await syncToDashboard(e.meta,"frozen",o)):void console.log(chalk.red(`[Error] ${e.message}\n`))}case"resume":case"unfreeze":{if(a("resume <sandbox-id>"))return;const e=await s.resume(t);return e.success?(console.log(chalk.green(`[OK] ${e.message}`)),e.meta.hostPort&&console.log(chalk.dim(` http://localhost:${e.meta.hostPort}\n`)),void await syncToDashboard(e.meta,"resumed",o)):void console.log(chalk.red(`[Error] ${e.message}\n`))}case"ps":case"list":case"ls":return list();case"logs":{const o=e.includes("-f")||e.includes("--follow"),n=e.slice(1).find(e=>!e.startsWith("-"));return n?void await dockCommand("logs",[...o?["-f"]:["-n","200"],s.name(n)]):void console.log(chalk.red("[Error] Usage: thinknsh sandbox logs [-f] <sandbox-id>"))}case"stats":case"top":if(a("stats <sandbox-id>"))return;return void await dockCommand("stats",["--no-stream",s.name(t)]);case"destroy":case"rm":case"prune":{const e="--all"===t||"-a"===t;if(e||"prune"===n){const n=(await s.list()).filter(o=>e||"RUNNING"!==o.state);if(!n.length)return void console.log(chalk.yellow("\nNo sandboxes to remove.\n"));for(const e of n)await s.destroy(e.id),s.stopTunnelDaemon(e.id),await syncToDashboard({id:e.id,state:"DESTROYED"},"destroyed",o),console.log(chalk.green(`[OK] Destroyed "${e.id}"`));return}if(a("destroy <sandbox-id> (or --all)"))return;const r=await s.destroy(t);return s.stopTunnelDaemon(t),console.log((r.success?chalk.green:chalk.yellow)(`[${r.success?"OK":"Notice"}] ${r.message}\n`)),void(r.success&&await syncToDashboard({id:t,state:"DESTROYED"},"destroyed",o))}case"exec":return require("./exec").execute(e.slice(1),o);case"share":case"tunnel":return require("./share").execute(e.slice(1),o);default:console.log(chalk.cyan("\nThinkNCollab sandboxes (dock containers)")),console.log(chalk.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")),console.log(` ${chalk.green("thinknsh dev")} ${chalk.dim("detect the project and run it")}`),console.log(` ${chalk.green('thinknsh sandbox start <id> [--cmd "..."] [-p host:cont] [--image IMG]')}`),console.log(` ${chalk.green("thinknsh sandbox freeze <id>")} ${chalk.dim("0 RAM, files and dependencies kept")}`),console.log(` ${chalk.green("thinknsh sandbox resume <id>")}`),console.log(` ${chalk.green("thinknsh sandbox ps")}`),console.log(` ${chalk.green("thinknsh sandbox logs [-f] <id>")}`),console.log(` ${chalk.green("thinknsh sandbox stats <id>")}`),console.log(` ${chalk.green("thinknsh sandbox exec <id> [cmd]")}`),console.log(` ${chalk.green("thinknsh sandbox share <id>")} ${chalk.dim("preview URL for the room")}`),console.log(` ${chalk.green("thinknsh sandbox destroy <id> | --all")}\n`)}}};
|
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
5
|
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
6
|
*/
|
|
7
|
-
const chalk=require("chalk"),{
|
|
7
|
+
const chalk=require("chalk"),{getEngine,serverOf}=require("./common");module.exports={name:"tunnel",description:"Generate instant encrypted room preview URL & WebSocket tunnel for teammates",aliases:["preview"],requiresAuth:!1,async execute(e,n){const o=getEngine(),t=await o.list(),r=e[0]||(t.find(e=>"RUNNING"===e.state)||{}).id||null;if(!r)return console.log(chalk.red("\n[Error] No active sandboxes found to share.")),void console.log(chalk.dim(" Run: thinknsh dev first to start a sandbox.\n"));const l=t.find(e=>e.id===r);if(!l)return void console.log(chalk.red(`\n[Error] Sandbox "${r}" not found.`));if("RUNNING"!==l.state)return void console.log(chalk.yellow(`\n[Warning] Sandbox "${r}" is currently ${l.state}. Resume it first: thinknsh sandbox resume ${r}`));const s=l.hostPort||3e3,a=Math.random().toString(36).substring(2,10),i=serverOf(n),c=`${i}/thinknsh/preview/${encodeURIComponent(r)}?token=${a}`,d=`${i}/thinknsh/proxy/${encodeURIComponent(r)}/?token=${a}`;if(console.log(chalk.cyan(`\n[Tunnel] Generating Collaborative Live Tunnel for [${chalk.bold(r)}]...`)),console.log(chalk.green("\n[OK] Live Tunnel Active!")),console.log(` Sandbox ID : ${chalk.bold(r)}`),console.log(` Proxy Stream : ${chalk.cyan(d)}`),console.log(` Team Preview : ${chalk.bold.underline(c)}`),console.log(" Room Members : Instant access enabled with live hot-reload sync"),o.startTunnelDaemon(r,s,i,n?.api?.getUser?.()?._id||null)||console.log(chalk.yellow(" [Notice] Could not start the background tunnel.")),console.log(chalk.dim("\n Share this URL with your teammates to inspect your app in real time!\n")),n?.ws?.isConnected())try{n.ws.socket.emit("sandbox:share-created",{sandboxId:r,previewUrl:c,hostPort:s})}catch(e){}}};
|
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
5
|
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
6
|
*/
|
|
7
|
-
const chalk=require("chalk"),Table=require("cli-table3"),SnapshotManager=require("../../core/
|
|
7
|
+
const chalk=require("chalk"),Table=require("cli-table3"),SnapshotManager=require("../../core/snapshot-manager");module.exports={name:"snapshot",aliases:["snap"],description:"Instant volume snapshots with CoW clones (APFS / btrfs / reflink)",usage:"thinknsh snapshot <subcommand> [options]",examples:["thinknsh snapshot create myapp-v1","thinknsh snapshot create myapp-v1 ./data","thinknsh snapshot list","thinknsh snapshot restore myapp-v1-abc123","thinknsh snapshot diff snap1-id snap2-id","thinknsh snapshot delete snap1-id","thinknsh snapshot prune --keep 5","thinknsh snapshot info"],async execute(e=[],s){const o={},n=[];for(let s=0;s<e.length;s++){const a=e[s];if(a.startsWith("--")){const n=a.slice(2);if(n.includes("=")){const[e,s]=n.split("=");o[e]=s}else e[s+1]&&!e[s+1].startsWith("-")?o[n]=e[++s]:o[n]=!0}else if(a.startsWith("-")){const n=a.slice(1);e[s+1]&&!e[s+1].startsWith("-")?o[n]=e[++s]:o[n]=!0}else n.push(a)}return this.run(n,o,s)},async run(e,s={},o){const n=new SnapshotManager,a=e[0]||"list";switch(a){case"create":case"take":case"new":{const o=e[1];if(!o)return void console.log(chalk.red("Usage: thinknsh snapshot create <name> [path]"));const a=e[2]||process.cwd(),t=s.container||s.c||null;try{let e;if(t){const{getEngine:l}=require("./common"),c=l();e=await n.snapshotSafe(a,o,c,t,{description:s.desc||s.description||"",tags:s.tag?{tag:s.tag}:{}})}else e=await n.snapshot(a,o,{description:s.desc||s.description||"",tags:s.tag?{tag:s.tag}:{}});console.log(chalk.dim(`\n Snapshot ID: ${e.id}`)),console.log(chalk.dim(` Strategy: ${e.strategy} (${e.isCoW?"Copy-on-Write ✓":"full copy"})`)),console.log(chalk.dim(` Created in: ${e.elapsedMs}ms`))}catch(e){console.log(chalk.red(`[Error] ${e.message}`))}break}case"list":case"ls":{const e=n.list({name:s.name||s.filter});if(0===e.length)return console.log(chalk.yellow("\nNo snapshots found.")),void console.log(chalk.dim(" Try: thinknsh snapshot create mysnap"));const o=new Table({head:[chalk.cyan("ID"),chalk.cyan("Name"),chalk.cyan("Strategy"),chalk.cyan("CoW"),chalk.cyan("Size"),chalk.cyan("Created")],style:{head:[],border:[]}});for(const s of e)o.push([s.id.slice(-12),s.name,s.strategy,s.isCoW?chalk.green("✓"):chalk.dim("✗"),`${s.sizeMB}MB`,new Date(s.createdAt).toLocaleDateString()]);console.log(chalk.bold(`\nSnapshots (${e.length}):`)),console.log(o.toString());break}case"restore":{const o=e[1];if(!o)return void console.log(chalk.red("Usage: thinknsh snapshot restore <id> [target-path]"));const a=n.list().find(e=>e.id===o||e.id.endsWith(o)||e.name===o);if(!a)return void console.log(chalk.red(`Snapshot "${o}" not found.`));try{const o=await n.restore(a.id,e[2],{backup:s.backup});console.log(chalk.green(`[OK] Restored to: ${o.target}`))}catch(e){console.log(chalk.red(`[Error] ${e.message}`))}break}case"diff":{const s=e[1],o=e[2];if(!s||!o)return void console.log(chalk.red("Usage: thinknsh snapshot diff <id1> <id2>"));try{const e=n.diff(s,o);e.error||e.note?console.log(chalk.yellow(`[Diff] ${e.error||e.note}`)):(console.log(chalk.bold(`\nDiff: ${e.snapshot1} → ${e.snapshot2}`)),console.log(chalk.dim(` Changed files: ${e.changedFiles}`)),e.details?.length&&(console.log(chalk.dim("\n Changes:")),e.details.forEach(e=>console.log(chalk.dim(` ${e}`)))))}catch(e){console.log(chalk.red(`[Error] ${e.message}`))}break}case"delete":case"rm":case"remove":{const s=e[1];if(!s)return void console.log(chalk.red("Usage: thinknsh snapshot delete <id>"));const o=n.list().find(e=>e.id===s||e.id.endsWith(s)||e.name===s),a=o?o.id:s;try{n.delete(a),console.log(chalk.green(`[OK] Snapshot deleted: ${a}`))}catch(e){console.log(chalk.red(`[Error] ${e.message}`))}break}case"prune":{const o=parseInt(s.keep||e[1]||"3",10),a=n.prune(o);console.log(chalk.green(`[OK] Pruned ${a.pruned} old snapshots (keeping last ${o} per name)`));break}case"inspect":{const s=e[1],o=n.inspect(s);if(!o)return void console.log(chalk.red(`Snapshot "${s}" not found.`));console.log(JSON.stringify(o,null,2));break}case"info":case"status":case"system":{const e=n.systemInfo();console.log(chalk.bold("\nSnapshot System Info:")),console.log(chalk.dim(` Platform: ${e.platform}`)),console.log(chalk.dim(` Filesystem: ${e.fsType}`)),console.log(chalk.dim(` Strategy: ${e.strategy}`)),console.log(chalk.dim(` Copy-on-Write: ${e.isCoW?chalk.green("YES (instant, zero-disk)"):chalk.yellow("No (full copy)")}`)),console.log(chalk.dim(` Snapshot store: ${e.snapshotBase}`)),console.log(chalk.dim(` Total snapshots: ${e.totalSnapshots}`));break}default:console.log(chalk.yellow(`Unknown subcommand: ${a}`)),console.log(chalk.dim("Subcommands: create, list, restore, diff, delete, prune, info"))}}};
|