@justin0713/opspilot 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +142 -0
  2. package/bin/opspilot.js +280 -0
  3. package/package.json +24 -0
package/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # OpsPilot
2
+
3
+ OpsPilot is a Flask + Socket.IO based remote operations dashboard for SSH/Serial management, test execution, file transfer, and real-time collaboration (War Room).
4
+
5
+ ## Features
6
+
7
+ - Multi-target SSH command execution (single/batch/streaming shell)
8
+ - Serial console sessions (when `pyserial` is installed)
9
+ - Target/group management and command favorites
10
+ - SFTP file browser and upload/download
11
+ - Reboot/Network/SSD test workflows
12
+ - Share links for outputs/test snapshots
13
+ - War Room collaboration:
14
+ - shared terminal output
15
+ - guest approval/control
16
+ - chat (including rich text/image)
17
+ - WebRTC voice signaling
18
+ - Local JSON persistence (no external database required)
19
+ - Offline frontend vendor assets under `static/vendor/`
20
+
21
+ ## Project Structure
22
+
23
+ - `opspilot.py`: main startup entry (recommended)
24
+ - `web_app.py`: core Flask + Socket.IO app
25
+ - `ssh_agent.py`: SSH and connection pool logic
26
+ - `auth.py`: auth decorators and user helpers
27
+ - `templates/`: web UI templates (`index`, `login`, `collab`, `share`)
28
+ - `tests/`: reboot/network/ssd blueprints and test state handling
29
+ - `static/vendor/`: local frontend dependencies (React/Babel/Tailwind/xterm/socket.io/simple-peer/fonts)
30
+ - `*.json`: runtime/config/test/share/audit data files
31
+
32
+ ## Requirements
33
+
34
+ - Python 3.9+ recommended
35
+ - Windows/Linux supported (some X11 and serial paths are platform-specific)
36
+
37
+ Install Python dependencies:
38
+
39
+ ```bash
40
+ pip install -r requirements.txt
41
+ ```
42
+
43
+ Optional:
44
+
45
+ - `pyserial` for serial console support
46
+ - `pyOpenSSL` for local certificate generation (if cert files are missing)
47
+
48
+ ## Checkout Source Code
49
+
50
+ Clone by HTTPS:
51
+
52
+ ```bash
53
+ git clone https://github.com/<org-or-user>/<repo>.git
54
+ cd <repo>
55
+ ```
56
+
57
+ Clone by SSH:
58
+
59
+ ```bash
60
+ git clone git@github.com:<org-or-user>/<repo>.git
61
+ cd <repo>
62
+ ```
63
+
64
+ Checkout a specific branch:
65
+
66
+ ```bash
67
+ git fetch --all --prune
68
+ git checkout <branch-name>
69
+ git pull --ff-only
70
+ ```
71
+
72
+ Checkout a specific commit:
73
+
74
+ ```bash
75
+ git fetch --all --prune
76
+ git checkout <commit-sha>
77
+ ```
78
+
79
+ Create a working branch from current HEAD:
80
+
81
+ ```bash
82
+ git switch -c <new-branch-name>
83
+ ```
84
+
85
+ ## Run
86
+
87
+ Default (HTTPS mode, auto cert handling):
88
+
89
+ ```bash
90
+ python opspilot.py
91
+ ```
92
+
93
+ HTTP mode:
94
+
95
+ ```bash
96
+ python opspilot.py --http
97
+ ```
98
+
99
+ Custom port:
100
+
101
+ ```bash
102
+ python opspilot.py --port 6000
103
+ ```
104
+
105
+ Then open:
106
+
107
+ - `https://<host-ip>:5000` (default)
108
+ - or `http://<host-ip>:5000` with `--http`
109
+
110
+ ## Authentication
111
+
112
+ - Login page: `/login`
113
+ - Default credential in UI hint: `admin / admin` (change immediately in real environments)
114
+
115
+ ## Notes
116
+
117
+ - This project currently uses Flask debug/dev server settings for convenience.
118
+ - Data is persisted in local JSON files; back them up regularly.
119
+ - Frontend dependencies are available locally in `static/vendor`, so UI can run without public CDN access.
120
+ - Security baseline (2026-02-27):
121
+ - `llm_config.json` and `rooms.json` are local-only and ignored by git.
122
+ - Runtime logs (`*.log`) must not be committed.
123
+ - If secrets were ever committed, rotate credentials first, then rewrite history.
124
+
125
+ ## Security and Git Hygiene
126
+
127
+ - Keep secrets in environment variables or local untracked files only.
128
+ - Never commit bot tokens, API keys, chat IDs, or runtime logs.
129
+ - For Telegram bridge operations:
130
+ - restrict with `ALLOWED_USER_IDS` / `ALLOWED_CHAT_IDS`
131
+ - keep shell mode behind explicit intent (`shell:`) and safety policy
132
+ - monitor usage via `GET /api/telegram/bridge/metrics`
133
+ - If history rewrite is performed:
134
+ - force-push is required
135
+ - all collaborators must re-sync (`git fetch --all --prune` and reset/reclone)
136
+
137
+ ## Troubleshooting
138
+
139
+ - If style looks broken, force refresh (`Ctrl+F5`) to clear old browser cache.
140
+ - If `Cannot access 'xxx' before initialization` appears, ensure latest templates are deployed and cache is cleared.
141
+ - If HTTPS certificate warnings appear, trust local cert for internal use or run with `--http` in trusted networks.
142
+
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * OpsPilot Local — CLI
6
+ * Wraps Docker to pull, run, update, and manage the OpsPilot Local container.
7
+ *
8
+ * Usage:
9
+ * npx opspilot start --token <license-token>
10
+ * opspilot start --token <token> --port 5000
11
+ * opspilot stop
12
+ * opspilot logs [-f]
13
+ * opspilot update
14
+ * opspilot status
15
+ * opspilot config # show saved config
16
+ */
17
+
18
+ const { execSync, spawnSync } = require('child_process');
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const os = require('os');
22
+
23
+ // ── Constants ────────────────────────────────────────────────────────────────
24
+
25
+ const IMAGE = 'opspilot/local:latest';
26
+ const CONTAINER = 'opspilot';
27
+ const CONFIG_DIR = path.join(os.homedir(), '.opspilot');
28
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
29
+
30
+ const CYAN = '\x1b[36m';
31
+ const GREEN = '\x1b[32m';
32
+ const YELLOW = '\x1b[33m';
33
+ const RED = '\x1b[31m';
34
+ const BOLD = '\x1b[1m';
35
+ const RESET = '\x1b[0m';
36
+
37
+ // ── Helpers ──────────────────────────────────────────────────────────────────
38
+
39
+ function log(msg) { console.log(`${CYAN}[opspilot]${RESET} ${msg}`); }
40
+ function ok(msg) { console.log(`${GREEN}✓${RESET} ${msg}`); }
41
+ function warn(msg) { console.log(`${YELLOW}⚠${RESET} ${msg}`); }
42
+ function die(msg) { console.error(`${RED}✗${RESET} ${msg}`); process.exit(1); }
43
+
44
+ function run(cmd, opts = {}) {
45
+ return execSync(cmd, { stdio: opts.silent ? 'pipe' : 'inherit', encoding: 'utf8' });
46
+ }
47
+
48
+ function tryRun(cmd) {
49
+ try { return execSync(cmd, { stdio: 'pipe', encoding: 'utf8' }).trim(); }
50
+ catch { return null; }
51
+ }
52
+
53
+ function checkDocker() {
54
+ if (!tryRun('docker --version')) {
55
+ die(
56
+ 'Docker is not installed or not in PATH.\n' +
57
+ ' Install Docker Desktop: https://docs.docker.com/get-docker/'
58
+ );
59
+ }
60
+ }
61
+
62
+ // ── Config (persisted in ~/.opspilot/config.json) ────────────────────────────
63
+
64
+ function loadConfig() {
65
+ try {
66
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
67
+ } catch { return {}; }
68
+ }
69
+
70
+ function saveConfig(data) {
71
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
72
+ const merged = Object.assign(loadConfig(), data);
73
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2));
74
+ }
75
+
76
+ // ── Arg parser (zero dependencies) ──────────────────────────────────────────
77
+
78
+ function parseArgs(argv) {
79
+ const args = { flags: {}, positional: [] };
80
+ for (let i = 0; i < argv.length; i++) {
81
+ if (argv[i].startsWith('--')) {
82
+ const key = argv[i].slice(2);
83
+ if (argv[i + 1] && !argv[i + 1].startsWith('-')) {
84
+ args.flags[key] = argv[++i];
85
+ } else {
86
+ args.flags[key] = true;
87
+ }
88
+ } else if (argv[i].startsWith('-') && argv[i].length === 2) {
89
+ args.flags[argv[i].slice(1)] = true;
90
+ } else {
91
+ args.positional.push(argv[i]);
92
+ }
93
+ }
94
+ return args;
95
+ }
96
+
97
+ // ── Commands ─────────────────────────────────────────────────────────────────
98
+
99
+ function cmdStart(flags) {
100
+ checkDocker();
101
+
102
+ const cfg = loadConfig();
103
+ const token = flags.token || cfg.token || '';
104
+ const url = flags['cloud-url'] || cfg.cloudUrl || 'https://teams.codetop.net';
105
+ const port = flags.port || cfg.port || '5000';
106
+ const data = flags['data-dir'] || cfg.dataDir || 'opspilot-data';
107
+
108
+ if (!token) {
109
+ die(
110
+ 'License token required.\n' +
111
+ ' opspilot start --token <your-license-token>\n' +
112
+ ' Get a token at: https://teams.codetop.net/dashboard'
113
+ );
114
+ }
115
+
116
+ // Persist for next run
117
+ saveConfig({ token, cloudUrl: url, port, dataDir: data });
118
+
119
+ // Stop existing container if running
120
+ const existing = tryRun(`docker inspect -f "{{.State.Status}}" ${CONTAINER} 2>/dev/null`);
121
+ if (existing === 'running') {
122
+ log('Stopping existing container ...');
123
+ run(`docker stop ${CONTAINER}`, { silent: true });
124
+ }
125
+ if (existing) {
126
+ run(`docker rm -f ${CONTAINER}`, { silent: true });
127
+ }
128
+
129
+ // Pull latest image
130
+ log(`Pulling ${IMAGE} ...`);
131
+ run(`docker pull ${IMAGE}`);
132
+
133
+ // Volume arg: named volume or host path
134
+ const isAbsolute = path.isAbsolute(data);
135
+ const volArg = isAbsolute ? `${data}:/app/data` : `${data}:/app/data`;
136
+
137
+ log(`Starting OpsPilot Local on port ${port} ...`);
138
+ run(
139
+ `docker run -d ` +
140
+ `--name ${CONTAINER} ` +
141
+ `-p ${port}:5000 ` +
142
+ `-e OPSPILOT_CLOUD_URL=${url} ` +
143
+ `-e OPSPILOT_CLOUD_TOKEN=${token} ` +
144
+ `-e OPSPILOT_PORT=5000 ` +
145
+ `-v ${volArg} ` +
146
+ `--restart unless-stopped ` +
147
+ `${IMAGE}`
148
+ );
149
+
150
+ console.log('');
151
+ ok(`OpsPilot Local is running!`);
152
+ console.log(`${BOLD} URL :${RESET} http://localhost:${port}`);
153
+ console.log(`${BOLD} Data:${RESET} ${isAbsolute ? data : `docker volume '${data}'`}`);
154
+ console.log('');
155
+ console.log(` ${CYAN}opspilot logs -f${RESET} — tail live logs`);
156
+ console.log(` ${CYAN}opspilot stop${RESET} — stop the server`);
157
+ console.log(` ${CYAN}opspilot update${RESET} — upgrade to latest`);
158
+ }
159
+
160
+ function cmdStop() {
161
+ checkDocker();
162
+ const status = tryRun(`docker inspect -f "{{.State.Status}}" ${CONTAINER} 2>/dev/null`);
163
+ if (!status) {
164
+ warn(`No container named '${CONTAINER}' found.`);
165
+ return;
166
+ }
167
+ log('Stopping OpsPilot ...');
168
+ run(`docker stop ${CONTAINER}`, { silent: true });
169
+ run(`docker rm ${CONTAINER}`, { silent: true });
170
+ ok('Stopped.');
171
+ }
172
+
173
+ function cmdLogs(flags) {
174
+ checkDocker();
175
+ const follow = flags.f ? '-f' : '';
176
+ const result = spawnSync('docker', ['logs', follow, '--tail', '200', CONTAINER].filter(Boolean), { stdio: 'inherit' });
177
+ if (result.status !== 0) die(`Container '${CONTAINER}' not found. Run: opspilot start`);
178
+ }
179
+
180
+ function cmdUpdate() {
181
+ checkDocker();
182
+ const cfg = loadConfig();
183
+ if (!cfg.token) die('No saved token found. Run: opspilot start --token <token>');
184
+
185
+ log(`Pulling latest ${IMAGE} ...`);
186
+ run(`docker pull ${IMAGE}`);
187
+
188
+ log('Restarting with new image ...');
189
+ cmdStop();
190
+ cmdStart(cfg.flags || {}); // reuse saved config
191
+ }
192
+
193
+ function cmdStatus() {
194
+ checkDocker();
195
+ const status = tryRun(`docker inspect -f "{{.State.Status}}" ${CONTAINER} 2>/dev/null`);
196
+ if (!status) {
197
+ warn(`Container '${CONTAINER}' not found.`);
198
+ return;
199
+ }
200
+ const cfg = loadConfig();
201
+ const port = cfg.port || '5000';
202
+ console.log(`${BOLD}Container:${RESET} ${CONTAINER}`);
203
+ console.log(`${BOLD}Status :${RESET} ${status === 'running' ? GREEN : RED}${status}${RESET}`);
204
+ if (status === 'running') console.log(`${BOLD}URL :${RESET} http://localhost:${port}`);
205
+ }
206
+
207
+ function cmdConfig() {
208
+ const cfg = loadConfig();
209
+ if (Object.keys(cfg).length === 0) {
210
+ warn(`No config saved yet. Run: opspilot start --token <token>`);
211
+ return;
212
+ }
213
+ console.log(`${BOLD}Saved config${RESET} (${CONFIG_FILE}):`);
214
+ const safe = Object.assign({}, cfg);
215
+ if (safe.token) safe.token = safe.token.slice(0, 6) + '…'; // redact
216
+ console.log(JSON.stringify(safe, null, 2));
217
+ }
218
+
219
+ function printHelp() {
220
+ console.log(`
221
+ ${BOLD}${CYAN}OpsPilot Local${RESET} — CLI v1.0.0
222
+
223
+ ${BOLD}Usage:${RESET}
224
+ opspilot <command> [options]
225
+
226
+ ${BOLD}Commands:${RESET}
227
+ ${CYAN}start${RESET} Pull image and start the container
228
+ ${CYAN}stop${RESET} Stop and remove the container
229
+ ${CYAN}logs${RESET} Show container logs (-f to follow)
230
+ ${CYAN}update${RESET} Pull latest image and restart
231
+ ${CYAN}status${RESET} Show container status
232
+ ${CYAN}config${RESET} Show saved local config
233
+
234
+ ${BOLD}Start options:${RESET}
235
+ --token <token> License token from OpsPilot Cloud ${YELLOW}(required on first run)${RESET}
236
+ --cloud-url <url> Cloud server URL [default: https://teams.codetop.net]
237
+ --port <port> Local port [default: 5000]
238
+ --data-dir <path> Host data path or Docker volume name [default: opspilot-data]
239
+
240
+ ${BOLD}Examples:${RESET}
241
+ npx opspilot start --token eyJ...
242
+ opspilot start --token eyJ... --port 8080
243
+ opspilot logs -f
244
+ opspilot update
245
+ `);
246
+ }
247
+
248
+ // ── Main ─────────────────────────────────────────────────────────────────────
249
+
250
+ const argv = process.argv.slice(2);
251
+ const parsed = parseArgs(argv);
252
+ const cmd = parsed.positional[0];
253
+
254
+ switch (cmd) {
255
+ case 'start': cmdStart(parsed.flags); break;
256
+ case 'stop': cmdStop(); break;
257
+ case 'logs': cmdLogs(parsed.flags); break;
258
+ case 'update':
259
+ // cmdUpdate reuses saved config; allow overriding token inline
260
+ (function () {
261
+ const cfg = loadConfig();
262
+ if (parsed.flags.token) cfg.token = parsed.flags.token;
263
+ checkDocker();
264
+ log(`Pulling latest ${IMAGE} ...`);
265
+ run(`docker pull ${IMAGE}`);
266
+ log('Restarting ...');
267
+ cmdStop();
268
+ cmdStart(Object.assign({}, cfg, parsed.flags));
269
+ })();
270
+ break;
271
+ case 'status': cmdStatus(); break;
272
+ case 'config': cmdConfig(); break;
273
+ case undefined:
274
+ case 'help':
275
+ case '--help':
276
+ case '-h':
277
+ printHelp(); break;
278
+ default:
279
+ die(`Unknown command: ${cmd}\nRun: opspilot --help`);
280
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@justin0713/opspilot",
3
+ "version": "1.0.0",
4
+ "description": "CLI installer for OpsPilot Local — self-hosted SSH operations platform",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/Albert0977/ShellShare#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Albert0977/ShellShare.git"
10
+ },
11
+ "keywords": ["opspilot", "ssh", "terminal", "ops", "docker"],
12
+ "bin": {
13
+ "opspilot": "bin/opspilot.js"
14
+ },
15
+ "engines": {
16
+ "node": ">=18.0.0"
17
+ },
18
+ "files": [
19
+ "bin/"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public"
23
+ }
24
+ }