@askjo/camofox-browser 1.8.9 → 1.8.11
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/AGENTS.md +580 -0
- package/Dockerfile +2 -2
- package/README.md +69 -29
- package/lib/auth.js +6 -6
- package/lib/metrics.js +3 -3
- package/lib/openapi.js +1 -1
- package/lib/plugins.js +12 -12
- package/lib/proxy.js +9 -9
- package/lib/reporter.js +24 -24
- package/lib/request-utils.js +1 -1
- package/lib/resources.js +4 -4
- package/lib/snapshot.js +1 -1
- package/lib/tmp-cleanup.js +1 -1
- package/openclaw.plugin.json +97 -1
- package/package.json +46 -3
- package/plugins/vnc/index.js +2 -2
- package/plugins/vnc/vnc-launcher.js +2 -2
- package/plugins/vnc/vnc-watcher.sh +3 -3
- package/plugins/vnc/vnc.test.js +2 -2
- package/plugins/youtube/index.js +2 -2
- package/plugins/youtube/youtube.js +1 -1
- package/scripts/install-plugin-deps.sh +1 -1
- package/scripts/plugin.js +19 -19
- package/scripts/plugin.test.js +2 -2
- package/server.js +31 -31
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "camofox-browser",
|
|
3
3
|
"name": "Camofox Browser",
|
|
4
4
|
"description": "Anti-detection browser automation for AI agents using Camoufox (Firefox-based)",
|
|
5
|
-
"version": "1.8.
|
|
5
|
+
"version": "1.8.11",
|
|
6
6
|
"envVars": {
|
|
7
7
|
"CAMOFOX_API_KEY": {
|
|
8
8
|
"description": "Secret key for the cookie-import endpoint. Cookie import is disabled when unset. Only set this if you need to import browser cookies and the server is local or access-controlled.",
|
|
@@ -163,5 +163,101 @@
|
|
|
163
163
|
"label": "Crash Report Relay URL",
|
|
164
164
|
"placeholder": "https://camofox-crash-relay.askjo.workers.dev/report"
|
|
165
165
|
}
|
|
166
|
+
},
|
|
167
|
+
"tools": [
|
|
168
|
+
"camofox_create_tab",
|
|
169
|
+
"camofox_snapshot",
|
|
170
|
+
"camofox_click",
|
|
171
|
+
"camofox_type",
|
|
172
|
+
"camofox_navigate",
|
|
173
|
+
"camofox_scroll",
|
|
174
|
+
"camofox_screenshot",
|
|
175
|
+
"camofox_close_tab",
|
|
176
|
+
"camofox_list_tabs",
|
|
177
|
+
"camofox_import_cookies"
|
|
178
|
+
],
|
|
179
|
+
"runtimeDependencies": {
|
|
180
|
+
"camoufox": {
|
|
181
|
+
"description": "Camoufox anti-detection browser (Firefox fork). Downloaded at install time by camoufox-js from the official Camoufox GitHub releases.",
|
|
182
|
+
"source": "https://github.com/nicedayzhu/camoufox/releases",
|
|
183
|
+
"installer": "camoufox-js (npm)",
|
|
184
|
+
"installedBy": "postinstall script: npx camoufox-js fetch",
|
|
185
|
+
"sizeApprox": "300MB",
|
|
186
|
+
"platforms": [
|
|
187
|
+
"linux-x86_64",
|
|
188
|
+
"linux-aarch64",
|
|
189
|
+
"macos-x86_64",
|
|
190
|
+
"macos-aarch64"
|
|
191
|
+
],
|
|
192
|
+
"verifiedBy": "camoufox-js verifies download integrity via GitHub release checksums"
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
"permissions": {
|
|
196
|
+
"network": {
|
|
197
|
+
"outbound": [
|
|
198
|
+
{
|
|
199
|
+
"target": "User-specified URLs (browsed pages)",
|
|
200
|
+
"purpose": "Browser automation -- navigating to URLs the agent requests",
|
|
201
|
+
"gatedBy": "Agent request via API"
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"target": "https://camofox-crash-relay.askjo.workers.dev/report",
|
|
205
|
+
"purpose": "Anonymized crash/hang reporting",
|
|
206
|
+
"gatedBy": "CAMOFOX_CRASH_REPORT_ENABLED (default: true, set false to disable)"
|
|
207
|
+
}
|
|
208
|
+
],
|
|
209
|
+
"inbound": {
|
|
210
|
+
"target": "localhost:9377 (default)",
|
|
211
|
+
"purpose": "REST API for browser automation",
|
|
212
|
+
"gatedBy": "CAMOFOX_ACCESS_KEY (optional -- when set, all routes require Bearer auth)"
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
"filesystem": {
|
|
216
|
+
"read": [
|
|
217
|
+
{
|
|
218
|
+
"path": "~/.camofox/cookies/",
|
|
219
|
+
"purpose": "Read Netscape cookie files for authenticated browsing",
|
|
220
|
+
"gatedBy": "CAMOFOX_API_KEY (disabled entirely when unset)"
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"path": "/proc/self/status (Linux only)",
|
|
224
|
+
"purpose": "Memory and resource metrics for crash reports",
|
|
225
|
+
"gatedBy": "Only read when crash reporting is enabled"
|
|
226
|
+
}
|
|
227
|
+
],
|
|
228
|
+
"write": [
|
|
229
|
+
{
|
|
230
|
+
"path": "~/.camofox/profiles/<hashed-userId>/",
|
|
231
|
+
"purpose": "Persisted session state (cookies + localStorage) so users stay logged in across restarts",
|
|
232
|
+
"gatedBy": "persistence plugin (enabled by default, disable via camofox.config.json)"
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"path": "~/.camofox/traces/<hashed-userId>/",
|
|
236
|
+
"purpose": "Optional Playwright session traces for debugging",
|
|
237
|
+
"gatedBy": "trace: true flag on tab creation (opt-in per session, off by default)"
|
|
238
|
+
}
|
|
239
|
+
]
|
|
240
|
+
},
|
|
241
|
+
"subprocess": [
|
|
242
|
+
{
|
|
243
|
+
"binary": "Camoufox (Firefox fork)",
|
|
244
|
+
"purpose": "The browser engine -- core functionality",
|
|
245
|
+
"isolation": "lib/launcher.js (child_process isolated from route handlers)"
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"binary": "yt-dlp (optional)",
|
|
249
|
+
"purpose": "YouTube transcript extraction (fast path)",
|
|
250
|
+
"isolation": "plugins/youtube/youtube.js (child_process isolated from route handlers)"
|
|
251
|
+
}
|
|
252
|
+
]
|
|
253
|
+
},
|
|
254
|
+
"securityModel": {
|
|
255
|
+
"architecture": "All process.env reads are in lib/config.js. All child_process usage is in lib/launcher.js and plugins/youtube/youtube.js. server.js has route handlers but zero process.env reads and zero child_process imports. No single file combines secrets access with network sends.",
|
|
256
|
+
"cookieImport": "DISABLED by default. Requires CAMOFOX_API_KEY to be explicitly set. Without the key, the server rejects all cookie requests with 403. Cookie files are read from a sandboxed directory (~/.camofox/cookies/) with path traversal protection.",
|
|
257
|
+
"accessControl": "CAMOFOX_ACCESS_KEY provides global bearer auth for all routes (except /health). Recommended for any non-localhost deployment.",
|
|
258
|
+
"crashReporting": "Anonymized via lib/reporter.js (L28-290). Private domains are HMAC-hashed. No page content, cookies, tokens, IPs, or user data is ever sent. Relay source is in-repo and auditable. Verification endpoint: GET /source returns commit hash and sha256.",
|
|
259
|
+
"binaryDownload": "Camoufox is downloaded at npm install time by camoufox-js (an npm package from the Camoufox project). Downloaded from official GitHub releases with integrity verification by camoufox-js.",
|
|
260
|
+
"sessionPersistence": "User sessions are persisted to ~/.camofox/profiles/ so authenticated browsing survives restarts. UserIds are hashed for directory names. Disable via persistence plugin config.",
|
|
261
|
+
"noEmbeddedSecrets": "Zero credentials, private keys, or tokens ship in this package. All secrets are environment variables or Cloudflare Worker secrets."
|
|
166
262
|
}
|
|
167
263
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askjo/camofox-browser",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.11",
|
|
4
4
|
"description": "Headless browser automation server and OpenClaw plugin for AI agents - anti-detection, element refs, and session isolation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"run.sh",
|
|
46
46
|
"Dockerfile",
|
|
47
47
|
"README.md",
|
|
48
|
-
"LICENSE"
|
|
48
|
+
"LICENSE",
|
|
49
|
+
"AGENTS.md"
|
|
49
50
|
],
|
|
50
51
|
"openclaw": {
|
|
51
52
|
"extensions": [
|
|
@@ -56,7 +57,49 @@
|
|
|
56
57
|
},
|
|
57
58
|
"build": {
|
|
58
59
|
"openclawVersion": "2026.4.26"
|
|
59
|
-
}
|
|
60
|
+
},
|
|
61
|
+
"tools": [
|
|
62
|
+
{
|
|
63
|
+
"name": "camofox_create_tab",
|
|
64
|
+
"description": "Open a new browser tab at a URL"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"name": "camofox_snapshot",
|
|
68
|
+
"description": "Get accessibility snapshot with element refs"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"name": "camofox_click",
|
|
72
|
+
"description": "Click an element by ref or CSS selector"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "camofox_type",
|
|
76
|
+
"description": "Type text into an element"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "camofox_navigate",
|
|
80
|
+
"description": "Navigate to URL or search macro"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "camofox_scroll",
|
|
84
|
+
"description": "Scroll page up/down/left/right"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"name": "camofox_screenshot",
|
|
88
|
+
"description": "Capture page screenshot"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "camofox_close_tab",
|
|
92
|
+
"description": "Close a browser tab"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"name": "camofox_list_tabs",
|
|
96
|
+
"description": "List open tabs for a user"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"name": "camofox_import_cookies",
|
|
100
|
+
"description": "Import Netscape cookie file (requires CAMOFOX_API_KEY)"
|
|
101
|
+
}
|
|
102
|
+
]
|
|
60
103
|
},
|
|
61
104
|
"scripts": {
|
|
62
105
|
"start": "node server.js",
|
package/plugins/vnc/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* VNC plugin for camofox-browser.
|
|
3
3
|
*
|
|
4
4
|
* Exposes Camoufox's virtual display via noVNC so a human can interact with
|
|
5
|
-
* the browser visually
|
|
5
|
+
* the browser visually -- log into sites, solve CAPTCHAs, approve OAuth prompts.
|
|
6
6
|
* After interactive login, export the storage state via the API endpoint this
|
|
7
7
|
* plugin registers.
|
|
8
8
|
*
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* NOVNC_PORT=6080 noVNC web UI port
|
|
36
36
|
*
|
|
37
37
|
* Registers:
|
|
38
|
-
* GET /sessions/:userId/storage_state
|
|
38
|
+
* GET /sessions/:userId/storage_state -- export Playwright storageState as JSON
|
|
39
39
|
*
|
|
40
40
|
* Events emitted:
|
|
41
41
|
* vnc:watcher:started { pid }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* VNC launcher
|
|
2
|
+
* VNC launcher -- owns all process spawning and env reads.
|
|
3
3
|
* Isolated from route handlers for OpenClaw scanner compliance.
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -11,7 +11,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Resolve VNC configuration from pluginConfig + env var fallbacks.
|
|
14
|
-
* All process.env reads live here
|
|
14
|
+
* All process.env reads live here -- callers get a plain config object.
|
|
15
15
|
*/
|
|
16
16
|
export function resolveVncConfig(pluginConfig = {}) {
|
|
17
17
|
const enabled = process.env.ENABLE_VNC === '1' || pluginConfig.enabled === true;
|
|
@@ -32,7 +32,7 @@ else
|
|
|
32
32
|
log "x11vnc: NO password (bind $NOVNC_PORT to 127.0.0.1 on host + SSH tunnel)"
|
|
33
33
|
fi
|
|
34
34
|
|
|
35
|
-
# Start noVNC (websockify)
|
|
35
|
+
# Start noVNC (websockify) -- proxies to x11vnc regardless of whether it's up yet
|
|
36
36
|
NOVNC_DIR="/usr/share/novnc"
|
|
37
37
|
if [ ! -d "$NOVNC_DIR" ]; then
|
|
38
38
|
log "ERROR: $NOVNC_DIR not found; noVNC cannot start"
|
|
@@ -42,7 +42,7 @@ VNC_BIND="${VNC_BIND:-127.0.0.1}"
|
|
|
42
42
|
log "Starting noVNC (websockify) on $VNC_BIND:$NOVNC_PORT -> 127.0.0.1:$VNC_PORT"
|
|
43
43
|
websockify --web "$NOVNC_DIR" "$VNC_BIND:$NOVNC_PORT" "127.0.0.1:$VNC_PORT" >/var/log/novnc.log 2>&1 &
|
|
44
44
|
|
|
45
|
-
log "VNC watcher started
|
|
45
|
+
log "VNC watcher started -- will attach x11vnc when Camoufox's Xvfb appears"
|
|
46
46
|
|
|
47
47
|
while true; do
|
|
48
48
|
# Find Xvfb with our patched resolution
|
|
@@ -53,7 +53,7 @@ while true; do
|
|
|
53
53
|
' | head -1)
|
|
54
54
|
|
|
55
55
|
if [ -n "$FOUND" ] && [ "$FOUND" != "$CURRENT_DISPLAY" ]; then
|
|
56
|
-
# New or changed display
|
|
56
|
+
# New or changed display -- (re)attach x11vnc
|
|
57
57
|
if [ -n "$X11VNC_PID" ] && kill -0 "$X11VNC_PID" 2>/dev/null; then
|
|
58
58
|
log "Camoufox display changed ($CURRENT_DISPLAY -> $FOUND), restarting x11vnc"
|
|
59
59
|
kill "$X11VNC_PID" 2>/dev/null || true
|
package/plugins/vnc/vnc.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import { jest } from '@jest/globals';
|
|
3
3
|
|
|
4
|
-
// Mock the launcher module
|
|
4
|
+
// Mock the launcher module -- index.js no longer imports child_process directly
|
|
5
5
|
const mockWatcher = () => {
|
|
6
6
|
const proc = new EventEmitter();
|
|
7
7
|
proc.pid = 12345;
|
|
@@ -169,7 +169,7 @@ describe('vnc plugin', () => {
|
|
|
169
169
|
|
|
170
170
|
await handler(req, res);
|
|
171
171
|
expect(res.status).toHaveBeenCalledWith(500);
|
|
172
|
-
// safeError returns the message string
|
|
172
|
+
// safeError returns the message string -- not the raw Error object
|
|
173
173
|
expect(res.json).toHaveBeenCalledWith({ error: 'context destroyed' });
|
|
174
174
|
});
|
|
175
175
|
|
package/plugins/youtube/index.js
CHANGED
|
@@ -20,7 +20,7 @@ export async function register(app, ctx, pluginConfig = {}) {
|
|
|
20
20
|
await detectYtDlp(log);
|
|
21
21
|
|
|
22
22
|
// Auth is on by default; set { "auth": false } in camofox.config.json to disable
|
|
23
|
-
// Auth off by default
|
|
23
|
+
// Auth off by default -- matches pre-plugin behavior. Set { "auth": true } to require auth.
|
|
24
24
|
const middleware = pluginConfig.auth === true ? ctx.auth() : (_req, _res, next) => next();
|
|
25
25
|
|
|
26
26
|
app.post('/youtube/transcript', middleware, async (req, res) => {
|
|
@@ -73,7 +73,7 @@ export async function register(app, ctx, pluginConfig = {}) {
|
|
|
73
73
|
}
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
// Browser fallback
|
|
76
|
+
// Browser fallback -- play video, intercept timedtext network response
|
|
77
77
|
async function browserTranscript(reqId, url, videoId, lang) {
|
|
78
78
|
return await withUserLimit('__yt_transcript__', async () => {
|
|
79
79
|
await ensureBrowser();
|
|
@@ -94,7 +94,7 @@ async function detectYtDlp(log) {
|
|
|
94
94
|
return true;
|
|
95
95
|
} catch {}
|
|
96
96
|
}
|
|
97
|
-
log('warn', 'yt-dlp not found
|
|
97
|
+
log('warn', 'yt-dlp not found -- YouTube transcript endpoint will use browser fallback');
|
|
98
98
|
return false;
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -24,7 +24,7 @@ if [ -f "$CONFIG" ] && command -v node >/dev/null 2>&1; then
|
|
|
24
24
|
fi
|
|
25
25
|
|
|
26
26
|
if [ -z "$PLUGIN_LIST" ]; then
|
|
27
|
-
# No config or no plugins key
|
|
27
|
+
# No config or no plugins key -- use all plugin directories
|
|
28
28
|
PLUGIN_LIST=""
|
|
29
29
|
for d in "$PLUGINS_DIR"/*/; do
|
|
30
30
|
[ -d "$d" ] || continue
|
package/scripts/plugin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* camofox plugin manager
|
|
4
|
+
* camofox plugin manager -- install, remove, and list plugins.
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
7
|
* node scripts/plugin.js install <source> Install a plugin from git URL or local path
|
|
@@ -32,7 +32,7 @@ const ROOT = path.join(__dirname, '..');
|
|
|
32
32
|
const PLUGINS_DIR = path.join(ROOT, 'plugins');
|
|
33
33
|
const CONFIG_PATH = path.join(ROOT, 'camofox.config.json');
|
|
34
34
|
|
|
35
|
-
//
|
|
35
|
+
// -- Config helpers ----------------------------------------------------------
|
|
36
36
|
|
|
37
37
|
function readConfig() {
|
|
38
38
|
try {
|
|
@@ -94,7 +94,7 @@ function removeFromConfig(name) {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
//
|
|
97
|
+
// -- Source parsing ----------------------------------------------------------
|
|
98
98
|
|
|
99
99
|
function parseSource(source) {
|
|
100
100
|
// Local path
|
|
@@ -109,11 +109,11 @@ function parseSource(source) {
|
|
|
109
109
|
return { type: 'local', path: resolved, name: path.basename(resolved) };
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
// Git URL
|
|
112
|
+
// Git URL -- https://, ssh://, git@, git:
|
|
113
113
|
let gitUrl = source;
|
|
114
114
|
if (gitUrl.startsWith('git:')) {
|
|
115
115
|
gitUrl = gitUrl.slice(4);
|
|
116
|
-
// git:github.com/user/repo
|
|
116
|
+
// git:github.com/user/repo -> https://github.com/user/repo
|
|
117
117
|
if (!gitUrl.startsWith('http') && !gitUrl.startsWith('ssh://') && !gitUrl.startsWith('git@')) {
|
|
118
118
|
gitUrl = `https://${gitUrl}`;
|
|
119
119
|
}
|
|
@@ -132,7 +132,7 @@ function parseSource(source) {
|
|
|
132
132
|
return { type: 'git', url: cloneUrl, name };
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
//
|
|
135
|
+
// -- Install -----------------------------------------------------------------
|
|
136
136
|
|
|
137
137
|
function isPluginDir(dir) {
|
|
138
138
|
const indexPath = path.join(dir, 'index.js');
|
|
@@ -177,12 +177,12 @@ function installFromGit(url, name) {
|
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
if (installed.length === 0) {
|
|
180
|
-
fatal(`No plugins found in ${url}
|
|
180
|
+
fatal(`No plugins found in ${url} -- expected index.js with register() at root or in plugins/*/`);
|
|
181
181
|
}
|
|
182
182
|
return installed;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
fatal(`No plugins found in ${url}
|
|
185
|
+
fatal(`No plugins found in ${url} -- expected index.js with register() at root or plugins/*/ subdirs`);
|
|
186
186
|
} finally {
|
|
187
187
|
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true });
|
|
188
188
|
}
|
|
@@ -198,16 +198,16 @@ function installPluginDeps(name) {
|
|
|
198
198
|
execSync('npm install --omit=dev', { cwd: pluginDir, stdio: 'inherit' });
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
// Check for apt.txt / post-install.sh (just warn
|
|
201
|
+
// Check for apt.txt / post-install.sh (just warn -- can't run apt locally)
|
|
202
202
|
if (fs.existsSync(path.join(pluginDir, 'apt.txt'))) {
|
|
203
|
-
console.log(
|
|
203
|
+
console.log(`WARNING ${name} has apt.txt -- system packages need Docker build or manual install`);
|
|
204
204
|
}
|
|
205
205
|
if (fs.existsSync(path.join(pluginDir, 'post-install.sh'))) {
|
|
206
|
-
console.log(
|
|
206
|
+
console.log(`WARNING ${name} has post-install.sh -- run it manually or rebuild Docker image`);
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
//
|
|
210
|
+
// -- Remove ------------------------------------------------------------------
|
|
211
211
|
|
|
212
212
|
function removePlugin(name) {
|
|
213
213
|
const pluginDir = path.join(PLUGINS_DIR, name);
|
|
@@ -217,10 +217,10 @@ function removePlugin(name) {
|
|
|
217
217
|
|
|
218
218
|
fs.rmSync(pluginDir, { recursive: true });
|
|
219
219
|
removeFromConfig(name);
|
|
220
|
-
console.log(
|
|
220
|
+
console.log(`[ok] Removed plugin "${name}"`);
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
//
|
|
223
|
+
// -- List --------------------------------------------------------------------
|
|
224
224
|
|
|
225
225
|
function listPlugins() {
|
|
226
226
|
const config = readConfig();
|
|
@@ -244,7 +244,7 @@ function listPlugins() {
|
|
|
244
244
|
console.log('Installed plugins:\n');
|
|
245
245
|
for (const name of plugins.sort()) {
|
|
246
246
|
const enabled = configPlugins.size === 0 || configPlugins.has(name);
|
|
247
|
-
const status = enabled ? '
|
|
247
|
+
const status = enabled ? '[ok]' : 'o';
|
|
248
248
|
const hasTest = fs.existsSync(path.join(PLUGINS_DIR, name, `${name}.test.js`))
|
|
249
249
|
|| fs.readdirSync(path.join(PLUGINS_DIR, name)).some(f => f.endsWith('.test.js'));
|
|
250
250
|
const hasDeps = fs.existsSync(path.join(PLUGINS_DIR, name, 'apt.txt'))
|
|
@@ -263,11 +263,11 @@ function listPlugins() {
|
|
|
263
263
|
if (configPlugins.size > 0) {
|
|
264
264
|
console.log(`\n${configPlugins.size} plugin(s) enabled in camofox.config.json`);
|
|
265
265
|
} else {
|
|
266
|
-
console.log('\nNo plugins[] in config
|
|
266
|
+
console.log('\nNo plugins[] in config -- all plugins are loaded');
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
//
|
|
270
|
+
// -- Helpers -----------------------------------------------------------------
|
|
271
271
|
|
|
272
272
|
function copyDirSync(src, dest) {
|
|
273
273
|
fs.mkdirSync(dest, { recursive: true });
|
|
@@ -289,7 +289,7 @@ function fatal(msg) {
|
|
|
289
289
|
process.exit(1);
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
//
|
|
292
|
+
// -- CLI ---------------------------------------------------------------------
|
|
293
293
|
|
|
294
294
|
const [,, action, ...args] = process.argv;
|
|
295
295
|
|
|
@@ -308,7 +308,7 @@ switch (action) {
|
|
|
308
308
|
installPluginDeps(name);
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
console.log(`\n
|
|
311
|
+
console.log(`\n[ok] Installed: ${installed.join(', ')}`);
|
|
312
312
|
console.log(' Restart the server to load new plugin(s).');
|
|
313
313
|
break;
|
|
314
314
|
}
|
package/scripts/plugin.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for scripts/plugin.js
|
|
2
|
+
* Tests for scripts/plugin.js -- plugin install, remove, list.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import fs from 'fs';
|
|
@@ -32,7 +32,7 @@ describe('plugin list', () => {
|
|
|
32
32
|
test('lists youtube as enabled', () => {
|
|
33
33
|
const out = run('list');
|
|
34
34
|
expect(out).toContain('youtube');
|
|
35
|
-
expect(out).toContain('
|
|
35
|
+
expect(out).toContain('[ok]');
|
|
36
36
|
});
|
|
37
37
|
});
|
|
38
38
|
|