@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.
- package/README.md +142 -0
- package/bin/opspilot.js +280 -0
- 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
|
+
|
package/bin/opspilot.js
ADDED
|
@@ -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
|
+
}
|