esbi-cli 0.2.1__py3-none-any.whl
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.
- esbi_cli/__init__.py +8 -0
- esbi_cli/ask/__init__.py +0 -0
- esbi_cli/ask/answer.py +256 -0
- esbi_cli/bench/__init__.py +0 -0
- esbi_cli/bench/cases.py +57 -0
- esbi_cli/bench/metrics.py +23 -0
- esbi_cli/bench/report.py +117 -0
- esbi_cli/bench/runner.py +114 -0
- esbi_cli/capture/__init__.py +0 -0
- esbi_cli/capture/inbox.py +63 -0
- esbi_cli/capture/legacy.py +49 -0
- esbi_cli/cli.py +1387 -0
- esbi_cli/config.py +344 -0
- esbi_cli/doctor.py +391 -0
- esbi_cli/evaluate.py +91 -0
- esbi_cli/export.py +137 -0
- esbi_cli/extract/__init__.py +107 -0
- esbi_cli/extract/clip.py +30 -0
- esbi_cli/extract/html.py +60 -0
- esbi_cli/extract/image.py +58 -0
- esbi_cli/extract/pdf.py +109 -0
- esbi_cli/gitops.py +101 -0
- esbi_cli/index.py +303 -0
- esbi_cli/ingest/__init__.py +0 -0
- esbi_cli/ingest/apply.py +480 -0
- esbi_cli/ingest/chunks.py +49 -0
- esbi_cli/ingest/connect.py +87 -0
- esbi_cli/ingest/digest.py +91 -0
- esbi_cli/ingest/pipeline.py +176 -0
- esbi_cli/ingest/plan.py +231 -0
- esbi_cli/ingest/read.py +105 -0
- esbi_cli/ingest/retrieve.py +59 -0
- esbi_cli/init.py +176 -0
- esbi_cli/interrupts.py +90 -0
- esbi_cli/lang.py +341 -0
- esbi_cli/links.py +10 -0
- esbi_cli/lint/__init__.py +0 -0
- esbi_cli/lint/checks.py +178 -0
- esbi_cli/lint/report.py +60 -0
- esbi_cli/llm/__init__.py +0 -0
- esbi_cli/llm/adapter.py +393 -0
- esbi_cli/llm/schemas.py +146 -0
- esbi_cli/mail/__init__.py +0 -0
- esbi_cli/mail/convert.py +194 -0
- esbi_cli/mail/credentials.py +65 -0
- esbi_cli/mail/fetch.py +154 -0
- esbi_cli/mail/imap.py +92 -0
- esbi_cli/netguard.py +127 -0
- esbi_cli/privacy.py +81 -0
- esbi_cli/queue.py +179 -0
- esbi_cli/reingest.py +165 -0
- esbi_cli/report/__init__.py +0 -0
- esbi_cli/report/daily_index.py +235 -0
- esbi_cli/report/index_md.py +21 -0
- esbi_cli/report/readstate.py +26 -0
- esbi_cli/run.py +100 -0
- esbi_cli/runlock.py +31 -0
- esbi_cli/runlog.py +80 -0
- esbi_cli/schedule.py +106 -0
- esbi_cli/templates/SCHEMA.md +52 -0
- esbi_cli/templates/clipper-template.json +17 -0
- esbi_cli/templates/clipper-youtube-template.json +18 -0
- esbi_cli/templates/config.example.toml +108 -0
- esbi_cli/update.py +247 -0
- esbi_cli/vault.py +188 -0
- esbi_cli/wizards/clipper.sh +271 -0
- esbi_cli/wizards/email.sh +265 -0
- esbi_cli-0.2.1.dist-info/METADATA +167 -0
- esbi_cli-0.2.1.dist-info/RECORD +72 -0
- esbi_cli-0.2.1.dist-info/WHEEL +4 -0
- esbi_cli-0.2.1.dist-info/entry_points.txt +3 -0
- esbi_cli-0.2.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Wizard: connect the Obsidian Web Clipper to esbi-cli (install, vault, template, test clip).
|
|
4
|
+
# Generated with the /wizard skill; run it with: sb setup clipper
|
|
5
|
+
#
|
|
6
|
+
# Everything above the "STAGES" marker is the wizard library: do not hand-edit
|
|
7
|
+
# it. Author the per-step stages below the marker.
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
12
|
+
# Wizard library: delightful, consistent UX, identical across every wizard.
|
|
13
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && [[ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]]; then
|
|
16
|
+
BOLD=$(tput bold); DIM=$(tput dim); RESET=$(tput sgr0)
|
|
17
|
+
BLUE=$(tput setaf 4); GREEN=$(tput setaf 2); YELLOW=$(tput setaf 3); RED=$(tput setaf 1)
|
|
18
|
+
else
|
|
19
|
+
BOLD=""; DIM=""; RESET=""; BLUE=""; GREEN=""; YELLOW=""; RED=""
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# Author sets this at the top of the stages section.
|
|
23
|
+
TOTAL_STAGES=0
|
|
24
|
+
|
|
25
|
+
_STAGE_INDEX=0
|
|
26
|
+
ENV_FILE="${ENV_FILE:-.env}"
|
|
27
|
+
WRITTEN_ENV=() # KEYs written to ENV_FILE this run
|
|
28
|
+
WRITTEN_SECRET=() # secret NAMEs set this run
|
|
29
|
+
SKIPPED=() # things we couldn't do (e.g. gh missing)
|
|
30
|
+
|
|
31
|
+
# _clear wipes the terminal so only the current step is on screen. No-op when
|
|
32
|
+
# output isn't a terminal, so piped logs stay readable.
|
|
33
|
+
_clear() {
|
|
34
|
+
[[ -t 1 ]] || return 0
|
|
35
|
+
if command -v tput >/dev/null 2>&1; then tput clear; else printf '\033[2J\033[3J\033[H'; fi
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# banner "Title" shows the opening frame: what this wizard does.
|
|
39
|
+
banner() {
|
|
40
|
+
_clear
|
|
41
|
+
printf '\n%s%s %s%s\n' "$BOLD" "$BLUE" "$1" "$RESET"
|
|
42
|
+
printf '%s %s stages%s\n\n' "$DIM" "$TOTAL_STAGES" "$RESET"
|
|
43
|
+
printf '%s You drive the browser; this wizard tells you exactly what to do and\n' "$DIM"
|
|
44
|
+
printf ' captures the values you copy back. Stop any time with Ctrl-C and re-run\n'
|
|
45
|
+
printf ' later, since it remembers values already saved.%s\n' "$RESET"
|
|
46
|
+
pause "Ready to start?"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# stage "Name" clears the screen, then announces a stage and shows progress.
|
|
50
|
+
# Clearing keeps only the current step on screen.
|
|
51
|
+
stage() {
|
|
52
|
+
_clear
|
|
53
|
+
_STAGE_INDEX=$((_STAGE_INDEX + 1))
|
|
54
|
+
printf '\n%s%s▸ Stage %s/%s · %s%s\n' \
|
|
55
|
+
"$BOLD" "$BLUE" "$_STAGE_INDEX" "$TOTAL_STAGES" "$1" "$RESET"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# say "..." prints a plain instruction line.
|
|
59
|
+
say() { printf ' %s\n' "$1"; }
|
|
60
|
+
# step "..." is a numbered-feeling action the human takes in the browser.
|
|
61
|
+
step() { printf ' %s•%s %s\n' "$BLUE" "$RESET" "$1"; }
|
|
62
|
+
note() { printf ' %s%s%s\n' "$DIM" "$1" "$RESET"; }
|
|
63
|
+
warn() { printf ' %s⚠ %s%s\n' "$YELLOW" "$1" "$RESET"; }
|
|
64
|
+
|
|
65
|
+
# open_url URL opens it in the human's browser, cross-platform incl. WSL.
|
|
66
|
+
open_url() {
|
|
67
|
+
local url="$1"
|
|
68
|
+
printf ' %s↗ opening%s %s\n' "$GREEN" "$RESET" "$url"
|
|
69
|
+
{ if command -v wslview >/dev/null 2>&1; then wslview "$url"
|
|
70
|
+
elif command -v explorer.exe >/dev/null 2>&1; then explorer.exe "$url"
|
|
71
|
+
elif command -v xdg-open >/dev/null 2>&1; then xdg-open "$url"
|
|
72
|
+
elif command -v open >/dev/null 2>&1; then open "$url"
|
|
73
|
+
else warn "couldn't open a browser; visit it manually: $url"; fi
|
|
74
|
+
} >/dev/null 2>&1 || warn "couldn't open a browser, so visit it manually: $url"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# pause "msg" waits for the human to confirm they've done the manual part.
|
|
78
|
+
pause() {
|
|
79
|
+
printf ' %s%s%s ' "$DIM" "${1:-Press Enter to continue}" "$RESET"
|
|
80
|
+
read -r _ || true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# confirm "question" is a y/N gate; returns success on yes.
|
|
84
|
+
confirm() {
|
|
85
|
+
local reply=""
|
|
86
|
+
printf ' %s? %s [y/N] ' "$YELLOW" "$1"
|
|
87
|
+
read -r reply || true
|
|
88
|
+
[[ "$reply" =~ ^[Yy] ]]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# _existing KEY: current value of KEY in ENV_FILE, if any.
|
|
92
|
+
_existing() {
|
|
93
|
+
[[ -f "$ENV_FILE" ]] || return 1
|
|
94
|
+
local line; line=$(grep -E "^${1}=" "$ENV_FILE" | tail -n1) || return 1
|
|
95
|
+
printf '%s' "${line#*=}"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# ask KEY "Prompt" reads a value into $KEY. Offers the existing .env value as
|
|
99
|
+
# a default on re-runs (Enter keeps it). Visible input (non-secret).
|
|
100
|
+
ask() {
|
|
101
|
+
local key="$1" prompt="$2" current input
|
|
102
|
+
current=$(_existing "$key" || true)
|
|
103
|
+
if [[ -n "$current" ]]; then
|
|
104
|
+
printf ' %s%s%s %s[Enter keeps current]%s ' "$BOLD" "$prompt" "$RESET" "$DIM" "$RESET"
|
|
105
|
+
else
|
|
106
|
+
printf ' %s%s%s ' "$BOLD" "$prompt" "$RESET"
|
|
107
|
+
fi
|
|
108
|
+
read -r input || true
|
|
109
|
+
[[ -z "$input" && -n "$current" ]] && input="$current"
|
|
110
|
+
printf -v "$key" '%s' "$input"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# ask_secret KEY "Prompt" is like ask, but input is hidden.
|
|
114
|
+
ask_secret() {
|
|
115
|
+
local key="$1" prompt="$2" current input
|
|
116
|
+
current=$(_existing "$key" || true)
|
|
117
|
+
if [[ -n "$current" ]]; then
|
|
118
|
+
printf ' %s%s%s %s[Enter keeps current]%s ' "$BOLD" "$prompt" "$RESET" "$DIM" "$RESET"
|
|
119
|
+
else
|
|
120
|
+
printf ' %s%s%s ' "$BOLD" "$prompt" "$RESET"
|
|
121
|
+
fi
|
|
122
|
+
read -rs input || true
|
|
123
|
+
printf '\n'
|
|
124
|
+
[[ -z "$input" && -n "$current" ]] && input="$current"
|
|
125
|
+
printf -v "$key" '%s' "$input"
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# write_env KEY VALUE upserts KEY=VALUE into ENV_FILE (creates it; replaces
|
|
129
|
+
# any existing line). Idempotent.
|
|
130
|
+
write_env() {
|
|
131
|
+
local key="$1" value="$2" tmp
|
|
132
|
+
touch "$ENV_FILE"
|
|
133
|
+
tmp=$(mktemp)
|
|
134
|
+
grep -vE "^${key}=" "$ENV_FILE" > "$tmp" || true
|
|
135
|
+
printf '%s=%s\n' "$key" "$value" >> "$tmp"
|
|
136
|
+
mv "$tmp" "$ENV_FILE"
|
|
137
|
+
WRITTEN_ENV+=("$key")
|
|
138
|
+
printf ' %s✓ wrote%s %s → %s\n' "$GREEN" "$RESET" "$key" "$ENV_FILE"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# set_secret NAME VALUE sets a GitHub Actions repo secret via gh. Falls back
|
|
142
|
+
# to a warning (and records it) if gh is unavailable or unauthenticated.
|
|
143
|
+
set_secret() {
|
|
144
|
+
local name="$1" value="$2"
|
|
145
|
+
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
|
|
146
|
+
if printf '%s' "$value" | gh secret set "$name" >/dev/null 2>&1; then
|
|
147
|
+
WRITTEN_SECRET+=("$name")
|
|
148
|
+
printf ' %s✓ set%s GitHub secret %s\n' "$GREEN" "$RESET" "$name"
|
|
149
|
+
return
|
|
150
|
+
fi
|
|
151
|
+
fi
|
|
152
|
+
SKIPPED+=("GitHub secret $name (set it manually: gh secret set $name)")
|
|
153
|
+
warn "skipped GitHub secret $name: gh not ready; set it later"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# set_var NAME VALUE sets a GitHub Actions repo variable (non-secret).
|
|
157
|
+
set_var() {
|
|
158
|
+
local name="$1" value="$2"
|
|
159
|
+
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
|
|
160
|
+
if gh variable set "$name" --body "$value" >/dev/null 2>&1; then
|
|
161
|
+
printf ' %s✓ set%s GitHub variable %s\n' "$GREEN" "$RESET" "$name"
|
|
162
|
+
return
|
|
163
|
+
fi
|
|
164
|
+
fi
|
|
165
|
+
SKIPPED+=("GitHub variable $name")
|
|
166
|
+
warn "skipped GitHub variable $name, gh not ready; set it later"
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# finish clears, then shows a closing summary of everything configured.
|
|
170
|
+
finish() {
|
|
171
|
+
_clear
|
|
172
|
+
printf '\n%s%s ✓ Setup complete%s\n' "$BOLD" "$GREEN" "$RESET"
|
|
173
|
+
(( ${#WRITTEN_ENV[@]} )) && note "wrote ${#WRITTEN_ENV[@]} value(s) to $ENV_FILE: ${WRITTEN_ENV[*]}"
|
|
174
|
+
(( ${#WRITTEN_SECRET[@]} )) && note "set ${#WRITTEN_SECRET[@]} GitHub secret(s): ${WRITTEN_SECRET[*]}"
|
|
175
|
+
if (( ${#SKIPPED[@]} )); then
|
|
176
|
+
printf '\n'; warn "still to do by hand:"
|
|
177
|
+
for s in "${SKIPPED[@]}"; do note " - $s"; done
|
|
178
|
+
fi
|
|
179
|
+
printf '\n'
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
183
|
+
# STAGES
|
|
184
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
# Run through `sb setup clipper`, which sets SB (the sb command), SB_CONFIG and SB_TEMPLATES.
|
|
187
|
+
SB="${SB:-sb}"
|
|
188
|
+
CONFIG="${SB_CONFIG:?run this wizard with: sb setup clipper}"
|
|
189
|
+
ENV_FILE=/dev/null # this wizard writes nothing: it guides the browser setup
|
|
190
|
+
TEMPLATE="${SB_TEMPLATES:?}/clipper-template.json"
|
|
191
|
+
YT_TEMPLATE="${SB_TEMPLATES}/clipper-youtube-template.json"
|
|
192
|
+
|
|
193
|
+
TOTAL_STAGES=5
|
|
194
|
+
|
|
195
|
+
# Facts from the config: where the vault is (the extension asks for its folder name) and whether
|
|
196
|
+
# Obsidian is used at all.
|
|
197
|
+
INFO=$("$SB" info --config "$CONFIG") || { warn "could not read $CONFIG"; exit 1; }
|
|
198
|
+
VAULT=$(printf '%s\n' "$INFO" | sed -n 's/^vault=//p')
|
|
199
|
+
VIEWER=$(printf '%s\n' "$INFO" | sed -n 's/^viewer=//p')
|
|
200
|
+
VAULT_NAME=$(basename "$VAULT")
|
|
201
|
+
|
|
202
|
+
if [[ "$VIEWER" == "none" ]]; then
|
|
203
|
+
printf '\n The Web Clipper is a feature of Obsidian: it saves pages into an Obsidian vault.\n'
|
|
204
|
+
printf ' Your config says you do not use Obsidian (viewer = "none"), so there is nothing to connect.\n\n'
|
|
205
|
+
printf ' You can still add links with sb add URL , or save a page as a Markdown file with "title:" and\n'
|
|
206
|
+
printf ' "source:" (the URL) at the top and drop it in %s/inbox/ .\n' "$VAULT"
|
|
207
|
+
printf ' If you start using Obsidian: set viewer = "obsidian" under [notes] in the config, then run this again.\n\n'
|
|
208
|
+
exit 0
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
banner "esbi-cli: Obsidian Web Clipper (optional, needs Obsidian)"
|
|
212
|
+
say "Not needed for the basics: links and PDFs work with 'sb add' and the inbox/ folder."
|
|
213
|
+
say "The Clipper saves a web page, a LinkedIn or Reddit post, or a YouTube transcript from your browser."
|
|
214
|
+
pause "Continue?"
|
|
215
|
+
|
|
216
|
+
# ── 1 ─────────────────────────────────────────────────────────────────────
|
|
217
|
+
stage "Install the extension"
|
|
218
|
+
say "The Web Clipper saves a web page (or a LinkedIn / X / Reddit post) as a note in your vault."
|
|
219
|
+
open_url "https://obsidian.md/clipper"
|
|
220
|
+
step "Click the button for your browser and add the extension."
|
|
221
|
+
step "Pin it to the toolbar (the puzzle-piece menu > pin), so the icon is always one click away."
|
|
222
|
+
pause "Extension installed and pinned? Press Enter."
|
|
223
|
+
|
|
224
|
+
# ── 2 ─────────────────────────────────────────────────────────────────────
|
|
225
|
+
stage "Point it at your vault"
|
|
226
|
+
say "Your vault is: $VAULT"
|
|
227
|
+
step "Click the Clipper icon, then the gear (Settings)."
|
|
228
|
+
step "Under 'General' > 'Vaults' add the vault name exactly as: $VAULT_NAME"
|
|
229
|
+
note "Obsidian must know this vault: opened once with 'Open folder as vault' (sb doctor checks it)."
|
|
230
|
+
pause "Vault added? Press Enter."
|
|
231
|
+
|
|
232
|
+
# ── 3 ─────────────────────────────────────────────────────────────────────
|
|
233
|
+
stage "Import the esbi-cli template"
|
|
234
|
+
say "The template saves clips into the vault's inbox/ folder with the properties the worker reads:"
|
|
235
|
+
say "title, source (the page URL) and kind."
|
|
236
|
+
open -R "$TEMPLATE" >/dev/null 2>&1 || note "The file is: $TEMPLATE"
|
|
237
|
+
note "Second template, for YouTube: $YT_TEMPLATE"
|
|
238
|
+
step "In the Clipper settings open 'Templates' and click 'Import' (top right), or drag the file shown in Finder into the window."
|
|
239
|
+
step "Also import clipper-youtube-template.json (same folder) the same way: it is used by itself on YouTube pages."
|
|
240
|
+
step "Make 'esbi-cli' the first template in the list, so it is the one used by default."
|
|
241
|
+
pause "Template imported? Press Enter."
|
|
242
|
+
|
|
243
|
+
# ── 4 ─────────────────────────────────────────────────────────────────────
|
|
244
|
+
stage "Clip a test page"
|
|
245
|
+
BEFORE=$(ls "$VAULT/inbox" 2>/dev/null | wc -l | tr -d ' ')
|
|
246
|
+
open_url "https://en.wikipedia.org/wiki/Zettelkasten"
|
|
247
|
+
step "Click the Clipper icon. Check the template says 'esbi-cli'."
|
|
248
|
+
step "Click 'Add to Obsidian'. Obsidian opens or comes forward: that is normal."
|
|
249
|
+
pause "Clipped? Press Enter to look for the note."
|
|
250
|
+
AFTER=$(ls "$VAULT/inbox" 2>/dev/null | wc -l | tr -d ' ')
|
|
251
|
+
if (( AFTER > BEFORE )); then
|
|
252
|
+
NEWEST=$(ls -t "$VAULT/inbox"/*.md | head -n1)
|
|
253
|
+
printf ' %s✓ found%s %s\n\n' "$GREEN" "$RESET" "$NEWEST"
|
|
254
|
+
head -n 14 "$NEWEST" | sed 's/^/ /'
|
|
255
|
+
else
|
|
256
|
+
warn "no new file in $VAULT/inbox/. Check that the template's folder is 'inbox' and the vault name is $VAULT_NAME."
|
|
257
|
+
SKIPPED+=("test clip: nothing arrived in $VAULT/inbox/")
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
# ── 5 ─────────────────────────────────────────────────────────────────────
|
|
261
|
+
stage "Let the worker pick it up"
|
|
262
|
+
if "$SB" scan --config "$CONFIG"; then
|
|
263
|
+
"$SB" status --config "$CONFIG"
|
|
264
|
+
say ""
|
|
265
|
+
say "'Queued 1' means the worker read the clip. 'sb run' (or tonight's run) writes its note."
|
|
266
|
+
else
|
|
267
|
+
warn "the scan failed; the message above says why."
|
|
268
|
+
SKIPPED+=("scan: fix the error above, then run: sb scan")
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
finish
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Wizard: connect esbi-cli to a dedicated Gmail mailbox (label + app password).
|
|
4
|
+
# Generated with the /wizard skill; run it with: sb setup email
|
|
5
|
+
#
|
|
6
|
+
# Everything above the "STAGES" marker is the wizard library: do not hand-edit
|
|
7
|
+
# it. Author the per-step stages below the marker.
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
12
|
+
# Wizard library: delightful, consistent UX, identical across every wizard.
|
|
13
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && [[ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]]; then
|
|
16
|
+
BOLD=$(tput bold); DIM=$(tput dim); RESET=$(tput sgr0)
|
|
17
|
+
BLUE=$(tput setaf 4); GREEN=$(tput setaf 2); YELLOW=$(tput setaf 3); RED=$(tput setaf 1)
|
|
18
|
+
else
|
|
19
|
+
BOLD=""; DIM=""; RESET=""; BLUE=""; GREEN=""; YELLOW=""; RED=""
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# Author sets this at the top of the stages section.
|
|
23
|
+
TOTAL_STAGES=0
|
|
24
|
+
|
|
25
|
+
_STAGE_INDEX=0
|
|
26
|
+
ENV_FILE="${ENV_FILE:-.env}"
|
|
27
|
+
WRITTEN_ENV=() # KEYs written to ENV_FILE this run
|
|
28
|
+
WRITTEN_SECRET=() # secret NAMEs set this run
|
|
29
|
+
SKIPPED=() # things we couldn't do (e.g. gh missing)
|
|
30
|
+
|
|
31
|
+
# _clear wipes the terminal so only the current step is on screen. No-op when
|
|
32
|
+
# output isn't a terminal, so piped logs stay readable.
|
|
33
|
+
_clear() {
|
|
34
|
+
[[ -t 1 ]] || return 0
|
|
35
|
+
if command -v tput >/dev/null 2>&1; then tput clear; else printf '\033[2J\033[3J\033[H'; fi
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# banner "Title" shows the opening frame: what this wizard does.
|
|
39
|
+
banner() {
|
|
40
|
+
_clear
|
|
41
|
+
printf '\n%s%s %s%s\n' "$BOLD" "$BLUE" "$1" "$RESET"
|
|
42
|
+
printf '%s %s stages%s\n\n' "$DIM" "$TOTAL_STAGES" "$RESET"
|
|
43
|
+
printf '%s You drive the browser; this wizard tells you exactly what to do and\n' "$DIM"
|
|
44
|
+
printf ' captures the values you copy back. Stop any time with Ctrl-C and re-run\n'
|
|
45
|
+
printf ' later, since it remembers values already saved.%s\n' "$RESET"
|
|
46
|
+
pause "Ready to start?"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# stage "Name" clears the screen, then announces a stage and shows progress.
|
|
50
|
+
# Clearing keeps only the current step on screen.
|
|
51
|
+
stage() {
|
|
52
|
+
_clear
|
|
53
|
+
_STAGE_INDEX=$((_STAGE_INDEX + 1))
|
|
54
|
+
printf '\n%s%s▸ Stage %s/%s · %s%s\n' \
|
|
55
|
+
"$BOLD" "$BLUE" "$_STAGE_INDEX" "$TOTAL_STAGES" "$1" "$RESET"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# say "..." prints a plain instruction line.
|
|
59
|
+
say() { printf ' %s\n' "$1"; }
|
|
60
|
+
# step "..." is a numbered-feeling action the human takes in the browser.
|
|
61
|
+
step() { printf ' %s•%s %s\n' "$BLUE" "$RESET" "$1"; }
|
|
62
|
+
note() { printf ' %s%s%s\n' "$DIM" "$1" "$RESET"; }
|
|
63
|
+
warn() { printf ' %s⚠ %s%s\n' "$YELLOW" "$1" "$RESET"; }
|
|
64
|
+
|
|
65
|
+
# open_url URL opens it in the human's browser, cross-platform incl. WSL.
|
|
66
|
+
open_url() {
|
|
67
|
+
local url="$1"
|
|
68
|
+
printf ' %s↗ opening%s %s\n' "$GREEN" "$RESET" "$url"
|
|
69
|
+
{ if command -v wslview >/dev/null 2>&1; then wslview "$url"
|
|
70
|
+
elif command -v explorer.exe >/dev/null 2>&1; then explorer.exe "$url"
|
|
71
|
+
elif command -v xdg-open >/dev/null 2>&1; then xdg-open "$url"
|
|
72
|
+
elif command -v open >/dev/null 2>&1; then open "$url"
|
|
73
|
+
else warn "couldn't open a browser; visit it manually: $url"; fi
|
|
74
|
+
} >/dev/null 2>&1 || warn "couldn't open a browser, so visit it manually: $url"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# pause "msg" waits for the human to confirm they've done the manual part.
|
|
78
|
+
pause() {
|
|
79
|
+
printf ' %s%s%s ' "$DIM" "${1:-Press Enter to continue}" "$RESET"
|
|
80
|
+
read -r _ || true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# confirm "question" is a y/N gate; returns success on yes.
|
|
84
|
+
confirm() {
|
|
85
|
+
local reply=""
|
|
86
|
+
printf ' %s? %s [y/N] ' "$YELLOW" "$1"
|
|
87
|
+
read -r reply || true
|
|
88
|
+
[[ "$reply" =~ ^[Yy] ]]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# _existing KEY: current value of KEY in ENV_FILE, if any.
|
|
92
|
+
_existing() {
|
|
93
|
+
[[ -f "$ENV_FILE" ]] || return 1
|
|
94
|
+
local line; line=$(grep -E "^${1}=" "$ENV_FILE" | tail -n1) || return 1
|
|
95
|
+
printf '%s' "${line#*=}"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# ask KEY "Prompt" reads a value into $KEY. Offers the existing .env value as
|
|
99
|
+
# a default on re-runs (Enter keeps it). Visible input (non-secret).
|
|
100
|
+
ask() {
|
|
101
|
+
local key="$1" prompt="$2" current input
|
|
102
|
+
current=$(_existing "$key" || true)
|
|
103
|
+
if [[ -n "$current" ]]; then
|
|
104
|
+
printf ' %s%s%s %s[Enter keeps current]%s ' "$BOLD" "$prompt" "$RESET" "$DIM" "$RESET"
|
|
105
|
+
else
|
|
106
|
+
printf ' %s%s%s ' "$BOLD" "$prompt" "$RESET"
|
|
107
|
+
fi
|
|
108
|
+
read -r input || true
|
|
109
|
+
[[ -z "$input" && -n "$current" ]] && input="$current"
|
|
110
|
+
printf -v "$key" '%s' "$input"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# ask_secret KEY "Prompt" is like ask, but input is hidden.
|
|
114
|
+
ask_secret() {
|
|
115
|
+
local key="$1" prompt="$2" current input
|
|
116
|
+
current=$(_existing "$key" || true)
|
|
117
|
+
if [[ -n "$current" ]]; then
|
|
118
|
+
printf ' %s%s%s %s[Enter keeps current]%s ' "$BOLD" "$prompt" "$RESET" "$DIM" "$RESET"
|
|
119
|
+
else
|
|
120
|
+
printf ' %s%s%s ' "$BOLD" "$prompt" "$RESET"
|
|
121
|
+
fi
|
|
122
|
+
read -rs input || true
|
|
123
|
+
printf '\n'
|
|
124
|
+
[[ -z "$input" && -n "$current" ]] && input="$current"
|
|
125
|
+
printf -v "$key" '%s' "$input"
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# write_env KEY VALUE upserts KEY=VALUE into ENV_FILE (creates it; replaces
|
|
129
|
+
# any existing line). Idempotent.
|
|
130
|
+
write_env() {
|
|
131
|
+
local key="$1" value="$2" tmp
|
|
132
|
+
touch "$ENV_FILE"
|
|
133
|
+
tmp=$(mktemp)
|
|
134
|
+
grep -vE "^${key}=" "$ENV_FILE" > "$tmp" || true
|
|
135
|
+
printf '%s=%s\n' "$key" "$value" >> "$tmp"
|
|
136
|
+
mv "$tmp" "$ENV_FILE"
|
|
137
|
+
WRITTEN_ENV+=("$key")
|
|
138
|
+
printf ' %s✓ wrote%s %s → %s\n' "$GREEN" "$RESET" "$key" "$ENV_FILE"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# set_secret NAME VALUE sets a GitHub Actions repo secret via gh. Falls back
|
|
142
|
+
# to a warning (and records it) if gh is unavailable or unauthenticated.
|
|
143
|
+
set_secret() {
|
|
144
|
+
local name="$1" value="$2"
|
|
145
|
+
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
|
|
146
|
+
if printf '%s' "$value" | gh secret set "$name" >/dev/null 2>&1; then
|
|
147
|
+
WRITTEN_SECRET+=("$name")
|
|
148
|
+
printf ' %s✓ set%s GitHub secret %s\n' "$GREEN" "$RESET" "$name"
|
|
149
|
+
return
|
|
150
|
+
fi
|
|
151
|
+
fi
|
|
152
|
+
SKIPPED+=("GitHub secret $name (set it manually: gh secret set $name)")
|
|
153
|
+
warn "skipped GitHub secret $name: gh not ready; set it later"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# set_var NAME VALUE sets a GitHub Actions repo variable (non-secret).
|
|
157
|
+
set_var() {
|
|
158
|
+
local name="$1" value="$2"
|
|
159
|
+
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
|
|
160
|
+
if gh variable set "$name" --body "$value" >/dev/null 2>&1; then
|
|
161
|
+
printf ' %s✓ set%s GitHub variable %s\n' "$GREEN" "$RESET" "$name"
|
|
162
|
+
return
|
|
163
|
+
fi
|
|
164
|
+
fi
|
|
165
|
+
SKIPPED+=("GitHub variable $name")
|
|
166
|
+
warn "skipped GitHub variable $name, gh not ready; set it later"
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# finish clears, then shows a closing summary of everything configured.
|
|
170
|
+
finish() {
|
|
171
|
+
_clear
|
|
172
|
+
printf '\n%s%s ✓ Setup complete%s\n' "$BOLD" "$GREEN" "$RESET"
|
|
173
|
+
(( ${#WRITTEN_ENV[@]} )) && note "wrote ${#WRITTEN_ENV[@]} value(s) to $ENV_FILE: ${WRITTEN_ENV[*]}"
|
|
174
|
+
(( ${#WRITTEN_SECRET[@]} )) && note "set ${#WRITTEN_SECRET[@]} GitHub secret(s): ${WRITTEN_SECRET[*]}"
|
|
175
|
+
if (( ${#SKIPPED[@]} )); then
|
|
176
|
+
printf '\n'; warn "still to do by hand:"
|
|
177
|
+
for s in "${SKIPPED[@]}"; do note " - $s"; done
|
|
178
|
+
fi
|
|
179
|
+
printf '\n'
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
183
|
+
# STAGES
|
|
184
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
# Run through `sb setup email`, which sets SB (the sb command), SB_CONFIG and SB_TEMPLATES.
|
|
187
|
+
SB="${SB:-sb}"
|
|
188
|
+
CONFIG="${SB_CONFIG:?run this wizard with: sb setup email}"
|
|
189
|
+
ENV_FILE=/dev/null # this wizard writes config.toml + the Keychain, not a .env
|
|
190
|
+
LABEL="esbi-cli" # must match [email].mailbox in config.toml
|
|
191
|
+
|
|
192
|
+
TOTAL_STAGES=6
|
|
193
|
+
|
|
194
|
+
banner "esbi-cli: connect a Gmail mailbox (optional)"
|
|
195
|
+
say "Not needed for the basics: you can always add PDFs and links with 'sb add' or by dropping them in inbox/."
|
|
196
|
+
say "This lets you forward an email to your own address and have it become a note."
|
|
197
|
+
say "Press Ctrl-C at any time to stop; nothing is changed until stage 5."
|
|
198
|
+
pause "Continue?"
|
|
199
|
+
|
|
200
|
+
# ── 1 ─────────────────────────────────────────────────────────────────────
|
|
201
|
+
stage "Turn on 2-Step Verification"
|
|
202
|
+
say "Google only offers app passwords on accounts with 2-Step Verification."
|
|
203
|
+
open_url "https://myaccount.google.com/security"
|
|
204
|
+
step "Under 'How you sign in to Google', check that 2-Step Verification is On."
|
|
205
|
+
step "If it is Off, turn it on and finish Google's prompts."
|
|
206
|
+
pause "2-Step Verification is on? Press Enter."
|
|
207
|
+
|
|
208
|
+
# ── 2 ─────────────────────────────────────────────────────────────────────
|
|
209
|
+
stage "Create the Gmail label"
|
|
210
|
+
say "esbi-cli reads one label. Mail you want to capture ends up under it."
|
|
211
|
+
open_url "https://mail.google.com/mail/u/0/"
|
|
212
|
+
step "In the left sidebar choose 'Create new label' (under 'More' > Labels if hidden)."
|
|
213
|
+
step "Name it exactly: $LABEL"
|
|
214
|
+
pause "Label '$LABEL' created? Press Enter."
|
|
215
|
+
|
|
216
|
+
# ── 3 ─────────────────────────────────────────────────────────────────────
|
|
217
|
+
stage "Route forwarded mail into the label"
|
|
218
|
+
ask EMAIL_ADDRESS "Your Gmail address (e.g. you@gmail.com):"
|
|
219
|
+
[[ "$EMAIL_ADDRESS" == *@* ]] || { warn "that does not look like an email address"; exit 1; }
|
|
220
|
+
[[ "$EMAIL_ADDRESS" == *@gmail.com || "$EMAIL_ADDRESS" == *@googlemail.com ]] \
|
|
221
|
+
|| warn "this wizard assumes Gmail; other providers need their own IMAP host and label"
|
|
222
|
+
PLUS_ADDRESS="${EMAIL_ADDRESS%@*}+esbi-cli@${EMAIL_ADDRESS#*@}"
|
|
223
|
+
say ""
|
|
224
|
+
say "Anything you forward to this address lands in your own inbox:"
|
|
225
|
+
printf ' %s%s%s\n\n' "$BOLD" "$PLUS_ADDRESS" "$RESET"
|
|
226
|
+
open_url "https://mail.google.com/mail/u/0/#settings/filters"
|
|
227
|
+
step "Click 'Create a new filter'."
|
|
228
|
+
step "In the 'To' field enter: $PLUS_ADDRESS then 'Create filter'."
|
|
229
|
+
step "Tick 'Apply the label' and pick '$LABEL'."
|
|
230
|
+
step "Also tick 'Skip the Inbox (Archive it)' so you never open these mails by accident."
|
|
231
|
+
note "The worker reads the label's last 14 days, read or unread, and skips what it already saved."
|
|
232
|
+
step "Click 'Create filter'."
|
|
233
|
+
pause "Filter created? Press Enter."
|
|
234
|
+
|
|
235
|
+
# ── 4 ─────────────────────────────────────────────────────────────────────
|
|
236
|
+
stage "Create an app password"
|
|
237
|
+
say "A separate password just for esbi-cli; you can revoke it any time."
|
|
238
|
+
open_url "https://myaccount.google.com/apppasswords"
|
|
239
|
+
step "Name the app 'esbi-cli' and click Create."
|
|
240
|
+
step "Copy the 16-character password Google shows."
|
|
241
|
+
note "Don't paste it here: the next stage stores it straight into your Keychain."
|
|
242
|
+
pause "Password copied? Press Enter."
|
|
243
|
+
|
|
244
|
+
# ── 5 ─────────────────────────────────────────────────────────────────────
|
|
245
|
+
stage "Save the settings and the password"
|
|
246
|
+
"$SB" email configure --user "$EMAIL_ADDRESS" --label "$LABEL" --config "$CONFIG"
|
|
247
|
+
printf ' %s✓ wrote%s [email] settings → %s\n' "$GREEN" "$RESET" "$CONFIG"
|
|
248
|
+
say "Now paste the app password (it stays hidden and goes only into the Keychain):"
|
|
249
|
+
"$SB" email set-password --config "$CONFIG"
|
|
250
|
+
|
|
251
|
+
# ── 6 ─────────────────────────────────────────────────────────────────────
|
|
252
|
+
stage "Test it end to end"
|
|
253
|
+
step "Forward any email to $PLUS_ADDRESS (or send one to yourself at that address)."
|
|
254
|
+
step "In Gmail, check it shows up under the '$LABEL' label."
|
|
255
|
+
pause "Mail is under the label? Press Enter to fetch it."
|
|
256
|
+
if "$SB" email fetch --config "$CONFIG"; then
|
|
257
|
+
say ""
|
|
258
|
+
say "If it says 'Mail: 1 saved', it works: the mail is now in the vault's inbox/ and"
|
|
259
|
+
say "the next 'sb run' turns it into a note."
|
|
260
|
+
else
|
|
261
|
+
warn "the fetch failed; the message above says why (wrong app password? label name?)."
|
|
262
|
+
SKIPPED+=("mail test: fix the error above, then run: sb email fetch")
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
finish
|