browser-automation-skill 0.71.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/LICENSE +21 -0
- package/README.md +144 -0
- package/SECURITY.md +39 -0
- package/SKILL.md +206 -0
- package/bin/cli.mjs +55 -0
- package/install.sh +143 -0
- package/package.json +54 -0
- package/references/adapter-candidates.md +40 -0
- package/references/browser-mcp-cheatsheet.md +132 -0
- package/references/browser-stats-cheatsheet.md +155 -0
- package/references/chrome-devtools-mcp-cheatsheet.md +232 -0
- package/references/midscene-integration.md +359 -0
- package/references/obscura-cheatsheet.md +103 -0
- package/references/playwright-cli-cheatsheet.md +64 -0
- package/references/playwright-lib-cheatsheet.md +90 -0
- package/references/recipes/add-a-tool-adapter.md +134 -0
- package/references/recipes/agent-workflows/README.md +37 -0
- package/references/recipes/agent-workflows/cache-driven-bulk-operation.md +110 -0
- package/references/recipes/agent-workflows/flow-record-and-replay.md +102 -0
- package/references/recipes/agent-workflows/incremental-pattern-discovery.md +125 -0
- package/references/recipes/agent-workflows/login-then-scrape.md +100 -0
- package/references/recipes/anti-patterns-tool-extension.md +182 -0
- package/references/recipes/body-bytes-not-body.md +139 -0
- package/references/recipes/cache-write-security.md +210 -0
- package/references/recipes/fingerprint-rescue.md +154 -0
- package/references/recipes/model-routing.md +143 -0
- package/references/recipes/path-security.md +138 -0
- package/references/recipes/privacy-canary.md +96 -0
- package/references/recipes/visual-rescue-hook.md +182 -0
- package/references/stats-prices.json +42 -0
- package/references/stats-schema.json +77 -0
- package/references/tool-versions.md +8 -0
- package/scripts/browser-add-site.sh +113 -0
- package/scripts/browser-assert.sh +106 -0
- package/scripts/browser-audit.sh +68 -0
- package/scripts/browser-baseline.sh +135 -0
- package/scripts/browser-click.sh +100 -0
- package/scripts/browser-creds-add.sh +254 -0
- package/scripts/browser-creds-list.sh +67 -0
- package/scripts/browser-creds-migrate.sh +122 -0
- package/scripts/browser-creds-remove.sh +69 -0
- package/scripts/browser-creds-rotate-totp.sh +109 -0
- package/scripts/browser-creds-show.sh +82 -0
- package/scripts/browser-creds-totp.sh +94 -0
- package/scripts/browser-do.sh +630 -0
- package/scripts/browser-doctor.sh +365 -0
- package/scripts/browser-drag.sh +90 -0
- package/scripts/browser-extract.sh +192 -0
- package/scripts/browser-fill.sh +142 -0
- package/scripts/browser-flow.sh +316 -0
- package/scripts/browser-history.sh +187 -0
- package/scripts/browser-hover.sh +92 -0
- package/scripts/browser-inspect.sh +188 -0
- package/scripts/browser-list-sessions.sh +78 -0
- package/scripts/browser-list-sites.sh +42 -0
- package/scripts/browser-login.sh +279 -0
- package/scripts/browser-mcp.sh +65 -0
- package/scripts/browser-migrate.sh +195 -0
- package/scripts/browser-open.sh +134 -0
- package/scripts/browser-press.sh +80 -0
- package/scripts/browser-remove-session.sh +72 -0
- package/scripts/browser-remove-site.sh +68 -0
- package/scripts/browser-replay.sh +206 -0
- package/scripts/browser-route.sh +174 -0
- package/scripts/browser-select.sh +122 -0
- package/scripts/browser-show-session.sh +57 -0
- package/scripts/browser-show-site.sh +37 -0
- package/scripts/browser-snapshot.sh +176 -0
- package/scripts/browser-stats.sh +522 -0
- package/scripts/browser-tab-close.sh +112 -0
- package/scripts/browser-tab-list.sh +70 -0
- package/scripts/browser-tab-switch.sh +111 -0
- package/scripts/browser-upload.sh +132 -0
- package/scripts/browser-use.sh +60 -0
- package/scripts/browser-vlm.sh +707 -0
- package/scripts/browser-wait.sh +97 -0
- package/scripts/install-git-hooks.sh +16 -0
- package/scripts/lib/capture.sh +356 -0
- package/scripts/lib/common.sh +262 -0
- package/scripts/lib/credential.sh +237 -0
- package/scripts/lib/fingerprint-rescue.js +123 -0
- package/scripts/lib/flow.sh +448 -0
- package/scripts/lib/flow_record.sh +210 -0
- package/scripts/lib/mask.sh +49 -0
- package/scripts/lib/memory.sh +427 -0
- package/scripts/lib/migrate.sh +390 -0
- package/scripts/lib/migrators/README.md +23 -0
- package/scripts/lib/migrators/memory/v1_to_v2.sh +15 -0
- package/scripts/lib/migrators/recent_urls/README.md +13 -0
- package/scripts/lib/migrators/stats/README.md +24 -0
- package/scripts/lib/node/chrome-devtools-bridge.mjs +1812 -0
- package/scripts/lib/node/mcp-server.mjs +531 -0
- package/scripts/lib/node/mcp-tools.json +68 -0
- package/scripts/lib/node/playwright-driver.mjs +1104 -0
- package/scripts/lib/node/totp-core.mjs +52 -0
- package/scripts/lib/node/totp.mjs +52 -0
- package/scripts/lib/node/url-pattern-cluster.mjs +102 -0
- package/scripts/lib/node/url-pattern-resolver.mjs +77 -0
- package/scripts/lib/output.sh +79 -0
- package/scripts/lib/router.sh +342 -0
- package/scripts/lib/sanitize.sh +107 -0
- package/scripts/lib/secret/keychain.sh +91 -0
- package/scripts/lib/secret/libsecret.sh +74 -0
- package/scripts/lib/secret/plaintext.sh +75 -0
- package/scripts/lib/secret_backend_select.sh +57 -0
- package/scripts/lib/session.sh +153 -0
- package/scripts/lib/site.sh +126 -0
- package/scripts/lib/stats.sh +419 -0
- package/scripts/lib/tool/.gitkeep +0 -0
- package/scripts/lib/tool/chrome-devtools-mcp.sh +349 -0
- package/scripts/lib/tool/obscura.sh +249 -0
- package/scripts/lib/tool/playwright-cli.sh +155 -0
- package/scripts/lib/tool/playwright-lib.sh +106 -0
- package/scripts/lib/verb_helpers.sh +222 -0
- package/scripts/lib/visual-rescue-default.sh +145 -0
- package/scripts/regenerate-docs.sh +99 -0
- package/uninstall.sh +51 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# scripts/lib/secret_backend_select.sh
|
|
2
|
+
# Smart per-OS credentials backend auto-detection per parent spec §1.
|
|
3
|
+
#
|
|
4
|
+
# detect_backend → echoes one of: keychain | libsecret | plaintext
|
|
5
|
+
#
|
|
6
|
+
# Resolution order:
|
|
7
|
+
# 1. ${BROWSER_SKILL_FORCE_BACKEND} ∈ {keychain, libsecret, plaintext} →
|
|
8
|
+
# echoed verbatim. (Test override; also a user knob if auto-detect picks
|
|
9
|
+
# wrong on their box — e.g. Linux without a running D-Bus session.)
|
|
10
|
+
# 2. uname -s gates per-OS:
|
|
11
|
+
# - Darwin + ${KEYCHAIN_SECURITY_BIN:-security} on PATH → keychain
|
|
12
|
+
# - Linux + ${LIBSECRET_TOOL_BIN:-secret-tool} on PATH → libsecret
|
|
13
|
+
# - Anything else → plaintext
|
|
14
|
+
# 3. We do NOT probe D-Bus reachability for libsecret. That probe is brittle
|
|
15
|
+
# (no clean way to tell "no agent" from "no item matching"), and the user
|
|
16
|
+
# can override via BROWSER_SKILL_FORCE_BACKEND=plaintext when needed.
|
|
17
|
+
#
|
|
18
|
+
# Source from any verb that needs to choose a backend (creds-add, future
|
|
19
|
+
# creds-migrate, etc). Requires lib/common.sh sourced first (assert_safe_name,
|
|
20
|
+
# EXIT_*).
|
|
21
|
+
|
|
22
|
+
[ -n "${BROWSER_SKILL_SECRET_BACKEND_SELECT_LOADED:-}" ] && return 0
|
|
23
|
+
readonly BROWSER_SKILL_SECRET_BACKEND_SELECT_LOADED=1
|
|
24
|
+
|
|
25
|
+
readonly _BACKEND_VALID_SET="keychain libsecret plaintext"
|
|
26
|
+
|
|
27
|
+
detect_backend() {
|
|
28
|
+
if [ -n "${BROWSER_SKILL_FORCE_BACKEND:-}" ]; then
|
|
29
|
+
case " ${_BACKEND_VALID_SET} " in
|
|
30
|
+
*" ${BROWSER_SKILL_FORCE_BACKEND} "*)
|
|
31
|
+
printf '%s\n' "${BROWSER_SKILL_FORCE_BACKEND}"
|
|
32
|
+
return 0
|
|
33
|
+
;;
|
|
34
|
+
*)
|
|
35
|
+
# Invalid override — fall through to auto-detect (don't fail; the
|
|
36
|
+
# user typo'd a backend name but we still want a working default).
|
|
37
|
+
;;
|
|
38
|
+
esac
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
case "$(uname -s)" in
|
|
42
|
+
Darwin)
|
|
43
|
+
if command -v "${KEYCHAIN_SECURITY_BIN:-security}" >/dev/null 2>&1; then
|
|
44
|
+
printf 'keychain\n'
|
|
45
|
+
return 0
|
|
46
|
+
fi
|
|
47
|
+
;;
|
|
48
|
+
Linux)
|
|
49
|
+
if command -v "${LIBSECRET_TOOL_BIN:-secret-tool}" >/dev/null 2>&1; then
|
|
50
|
+
printf 'libsecret\n'
|
|
51
|
+
return 0
|
|
52
|
+
fi
|
|
53
|
+
;;
|
|
54
|
+
esac
|
|
55
|
+
|
|
56
|
+
printf 'plaintext\n'
|
|
57
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# scripts/lib/session.sh
|
|
2
|
+
# Read/write helpers for Playwright storageState files plus their meta sidecar.
|
|
3
|
+
# Source from any verb that needs to load or save a session.
|
|
4
|
+
# Requires lib/common.sh sourced first (init_paths must have run).
|
|
5
|
+
|
|
6
|
+
[ -n "${BROWSER_SKILL_SESSION_LOADED:-}" ] && return 0
|
|
7
|
+
readonly BROWSER_SKILL_SESSION_LOADED=1
|
|
8
|
+
|
|
9
|
+
_session_path() { printf '%s/%s.json' "${SESSIONS_DIR}" "$1"; }
|
|
10
|
+
_session_meta_path() { printf '%s/%s.meta.json' "${SESSIONS_DIR}" "$1"; }
|
|
11
|
+
|
|
12
|
+
session_exists() {
|
|
13
|
+
[ -f "$(_session_path "$1")" ]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
# session_save NAME STORAGE_STATE_JSON META_JSON
|
|
17
|
+
# Validates that storageState has top-level `cookies` and `origins` arrays
|
|
18
|
+
# (Playwright shape), then writes both files atomically at mode 0600.
|
|
19
|
+
session_save() {
|
|
20
|
+
local name="$1" ss_json="$2" meta_json="$3"
|
|
21
|
+
assert_safe_name "${name}" "session-name"
|
|
22
|
+
|
|
23
|
+
if ! printf '%s' "${ss_json}" | jq -e . >/dev/null 2>&1; then
|
|
24
|
+
die "${EXIT_USAGE_ERROR}" "session_save: storageState JSON is not valid"
|
|
25
|
+
fi
|
|
26
|
+
if ! printf '%s' "${ss_json}" | jq -e '.cookies | type == "array"' >/dev/null 2>&1; then
|
|
27
|
+
die "${EXIT_USAGE_ERROR}" "session_save: storageState missing cookies array"
|
|
28
|
+
fi
|
|
29
|
+
if ! printf '%s' "${ss_json}" | jq -e '.origins | type == "array"' >/dev/null 2>&1; then
|
|
30
|
+
die "${EXIT_USAGE_ERROR}" "session_save: storageState missing origins array"
|
|
31
|
+
fi
|
|
32
|
+
# Playwright's context.addInitScript() / .storageState() round-trip requires
|
|
33
|
+
# each origin to declare a localStorage array (may be empty). Hand-edited
|
|
34
|
+
# storageState files trip on this at browser launch — surface it here.
|
|
35
|
+
if ! printf '%s' "${ss_json}" | jq -e 'all(.origins[]; .localStorage | type == "array")' >/dev/null 2>&1; then
|
|
36
|
+
die "${EXIT_USAGE_ERROR}" "session_save: storageState origins[*].localStorage must be an array (use [] for empty); see Playwright storageState shape"
|
|
37
|
+
fi
|
|
38
|
+
if ! printf '%s' "${meta_json}" | jq -e . >/dev/null 2>&1; then
|
|
39
|
+
die "${EXIT_USAGE_ERROR}" "session_save: meta JSON is not valid"
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
mkdir -p "${SESSIONS_DIR}"
|
|
43
|
+
chmod 700 "${SESSIONS_DIR}"
|
|
44
|
+
|
|
45
|
+
local ss_path meta_path ss_tmp meta_tmp
|
|
46
|
+
ss_path="$(_session_path "${name}")"
|
|
47
|
+
meta_path="$(_session_meta_path "${name}")"
|
|
48
|
+
ss_tmp="${ss_path}.tmp.$$"
|
|
49
|
+
meta_tmp="${meta_path}.tmp.$$"
|
|
50
|
+
|
|
51
|
+
(
|
|
52
|
+
umask 077
|
|
53
|
+
printf '%s\n' "${ss_json}" | jq . > "${ss_tmp}"
|
|
54
|
+
printf '%s\n' "${meta_json}" | jq . > "${meta_tmp}"
|
|
55
|
+
)
|
|
56
|
+
chmod 600 "${ss_tmp}" "${meta_tmp}"
|
|
57
|
+
mv "${ss_tmp}" "${ss_path}"
|
|
58
|
+
mv "${meta_tmp}" "${meta_path}"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# session_load NAME → echoes the storageState JSON (Playwright shape).
|
|
62
|
+
# Missing session → exit 22 (SESSION_EXPIRED) per spec §5.5; the caller
|
|
63
|
+
# can decide whether to relogin (Phase 5) or surface to the user.
|
|
64
|
+
session_load() {
|
|
65
|
+
local name="$1"
|
|
66
|
+
local path
|
|
67
|
+
path="$(_session_path "${name}")"
|
|
68
|
+
if [ ! -f "${path}" ]; then
|
|
69
|
+
die "${EXIT_SESSION_EXPIRED}" "session not found: ${name}"
|
|
70
|
+
fi
|
|
71
|
+
cat "${path}"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# session_delete NAME — remove session storageState + meta files.
|
|
75
|
+
# Idempotent: succeeds even if files are already gone (dangling references
|
|
76
|
+
# can come from a cascading site removal that didn't clean sessions).
|
|
77
|
+
# Does NOT clear site.default_session pointers — Phase 5 may add that
|
|
78
|
+
# cascade once the credential lifecycle is more developed.
|
|
79
|
+
session_delete() {
|
|
80
|
+
local name="$1"
|
|
81
|
+
assert_safe_name "${name}" "session-name"
|
|
82
|
+
local ss_path meta_path
|
|
83
|
+
ss_path="$(_session_path "${name}")"
|
|
84
|
+
meta_path="$(_session_meta_path "${name}")"
|
|
85
|
+
rm -f "${ss_path}" "${meta_path}"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
session_meta_load() {
|
|
89
|
+
local name="$1"
|
|
90
|
+
local path
|
|
91
|
+
path="$(_session_meta_path "${name}")"
|
|
92
|
+
if [ ! -f "${path}" ]; then
|
|
93
|
+
die "${EXIT_SESSION_EXPIRED}" "session meta not found: ${name}"
|
|
94
|
+
fi
|
|
95
|
+
cat "${path}"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# url_origin URL → echoes scheme://host[:port] from a URL string.
|
|
99
|
+
# Bash-only, no python3 dep. Examples:
|
|
100
|
+
# https://app.example.com/x -> https://app.example.com
|
|
101
|
+
# https://app.example.com:8443/ -> https://app.example.com:8443
|
|
102
|
+
# http://localhost -> http://localhost
|
|
103
|
+
url_origin() {
|
|
104
|
+
local url="$1"
|
|
105
|
+
case "${url}" in
|
|
106
|
+
http://*) ;;
|
|
107
|
+
https://*) ;;
|
|
108
|
+
*) die "${EXIT_USAGE_ERROR}" "url must start with http:// or https:// (got: ${url})" ;;
|
|
109
|
+
esac
|
|
110
|
+
# Strip the path/query/fragment after the host[:port].
|
|
111
|
+
printf '%s' "${url}" | awk '
|
|
112
|
+
{
|
|
113
|
+
n = index($0, "://")
|
|
114
|
+
scheme = substr($0, 1, n + 2)
|
|
115
|
+
rest = substr($0, n + 3)
|
|
116
|
+
slash = index(rest, "/")
|
|
117
|
+
if (slash > 0) rest = substr(rest, 1, slash - 1)
|
|
118
|
+
q = index(rest, "?")
|
|
119
|
+
if (q > 0) rest = substr(rest, 1, q - 1)
|
|
120
|
+
h = index(rest, "#")
|
|
121
|
+
if (h > 0) rest = substr(rest, 1, h - 1)
|
|
122
|
+
printf "%s%s", scheme, rest
|
|
123
|
+
}'
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# session_origin_check NAME TARGET_URL
|
|
127
|
+
# Compares the session's stored origin (from meta sidecar) against the URL's
|
|
128
|
+
# origin. Exits EXIT_SESSION_EXPIRED on mismatch (spec §5.5).
|
|
129
|
+
session_origin_check() {
|
|
130
|
+
local name="$1" target_url="$2"
|
|
131
|
+
local meta_origin target_origin
|
|
132
|
+
meta_origin="$(session_meta_load "${name}" | jq -r .origin)"
|
|
133
|
+
target_origin="$(url_origin "${target_url}")"
|
|
134
|
+
if [ "${meta_origin}" != "${target_origin}" ]; then
|
|
135
|
+
die "${EXIT_SESSION_EXPIRED}" \
|
|
136
|
+
"origin mismatch: session origin=${meta_origin}, target origin=${target_origin}"
|
|
137
|
+
fi
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# session_expiry_summary NAME → emits a single-line JSON object:
|
|
141
|
+
# {"session": NAME, "captured_at": ..., "expires_in_hours": ..., "origin": ...}
|
|
142
|
+
# Used by login + later phases' relogin / doctor to surface session staleness.
|
|
143
|
+
session_expiry_summary() {
|
|
144
|
+
local name="$1"
|
|
145
|
+
local meta
|
|
146
|
+
meta="$(session_meta_load "${name}")"
|
|
147
|
+
jq -c --arg n "${name}" '{
|
|
148
|
+
session: $n,
|
|
149
|
+
origin: (.origin // null),
|
|
150
|
+
captured_at: (.captured_at // null),
|
|
151
|
+
expires_in_hours: (.expires_in_hours // null)
|
|
152
|
+
}' <<< "${meta}"
|
|
153
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# scripts/lib/site.sh
|
|
2
|
+
# Site profile read/write/list/delete + `current` file helpers.
|
|
3
|
+
# Source from any verb that needs to read or write a site profile.
|
|
4
|
+
# Requires lib/common.sh to be sourced first (init_paths must have run).
|
|
5
|
+
|
|
6
|
+
[ -n "${BROWSER_SKILL_SITE_LOADED:-}" ] && return 0
|
|
7
|
+
readonly BROWSER_SKILL_SITE_LOADED=1
|
|
8
|
+
|
|
9
|
+
# Internal: path of <name>'s profile JSON inside SITES_DIR.
|
|
10
|
+
_site_path() {
|
|
11
|
+
printf '%s/%s.json' "${SITES_DIR}" "$1"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
# Internal: path of <name>'s meta sidecar.
|
|
15
|
+
_site_meta_path() {
|
|
16
|
+
printf '%s/%s.meta.json' "${SITES_DIR}" "$1"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# True iff a site profile JSON file exists for the given name.
|
|
20
|
+
site_exists() {
|
|
21
|
+
[ -f "$(_site_path "$1")" ]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# site_save NAME PROFILE_JSON META_JSON
|
|
25
|
+
# Validates both JSON blobs, writes atomically (tmp + mv), mode 0600.
|
|
26
|
+
# Caller is responsible for shape — site.sh only validates "is it JSON".
|
|
27
|
+
site_save() {
|
|
28
|
+
local name="$1" profile_json="$2" meta_json="$3"
|
|
29
|
+
assert_safe_name "${name}" "site-name"
|
|
30
|
+
|
|
31
|
+
if ! printf '%s' "${profile_json}" | jq -e . >/dev/null 2>&1; then
|
|
32
|
+
die "${EXIT_USAGE_ERROR}" "site_save: profile JSON is not valid"
|
|
33
|
+
fi
|
|
34
|
+
if ! printf '%s' "${meta_json}" | jq -e . >/dev/null 2>&1; then
|
|
35
|
+
die "${EXIT_USAGE_ERROR}" "site_save: meta JSON is not valid"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
mkdir -p "${SITES_DIR}"
|
|
39
|
+
chmod 700 "${SITES_DIR}"
|
|
40
|
+
|
|
41
|
+
local profile_path meta_path profile_tmp meta_tmp
|
|
42
|
+
profile_path="$(_site_path "${name}")"
|
|
43
|
+
meta_path="$(_site_meta_path "${name}")"
|
|
44
|
+
profile_tmp="${profile_path}.tmp.$$"
|
|
45
|
+
meta_tmp="${meta_path}.tmp.$$"
|
|
46
|
+
|
|
47
|
+
(
|
|
48
|
+
umask 077
|
|
49
|
+
printf '%s\n' "${profile_json}" | jq . > "${profile_tmp}"
|
|
50
|
+
printf '%s\n' "${meta_json}" | jq . > "${meta_tmp}"
|
|
51
|
+
)
|
|
52
|
+
chmod 600 "${profile_tmp}" "${meta_tmp}"
|
|
53
|
+
mv "${profile_tmp}" "${profile_path}"
|
|
54
|
+
mv "${meta_tmp}" "${meta_path}"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# site_load NAME → echoes the profile JSON (un-jq'd, exactly as on disk).
|
|
58
|
+
site_load() {
|
|
59
|
+
local name="$1"
|
|
60
|
+
local path
|
|
61
|
+
path="$(_site_path "${name}")"
|
|
62
|
+
if [ ! -f "${path}" ]; then
|
|
63
|
+
die "${EXIT_SITE_NOT_FOUND}" "site not found: ${name}"
|
|
64
|
+
fi
|
|
65
|
+
cat "${path}"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# site_meta_load NAME → echoes the meta JSON.
|
|
69
|
+
site_meta_load() {
|
|
70
|
+
local name="$1"
|
|
71
|
+
local path
|
|
72
|
+
path="$(_site_meta_path "${name}")"
|
|
73
|
+
if [ ! -f "${path}" ]; then
|
|
74
|
+
die "${EXIT_SITE_NOT_FOUND}" "site meta not found: ${name}"
|
|
75
|
+
fi
|
|
76
|
+
cat "${path}"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# site_list_names → echoes each registered site name on its own line, sorted.
|
|
80
|
+
# Excludes *.meta.json files; an empty SITES_DIR (or missing) prints nothing.
|
|
81
|
+
site_list_names() {
|
|
82
|
+
if [ ! -d "${SITES_DIR}" ]; then
|
|
83
|
+
return 0
|
|
84
|
+
fi
|
|
85
|
+
find "${SITES_DIR}" -maxdepth 1 -type f -name '*.json' ! -name '*.meta.json' \
|
|
86
|
+
-exec basename {} .json \; 2>/dev/null | sort
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# site_delete NAME → rm -f the profile + meta. Idempotent.
|
|
90
|
+
# If CURRENT_FILE points at this site, clear it (orphan reference fix).
|
|
91
|
+
site_delete() {
|
|
92
|
+
local name="$1"
|
|
93
|
+
rm -f "$(_site_path "${name}")" "$(_site_meta_path "${name}")"
|
|
94
|
+
|
|
95
|
+
if [ -f "${CURRENT_FILE}" ]; then
|
|
96
|
+
local current
|
|
97
|
+
current="$(tr -d '[:space:]' < "${CURRENT_FILE}" 2>/dev/null || true)"
|
|
98
|
+
if [ "${current}" = "${name}" ]; then
|
|
99
|
+
rm -f "${CURRENT_FILE}"
|
|
100
|
+
fi
|
|
101
|
+
fi
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# current_get → echo current site name (empty string if unset).
|
|
105
|
+
current_get() {
|
|
106
|
+
if [ -f "${CURRENT_FILE}" ]; then
|
|
107
|
+
tr -d '[:space:]' < "${CURRENT_FILE}"
|
|
108
|
+
fi
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# current_set NAME → set CURRENT_FILE to NAME (must be a registered site).
|
|
112
|
+
current_set() {
|
|
113
|
+
local name="$1"
|
|
114
|
+
assert_safe_name "${name}" "site-name"
|
|
115
|
+
if ! site_exists "${name}"; then
|
|
116
|
+
die "${EXIT_SITE_NOT_FOUND}" "cannot set current: site not found: ${name}"
|
|
117
|
+
fi
|
|
118
|
+
mkdir -p "${BROWSER_SKILL_HOME}"
|
|
119
|
+
( umask 077; printf '%s\n' "${name}" > "${CURRENT_FILE}" )
|
|
120
|
+
chmod 600 "${CURRENT_FILE}"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# current_clear → rm -f CURRENT_FILE (idempotent).
|
|
124
|
+
current_clear() {
|
|
125
|
+
rm -f "${CURRENT_FILE}"
|
|
126
|
+
}
|