jekyll-theme-zer0 1.21.0 → 1.22.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/_data/backlog.yml +124 -2
- data/_includes/components/component-showcase.html +18 -10
- data/_includes/components/js-cdn.html +10 -2
- data/_includes/components/post-card.html +18 -4
- data/_includes/content/backlinks.html +47 -18
- data/_includes/content/giscus.html +30 -44
- data/_includes/core/footer-fabs.html +5 -2
- data/_includes/navigation/local-graph-fab.html +1 -1
- data/_includes/navigation/local-graph.html +4 -3
- data/_includes/navigation/section-sidebar.html +10 -3
- data/_includes/obsidian/full-graph.html +7 -5
- data/_layouts/article.html +23 -6
- data/_plugins/obsidian_links.rb +84 -15
- data/_sass/core/_obsidian.scss +59 -8
- data/assets/data/wiki-index.json +50 -3
- data/assets/js/obsidian-graph.js +66 -21
- data/assets/js/obsidian-local-graph.js +125 -32
- data/assets/js/obsidian-wiki-links.js +118 -21
- data/assets/js/search-modal.js +45 -9
- data/scripts/bin/giscus-discussions +509 -0
- metadata +3 -2
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# giscus-discussions — read, draft, and post the GitHub Discussions that back
|
|
4
|
+
# the site's Giscus comment threads. This is the engine the Claude Code
|
|
5
|
+
# `giscus-conversation` skill drives to build conversations from page comments.
|
|
6
|
+
#
|
|
7
|
+
# Subcommands:
|
|
8
|
+
# categories List the repo's discussion categories + node IDs (setup helper)
|
|
9
|
+
# list List discussions in the configured Giscus category
|
|
10
|
+
# thread Print a discussion's full conversation (comments + replies)
|
|
11
|
+
# draft Write a Markdown draft scaffold (thread context + reply slot)
|
|
12
|
+
# seed Create a new discussion for a page (starts a conversation)
|
|
13
|
+
# post Add a comment or reply to an existing discussion
|
|
14
|
+
#
|
|
15
|
+
# Reads owner/repo from `gh repo view` and the Giscus category from
|
|
16
|
+
# _config.yml (giscus.data-category-id). Both are overridable via flags/env.
|
|
17
|
+
#
|
|
18
|
+
# Usage: ./scripts/bin/giscus-discussions <subcommand> [options]
|
|
19
|
+
# Requires: gh (authenticated). Writes (seed/post) honor --dry-run.
|
|
20
|
+
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
|
+
LIB_DIR="$SCRIPT_DIR/../lib"
|
|
25
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
26
|
+
|
|
27
|
+
# shellcheck source=/dev/null
|
|
28
|
+
source "$LIB_DIR/common.sh"
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Configuration resolution
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
CONFIG_FILE="$REPO_ROOT/_config.yml"
|
|
35
|
+
|
|
36
|
+
# Owner/repo: env GISCUS_REPO or --repo override; else derive from gh.
|
|
37
|
+
REPO_NWO="${GISCUS_REPO:-}"
|
|
38
|
+
# Giscus category node id: env GISCUS_CATEGORY_ID or --category-id; else config.
|
|
39
|
+
CATEGORY_ID="${GISCUS_CATEGORY_ID:-}"
|
|
40
|
+
|
|
41
|
+
resolve_repo() {
|
|
42
|
+
if [[ -n "$REPO_NWO" ]]; then
|
|
43
|
+
return 0
|
|
44
|
+
fi
|
|
45
|
+
require_command gh "install: https://cli.github.com/"
|
|
46
|
+
REPO_NWO="$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || true)"
|
|
47
|
+
if [[ -z "$REPO_NWO" ]]; then
|
|
48
|
+
error "Could not determine repository. Pass --repo owner/name or set GISCUS_REPO."
|
|
49
|
+
fi
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Extract giscus.data-category-id from _config.yml (block-scoped).
|
|
53
|
+
config_category_id() {
|
|
54
|
+
[[ -f "$CONFIG_FILE" ]] || return 0
|
|
55
|
+
awk '/^giscus:/{f=1;next} f&&/^[^[:space:]#]/{f=0} f' "$CONFIG_FILE" \
|
|
56
|
+
| grep -E '^[[:space:]]+data-category-id:' \
|
|
57
|
+
| head -1 \
|
|
58
|
+
| sed -E 's/^[[:space:]]+data-category-id:[[:space:]]*//; s/^"//; s/"[[:space:]]*$//; s/^'"'"'//; s/'"'"'[[:space:]]*$//'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
resolve_category() {
|
|
62
|
+
if [[ -n "$CATEGORY_ID" ]]; then
|
|
63
|
+
return 0
|
|
64
|
+
fi
|
|
65
|
+
CATEGORY_ID="$(config_category_id)"
|
|
66
|
+
if [[ -z "$CATEGORY_ID" ]]; then
|
|
67
|
+
error "No category id. Pass --category-id, set GISCUS_CATEGORY_ID, or fix giscus.data-category-id in _config.yml (see: $0 categories)."
|
|
68
|
+
fi
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
owner_of() { printf '%s' "${1%%/*}"; }
|
|
72
|
+
name_of() { printf '%s' "${1##*/}"; }
|
|
73
|
+
|
|
74
|
+
# Normalize a page path so it matches a Giscus pathname-mapped discussion title.
|
|
75
|
+
normalize_page() {
|
|
76
|
+
local p="$1"
|
|
77
|
+
if [[ "$p" == *"://"* ]]; then
|
|
78
|
+
p="${p#*://}" # strip scheme
|
|
79
|
+
p="/${p#*/}" # strip host, keep path with a leading slash
|
|
80
|
+
fi
|
|
81
|
+
[[ "$p" != /* ]] && p="/$p"
|
|
82
|
+
printf '%s' "$p"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# GraphQL helpers
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
repo_node_id() {
|
|
90
|
+
gh api graphql -f query='
|
|
91
|
+
query($owner:String!,$name:String!){
|
|
92
|
+
repository(owner:$owner,name:$name){ id }
|
|
93
|
+
}' -F owner="$(owner_of "$REPO_NWO")" -F name="$(name_of "$REPO_NWO")" \
|
|
94
|
+
--jq '.data.repository.id'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Find a discussion number whose title equals the (normalized) page path.
|
|
98
|
+
# Echoes the number, or nothing if not found.
|
|
99
|
+
discussion_number_for_page() {
|
|
100
|
+
local page; page="$(normalize_page "$1")"
|
|
101
|
+
local page_noslash="${page%/}"
|
|
102
|
+
gh api graphql -f query='
|
|
103
|
+
query($owner:String!,$name:String!,$categoryId:ID,$after:String){
|
|
104
|
+
repository(owner:$owner,name:$name){
|
|
105
|
+
discussions(first:100,after:$after,categoryId:$categoryId){
|
|
106
|
+
nodes{ number title }
|
|
107
|
+
pageInfo{ hasNextPage endCursor }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}' -F owner="$(owner_of "$REPO_NWO")" -F name="$(name_of "$REPO_NWO")" \
|
|
111
|
+
-f categoryId="$CATEGORY_ID" \
|
|
112
|
+
--jq ".data.repository.discussions.nodes[]
|
|
113
|
+
| select(.title == \"$page\" or .title == \"$page_noslash\")
|
|
114
|
+
| .number" | head -1
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
discussion_node_id() {
|
|
118
|
+
local number="$1"
|
|
119
|
+
gh api graphql -f query='
|
|
120
|
+
query($owner:String!,$name:String!,$number:Int!){
|
|
121
|
+
repository(owner:$owner,name:$name){ discussion(number:$number){ id } }
|
|
122
|
+
}' -F owner="$(owner_of "$REPO_NWO")" -F name="$(name_of "$REPO_NWO")" \
|
|
123
|
+
-F number="$number" \
|
|
124
|
+
--jq '.data.repository.discussion.id'
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
# Subcommand: categories
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
cmd_categories() {
|
|
132
|
+
resolve_repo
|
|
133
|
+
info "Discussion categories for $REPO_NWO:"
|
|
134
|
+
gh api graphql -f query='
|
|
135
|
+
query($owner:String!,$name:String!){
|
|
136
|
+
repository(owner:$owner,name:$name){
|
|
137
|
+
hasDiscussionsEnabled
|
|
138
|
+
discussionCategories(first:30){ nodes{ id name slug } }
|
|
139
|
+
}
|
|
140
|
+
}' -F owner="$(owner_of "$REPO_NWO")" -F name="$(name_of "$REPO_NWO")" \
|
|
141
|
+
--jq '.data.repository as $r
|
|
142
|
+
| if $r.hasDiscussionsEnabled then empty
|
|
143
|
+
else "WARNING: Discussions are NOT enabled on this repo." end,
|
|
144
|
+
($r.discussionCategories.nodes[] | " \(.id)\t\(.name)")'
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
# Subcommand: list
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
cmd_list() {
|
|
152
|
+
local as_json=false
|
|
153
|
+
while [[ $# -gt 0 ]]; do
|
|
154
|
+
case "$1" in
|
|
155
|
+
--repo) REPO_NWO="$2"; shift 2 ;;
|
|
156
|
+
--category-id) CATEGORY_ID="$2"; shift 2 ;;
|
|
157
|
+
--json) as_json=true; shift ;;
|
|
158
|
+
*) error "list: unknown option '$1'" ;;
|
|
159
|
+
esac
|
|
160
|
+
done
|
|
161
|
+
resolve_repo; resolve_category
|
|
162
|
+
local owner name; owner="$(owner_of "$REPO_NWO")"; name="$(name_of "$REPO_NWO")"
|
|
163
|
+
local query='
|
|
164
|
+
query($owner:String!,$name:String!,$categoryId:ID){
|
|
165
|
+
repository(owner:$owner,name:$name){
|
|
166
|
+
discussions(first:100,categoryId:$categoryId,orderBy:{field:UPDATED_AT,direction:DESC}){
|
|
167
|
+
nodes{ number title url updatedAt comments{ totalCount } author{ login } }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}'
|
|
171
|
+
if [[ "$as_json" == true ]]; then
|
|
172
|
+
gh api graphql -f query="$query" -F owner="$owner" -F name="$name" \
|
|
173
|
+
-f categoryId="$CATEGORY_ID" --jq '.data.repository.discussions.nodes'
|
|
174
|
+
return 0
|
|
175
|
+
fi
|
|
176
|
+
local out
|
|
177
|
+
out="$(gh api graphql -f query="$query" -F owner="$owner" -F name="$name" \
|
|
178
|
+
-f categoryId="$CATEGORY_ID" \
|
|
179
|
+
--jq '.data.repository.discussions.nodes[]
|
|
180
|
+
| "#\(.number)\t\(.comments.totalCount) comment(s)\t\(.title)\n\t\(.url)"')"
|
|
181
|
+
if [[ -z "$out" ]]; then
|
|
182
|
+
info "No discussions yet in the Giscus category for $REPO_NWO."
|
|
183
|
+
info "Giscus creates one on the first visitor comment, or seed one:"
|
|
184
|
+
info " $0 seed --page /path/ --title ... --body ..."
|
|
185
|
+
return 0
|
|
186
|
+
fi
|
|
187
|
+
info "Discussions in the Giscus category for $REPO_NWO:"
|
|
188
|
+
printf '%s\n' "$out"
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
# Subcommand: thread
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
THREAD_QUERY='
|
|
196
|
+
query($owner:String!,$name:String!,$number:Int!){
|
|
197
|
+
repository(owner:$owner,name:$name){
|
|
198
|
+
discussion(number:$number){
|
|
199
|
+
number title url body createdAt id
|
|
200
|
+
author{ login }
|
|
201
|
+
comments(first:100){
|
|
202
|
+
totalCount
|
|
203
|
+
nodes{
|
|
204
|
+
id body createdAt author{ login }
|
|
205
|
+
replies(first:100){ nodes{ id body createdAt author{ login } } }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}'
|
|
211
|
+
|
|
212
|
+
# jq program that renders a discussion thread as readable Markdown.
|
|
213
|
+
THREAD_MD_JQ='
|
|
214
|
+
.data.repository.discussion as $d
|
|
215
|
+
| "# \($d.title)\n\n- URL: \($d.url)\n- Discussion: #\($d.number) (node: \($d.id))\n- Opened by: @\($d.author.login // "unknown") on \($d.createdAt)\n\n## Original post\n\n\($d.body)\n\n## Comments (\($d.comments.totalCount))\n"
|
|
216
|
+
+ ( [ $d.comments.nodes[]
|
|
217
|
+
| "\n### @\(.author.login // "unknown") — \(.createdAt)\n_comment id: `\(.id)`_\n\n\(.body)\n"
|
|
218
|
+
+ ( if (.replies.nodes | length) > 0
|
|
219
|
+
then "\n" + ( [ .replies.nodes[]
|
|
220
|
+
| " - **@\(.author.login // "unknown")** (\(.createdAt)) _reply id: `\(.id)`_\n\n \(.body | gsub("\n"; "\n "))" ] | join("\n") ) + "\n"
|
|
221
|
+
else "" end )
|
|
222
|
+
] | join("\n---\n") )'
|
|
223
|
+
|
|
224
|
+
# Render a thread as Markdown (uses gh's bundled jq — no external jq needed).
|
|
225
|
+
fetch_thread_md() {
|
|
226
|
+
local number="$1"
|
|
227
|
+
gh api graphql -f query="$THREAD_QUERY" \
|
|
228
|
+
-F owner="$(owner_of "$REPO_NWO")" -F name="$(name_of "$REPO_NWO")" \
|
|
229
|
+
-F number="$number" --jq "$THREAD_MD_JQ"
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# Fetch the raw discussion JSON.
|
|
233
|
+
fetch_thread_raw() {
|
|
234
|
+
local number="$1"
|
|
235
|
+
gh api graphql -f query="$THREAD_QUERY" \
|
|
236
|
+
-F owner="$(owner_of "$REPO_NWO")" -F name="$(name_of "$REPO_NWO")" \
|
|
237
|
+
-F number="$number"
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
resolve_number_arg() {
|
|
241
|
+
# Sets NUMBER from --number, --url, or --page. Used by thread/draft/post.
|
|
242
|
+
local number="" url="" page=""
|
|
243
|
+
REMAINING_ARGS=()
|
|
244
|
+
while [[ $# -gt 0 ]]; do
|
|
245
|
+
case "$1" in
|
|
246
|
+
--number) number="$2"; shift 2 ;;
|
|
247
|
+
--url) url="$2"; shift 2 ;;
|
|
248
|
+
--page) page="$2"; shift 2 ;;
|
|
249
|
+
--repo) REPO_NWO="$2"; shift 2 ;;
|
|
250
|
+
--category-id) CATEGORY_ID="$2"; shift 2 ;;
|
|
251
|
+
*) REMAINING_ARGS+=("$1"); shift ;;
|
|
252
|
+
esac
|
|
253
|
+
done
|
|
254
|
+
resolve_repo
|
|
255
|
+
if [[ -n "$number" ]]; then
|
|
256
|
+
NUMBER="$number"
|
|
257
|
+
elif [[ -n "$url" ]]; then
|
|
258
|
+
NUMBER="${url##*/}"
|
|
259
|
+
elif [[ -n "$page" ]]; then
|
|
260
|
+
resolve_category
|
|
261
|
+
NUMBER="$(discussion_number_for_page "$page")"
|
|
262
|
+
[[ -z "$NUMBER" ]] && error "No discussion found for page '$page'. Seed one with: $0 seed --page '$page' --title ... --body ..."
|
|
263
|
+
else
|
|
264
|
+
error "Provide --number N, --url URL, or --page PATH."
|
|
265
|
+
fi
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
cmd_thread() {
|
|
269
|
+
local as_json=false
|
|
270
|
+
local args=()
|
|
271
|
+
for a in "$@"; do
|
|
272
|
+
[[ "$a" == "--json" ]] && { as_json=true; continue; }
|
|
273
|
+
args+=("$a")
|
|
274
|
+
done
|
|
275
|
+
resolve_number_arg "${args[@]}"
|
|
276
|
+
if [[ "$as_json" == true ]]; then
|
|
277
|
+
fetch_thread_raw "$NUMBER"
|
|
278
|
+
else
|
|
279
|
+
fetch_thread_md "$NUMBER"
|
|
280
|
+
fi
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
# ---------------------------------------------------------------------------
|
|
284
|
+
# Subcommand: draft
|
|
285
|
+
# ---------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
cmd_draft() {
|
|
288
|
+
local out=""
|
|
289
|
+
local args=()
|
|
290
|
+
while [[ $# -gt 0 ]]; do
|
|
291
|
+
case "$1" in
|
|
292
|
+
--out) out="$2"; shift 2 ;;
|
|
293
|
+
*) args+=("$1"); shift ;;
|
|
294
|
+
esac
|
|
295
|
+
done
|
|
296
|
+
resolve_number_arg "${args[@]}"
|
|
297
|
+
[[ -z "$out" ]] && out="$REPO_ROOT/giscus-draft-${NUMBER}.md"
|
|
298
|
+
{
|
|
299
|
+
echo "<!-- Giscus reply draft. Edit the REPLY section, then post with:"
|
|
300
|
+
echo " ./scripts/bin/giscus-discussions post --number $NUMBER --body-file \"$out\" [--reply-to <comment id>] [--dry-run]"
|
|
301
|
+
echo " Everything above the REPLY marker is context and is NOT posted. -->"
|
|
302
|
+
echo
|
|
303
|
+
echo "## Conversation context"
|
|
304
|
+
echo
|
|
305
|
+
fetch_thread_md "$NUMBER"
|
|
306
|
+
echo
|
|
307
|
+
echo "<!-- ===== REPLY (everything below this line is your draft) ===== -->"
|
|
308
|
+
echo
|
|
309
|
+
echo "_Draft your reply here._"
|
|
310
|
+
echo
|
|
311
|
+
} > "$out"
|
|
312
|
+
success "Draft scaffold written: $out"
|
|
313
|
+
info "Fill in the REPLY section, then: $0 post --number $NUMBER --body-file \"$out\" --reply-to <id> --dry-run"
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
# Extract just the reply portion (everything below the REPLY marker) from a
|
|
317
|
+
# draft file produced by `draft`, dropping leading blank lines.
|
|
318
|
+
extract_reply_body() {
|
|
319
|
+
awk 'f{print} /===== REPLY/{f=1}' "$1" | sed '/./,$!d'
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
# ---------------------------------------------------------------------------
|
|
323
|
+
# Subcommand: seed (create a new discussion for a page)
|
|
324
|
+
# ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
cmd_seed() {
|
|
327
|
+
local page="" title="" body="" body_file=""
|
|
328
|
+
while [[ $# -gt 0 ]]; do
|
|
329
|
+
case "$1" in
|
|
330
|
+
--page) page="$2"; shift 2 ;;
|
|
331
|
+
--title) title="$2"; shift 2 ;;
|
|
332
|
+
--body) body="$2"; shift 2 ;;
|
|
333
|
+
--body-file) body_file="$2"; shift 2 ;;
|
|
334
|
+
--repo) REPO_NWO="$2"; shift 2 ;;
|
|
335
|
+
--category-id) CATEGORY_ID="$2"; shift 2 ;;
|
|
336
|
+
--dry-run) DRY_RUN=true; shift ;;
|
|
337
|
+
*) error "seed: unknown option '$1'" ;;
|
|
338
|
+
esac
|
|
339
|
+
done
|
|
340
|
+
resolve_repo; resolve_category
|
|
341
|
+
[[ -z "$page" ]] && error "seed: --page PATH is required (Giscus maps the discussion title to the page path)."
|
|
342
|
+
page="$(normalize_page "$page")"
|
|
343
|
+
[[ -z "$title" ]] && title="$page"
|
|
344
|
+
if [[ -n "$body_file" ]]; then
|
|
345
|
+
require_file "$body_file" "body file"
|
|
346
|
+
body="$(cat "$body_file")"
|
|
347
|
+
fi
|
|
348
|
+
[[ -z "$body" ]] && error "seed: provide --body TEXT or --body-file FILE."
|
|
349
|
+
|
|
350
|
+
local existing; existing="$(discussion_number_for_page "$page" || true)"
|
|
351
|
+
if [[ -n "$existing" ]]; then
|
|
352
|
+
error "A discussion already exists for '$page' (#$existing). Use: $0 post --number $existing ..."
|
|
353
|
+
fi
|
|
354
|
+
|
|
355
|
+
if [[ "$DRY_RUN" == "true" ]]; then
|
|
356
|
+
info "[DRY RUN] Would create discussion in $REPO_NWO (category $CATEGORY_ID):"
|
|
357
|
+
info " title: $title"
|
|
358
|
+
info " body: $(printf '%s' "$body" | head -c 200)"
|
|
359
|
+
return 0
|
|
360
|
+
fi
|
|
361
|
+
|
|
362
|
+
local repo_id; repo_id="$(repo_node_id)"
|
|
363
|
+
gh api graphql -f query='
|
|
364
|
+
mutation($repoId:ID!,$categoryId:ID!,$title:String!,$body:String!){
|
|
365
|
+
createDiscussion(input:{repositoryId:$repoId,categoryId:$categoryId,title:$title,body:$body}){
|
|
366
|
+
discussion{ number url }
|
|
367
|
+
}
|
|
368
|
+
}' -F repoId="$repo_id" -F categoryId="$CATEGORY_ID" \
|
|
369
|
+
-F title="$title" -F body="$body" \
|
|
370
|
+
--jq '.data.createDiscussion.discussion | "Created discussion #\(.number): \(.url)"'
|
|
371
|
+
success "Discussion created for $page"
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
# ---------------------------------------------------------------------------
|
|
375
|
+
# Subcommand: post (comment on / reply to an existing discussion)
|
|
376
|
+
# ---------------------------------------------------------------------------
|
|
377
|
+
|
|
378
|
+
cmd_post() {
|
|
379
|
+
local number="" url="" page="" body="" body_file="" reply_to=""
|
|
380
|
+
while [[ $# -gt 0 ]]; do
|
|
381
|
+
case "$1" in
|
|
382
|
+
--number) number="$2"; shift 2 ;;
|
|
383
|
+
--url) url="$2"; shift 2 ;;
|
|
384
|
+
--page) page="$2"; shift 2 ;;
|
|
385
|
+
--body) body="$2"; shift 2 ;;
|
|
386
|
+
--body-file) body_file="$2"; shift 2 ;;
|
|
387
|
+
--reply-to) reply_to="$2"; shift 2 ;;
|
|
388
|
+
--repo) REPO_NWO="$2"; shift 2 ;;
|
|
389
|
+
--category-id) CATEGORY_ID="$2"; shift 2 ;;
|
|
390
|
+
--dry-run) DRY_RUN=true; shift ;;
|
|
391
|
+
*) error "post: unknown option '$1'" ;;
|
|
392
|
+
esac
|
|
393
|
+
done
|
|
394
|
+
resolve_repo
|
|
395
|
+
# Resolve the target discussion number.
|
|
396
|
+
if [[ -n "$number" ]]; then
|
|
397
|
+
NUMBER="$number"
|
|
398
|
+
elif [[ -n "$url" ]]; then
|
|
399
|
+
NUMBER="${url##*/}"
|
|
400
|
+
elif [[ -n "$page" ]]; then
|
|
401
|
+
resolve_category
|
|
402
|
+
NUMBER="$(discussion_number_for_page "$page")"
|
|
403
|
+
[[ -z "$NUMBER" ]] && error "No discussion for page '$page'. Seed one first: $0 seed --page '$page' ..."
|
|
404
|
+
else
|
|
405
|
+
error "post: provide --number N, --url URL, or --page PATH."
|
|
406
|
+
fi
|
|
407
|
+
|
|
408
|
+
if [[ -n "$body_file" ]]; then
|
|
409
|
+
require_file "$body_file" "body file"
|
|
410
|
+
# If it's a draft scaffold, post only the REPLY section; else the whole file.
|
|
411
|
+
if grep -q '===== REPLY' "$body_file"; then
|
|
412
|
+
body="$(extract_reply_body "$body_file")"
|
|
413
|
+
else
|
|
414
|
+
body="$(cat "$body_file")"
|
|
415
|
+
fi
|
|
416
|
+
fi
|
|
417
|
+
# Trim leading/trailing blank lines.
|
|
418
|
+
body="$(printf '%s' "$body" | sed -e 's/[[:space:]]*$//')"
|
|
419
|
+
[[ -z "${body//[[:space:]]/}" ]] && error "post: empty body. Provide --body TEXT or --body-file FILE with content under the REPLY marker."
|
|
420
|
+
|
|
421
|
+
if [[ "$DRY_RUN" == "true" ]]; then
|
|
422
|
+
info "[DRY RUN] Would post to $REPO_NWO discussion #$NUMBER${reply_to:+ (reply to $reply_to)}:"
|
|
423
|
+
printf '%s\n' "$body" | sed 's/^/ | /'
|
|
424
|
+
return 0
|
|
425
|
+
fi
|
|
426
|
+
|
|
427
|
+
local disc_id; disc_id="$(discussion_node_id "$NUMBER")"
|
|
428
|
+
[[ -z "$disc_id" || "$disc_id" == "null" ]] && error "Could not resolve discussion #$NUMBER on $REPO_NWO."
|
|
429
|
+
|
|
430
|
+
if [[ -n "$reply_to" ]]; then
|
|
431
|
+
gh api graphql -f query='
|
|
432
|
+
mutation($discussionId:ID!,$body:String!,$replyToId:ID!){
|
|
433
|
+
addDiscussionComment(input:{discussionId:$discussionId,body:$body,replyToId:$replyToId}){
|
|
434
|
+
comment{ url }
|
|
435
|
+
}
|
|
436
|
+
}' -F discussionId="$disc_id" -F body="$body" -F replyToId="$reply_to" \
|
|
437
|
+
--jq '"Posted reply: \(.data.addDiscussionComment.comment.url)"'
|
|
438
|
+
else
|
|
439
|
+
gh api graphql -f query='
|
|
440
|
+
mutation($discussionId:ID!,$body:String!){
|
|
441
|
+
addDiscussionComment(input:{discussionId:$discussionId,body:$body}){
|
|
442
|
+
comment{ url }
|
|
443
|
+
}
|
|
444
|
+
}' -F discussionId="$disc_id" -F body="$body" \
|
|
445
|
+
--jq '"Posted comment: \(.data.addDiscussionComment.comment.url)"'
|
|
446
|
+
fi
|
|
447
|
+
success "Comment posted to discussion #$NUMBER"
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
# ---------------------------------------------------------------------------
|
|
451
|
+
# Usage / dispatch
|
|
452
|
+
# ---------------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
show_usage() {
|
|
455
|
+
cat << 'EOF'
|
|
456
|
+
giscus-discussions — read, draft, and post the GitHub Discussions behind Giscus comments
|
|
457
|
+
|
|
458
|
+
USAGE:
|
|
459
|
+
./scripts/bin/giscus-discussions <subcommand> [options]
|
|
460
|
+
|
|
461
|
+
SUBCOMMANDS:
|
|
462
|
+
categories List discussion categories + node IDs (setup helper)
|
|
463
|
+
list [--json] List discussions in the Giscus category
|
|
464
|
+
thread <target> [--json] Show a discussion's full conversation
|
|
465
|
+
draft <target> [--out F] Write a Markdown reply-draft scaffold
|
|
466
|
+
seed --page P --title T (--body B | --body-file F) [--dry-run]
|
|
467
|
+
Create a new discussion for a page
|
|
468
|
+
post <target> (--body B | --body-file F) [--reply-to ID] [--dry-run]
|
|
469
|
+
Add a comment / reply to an existing discussion
|
|
470
|
+
|
|
471
|
+
<target> is one of: --number N | --url URL | --page /path/
|
|
472
|
+
|
|
473
|
+
COMMON OPTIONS:
|
|
474
|
+
--repo owner/name Override repository (default: gh repo view)
|
|
475
|
+
--category-id ID Override Giscus category (default: _config.yml)
|
|
476
|
+
--dry-run Preview writes (seed/post) without calling the API
|
|
477
|
+
|
|
478
|
+
ENVIRONMENT:
|
|
479
|
+
GISCUS_REPO Default --repo
|
|
480
|
+
GISCUS_CATEGORY_ID Default --category-id
|
|
481
|
+
|
|
482
|
+
EXAMPLES:
|
|
483
|
+
./scripts/bin/giscus-discussions categories
|
|
484
|
+
./scripts/bin/giscus-discussions list
|
|
485
|
+
./scripts/bin/giscus-discussions thread --page /posts/2025/01/21/remote-work-revolution/
|
|
486
|
+
./scripts/bin/giscus-discussions draft --number 7 --out /tmp/reply.md
|
|
487
|
+
./scripts/bin/giscus-discussions post --number 7 --body-file /tmp/reply.md --reply-to DC_xxx --dry-run
|
|
488
|
+
./scripts/bin/giscus-discussions seed --page /posts/new/ --title "/posts/new/" --body "Discussion thread." --dry-run
|
|
489
|
+
|
|
490
|
+
Requires an authenticated GitHub CLI (`gh auth login`).
|
|
491
|
+
EOF
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
main() {
|
|
495
|
+
local sub="${1:-}"
|
|
496
|
+
[[ $# -gt 0 ]] && shift || true
|
|
497
|
+
case "$sub" in
|
|
498
|
+
categories) cmd_categories "$@" ;;
|
|
499
|
+
list) cmd_list "$@" ;;
|
|
500
|
+
thread) cmd_thread "$@" ;;
|
|
501
|
+
draft) cmd_draft "$@" ;;
|
|
502
|
+
seed) cmd_seed "$@" ;;
|
|
503
|
+
post) cmd_post "$@" ;;
|
|
504
|
+
-h|--help|help|"") show_usage ;;
|
|
505
|
+
*) error "Unknown subcommand '$sub' (try: $0 --help)" ;;
|
|
506
|
+
esac
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
main "$@"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jekyll-theme-zer0
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.22.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Amr Abdel
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: jekyll
|
|
@@ -376,6 +376,7 @@ files:
|
|
|
376
376
|
- scripts/analyze-commits.sh
|
|
377
377
|
- scripts/bin/audit-consumer
|
|
378
378
|
- scripts/bin/build
|
|
379
|
+
- scripts/bin/giscus-discussions
|
|
379
380
|
- scripts/bin/install
|
|
380
381
|
- scripts/bin/manifest
|
|
381
382
|
- scripts/bin/release
|