thinkncollab-shell 2.0.0-beta.2 → 2.0.0-beta.3
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 +41 -10
- package/dist/native/tnc-tray/build-macos.sh +54 -0
- package/dist/native/tnc-tray/main.swift +201 -0
- package/dist/native/tnc-tray/tray.ps1 +136 -0
- package/dist/src/commands/dock/containers.js +1 -1
- package/dist/src/commands/dock/dock.js +1 -1
- package/dist/src/dock/api/routes.js +1 -1
- package/dist/src/dock/api/server.js +1 -1
- package/dist/src/dock/api/websocket.js +7 -0
- package/dist/src/dock/api/wire.js +1 -1
- package/dist/src/dock/build/builder.js +1 -1
- package/dist/src/dock/build/buildkit.js +1 -1
- package/dist/src/dock/build/cache.js +7 -0
- package/dist/src/dock/build/sshagent.js +7 -0
- package/dist/src/dock/cli/api.js +1 -1
- package/dist/src/dock/cli/commands.js +1 -1
- package/dist/src/dock/cli/compose.js +1 -1
- package/dist/src/dock/cli/trust.js +7 -0
- package/dist/src/dock/compat/cases.js +1 -1
- package/dist/src/dock/config.js +1 -1
- package/dist/src/dock/daemon-main.js +1 -1
- package/dist/src/dock/daemon.js +1 -1
- package/dist/src/dock/engine.js +1 -1
- package/dist/src/dock/image/auth.js +1 -1
- package/dist/src/dock/image/credhelpers.js +7 -0
- package/dist/src/dock/image/keychain.js +7 -0
- package/dist/src/dock/image/registry.js +1 -1
- package/dist/src/dock/image/scan.js +7 -0
- package/dist/src/dock/image/sign.js +7 -0
- package/dist/src/dock/linux/host.js +1 -1
- package/dist/src/dock/paths.js +1 -1
- package/dist/src/dock/runtime/cdi.js +7 -0
- package/dist/src/dock/runtime/fsops.js +1 -1
- package/dist/src/dock/runtime/network.js +1 -1
- package/dist/src/dock/runtime/overlaydns.js +7 -0
- package/dist/src/dock/runtime/ports.js +1 -1
- package/dist/src/dock/runtime/rootfs.js +1 -1
- package/dist/src/dock/runtime/spec.js +1 -1
- package/dist/src/dock/runtime/stats.js +1 -1
- package/dist/src/dock/runtime/userns.js +7 -0
- package/dist/src/dock/runtime/volumes.js +1 -1
- package/dist/src/dock/setup/autostart.js +1 -1
- package/dist/src/dock/setup/diagnose.js +7 -0
- package/dist/src/dock/setup/index.js +1 -1
- package/dist/src/dock/tray/agent.js +7 -0
- package/dist/src/dock/tray/install.js +7 -0
- package/dist/src/dock/ui/page.js +228 -15
- package/dist/src/dock/ui/server.js +1 -1
- package/package.json +1 -1
- package/dist/src/dock/setup/app.js +0 -7
package/Readme.md
CHANGED
|
@@ -43,17 +43,17 @@ Every command has its own help, for example `thinknsh run --help`. The full list
|
|
|
43
43
|
| Area | Commands |
|
|
44
44
|
| :--- | :--- |
|
|
45
45
|
| Containers | `run`, `create`, `start`, `stop`, `restart`, `kill`, `rm`, `ps`, `logs`, `exec`, `wait`, `inspect` |
|
|
46
|
-
| Images | `images`, `pull`, `push`, `build
|
|
46
|
+
| Images | `images`, `pull`, `push`, `build` (`--platform a,b --push`), `tag`, `rmi`, `image prune`, `search`, `manifest create / inspect / push / rm` |
|
|
47
47
|
| Registries | `login [-u USER] [--password-stdin] [REGISTRY]`, `logout [REGISTRY]` |
|
|
48
48
|
| Agent workspaces | `workspace up / open / apps / ls / shell / exec / ssh-config / stop / rm` |
|
|
49
49
|
| Dev services | `service start postgres\|redis\|mysql [-p PORT]`, `service stop / restart / status / logs / rm [-v]` |
|
|
50
50
|
| Local registry | `registry start [PORT] / stop / status / push IMAGE / pull IMAGE` |
|
|
51
|
-
| Apps | `compose up / down / ps / logs / exec / build / pull / start / stop / restart / rm / config` |
|
|
51
|
+
| 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` |
|
|
52
52
|
| Storage and networks | `volume ls / create / rm / inspect / prune`, `network ls / create / rm / connect / disconnect / prune` |
|
|
53
53
|
| Files and state | `cp`, `diff`, `export`, `commit`, `save`, `load`, `import`, `history`, `stats`, `top`, `pause` / `unpause`, `rename`, `update`, `port` |
|
|
54
|
-
| Engine | `system info / df / prune`, `dock setup / start / stop / status / selftest / check / config
|
|
54
|
+
| Engine | `system info / df / prune`, `events`, `dock setup / start / stop / status / selftest / check / config / autostart / diagnose / reset` |
|
|
55
55
|
| Other tools and engines | `dock use [--system] [--off]` (engine CLI, compose, buildx, VS Code Dev Containers, testcontainers, the standard socket path), `dock env`, `dock migrate` |
|
|
56
|
-
|
|
|
56
|
+
| Desktop | `dock app install` (menu-bar app on macOS, tray icon on Windows), `dock ui` — dashboard in the browser: containers by compose app, logs with a filter, live stats, a terminal, images, volumes, networks, settings, troubleshooting |
|
|
57
57
|
| CI on this machine | `ci run [WORKFLOW] [--job ID]`, `ci list` — `.tnc/workflows` jobs, matrix legs for amd64 and arm64, services, JavaScript / composite / container actions, artifacts, each in a fresh container |
|
|
58
58
|
|
|
59
59
|
### One container for all your coding agents: `thinknsh workspace`
|
|
@@ -107,21 +107,44 @@ New here? Read the step-by-step guide: [`docs/HOW-TO-USE.md`](docs/HOW-TO-USE.md
|
|
|
107
107
|
| :--- | :--- |
|
|
108
108
|
| Run, attach, interactive terminals, exec, logs, exit codes, signals | ✅ |
|
|
109
109
|
| Pull from public and private OCI registries (token and basic auth) | ✅ |
|
|
110
|
-
| Build: Containerfile, multi-stage, build args, targets, layer cache, `.tncignore`, `RUN --mount` (cache, secret, bind, tmpfs), heredocs | ✅ |
|
|
110
|
+
| Build: Containerfile, multi-stage, build args, targets, layer cache, `.tncignore`, `RUN --mount` (cache, secret, bind, tmpfs, ssh), heredocs | ✅ |
|
|
111
|
+
| Build: independent stages at the same time; `--cache-from` / `--cache-to` (registry, local folder, inline) to share the cache with CI and other machines | ✅ |
|
|
112
|
+
| Build: `--ssh default` (your SSH agent for `RUN --mount=type=ssh`, e.g. `git clone` of private repos; keys never leave the agent) | ✅ needs runtime bundle 0.2.3+ |
|
|
113
|
+
| Pushes and pulls that survive a dropped connection (chunked uploads, resumed downloads) | ✅ |
|
|
114
|
+
| 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 |
|
|
115
|
+
| 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 |
|
|
116
|
+
| 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+ |
|
|
117
|
+
| Overlay networks across machines (`network create -d overlay`, VXLAN), names of every machine's containers | ✅ Linux as root; the tunnel is not encrypted |
|
|
111
118
|
| BuildKit: buildx and BuildKit-mode builds through the engine CLI (`--secret`, cache mounts, and SSH-agent forwarding in BuildKit mode) | ✅ |
|
|
119
|
+
| buildx multi-platform: `buildx build --platform linux/amd64,linux/arm64 --push` (one OCI index, pushed with dock's logins), `--load` | ✅ |
|
|
112
120
|
| Compose: build, depends_on with `service_healthy`, healthchecks, profiles, `.env`, `extends`, `secrets`, `configs`, recreate on change | ✅ |
|
|
121
|
+
| Compose `run`, `watch` (sync, sync+restart, rebuild), `scale`, `cp`, `top`, `port`, `ls`, `events` and the other verbs | ✅ |
|
|
122
|
+
| Multi-platform images: `build --platform linux/amd64,linux/arm64 --push`, `manifest`; image `search` | ✅ |
|
|
113
123
|
| Networks with name resolution, named volumes, bind mounts, published TCP and UDP ports, `host.tnc.internal` | ✅ |
|
|
124
|
+
| IPv6 dual-stack networks (`network create --ipv6 [--subnet …/64]`, compose `enable_ipv6`): addresses, routes, AAAA, outbound NAT | ✅ |
|
|
125
|
+
| macvlan / ipvlan networks (containers with their own address on the LAN) | ✅ Linux as root; ❌ not possible inside the macOS VM, WSL or rootless (they sit behind NAT) |
|
|
126
|
+
| Embedded DNS on user networks (`127.0.0.11`: every replica of a scaled service in turn, PTR, TCP, forwarding; needs runtime bundle 0.2.3+) | ✅ |
|
|
127
|
+
| systemd as PID 1 and kind/k3d nodes (`--privileged`: writable cgroups, host devices, `SIGRTMIN+3` stop signal) | ✅ |
|
|
128
|
+
| cgroup v1 Linux hosts (for example RHEL/CentOS 8): limits, `stats`, `top` | ✅ (CI covers v2 only) |
|
|
114
129
|
| `linux/amd64` images on Apple Silicon (Rosetta 2; P8 compatibility check is optional) | ✅ |
|
|
115
130
|
| Copy files in and out (`cp`), commit, save/load, export/import, stats, top | ✅ |
|
|
116
131
|
| Unmodified tools: engine CLI, compose, VS Code Dev Containers (incl. Features), testcontainers | ✅ checked by `tests/ecosystem` |
|
|
117
132
|
| Move images and volumes over from another engine (`dock migrate`) | ✅ |
|
|
118
|
-
| Dashboard in the browser (`dock ui`) | ✅ |
|
|
133
|
+
| Dashboard in the browser (`dock ui`), with settings, troubleshooting and a terminal | ✅ |
|
|
119
134
|
| Local CI (`ci run`, and TNC CI/CD runs from the dashboard): jobs, `needs`, matrix, both CPUs on one machine, `services` on localhost, `GITHUB_ENV`/`GITHUB_OUTPUT` | ✅ (macOS/Windows jobs and third-party actions are skipped) |
|
|
120
135
|
| `--network container:<name>` and compose `network_mode: service:<name>` | ✅ |
|
|
121
|
-
|
|
|
136
|
+
| Menu-bar app (macOS) / tray icon (Windows), start at login (`dock autostart`) | ✅ |
|
|
137
|
+
| Installers with their own Node.js: `.pkg`/`.dmg`, Windows setup, Linux tarball + systemd | ✅ built in CI (`dock-bundle.yml`); signing when the certificates are set |
|
|
138
|
+
| Corporate proxy and CA certificates, registry mirrors, insecure registries (`dock config`) | ✅ |
|
|
139
|
+
| Registry passwords in the system keychain (macOS Keychain, Windows Credential Manager, Secret Service) | ✅ |
|
|
140
|
+
| Diagnostics bundle, factory reset (`dock diagnose`, `dock reset`) | ✅ |
|
|
141
|
+
| GPUs (`--gpus`, compose `deploy.resources.reservations.devices`, CDI names like `nvidia.com/gpu=all`) | ✅ Linux and WSL2 through CDI (NVIDIA Container Toolkit); ❌ not possible on macOS |
|
|
142
|
+
| Registry logins from credential helpers (ECR, GCR, OS keychains: `credHelpers` / `credsStore` of the engine CLI config, or `dock config --credential-helper`) | ✅ |
|
|
143
|
+
| zstd-compressed image layers | ✅ |
|
|
144
|
+
| Prometheus metrics (`GET /metrics` on the engine socket) | ✅ |
|
|
122
145
|
| Push to a registry (skips existing layers, cross-repo mount), registry login/logout | ✅ |
|
|
123
146
|
| Agent workspaces over SSH (key-only, loopback, no capabilities) | ✅ |
|
|
124
|
-
| Rootless mode
|
|
147
|
+
| Rootless mode on Linux (a normal user, no sudo: user namespaces, pasta / slirp4netns; limits and pause through the cgroup systemd delegates to the user) | ✅ |
|
|
125
148
|
| Deploy compose apps to your own servers | ✅ `thinknsh cluster deploy` |
|
|
126
149
|
|
|
127
150
|
### Platforms
|
|
@@ -147,6 +170,13 @@ Measured on an Apple Silicon Mac with `thinknsh bench`. Run the same command on
|
|
|
147
170
|
|
|
148
171
|
Downloads and the full guide are at **https://thinkncollab.com/dock**.
|
|
149
172
|
|
|
173
|
+
**Installers** (no Node.js needed; they bring Node 24): `Thinknsh-<version>.dmg` for macOS,
|
|
174
|
+
`Thinknsh-<version>-<arch>-setup.exe` for Windows, `thinknsh-<version>-linux-<arch>.tar.gz`
|
|
175
|
+
for Linux (`sudo ./thinknsh/install.sh`). They set up the runtime, start dock at login and
|
|
176
|
+
install the menu-bar / tray app. Built by `native/installer/` (see `.github/workflows/dock-bundle.yml`).
|
|
177
|
+
|
|
178
|
+
**With npm:**
|
|
179
|
+
|
|
150
180
|
1. **Node.js 22.5 or newer.** dock stores its state with `node:sqlite`.
|
|
151
181
|
2. **Windows only:** turn on WSL2 in an administrator PowerShell, then reboot. If WSL is already installed, run `wsl --update` instead.
|
|
152
182
|
```powershell
|
|
@@ -157,13 +187,14 @@ Downloads and the full guide are at **https://thinkncollab.com/dock**.
|
|
|
157
187
|
npm install -g https://thinkncollab.com/downloads/dock/0.2.0/thinkncollab-shell.tgz
|
|
158
188
|
thinknsh dock setup
|
|
159
189
|
```
|
|
160
|
-
On Linux, dock runs
|
|
190
|
+
On Linux, as a normal user, dock runs **rootless** (no sudo): it needs `uidmap` and `passt`, and subordinate ids (`sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER`); `thinknsh dock setup` lists anything missing. To run it as a system service instead, run `sudo thinknsh dock setup` and `sudo thinknsh dock start` (or `sudo thinknsh dock autostart on` for a systemd service), then `sudo groupadd tnc-dock && sudo usermod -aG tnc-dock $USER` and restart dock to use the commands without sudo. The engine listens on `/run/thinknsh/dock.sock` for every user; settings are system-wide in `/etc/thinknsh/config.json` (change them with `sudo thinknsh dock config …`).
|
|
161
191
|
On a machine without internet, download the files for your platform and `manifest.json` from the downloads page into one folder, then run `thinknsh dock setup --from <folder>`.
|
|
162
192
|
4. **Run something:**
|
|
163
193
|
```bash
|
|
164
194
|
thinknsh run --rm -p 8080:80 nginx:alpine
|
|
165
195
|
```
|
|
166
196
|
Then open http://localhost:8080.
|
|
197
|
+
5. **Optional:** `thinknsh dock autostart on` (start at login) and `thinknsh dock app install` (menu-bar / tray app).
|
|
167
198
|
|
|
168
199
|
### Check it works on your machine
|
|
169
200
|
|
|
@@ -318,4 +349,4 @@ thinknsh dep sync # Reconcile environment from thinkncollab.yml
|
|
|
318
349
|
## 📄 License
|
|
319
350
|
|
|
320
351
|
Apache License 2.0. See [LICENSE](LICENSE) for details.
|
|
321
|
-
Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
|
|
352
|
+
Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# native/tnc-tray/build-macos.sh — builds Thinknsh.app, the menu-bar app (Intel + Apple Silicon).
|
|
4
|
+
#
|
|
5
|
+
# build-macos.sh <outdir> <outdir>/Thinknsh.app
|
|
6
|
+
#
|
|
7
|
+
# Signing: TNC_SIGN_IDENTITY="Developer ID Application: …" signs with the
|
|
8
|
+
# hardened runtime (needed for notarization); without it the app is signed ad hoc.
|
|
9
|
+
#
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
13
|
+
OUT="${1:?usage: build-macos.sh <outdir>}"
|
|
14
|
+
VERSION="${TNC_APP_VERSION:-$(node -p "require('$HERE/../../package.json').version" 2>/dev/null || echo 0.0.0)}"
|
|
15
|
+
mkdir -p "$OUT"
|
|
16
|
+
OUT="$(cd "$OUT" && pwd)"
|
|
17
|
+
APP="$OUT/Thinknsh.app"
|
|
18
|
+
WORK="$(mktemp -d)"
|
|
19
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
20
|
+
|
|
21
|
+
rm -rf "$APP"
|
|
22
|
+
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
|
|
23
|
+
|
|
24
|
+
for arch in arm64 x86_64; do
|
|
25
|
+
xcrun swiftc -O -target "$arch-apple-macos13" -o "$WORK/tray-$arch" "$HERE/main.swift"
|
|
26
|
+
done
|
|
27
|
+
lipo -create -output "$APP/Contents/MacOS/thinknsh-tray" "$WORK/tray-arm64" "$WORK/tray-x86_64"
|
|
28
|
+
|
|
29
|
+
cat > "$APP/Contents/Info.plist" <<PLIST
|
|
30
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
31
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
32
|
+
<plist version="1.0">
|
|
33
|
+
<dict>
|
|
34
|
+
<key>CFBundleName</key><string>Thinknsh</string>
|
|
35
|
+
<key>CFBundleDisplayName</key><string>Thinknsh</string>
|
|
36
|
+
<key>CFBundleIdentifier</key><string>com.thinkncollab.thinknsh</string>
|
|
37
|
+
<key>CFBundleExecutable</key><string>thinknsh-tray</string>
|
|
38
|
+
<key>CFBundlePackageType</key><string>APPL</string>
|
|
39
|
+
<key>CFBundleShortVersionString</key><string>${VERSION}</string>
|
|
40
|
+
<key>CFBundleVersion</key><string>${VERSION}</string>
|
|
41
|
+
<key>LSMinimumSystemVersion</key><string>13.0</string>
|
|
42
|
+
<key>LSUIElement</key><true/>
|
|
43
|
+
<key>NSHighResolutionCapable</key><true/>
|
|
44
|
+
<key>NSAppleEventsUsageDescription</key><string>Opens Terminal windows for container logs and shells.</string>
|
|
45
|
+
</dict>
|
|
46
|
+
</plist>
|
|
47
|
+
PLIST
|
|
48
|
+
|
|
49
|
+
if [[ -n "${TNC_SIGN_IDENTITY:-}" ]]; then
|
|
50
|
+
codesign --force --options runtime --timestamp --sign "$TNC_SIGN_IDENTITY" "$APP"
|
|
51
|
+
else
|
|
52
|
+
codesign --force --sign - "$APP"
|
|
53
|
+
fi
|
|
54
|
+
echo "✓ $APP"
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// native/tnc-tray/main.swift — the thinknsh menu-bar app (macOS 13+).
|
|
2
|
+
//
|
|
3
|
+
// A thin shell: it runs `thinknsh dock tray` (src/dock/tray/agent.js) and draws
|
|
4
|
+
// the menu that process sends, one JSON object per line on its stdout. Clicks
|
|
5
|
+
// go back as {"click": ID} on its stdin, after a confirm dialog when the item
|
|
6
|
+
// asks for one. All engine logic lives in the agent.
|
|
7
|
+
//
|
|
8
|
+
// The agent command comes from the bundle: Resources/node/bin/node and
|
|
9
|
+
// Resources/cli/bin/thinknsh.js (installer builds), or Resources/agent.json
|
|
10
|
+
// {"node": …, "cli": …} written by `thinknsh dock app install`.
|
|
11
|
+
|
|
12
|
+
import Cocoa
|
|
13
|
+
|
|
14
|
+
final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
15
|
+
private var statusItem: NSStatusItem!
|
|
16
|
+
private var agent: Process?
|
|
17
|
+
private var agentInput: FileHandle?
|
|
18
|
+
private var buffer = Data()
|
|
19
|
+
private var restarts = 0
|
|
20
|
+
private var quitting = false
|
|
21
|
+
|
|
22
|
+
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
23
|
+
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
|
24
|
+
setIcon("stopped")
|
|
25
|
+
let menu = NSMenu()
|
|
26
|
+
menu.autoenablesItems = false
|
|
27
|
+
menu.delegate = self
|
|
28
|
+
statusItem.menu = menu
|
|
29
|
+
showMessage("Starting…")
|
|
30
|
+
startAgent()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func applicationWillTerminate(_ notification: Notification) {
|
|
34
|
+
quitting = true
|
|
35
|
+
agent?.terminate()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// MARK: agent process
|
|
39
|
+
|
|
40
|
+
private func agentCommand() -> (String, [String])? {
|
|
41
|
+
guard let res = Bundle.main.resourceURL else { return nil }
|
|
42
|
+
let fm = FileManager.default
|
|
43
|
+
let node = res.appendingPathComponent("node/bin/node").path
|
|
44
|
+
let cli = res.appendingPathComponent("cli/bin/thinknsh.js").path
|
|
45
|
+
if fm.isExecutableFile(atPath: node) && fm.fileExists(atPath: cli) { return (node, [cli, "dock", "tray"]) }
|
|
46
|
+
if let data = try? Data(contentsOf: res.appendingPathComponent("agent.json")),
|
|
47
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: String],
|
|
48
|
+
let n = json["node"], let c = json["cli"] {
|
|
49
|
+
return (n, [c, "dock", "tray"])
|
|
50
|
+
}
|
|
51
|
+
return nil
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private func startAgent() {
|
|
55
|
+
guard let (exe, args) = agentCommand() else {
|
|
56
|
+
showMessage("thinknsh not found: run thinknsh dock app install")
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
let p = Process()
|
|
60
|
+
p.executableURL = URL(fileURLWithPath: exe)
|
|
61
|
+
p.arguments = args
|
|
62
|
+
var env = ProcessInfo.processInfo.environment
|
|
63
|
+
env["PATH"] = "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin:" + (env["PATH"] ?? "")
|
|
64
|
+
env["FORCE_COLOR"] = "0"
|
|
65
|
+
p.environment = env
|
|
66
|
+
let out = Pipe(), inp = Pipe()
|
|
67
|
+
p.standardOutput = out
|
|
68
|
+
p.standardInput = inp
|
|
69
|
+
p.standardError = FileHandle.nullDevice
|
|
70
|
+
out.fileHandleForReading.readabilityHandler = { [weak self] h in
|
|
71
|
+
let d = h.availableData
|
|
72
|
+
DispatchQueue.main.async { self?.received(d) }
|
|
73
|
+
}
|
|
74
|
+
p.terminationHandler = { [weak self] _ in
|
|
75
|
+
DispatchQueue.main.async { self?.agentEnded() }
|
|
76
|
+
}
|
|
77
|
+
do {
|
|
78
|
+
try p.run()
|
|
79
|
+
agent = p
|
|
80
|
+
agentInput = inp.fileHandleForWriting
|
|
81
|
+
} catch {
|
|
82
|
+
showMessage("Cannot start thinknsh: \(error.localizedDescription)")
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private func agentEnded() {
|
|
87
|
+
agent = nil
|
|
88
|
+
agentInput = nil
|
|
89
|
+
if quitting { NSApp.terminate(nil); return }
|
|
90
|
+
restarts += 1
|
|
91
|
+
if restarts > 5 { showMessage("thinknsh tray stopped; reopen the app"); return }
|
|
92
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in self?.startAgent() }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private func received(_ data: Data) {
|
|
96
|
+
if data.isEmpty { return }
|
|
97
|
+
buffer.append(data)
|
|
98
|
+
while let nl = buffer.firstIndex(of: 0x0a) {
|
|
99
|
+
let line = buffer.subdata(in: buffer.startIndex..<nl)
|
|
100
|
+
buffer.removeSubrange(buffer.startIndex...nl)
|
|
101
|
+
handle(line)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private func handle(_ line: Data) {
|
|
106
|
+
guard let msg = (try? JSONSerialization.jsonObject(with: line)) as? [String: Any] else { return }
|
|
107
|
+
if let menu = msg["menu"] as? [String: Any] { render(menu) }
|
|
108
|
+
if let text = msg["notify"] as? String { notify(text) }
|
|
109
|
+
if msg["exit"] as? Bool == true {
|
|
110
|
+
quitting = true
|
|
111
|
+
NSApp.terminate(nil)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private func send(_ obj: [String: Any]) {
|
|
116
|
+
guard let input = agentInput, let d = try? JSONSerialization.data(withJSONObject: obj) else { return }
|
|
117
|
+
var line = d
|
|
118
|
+
line.append(0x0a)
|
|
119
|
+
try? input.write(contentsOf: line)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// MARK: menu
|
|
123
|
+
|
|
124
|
+
private func setIcon(_ status: String) {
|
|
125
|
+
let name = status == "running" ? "shippingbox.fill" : "shippingbox"
|
|
126
|
+
let img = NSImage(systemSymbolName: name, accessibilityDescription: "thinknsh dock \(status)")
|
|
127
|
+
img?.isTemplate = true
|
|
128
|
+
statusItem.button?.image = img
|
|
129
|
+
statusItem.button?.appearsDisabled = status != "running"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private func showMessage(_ text: String) {
|
|
133
|
+
let menu = statusItem.menu!
|
|
134
|
+
menu.removeAllItems()
|
|
135
|
+
let item = NSMenuItem(title: text, action: nil, keyEquivalent: "")
|
|
136
|
+
item.isEnabled = false
|
|
137
|
+
menu.addItem(item)
|
|
138
|
+
menu.addItem(.separator())
|
|
139
|
+
menu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private func render(_ model: [String: Any]) {
|
|
143
|
+
setIcon(model["status"] as? String ?? "stopped")
|
|
144
|
+
let title = model["title"] as? String ?? ""
|
|
145
|
+
statusItem.button?.title = title.isEmpty ? "" : " \(title)"
|
|
146
|
+
let menu = statusItem.menu!
|
|
147
|
+
menu.removeAllItems()
|
|
148
|
+
fill(menu, model["items"] as? [[String: Any]] ?? [])
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private func fill(_ menu: NSMenu, _ items: [[String: Any]]) {
|
|
152
|
+
for it in items {
|
|
153
|
+
if it["separator"] as? Bool == true { menu.addItem(.separator()); continue }
|
|
154
|
+
let item = NSMenuItem(title: it["label"] as? String ?? "", action: nil, keyEquivalent: "")
|
|
155
|
+
item.isEnabled = it["enabled"] as? Bool ?? true
|
|
156
|
+
if it["checked"] as? Bool == true { item.state = .on }
|
|
157
|
+
if let sub = it["items"] as? [[String: Any]] {
|
|
158
|
+
let submenu = NSMenu()
|
|
159
|
+
submenu.autoenablesItems = false
|
|
160
|
+
fill(submenu, sub)
|
|
161
|
+
item.submenu = submenu
|
|
162
|
+
} else {
|
|
163
|
+
item.target = self
|
|
164
|
+
item.action = #selector(clicked(_:))
|
|
165
|
+
item.representedObject = it
|
|
166
|
+
}
|
|
167
|
+
menu.addItem(item)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@objc private func clicked(_ sender: NSMenuItem) {
|
|
172
|
+
guard let it = sender.representedObject as? [String: Any], let id = it["id"] as? String else { return }
|
|
173
|
+
if let question = it["confirm"] as? String {
|
|
174
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
175
|
+
let alert = NSAlert()
|
|
176
|
+
alert.messageText = question
|
|
177
|
+
alert.alertStyle = .warning
|
|
178
|
+
alert.addButton(withTitle: "Continue")
|
|
179
|
+
alert.addButton(withTitle: "Cancel")
|
|
180
|
+
if alert.runModal() != .alertFirstButtonReturn { return }
|
|
181
|
+
}
|
|
182
|
+
send(["click": id])
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
func menuWillOpen(_ menu: NSMenu) {
|
|
186
|
+
send(["refresh": true])
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private func notify(_ text: String) {
|
|
190
|
+
let p = Process()
|
|
191
|
+
p.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
|
192
|
+
p.arguments = ["-e", "on run argv\ndisplay notification (item 1 of argv) with title \"thinknsh dock\"\nend run", text]
|
|
193
|
+
try? p.run()
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let app = NSApplication.shared
|
|
198
|
+
let delegate = AppDelegate()
|
|
199
|
+
app.delegate = delegate
|
|
200
|
+
app.setActivationPolicy(.accessory)
|
|
201
|
+
app.run()
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# native/tnc-tray/tray.ps1 — the thinknsh notification-area (tray) icon on Windows.
|
|
2
|
+
#
|
|
3
|
+
# Same design as the macOS app: runs `thinknsh dock tray` (src/dock/tray/agent.js)
|
|
4
|
+
# and draws the menu it sends, one JSON object per line; clicks go back as
|
|
5
|
+
# {"click": ID}. Installed and started by `thinknsh dock app install`.
|
|
6
|
+
#
|
|
7
|
+
# powershell -WindowStyle Hidden -ExecutionPolicy Bypass -File tray.ps1 -Node <node.exe> -Cli <thinknsh.js>
|
|
8
|
+
# powershell -File tray.ps1 -CompileOnly compile the C# part and exit (CI)
|
|
9
|
+
|
|
10
|
+
param(
|
|
11
|
+
[string]$Node,
|
|
12
|
+
[string]$Cli,
|
|
13
|
+
[switch]$CompileOnly
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
$ErrorActionPreference = 'Stop'
|
|
17
|
+
|
|
18
|
+
Add-Type -ReferencedAssemblies System.Windows.Forms, System.Drawing, System.Web.Extensions -TypeDefinition @'
|
|
19
|
+
using System;
|
|
20
|
+
using System.Collections;
|
|
21
|
+
using System.Collections.Concurrent;
|
|
22
|
+
using System.Collections.Generic;
|
|
23
|
+
using System.Diagnostics;
|
|
24
|
+
using System.Drawing;
|
|
25
|
+
using System.Reflection;
|
|
26
|
+
using System.Threading;
|
|
27
|
+
using System.Web.Script.Serialization;
|
|
28
|
+
using System.Windows.Forms;
|
|
29
|
+
|
|
30
|
+
public static class TncTray {
|
|
31
|
+
static Process agent;
|
|
32
|
+
static NotifyIcon icon;
|
|
33
|
+
static readonly ConcurrentQueue<string> lines = new ConcurrentQueue<string>();
|
|
34
|
+
static readonly JavaScriptSerializer json = new JavaScriptSerializer();
|
|
35
|
+
|
|
36
|
+
public static void Run(string node, string cli) {
|
|
37
|
+
var psi = new ProcessStartInfo(node, "\"" + cli + "\" dock tray");
|
|
38
|
+
psi.UseShellExecute = false;
|
|
39
|
+
psi.RedirectStandardInput = true;
|
|
40
|
+
psi.RedirectStandardOutput = true;
|
|
41
|
+
psi.RedirectStandardError = true;
|
|
42
|
+
psi.CreateNoWindow = true;
|
|
43
|
+
psi.StandardOutputEncoding = System.Text.Encoding.UTF8; // menu labels are UTF-8 (●, …)
|
|
44
|
+
psi.EnvironmentVariables["FORCE_COLOR"] = "0";
|
|
45
|
+
agent = Process.Start(psi);
|
|
46
|
+
var reader = new Thread(() => {
|
|
47
|
+
string l;
|
|
48
|
+
while ((l = agent.StandardOutput.ReadLine()) != null) lines.Enqueue(l);
|
|
49
|
+
lines.Enqueue("{\"exit\":true}");
|
|
50
|
+
});
|
|
51
|
+
reader.IsBackground = true;
|
|
52
|
+
reader.Start();
|
|
53
|
+
var errors = new Thread(() => { agent.StandardError.ReadToEnd(); });
|
|
54
|
+
errors.IsBackground = true;
|
|
55
|
+
errors.Start();
|
|
56
|
+
|
|
57
|
+
icon = new NotifyIcon();
|
|
58
|
+
icon.Icon = SystemIcons.Application;
|
|
59
|
+
icon.Text = "thinknsh dock";
|
|
60
|
+
icon.Visible = true;
|
|
61
|
+
icon.ContextMenuStrip = new ContextMenuStrip();
|
|
62
|
+
icon.ContextMenuStrip.Items.Add(new ToolStripMenuItem("Starting...") { Enabled = false });
|
|
63
|
+
icon.ContextMenuStrip.Opening += (s, e) => Send("{\"refresh\":true}");
|
|
64
|
+
icon.MouseClick += (s, e) => {
|
|
65
|
+
if (e.Button != MouseButtons.Left) return;
|
|
66
|
+
var show = typeof(NotifyIcon).GetMethod("ShowContextMenu", BindingFlags.Instance | BindingFlags.NonPublic);
|
|
67
|
+
if (show != null) show.Invoke(icon, null);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
var timer = new System.Windows.Forms.Timer();
|
|
71
|
+
timer.Interval = 150;
|
|
72
|
+
timer.Tick += (s, e) => Drain();
|
|
73
|
+
timer.Start();
|
|
74
|
+
Application.Run();
|
|
75
|
+
icon.Visible = false;
|
|
76
|
+
try { if (!agent.HasExited) agent.Kill(); } catch (Exception) { }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static void Send(string line) {
|
|
80
|
+
try { agent.StandardInput.WriteLine(line); agent.StandardInput.Flush(); } catch (Exception) { }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static void Drain() {
|
|
84
|
+
string l;
|
|
85
|
+
while (lines.TryDequeue(out l)) {
|
|
86
|
+
Dictionary<string, object> m;
|
|
87
|
+
try { m = json.Deserialize<Dictionary<string, object>>(l); } catch (Exception) { continue; }
|
|
88
|
+
if (m == null) continue;
|
|
89
|
+
if (m.ContainsKey("notify")) icon.ShowBalloonTip(5000, "thinknsh dock", Convert.ToString(m["notify"]), ToolTipIcon.Info);
|
|
90
|
+
if (m.ContainsKey("menu")) Render(m["menu"] as IDictionary<string, object>);
|
|
91
|
+
if (m.ContainsKey("exit")) { Application.Exit(); return; }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
static void Render(IDictionary<string, object> menu) {
|
|
96
|
+
if (menu == null) return;
|
|
97
|
+
var strip = icon.ContextMenuStrip;
|
|
98
|
+
strip.Items.Clear();
|
|
99
|
+
Fill(strip.Items, menu["items"] as IEnumerable);
|
|
100
|
+
string status = Convert.ToString(menu["status"]);
|
|
101
|
+
string title = menu.ContainsKey("title") ? Convert.ToString(menu["title"]) : "";
|
|
102
|
+
string text = "thinknsh dock: " + status + (title.Length > 0 ? " (" + title + " running)" : "");
|
|
103
|
+
icon.Text = text.Length > 63 ? text.Substring(0, 63) : text;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
static void Fill(ToolStripItemCollection items, IEnumerable list) {
|
|
107
|
+
if (list == null) return;
|
|
108
|
+
foreach (object o in list) {
|
|
109
|
+
var it = o as IDictionary<string, object>;
|
|
110
|
+
if (it == null) continue;
|
|
111
|
+
if (it.ContainsKey("separator")) { items.Add(new ToolStripSeparator()); continue; }
|
|
112
|
+
var mi = new ToolStripMenuItem(Convert.ToString(it["label"]));
|
|
113
|
+
if (it.ContainsKey("enabled") && !Convert.ToBoolean(it["enabled"])) mi.Enabled = false;
|
|
114
|
+
if (it.ContainsKey("checked") && Convert.ToBoolean(it["checked"])) mi.Checked = true;
|
|
115
|
+
if (it.ContainsKey("items")) {
|
|
116
|
+
Fill(mi.DropDownItems, it["items"] as IEnumerable);
|
|
117
|
+
} else {
|
|
118
|
+
string id = Convert.ToString(it["id"]);
|
|
119
|
+
string confirm = it.ContainsKey("confirm") ? Convert.ToString(it["confirm"]) : null;
|
|
120
|
+
mi.Click += (s, e) => {
|
|
121
|
+
if (confirm != null && MessageBox.Show(confirm, "thinknsh dock", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning) != DialogResult.OK) return;
|
|
122
|
+
var click = new Dictionary<string, object>();
|
|
123
|
+
click["click"] = id;
|
|
124
|
+
Send(json.Serialize(click));
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
items.Add(mi);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
'@
|
|
132
|
+
|
|
133
|
+
if ($CompileOnly) { Write-Output 'tray compiled'; exit 0 }
|
|
134
|
+
if (-not $Node -or -not $Cli) { throw 'usage: tray.ps1 -Node <node.exe> -Cli <thinknsh.js>' }
|
|
135
|
+
[System.Windows.Forms.Application]::EnableVisualStyles()
|
|
136
|
+
[TncTray]::Run($Node, $Cli)
|
|
@@ -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",build:"Build an image from a Containerfile",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,
|
|
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||[]]);
|
|
@@ -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"),{spawn}=require("child_process"),{dockPaths}=require("../../dock/paths"),{request,ping}=require("../../dock/client"),{readPid,pidAlive}=require("../../dock/daemon"),{PHASE}=require("../../dock/version"),sleep=e=>new Promise(o=>setTimeout(o,e));async function setupCmd(e){const{setup:o}=require("../../dock/setup"),s=o=>{const s=e.indexOf(o);return-1===s?void 0:e[s+1]};try{const t=await o({from:s("--from"),baseUrl:s("--url"),force:e.includes("--force"),log:e=>console.log(chalk.dim(` ${e}`))});return console.log(chalk.green(`✓ dock runtime ${t} installed for ${process.platform}/${process.arch}`)),console.log(chalk.dim(" next: thinknsh dock start")),!0}catch(e){return console.log(chalk.red(`✗ setup failed: ${e.message}`)),!1}}async function start(e){if(await ping(e.socket))return console.log(chalk.green(`dock is already running (pid ${readPid(e.pidFile)||"?"})`)),!0;fs.mkdirSync(e.home,{recursive:!0,mode:448});const o=fs.openSync(e.logFile,"a"),s=[];try{require("node:sqlite")}catch(e){s.push("--experimental-sqlite")}const t=spawn(process.execPath,[...s,path.join(__dirname,"..","..","dock","daemon-main.js")],{detached:!0,stdio:["ignore",o,o],env:process.env});t.unref(),fs.closeSync(o);for(let o=0;o<50;o++){if(await ping(e.socket))return console.log(chalk.green(`✓ dock started (pid ${t.pid})`)),console.log(chalk.dim(` socket: ${e.socket}`)),console.log(chalk.dim(" try: thinknsh run --rm alpine echo hello")),!0;if(null!==t.exitCode)break;await sleep(100)}console.log(chalk.red(`✗ dock failed to start. Last log lines (${e.logFile}):`));try{console.log(chalk.dim(fs.readFileSync(e.logFile,"utf8").trim().split("\n").slice(-5).join("\n")))}catch(e){}return!1}async function stop(e){const o=readPid(e.pidFile);if(!o||!pidAlive(o))return console.log(chalk.dim("dock is not running")),!0;process.kill(o,"SIGTERM");for(let e=0;e<50;e++){if(!pidAlive(o))return console.log(chalk.green(`✓ dock stopped (pid ${o})`)),!0;await sleep(100)}return console.log(chalk.red(`✗ dock (pid ${o}) did not stop within 5s`)),!1}async function status(e){if(!await ping(e.socket))return console.log(`${chalk.red("●")} dock is not running ${chalk.dim("start it with: thinknsh dock start")}`),!1;const[o,s]=await Promise.all([request(e.socket,"GET","/version"),request(e.socket,"GET","/info")]);if(console.log(`${chalk.green("●")} dock ${o.body.Version} running (pid ${readPid(e.pidFile)||"?"})`),console.log(` socket ${e.socket}`),console.log(` API version ${o.body.ApiVersion} (min ${o.body.MinAPIVersion})`),console.log(` phase ${PHASE}`),console.log(` containers ${s.body.Containers} (${s.body.ContainersRunning} running)`),console.log(` images ${s.body.Images}`),"darwin"===process.platform){const e=require("../../dock/setup/autostart").installed();console.log(` autostart ${e?"on (starts when you log in)":chalk.dim("off (turn on: thinknsh dock autostart)")}`)}for(const e of s.body.Warnings||[])console.log(chalk.yellow(` ${e}`));return!0}async function compat(e,o){const{runCompat:s,toMarkdown:t}=require("../../dock/compat/runner");let n=e.socket,r=null;if(!await ping(n)){const e=require("os"),{DockDaemon:o}=require("../../dock/daemon"),s=fs.mkdtempSync(path.join(e.tmpdir(),"tnc-dock-check-"));r={daemon:new o({home:s}),home:s},await r.daemon.start(),n=r.daemon.paths.socket}try{const a=await s({socketPath:n,home:r?r.home:e.home}),c={pass:chalk.green,fail:chalk.red,pending:chalk.dim,early:chalk.cyan};console.log(chalk.bold(`\ndock check — phase ${a.phase}${r?chalk.dim(" (temporary daemon)"):""}\n`));for(const e of a.results){const o=c[e.result](e.result.padEnd(8));console.log(` ${o} ${e.phase} ${e.id.padEnd(20)} ${chalk.dim(e.detail)}`)}const i=a.tally;console.log(`\n ${chalk.green(`${i.pass||0} pass`)} ${chalk.red(`${i.fail||0} fail`)} ${i.pending||0} pending ${i.early||0} early`);const l=o.indexOf("--md");return-1!==l&&o[l+1]&&(fs.writeFileSync(o[l+1],t(a)),console.log(chalk.dim(` report written to ${o[l+1]}`))),a.ok}finally{r&&(await r.daemon.stop(),fs.rmSync(r.home,{recursive:!0,force:!0}))}}async function selftest(e){if(!await ping(e.socket)&&!await start(e))return!1;console.log(chalk.bold(`\ndock selftest — ${process.platform}/${process.arch}\n`));const{runSelftest:o}=require("../../dock/selftest"),{report:s,file:t}=await o({log:e=>console.log(` ${e.startsWith("✓")?chalk.green(e):chalk.red(e)}`)}),n=s.passed===s.total?chalk.green:chalk.red;return console.log(n(`\n ${s.passed}/${s.total} checks passed`)),console.log(chalk.dim(` report: ${t} (share this file)\n`)),s.passed===s.total}function parseSize(e){const o=/^(\d+(?:\.\d+)?)\s*([mg]?)b?$/i.exec(String(e||"").trim());if(!o)throw new Error(`invalid size "${e}" (examples: 4g, 6144m)`);return Math.round(parseFloat(o[1])*("m"===o[2].toLowerCase()?1:1024))}async function configCmd(e){const{readConfig:o,writeConfig:s,effective:t,configFile:n}=require("../../dock/config"),r=o();let a=!1;for(let o=0;o<e.length;o++){const s=e[o],t=()=>{if(o+1>=e.length)throw new Error(`${s} needs a value`);return e[++o]};if("--cpus"===s)r.cpus=Math.max(1,parseInt(t(),10)),a=!0;else if("--memory"===s||"-m"===s)r.memoryMB=parseSize(t()),a=!0;else if("--disk-size"===s)r.diskGB=parseInt(t(),10),a=!0;else if("--share"===s){const e=path.resolve(t().replace(/^~(?=\/|$)/,require("os").homedir()));if(!fs.existsSync(e)||!fs.statSync(e).isDirectory())throw new Error(`${e} is not a folder`);r.shares=[...new Set([...r.shares||[],e])],a=!0}else if("--unshare"===s){const e=path.resolve(t());r.shares=(r.shares||[]).filter(o=>o!==e),a=!0}else if("--publish-address"===s){const e=t();if(!require("net").isIP(e))throw new Error(`--publish-address needs an IP address (127.0.0.1, 0.0.0.0 …), not "${e}"`);r.publishAddress=e,a=!0}else{if("--reset"!==s)throw new Error(`unknown option ${s} (--cpus N, --memory 8g, --disk-size GB, --share PATH, --unshare PATH, --publish-address IP, --reset)`);for(const e of Object.keys(r))delete r[e];a=!0}}a&&s(r);const c=t(),i=(e,s)=>void 0!==o()[e]?s:`${s} ${chalk.dim("(default)")}`;console.log(` cpus ${i("cpus",c.cpus)}`),console.log(` memory ${i("memoryMB",`${c.memoryMB} MB`)} ${chalk.dim("a ceiling: macOS backs only what containers use")}`),console.log(` disk size ${i("diskGB",`${c.diskGB} GB`)} ${chalk.dim("used when the data disk is created")}`),console.log(` shared ${[require("os").homedir(),"/private/tmp","/private/var/folders","/Volumes",...c.shares].join(", ")}`),console.log(` ports ${i("publishAddress",c.publishAddress)} ${chalk.dim("127.0.0.1"===c.publishAddress?"-p without an address: this machine only":"-p without an address: open to the network")}`),console.log(chalk.dim(` file: ${n()}`));const l=e.filter(e=>e.startsWith("-")&&"--publish-address"!==e);return a&&l.length?console.log(chalk.yellow(" applies when the Linux VM next boots: thinknsh dock vm-restart (stops running containers)")):a&&console.log(chalk.dim(" ports: applies to containers started from now on")),!0}async function migrateCmd(e,o){const{migrate:s}=require("../../dock/setup/migrate"),t=e=>{const s=o.indexOf(e);return-1===s?void 0:o[s+1]};if(!await ping(e.socket))return console.log(chalk.red("✗ dock is not running. Start it with: thinknsh dock start")),!1;try{const n=await s({from:t("--from"),to:`unix://${e.socket}`,images:o.includes("--images"),volumes:o.includes("--volumes"),dryRun:o.includes("--dry-run"),overwrite:o.includes("--overwrite"),log:e=>console.log(chalk.dim(` ${e}`))}),r=(e,o)=>o?`${e}: ${o.moved} copied, ${o.skipped} already here`:null;return console.log(chalk.green(`✓ ${[r("images",n.images),r("volumes",n.volumes)].filter(Boolean).join("; ")}`)),console.log(chalk.dim(" containers are not moved: recreate them here (thinknsh compose up / thinknsh run)")),!0}catch(e){return console.log(chalk.red(`✗ migrate failed: ${e.message}`)),!1}}async function uiCmd(e,o){const{startUi:s}=require("../../dock/ui/server");if(!await ping(e.socket))return console.log(chalk.red("✗ dock is not running. Start it with: thinknsh dock start")),!1;const t=await s({socketPath:e.socket,port:Number((()=>{const e=o.indexOf("--port");return-1===e?void 0:o[e+1]})())||0});if(console.log(chalk.green("✓ dock dashboard")),console.log(` ${t.url}`),console.log(chalk.dim(" the link carries this session's token; Ctrl-C stops the dashboard")),!o.includes("--no-open")){const e="darwin"===process.platform?["open",[t.url]]:"win32"===process.platform?["cmd",["/c","start","",t.url]]:["xdg-open",[t.url]];try{spawn(e[0],e[1],{stdio:"ignore",detached:!0}).on("error",()=>{}).unref()}catch(e){}}return await new Promise(e=>{const o=()=>{t.close().then(e)};process.once("SIGINT",o),process.once("SIGTERM",o)}),!0}async function useCmd(e,o){const{use:s}=require("../../dock/setup/integrate");try{for(const t of s(e.socket,{off:o.includes("--off"),system:o.includes("--system")}))console.log(` ${t}`);return o.includes("--off")||console.log(chalk.green("✓ tools built for other engines now use dock")+chalk.dim(" (undo: thinknsh dock use --off)")),!0}catch(e){return console.log(chalk.red(`✗ ${e.message}`)),!1}}async function vmRestart(e,o){if(await ping(e.socket)){const s=((await request(e.socket,"GET","/containers/json")).body||[]).map(e=>e.Names[0].slice(1));if(s.length&&!o.includes("--force"))return console.log(chalk.yellow(` running containers would stop: ${s.join(", ")}`)),console.log(chalk.dim(" stop them first, or pass --force")),!1}if(await stop(e),"darwin"===process.platform){const{spawnSync:e}=require("child_process");e(path.join(require("os").homedir(),".tnc","bin","tnc-vz"),["stop"],{stdio:"ignore"})}else if("win32"===process.platform){const{spawnSync:e}=require("child_process");e("wsl.exe",["--terminate",require("../../dock/setup").WSL_DISTRO],{stdio:"ignore"})}const s=await start(e);return s&&console.log(chalk.dim(" the Linux VM boots with the new settings on the next command that needs it")),s}function appCmd(e,o){try{const s=require("../../dock/setup/app").app({off:o.includes("--off"),home:e.home,socket:e.socket,cli:process.argv[1],version:require("../../dock/setup").BUNDLE_VERSION,log:e=>console.log(chalk.dim(` ${e}`))});for(const e of s)console.log(` ${e}`);return!0}catch(e){return console.log(chalk.red(`✗ ${e.message}`)),!1}}function autostartCmd(e,o){try{const s=require("../../dock/setup/autostart").autostart({off:o.includes("--off"),cli:process.argv[1],logFile:e.logFile});for(const e of s)console.log(` ${e}`);return!0}catch(e){return console.log(chalk.red(`✗ ${e.message}`)),!1}}async function foreground(){require("../../dock/daemon-main"),await new Promise(()=>{})}module.exports={name:"dock",description:"dock: ThinkNCollab container engine daemon (TNC Engine API)",usage:"thinknsh dock <setup|start|stop|status|check|selftest|config|use|env|autostart|app|vm-restart|migrate|ui|daemon>",requiresAuth:!1,examples:["thinknsh dock setup","thinknsh dock start","thinknsh dock selftest","thinknsh dock check --md check.md","thinknsh dock config --memory 8g --cpus 6","thinknsh dock use","thinknsh dock autostart","thinknsh dock app"],async execute(e=[]){const o=(e[0]||"status").toLowerCase(),s=dockPaths();let t=!0;switch(o){case"setup":t=await setupCmd(e.slice(1));break;case"start":t=await start(s);break;case"stop":t=await stop(s);break;case"status":t=await status(s);break;case"check":t=await compat(s,e.slice(1));break;case"selftest":t=await selftest(s);break;case"daemon":await foreground();break;case"config":try{t=await configCmd(e.slice(1))}catch(e){console.log(chalk.red(`✗ ${e.message}`)),t=!1}break;case"use":t=await useCmd(s,e.slice(1));break;case"env":console.log(require("../../dock/setup/integrate").env(s.socket));break;case"vm-restart":t=await vmRestart(s,e.slice(1));break;case"migrate":t=await migrateCmd(s,e.slice(1));break;case"ui":t=await uiCmd(s,e.slice(1));break;case"autostart":t=autostartCmd(s,e.slice(1));break;case"app":t=appCmd(s,e.slice(1));break;default:console.log(chalk.yellow(`unknown subcommand "${o}". Usage: ${module.exports.usage}`)),t=!1}t||(process.exitCode=1)}};
|
|
7
|
+
"use strict";const fs=require("fs"),path=require("path"),chalk=require("chalk"),{spawn}=require("child_process"),{dockPaths}=require("../../dock/paths"),{request,ping}=require("../../dock/client"),{readPid,pidAlive}=require("../../dock/daemon"),{PHASE}=require("../../dock/version"),sleep=e=>new Promise(o=>setTimeout(o,e));async function setupCmd(e){const{setup:o}=require("../../dock/setup"),s=o=>{const s=e.indexOf(o);return-1===s?void 0:e[s+1]};try{const t=await o({from:s("--from"),baseUrl:s("--url"),force:e.includes("--force"),log:e=>console.log(chalk.dim(` ${e}`))});return console.log(chalk.green(`✓ dock runtime ${t} installed for ${process.platform}/${process.arch}`)),console.log(chalk.dim(" next: thinknsh dock start")),!0}catch(e){return console.log(chalk.red(`✗ setup failed: ${e.message}`)),!1}}async function start(e){if(await ping(e.socket))return console.log(chalk.green(`dock is already running (pid ${readPid(e.pidFile)||"?"})`)),!0;fs.mkdirSync(e.home,{recursive:!0,mode:448});let o={pid:null,exitCode:null};if(!require("../../dock/setup/autostart").kickstart()){const s=fs.openSync(e.logFile,"a"),t=[];try{require("node:sqlite")}catch(e){t.push("--experimental-sqlite")}o=spawn(process.execPath,[...t,path.join(__dirname,"..","..","dock","daemon-main.js")],{detached:!0,stdio:["ignore",s,s],env:process.env,windowsHide:!0}),o.unref(),fs.closeSync(s)}for(let s=0;s<50;s++){if(await ping(e.socket))return console.log(chalk.green(`✓ dock started (pid ${readPid(e.pidFile)||o.pid})`)),console.log(chalk.dim(` socket: ${e.socket}`)),console.log(chalk.dim(" try: thinknsh run --rm alpine echo hello")),!0;if(null!==o.exitCode)break;await sleep(100)}console.log(chalk.red(`✗ dock failed to start. Last log lines (${e.logFile}):`));try{console.log(chalk.dim(fs.readFileSync(e.logFile,"utf8").trim().split("\n").slice(-5).join("\n")))}catch(e){}return!1}async function stop(e){const o=readPid(e.pidFile);if(!o||!pidAlive(o))return console.log(chalk.dim("dock is not running")),!0;process.kill(o,"SIGTERM");for(let e=0;e<50;e++){if(!pidAlive(o))return console.log(chalk.green(`✓ dock stopped (pid ${o})`)),!0;await sleep(100)}return console.log(chalk.red(`✗ dock (pid ${o}) did not stop within 5s`)),!1}async function status(e){if(!await ping(e.socket))return console.log(`${chalk.red("●")} dock is not running ${chalk.dim("start it with: thinknsh dock start")}`),!1;const[o,s]=await Promise.all([request(e.socket,"GET","/version"),request(e.socket,"GET","/info")]);console.log(`${chalk.green("●")} dock ${o.body.Version} running (pid ${readPid(e.pidFile)||"?"})`),console.log(` socket ${e.socket}`),console.log(` API version ${o.body.ApiVersion} (min ${o.body.MinAPIVersion})`),console.log(` phase ${PHASE}`),s.body.SecurityOptions?.includes("name=rootless")&&console.log(" mode rootless (no root)"),console.log(` containers ${s.body.Containers} (${s.body.ContainersRunning} running)`),console.log(` images ${s.body.Images}`);const t=require("../../dock/setup/autostart").status().enabled;console.log(` autostart ${t?"on (starts when you log in)":chalk.dim("off (turn on: thinknsh dock autostart)")}`);for(const e of s.body.Warnings||[])console.log(chalk.yellow(` ${e}`));return!0}async function compat(e,o){const{runCompat:s,toMarkdown:t}=require("../../dock/compat/runner");let r=e.socket,n=null;if(!await ping(r)){const e=require("os"),{DockDaemon:o}=require("../../dock/daemon"),s=fs.mkdtempSync(path.join(e.tmpdir(),"tnc-dock-check-"));n={daemon:new o({home:s}),home:s},await n.daemon.start(),r=n.daemon.paths.socket}try{const i=await s({socketPath:r,home:n?n.home:e.home}),a={pass:chalk.green,fail:chalk.red,pending:chalk.dim,early:chalk.cyan};console.log(chalk.bold(`\ndock check — phase ${i.phase}${n?chalk.dim(" (temporary daemon)"):""}\n`));for(const e of i.results){const o=a[e.result](e.result.padEnd(8));console.log(` ${o} ${e.phase} ${e.id.padEnd(20)} ${chalk.dim(e.detail)}`)}const c=i.tally;console.log(`\n ${chalk.green(`${c.pass||0} pass`)} ${chalk.red(`${c.fail||0} fail`)} ${c.pending||0} pending ${c.early||0} early`);const l=o.indexOf("--md");return-1!==l&&o[l+1]&&(fs.writeFileSync(o[l+1],t(i)),console.log(chalk.dim(` report written to ${o[l+1]}`))),i.ok}finally{n&&(await n.daemon.stop(),fs.rmSync(n.home,{recursive:!0,force:!0}))}}async function selftest(e){if(!await ping(e.socket)&&!await start(e))return!1;console.log(chalk.bold(`\ndock selftest — ${process.platform}/${process.arch}\n`));const{runSelftest:o}=require("../../dock/selftest"),{report:s,file:t}=await o({log:e=>console.log(` ${e.startsWith("✓")?chalk.green(e):chalk.red(e)}`)}),r=s.passed===s.total?chalk.green:chalk.red;return console.log(r(`\n ${s.passed}/${s.total} checks passed`)),console.log(chalk.dim(` report: ${t} (share this file)\n`)),s.passed===s.total}function parseSize(e){const o=/^(\d+(?:\.\d+)?)\s*([mg]?)b?$/i.exec(String(e||"").trim());if(!o)throw new Error(`invalid size "${e}" (examples: 4g, 6144m)`);return Math.round(parseFloat(o[1])*("m"===o[2].toLowerCase()?1:1024))}async function configCmd(e){const{readConfig:o,writeConfig:s,effective:t,configFile:r}=require("../../dock/config"),n=o();let i=!1;for(let o=0;o<e.length;o++){const s=e[o],t=()=>{if(o+1>=e.length)throw new Error(`${s} needs a value`);return e[++o]};if("--cpus"===s)n.cpus=Math.max(1,parseInt(t(),10)),i=!0;else if("--memory"===s||"-m"===s)n.memoryMB=parseSize(t()),i=!0;else if("--disk-size"===s)n.diskGB=parseInt(t(),10),i=!0;else if("--share"===s){const e=path.resolve(t().replace(/^~(?=\/|$)/,require("os").homedir()));if(!fs.existsSync(e)||!fs.statSync(e).isDirectory())throw new Error(`${e} is not a folder`);n.shares=[...new Set([...n.shares||[],e])],i=!0}else if("--unshare"===s){const e=path.resolve(t());n.shares=(n.shares||[]).filter(o=>o!==e),i=!0}else if("--http-proxy"===s||"--https-proxy"===s||"--no-proxy"===s){const e={"--http-proxy":"http","--https-proxy":"https","--no-proxy":"noProxy"}[s],o=t();if(n.proxy={...n.proxy||{}},o){if("noProxy"!==e&&!/^https?:\/\/\S+$/.test(o))throw new Error(`${s} takes a URL like http://proxy.corp:3128`);n.proxy[e]=o}else delete n.proxy[e];i=!0}else if("--proxy-containers"===s){const e=t();if(!["on","off"].includes(e))throw new Error("--proxy-containers takes on or off");n.proxy={...n.proxy||{},containers:"on"===e},i=!0}else if("--ca-cert"===s){const e=path.resolve(t().replace(/^~(?=\/|$)/,require("os").homedir()));if(!fs.existsSync(e)||!/-----BEGIN CERTIFICATE-----/.test(fs.readFileSync(e,"utf8")))throw new Error(`${e} is not a PEM certificate file`);n.caCerts=[...new Set([...n.caCerts||[],e])],i=!0}else if("--no-ca-cert"===s){const e=path.resolve(t());n.caCerts=(n.caCerts||[]).filter(o=>o!==e),i=!0}else if("--mirror"===s){const e=t();if(!/^https?:\/\/[^\s/]+/.test(e))throw new Error("--mirror takes a URL like https://mirror.gcr.io");n.registryMirrors=[...new Set([...n.registryMirrors||[],e.replace(/\/+$/,"")])],i=!0}else if("--no-mirror"===s){const e=t().replace(/\/+$/,"");n.registryMirrors=(n.registryMirrors||[]).filter(o=>o!==e),i=!0}else if("--insecure-registry"===s){const e=t().toLowerCase();if(!/^[a-z0-9.-]+(:\d+)?$|^\[[0-9a-f:]+\](:\d+)?$/.test(e))throw new Error("--insecure-registry takes host or host:port");n.insecureRegistries=[...new Set([...n.insecureRegistries||[],e])],i=!0}else if("--no-insecure-registry"===s){const e=t().toLowerCase();n.insecureRegistries=(n.insecureRegistries||[]).filter(o=>o!==e),i=!0}else if("--credential-store"===s){const e=t();if(!["auto","file"].includes(e))throw new Error("--credential-store takes auto (system keychain) or file");n.credentialStore=e,i=!0}else if("--credential-helper"===s){const e=t(),o=/^([a-z0-9.-]+(?::\d+)?)=([A-Za-z0-9._-]*)$/i.exec(e);if(!o)throw new Error("--credential-helper takes HOST=HELPER, e.g. 123456789.dkr.ecr.eu-west-1.amazonaws.com=ecr-login (empty HELPER removes it)");n.credentialHelpers={...n.credentialHelpers||{}},o[2]?n.credentialHelpers[o[1].toLowerCase()]=o[2]:delete n.credentialHelpers[o[1].toLowerCase()],i=!0}else if("--publish-address"===s){const e=t();if(!require("net").isIP(e))throw new Error(`--publish-address needs an IP address (127.0.0.1, 0.0.0.0 …), not "${e}"`);n.publishAddress=e,i=!0}else if("--trust-key"===s){const e=t(),o=/^([^=]+)=(.+)$/.exec(e);if(!o)throw new Error("--trust-key takes SCOPE=PUBLIC_KEY, e.g. ghcr.io/me/*=~/keys/tnc-sign.pub");const s=path.resolve(o[2].replace(/^~(?=\/|$)/,require("os").homedir()));if(!fs.existsSync(s)||!/PUBLIC KEY/.test(fs.readFileSync(s,"utf8")))throw new Error(`${s} is not a PEM public key`);n.trustPolicy=[...(n.trustPolicy||[]).filter(e=>e.scope!==o[1]),{scope:o[1],key:s}],i=!0}else if("--no-trust-key"===s){const e=t();n.trustPolicy=(n.trustPolicy||[]).filter(o=>o.scope!==e),i=!0}else if("--userns-remap"===s){const e=t();if("off"!==e&&"default"!==e&&!/^[a-z_][a-z0-9_-]*(:[a-z_][a-z0-9_-]*)?$/i.test(e))throw new Error("--userns-remap takes default, a user name, user:group, or off");"off"===e?delete n.usernsRemap:n.usernsRemap=e,i=!0}else{if("--reset"!==s)throw new Error(`unknown option ${s} (--cpus N, --memory 8g, --disk-size GB, --share PATH, --unshare PATH, --http-proxy URL, --https-proxy URL, --no-proxy LIST, --proxy-containers on|off, --ca-cert FILE, --mirror URL, --insecure-registry HOST, --credential-store auto|file, --credential-helper HOST=NAME, --publish-address IP, --userns-remap default|USER|off, --trust-key SCOPE=KEY.pub, --no-trust-key SCOPE, --reset)`);for(const e of Object.keys(n))delete n[e];i=!0}}i&&s(n);const a=t(),c=(e,s)=>void 0!==o()[e]?s:`${s} ${chalk.dim("(default)")}`;console.log(` cpus ${c("cpus",a.cpus)}`),console.log(` memory ${c("memoryMB",`${a.memoryMB} MB`)} ${chalk.dim("a ceiling: macOS backs only what containers use")}`),console.log(` disk size ${c("diskGB",`${a.diskGB} GB`)} ${chalk.dim("used when the data disk is created")}`),console.log(` shared ${[require("os").homedir(),"/private/tmp","/private/var/folders","/Volumes",...a.shares].join(", ")}`),console.log(` ports ${c("publishAddress",a.publishAddress)} ${chalk.dim("127.0.0.1"===a.publishAddress?"-p without an address: this machine only":"-p without an address: open to the network")}`);const l=a.proxy||{},{redact:d}=require("../../dock/setup/diagnose"),u=e=>e||chalk.dim("none");console.log(` http proxy ${u(l.http&&d(l.http))}`),console.log(` https proxy ${u(l.https&&d(l.https))}${l.noProxy?` no proxy: ${l.noProxy}`:""}${(l.http||l.https)&&!1===l.containers?chalk.dim(" (not handed to containers)"):""}`),console.log(` CA certs ${u(a.caCerts.join(", "))}`),console.log(` mirrors ${u(a.registryMirrors.join(", "))}`),console.log(` insecure ${u(a.insecureRegistries.join(", "))}`),"linux"===process.platform&&console.log(` userns remap ${u(a.usernsRemap)}${a.usernsRemap?chalk.dim(" containers created from now: root is an unprivileged id range"):""}`);const h=a.trustPolicy||[];console.log(` signed only ${u(h.map(e=>`${e.scope} (${path.basename(e.key)})`).join(", "))}`),console.log(` logins in ${"file"===a.credentialStore?"auth.json":require("../../dock/image/keychain").describe()}`);const p=Object.entries(a.credentialHelpers||{});if(p.length&&console.log(` helpers ${p.map(([e,o])=>`${e} → ${o}`).join(", ")}`),console.log(chalk.dim(` file: ${r()}`)),i){const o=e.filter(e=>e.startsWith("-"));o.some(e=>["--cpus","--memory","-m","--disk-size","--share","--unshare","--reset"].includes(e))&&console.log(chalk.yellow(" VM settings apply when the Linux VM next boots: thinknsh dock vm-restart (stops running containers)")),o.some(e=>/proxy|ca-cert|--reset/.test(e))&&console.log(chalk.yellow(" proxy and CA settings apply when dock next starts: thinknsh dock stop && thinknsh dock start")),o.includes("--userns-remap")&&console.log(chalk.yellow(" userns-remap applies to containers created after dock next starts: thinknsh dock stop && thinknsh dock start")),o.includes("--publish-address")&&console.log(chalk.dim(" ports: applies to containers started from now on")),!l.http&&!l.https||require("../../dock/config").nodeHasEnvProxy()||console.log(chalk.red(` node ${process.version} ignores proxy settings: install Node 22.21+ or 24.5+`))}return!0}async function autostartCmd(e){const o=require("../../dock/setup/autostart"),s=e.includes("--off")?"off":e.find(e=>!e.startsWith("-"))||"on";try{if("on"===s){for(const s of o.enable({app:!e.includes("--no-app")}))console.log(` ${s}`);console.log(chalk.green("✓ dock starts at login")+chalk.dim(" (undo: thinknsh dock autostart off)"))}else if("off"===s){for(const e of o.disable())console.log(` ${e}`);console.log(chalk.green("✓ dock no longer starts at login")+chalk.dim(" (it keeps running now; thinknsh dock stop stops it)"))}else{if("status"!==s)throw new Error("usage: thinknsh dock autostart [on] [--no-app] | off | status");{const e=o.status();console.log(e.enabled?`${chalk.green("●")} starts at login ${chalk.dim(e.file)}${e.app?chalk.dim(" (with the menu-bar app)"):""}`:`${chalk.dim("○")} does not start at login ${chalk.dim("turn on: thinknsh dock autostart on")}`)}}return!0}catch(e){return console.log(chalk.red(`✗ ${e.message}`)),!1}}async function appCmd(e){const o=require("../../dock/tray/install"),s=e.includes("--off")?"uninstall":e.find(e=>!e.startsWith("-"))||(o.installedPath()?"open":"install");try{if("install"===s){for(const e of o.install({log:e=>console.log(chalk.dim(` ${e}`))}))console.log(` ${e}`);console.log(chalk.green("✓ tray app installed")+chalk.dim(" (it opens at login when dock autostart is on)"))}else if("uninstall"===s)for(const e of o.uninstall())console.log(` ${e}`);else{if("open"!==s)throw new Error("usage: thinknsh dock app install | uninstall | open");o.open()}return!0}catch(e){return console.log(chalk.red(`✗ ${e.message}`)),!1}}async function trayCmd(){const{TrayAgent:e}=require("../../dock/tray/agent");return await(new e).run(),!0}async function diagnoseCmd(e){const{diagnose:o}=require("../../dock/setup/diagnose"),s=await o({paths:e});return console.log(chalk.green(`✓ diagnostics bundle: ${s}`)),console.log(chalk.dim(" attach it to a bug report; it has no registry logins, env values or proxy passwords")),!0}async function resetCmd(e,o){const{reset:s}=require("../../dock/setup/diagnose"),t=o.includes("--all"),r=require("../../dock/paths").rootlessMode();if("linux"===process.platform&&0!==process.getuid?.()&&!r)throw new Error("dock runs as root on Linux: sudo thinknsh dock reset");if(console.log(chalk.yellow(" this deletes every container, image, volume and network, the build cache and the Linux VM disk"+(t?", plus settings and registry logins":""))),!o.includes("--yes")){if(!process.stdin.isTTY)throw new Error("refusing to reset without confirmation; pass --yes");const e=require("readline").createInterface({input:process.stdin,output:process.stderr}),o=await new Promise(o=>e.question("Reset dock to factory defaults? [y/N] ",o));if(e.close(),!/^y(es)?$/i.test(o.trim()))return!0}await stop(e);const{spawnSync:n}=require("child_process");if("darwin"===process.platform)n(path.join(require("os").homedir(),".tnc","bin","tnc-vz"),["stop"],{stdio:"ignore"});else if("win32"===process.platform){const e=require("../../dock/setup").WSL_DISTRO;n("wsl.exe",["-d",e,"-u","root","--","sh","-c","rm -rf /var/lib/tnc-dock/*"],{stdio:"ignore"}),n("wsl.exe",["--terminate",e],{stdio:"ignore"})}else r?((new(require("../../dock/linux/host").RootlessLinuxHost)).stop(),n("unshare",["--user","--map-root-user","--map-auto","--","rm","-rf",path.join(e.home,"rootless")],{stdio:"ignore"})):"linux"===process.platform&&n("sh",["-c",'for m in $(grep " /var/lib/tnc-dock" /proc/mounts | cut -d" " -f2 | sort -r); do umount -l "$m"; done; rm -rf /var/lib/tnc-dock'],{stdio:"ignore"});const i=s({paths:e,all:t});return console.log(chalk.green(`✓ dock reset (${i.length} items removed)`)),console.log(chalk.dim(" start again with: thinknsh dock start")),!0}async function migrateCmd(e,o){const{migrate:s}=require("../../dock/setup/migrate"),t=e=>{const s=o.indexOf(e);return-1===s?void 0:o[s+1]};if(!await ping(e.socket))return console.log(chalk.red("✗ dock is not running. Start it with: thinknsh dock start")),!1;try{const r=await s({from:t("--from"),to:`unix://${e.socket}`,images:o.includes("--images"),volumes:o.includes("--volumes"),dryRun:o.includes("--dry-run"),overwrite:o.includes("--overwrite"),log:e=>console.log(chalk.dim(` ${e}`))}),n=(e,o)=>o?`${e}: ${o.moved} copied, ${o.skipped} already here`:null;return console.log(chalk.green(`✓ ${[n("images",r.images),n("volumes",r.volumes)].filter(Boolean).join("; ")}`)),console.log(chalk.dim(" containers are not moved: recreate them here (thinknsh compose up / thinknsh run)")),!0}catch(e){return console.log(chalk.red(`✗ migrate failed: ${e.message}`)),!1}}async function uiCmd(e,o){const{startUi:s}=require("../../dock/ui/server");if(!await ping(e.socket))return console.log(chalk.red("✗ dock is not running. Start it with: thinknsh dock start")),!1;const t=await s({socketPath:e.socket,port:Number((()=>{const e=o.indexOf("--port");return-1===e?void 0:o[e+1]})())||0});return console.log(chalk.green("✓ dock dashboard")),console.log(` ${t.url}`),console.log(chalk.dim(" the link carries this session's token; Ctrl-C stops the dashboard")),o.includes("--no-open")||require("../../dock/tray/agent").openTarget(t.url),await new Promise(e=>{const o=()=>{t.close().then(e)};process.once("SIGINT",o),process.once("SIGTERM",o)}),!0}async function useCmd(e,o){const{use:s}=require("../../dock/setup/integrate");try{for(const t of s(e.socket,{off:o.includes("--off"),system:o.includes("--system")}))console.log(` ${t}`);return o.includes("--off")||console.log(chalk.green("✓ tools built for other engines now use dock")+chalk.dim(" (undo: thinknsh dock use --off)")),!0}catch(e){return console.log(chalk.red(`✗ ${e.message}`)),!1}}async function vmRestart(e,o){if(await ping(e.socket)){const s=((await request(e.socket,"GET","/containers/json")).body||[]).map(e=>e.Names[0].slice(1));if(s.length&&!o.includes("--force"))return console.log(chalk.yellow(` running containers would stop: ${s.join(", ")}`)),console.log(chalk.dim(" stop them first, or pass --force")),!1}if(await stop(e),"darwin"===process.platform){const{spawnSync:e}=require("child_process");e(path.join(require("os").homedir(),".tnc","bin","tnc-vz"),["stop"],{stdio:"ignore"})}else if("win32"===process.platform){const{spawnSync:e}=require("child_process");e("wsl.exe",["--terminate",require("../../dock/setup").WSL_DISTRO],{stdio:"ignore"})}else require("../../dock/paths").rootlessMode()&&(new(require("../../dock/linux/host").RootlessLinuxHost)).stop();const s=await start(e);return s&&console.log(chalk.dim(" the Linux VM boots with the new settings on the next command that needs it")),s}async function foreground(){require("../../dock/daemon-main"),await new Promise(()=>{})}module.exports={name:"dock",description:"dock: ThinkNCollab container engine daemon (TNC Engine API)",usage:"thinknsh dock <setup|start|stop|status|check|selftest|config|use|env|vm-restart|migrate|ui|autostart|app|tray|diagnose|reset|daemon>",requiresAuth:!1,examples:["thinknsh dock setup","thinknsh dock start","thinknsh dock selftest","thinknsh dock check --md check.md","thinknsh dock config --memory 8g --cpus 6","thinknsh dock use","thinknsh dock autostart","thinknsh dock app"],async execute(e=[]){const o=(e[0]||"status").toLowerCase(),s=dockPaths();let t=!0;switch(o){case"setup":t=await setupCmd(e.slice(1));break;case"start":t=await start(s);break;case"stop":t=await stop(s);break;case"status":t=await status(s);break;case"check":t=await compat(s,e.slice(1));break;case"selftest":t=await selftest(s);break;case"daemon":await foreground();break;case"config":try{t=await configCmd(e.slice(1))}catch(e){console.log(chalk.red(`✗ ${e.message}`)),t=!1}break;case"use":t=await useCmd(s,e.slice(1));break;case"env":console.log(require("../../dock/setup/integrate").env(s.socket));break;case"vm-restart":t=await vmRestart(s,e.slice(1));break;case"migrate":t=await migrateCmd(s,e.slice(1));break;case"ui":t=await uiCmd(s,e.slice(1));break;case"autostart":t=await autostartCmd(e.slice(1));break;case"app":t=await appCmd(e.slice(1));break;case"tray":t=await trayCmd();break;case"diagnose":t=await diagnoseCmd(s);break;case"reset":try{t=await resetCmd(s,e.slice(1))}catch(e){console.log(chalk.red(`✗ ${e.message}`)),t=!1}break;default:console.log(chalk.yellow(`unknown subcommand "${o}". Usage: ${module.exports.usage}`)),t=!1}t||(process.exitCode=1)}};
|