Compare commits
44
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c3e98fc9b | ||
|
|
9630dcd5f6 | ||
|
|
925dc59016 | ||
|
|
8a07f15fde | ||
|
|
bedbdc227f | ||
|
|
001da3988c | ||
|
|
82eed06ad1 | ||
|
|
72ba7b8801 | ||
|
|
8da586c481 | ||
|
|
2b759542a8 | ||
|
|
28a52c9f0c | ||
|
|
dfb9557992 | ||
|
|
d4466dfba8 | ||
|
|
e464047774 | ||
|
|
e389a81d61 | ||
|
|
3fe9b55840 | ||
|
|
8483f8d1bb | ||
|
|
a5b5d7362c | ||
|
|
1db01ca0f1 | ||
|
|
c3b46c982f | ||
|
|
653d54476b | ||
|
|
920c68087e | ||
|
|
8738f54b9d | ||
|
|
6a8fd95a2c | ||
|
|
5892feea53 | ||
|
|
73cca6e6ea | ||
|
|
5471b97635 | ||
|
|
c3c810c97b | ||
|
|
f9537d9981 | ||
|
|
6ce2822147 | ||
|
|
5b3aa3959b | ||
|
|
dbe800d7a7 | ||
|
|
dc17907554 | ||
|
|
a0c13f807a | ||
|
|
f8123e829b | ||
|
|
8f94069ceb | ||
|
|
768ae6a9fe | ||
|
|
3a2eb40bef | ||
|
|
36fc4de73c | ||
|
|
861f1a0ed2 | ||
|
|
1bb1a4f10b | ||
|
|
f65bb40cfc | ||
|
|
4f3ba9c8d3 | ||
|
|
19eb29fecf |
@@ -43,7 +43,7 @@
|
||||
"source": "url",
|
||||
"url": "https://gitea.jsc.idv.tw/plugins/hooks.git"
|
||||
},
|
||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄"
|
||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查"
|
||||
},
|
||||
{
|
||||
"name": "jsc-log",
|
||||
@@ -75,7 +75,7 @@
|
||||
"source": "url",
|
||||
"url": "https://gitea.jsc.idv.tw/plugins/review.git"
|
||||
},
|
||||
"description": "程式碼審查:Refactoring 壞味道六組 + 註解規範 + 淺模組"
|
||||
"description": "程式碼審查:Refactoring 壞味道六組、註解規範、淺模組"
|
||||
},
|
||||
{
|
||||
"name": "jsc-sdlc",
|
||||
@@ -83,7 +83,7 @@
|
||||
"source": "url",
|
||||
"url": "https://gitea.jsc.idv.tw/plugins/sdlc.git"
|
||||
},
|
||||
"description": "開發生命週期:規劃/分析/實作/維護(wiki 追蹤)"
|
||||
"description": "開發生命週期:規劃、分析、實作、維護(wiki 追蹤)"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"source": "url",
|
||||
"url": "https://gitea.jsc.idv.tw/plugins/hooks.git"
|
||||
},
|
||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄"
|
||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查"
|
||||
},
|
||||
{
|
||||
"name": "jsc-log",
|
||||
@@ -75,7 +75,7 @@
|
||||
"source": "url",
|
||||
"url": "https://gitea.jsc.idv.tw/plugins/review.git"
|
||||
},
|
||||
"description": "程式碼審查:Refactoring 壞味道六組 + 註解規範 + 淺模組"
|
||||
"description": "程式碼審查:Refactoring 壞味道六組、註解規範、淺模組"
|
||||
},
|
||||
{
|
||||
"name": "jsc-sdlc",
|
||||
@@ -83,7 +83,7 @@
|
||||
"source": "url",
|
||||
"url": "https://gitea.jsc.idv.tw/plugins/sdlc.git"
|
||||
},
|
||||
"description": "開發生命週期:規劃/分析/實作/維護(wiki 追蹤)"
|
||||
"description": "開發生命週期:規劃、分析、實作、維護(wiki 追蹤)"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jsc-gitea",
|
||||
"version": "0.0.9",
|
||||
"description": "Gitea API 工具、Wiki 讀寫與存取庫批次同步",
|
||||
"version": "0.2.0",
|
||||
"description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步",
|
||||
"skills": "./skills",
|
||||
"author": {
|
||||
"name": "JSC"
|
||||
@@ -13,5 +13,12 @@
|
||||
"gitea",
|
||||
"skills",
|
||||
"cross-tool"
|
||||
]
|
||||
],
|
||||
"jsc": {
|
||||
"requires": {
|
||||
"jsc-ask": ">=0.0.6",
|
||||
"jsc-git": ">=0.0.9",
|
||||
"jsc-meta": ">=0.2.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
{
|
||||
"name": "jsc-gitea",
|
||||
"version": "0.0.9",
|
||||
"description": "Gitea API 工具、Wiki 讀寫與存取庫批次同步",
|
||||
"skills": "./skills"
|
||||
"version": "0.2.0",
|
||||
"description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步",
|
||||
"skills": "./skills",
|
||||
"jsc": {
|
||||
"requires": {
|
||||
"jsc-ask": ">=0.0.6",
|
||||
"jsc-git": ">=0.0.9",
|
||||
"jsc-meta": ">=0.2.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# jsc-gitea — 給 AI 助理的指引
|
||||
|
||||
本 repo 是 jsc 技能組的 `gitea` domain(Gitea API 工具、Wiki 讀寫與存取庫批次同步),可同時被 Claude Code / Codex / Copilot / Antigravity / Kiro 使用。
|
||||
本 repo 是 jsc 技能組的 `gitea` domain(Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步),可同時被 Claude Code / Codex / Copilot / Antigravity / Kiro 使用。
|
||||
|
||||
## 規則
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
2. 技能位於 `skills/{name}/SKILL.md`;處理任務前先比對需求與各技能的 `description`,相符就載入並依其步驟執行。
|
||||
3. 技能準則的唯一來源:`plugins/meta` 存取庫的 `references/guidelines.md`。
|
||||
4. 所有 hook 只放在 `jsc-hooks`;gitea 操作一律經由 `jsc-gitea` 的 `tools/gitea.sh`;問使用者一律依 `jsc-ask:ask` 的決策樹規則。
|
||||
5. 主 agent 不需要處理細節的流程,一律建立 sub agent 處理。
|
||||
5. 盯 PR 等合併用 `tools/pr-watch.sh`,不要自己寫輪詢迴圈。它回 10 就代表有新留言,把留言接回決策樹處理完,再重新盯一次。
|
||||
6. 主 agent 不需要處理細節的流程,一律建立 sub agent 處理。
|
||||
|
||||
## 呼叫慣例
|
||||
|
||||
|
||||
@@ -28,17 +28,92 @@ gitea.sh repos <owner> # 列出 owner 的 repo 全名
|
||||
gitea.sh default-branch <owner>/<repo>
|
||||
gitea.sh clone-url <owner>/<repo>
|
||||
gitea.sh hash-id <text> # 產生 8 碼大寫 SHA-1;首碼 0-9/A/B/C 時改成 Hxxxxxxx
|
||||
gitea.sh wiki-repo <TYPE> # 解析頁面類型的 wiki 位置(TYPE = QUESTION / PLAN / ANALYZE / DELIVER / MAINTAIN / REPO / LOG / LEARN / ERROR)
|
||||
gitea.sh wiki-repo <TYPE> # 解析頁面類型的 wiki 位置(TYPE = QUESTION / PLAN / ANALYZE / DELIVER / MAINTAIN / REPO / LOG / LEARN / ERROR / CHECK / REPORT / SKILLSET / TOOLING)
|
||||
gitea.sh wiki-list <owner>/<repo>
|
||||
gitea.sh wiki-get <owner>/<repo> <page> # 不存在 exit 4
|
||||
gitea.sh wiki-put <owner>/<repo> <page> <file> # 自動判斷新建或更新
|
||||
gitea.sh wiki-put <owner>/<repo> <page> <file> # 自動判斷新建或更新;會先要求確認
|
||||
gitea.sh wiki-url <owner>/<repo> <page> # 印出 wiki 頁絕對網址(取自 API 的 html_url);跨存取庫連結用
|
||||
gitea.sh pr-create <owner>/<repo> <head> <base> <title> <body-file>
|
||||
gitea.sh pr-create <owner>/<repo> <head> <base> <title> <body-file> # 會先要求確認
|
||||
gitea.sh pr-status <owner>/<repo> <pr-index> # 印出 {state} {merged} {mergeable}
|
||||
gitea.sh pr-get <owner>/<repo> <pr-index> # 印出 PR 的標題、base 分支與描述,供呼叫端比對有沒有差
|
||||
# 前三行固定 title、base、body 三個標記,第 4 行起是描述原文
|
||||
gitea.sh pr-of-branch <owner>/<repo> <branch> # 印出該分支目前開啟中的 PR(比對 head.ref)
|
||||
# 第 1 行 number,接著 title、base、body 三個標記,第 5 行起是描述原文
|
||||
# 結束碼 0=找到、2=用法錯誤、3=該分支沒有開啟中的 PR、4=API 失敗
|
||||
# 「沒有 PR」用 3 不用 4:混用會把金鑰失效讀成沒開過 PR,接著開出重複的 PR
|
||||
gitea.sh pr-edit <owner>/<repo> <pr-index> <title> <body-file> # 會先要求確認
|
||||
# 更新 PR 的標題與描述;描述從檔案讀,裝得下多行
|
||||
gitea.sh pr-comments <owner>/<repo> <pr-index> # 印出所有留言(issue 留言、審查評語、行內留言),第三欄帶 #id,依時間排序
|
||||
gitea.sh comment-reply <owner>/<repo> <pr-index> <issue|review|inline> <comment-id> <body-file> # 會先要求確認
|
||||
# 回覆本輪處理過的 PR 留言;inline 走 review comment reply,其餘補一則 PR 留言
|
||||
gitea.sh pr-depend <owner>/<repo> <pr-index> <dep-owner>/<dep-repo> <dep-index> # 會先要求確認
|
||||
# 把 PR 掛上前置 PR 依賴;依賴未關閉前 Gitea 會阻擋合併
|
||||
gitea.sh repo-set <owner>/<repo> <description> [website] # 會先要求確認
|
||||
gitea.sh markdown <file> # markdown 檔渲染成 HTML 片段(走 /markdown/raw)
|
||||
gitea.sh api <METHOD> <path> [json-file]
|
||||
# 全腳本共用結束碼: 0=成功、1=api 子命令請求失敗、2=用法錯誤
|
||||
# 3=wiki-repo 沒設定或 pr-of-branch 查無 PR、4=找不到(404)與 PR 系列 API 失敗
|
||||
# 5=wiki 頁沒有 html_url、7=金鑰失效或權限不足(401/403)、8=其他 API 失敗
|
||||
# 7 與 8 一定要跟 4 分開:認證失敗若被讀成「頁面不存在」,
|
||||
# 附加寫入就會變成整頁覆蓋,舊紀錄直接消失
|
||||
hash-id <text> # 與 gitea.sh hash-id 相同
|
||||
repo-sync.sh <owner>/<repo> [target-dir] # 同步單一存取庫;印出 cloned、updated、dirty {分支} 或 failed {原因}
|
||||
# 基準分支的優先序只在這支腳本裡;dirty 會把解析好的分支帶出來當 PR 的 base
|
||||
check-wiki-rules.sh # 驗證 wiki repo 解析與 hash fallback 規則
|
||||
pr-watch.sh <owner>/<repo> <pr-index> [state-file]
|
||||
# 盯著一支 PR,直到它合併或關閉,不自動逾時退場
|
||||
# 退出碼 0 = 已合併或關閉、10 = 有新留言要接手、2 = 參數錯誤、3 = 查不到該 PR
|
||||
# 已回報過的留言記在狀態檔,預設 $JSC_HOME/pr-watch/{owner}-{repo}-{index}.seen
|
||||
```
|
||||
|
||||
`pr-watch.sh` 的退出碼 10 是設計重點:腳本只負責偵測,決策交回呼叫端。收到 10 就把印出來的留言丟進決策樹,處理完再重新盯一次。
|
||||
|
||||
議題與 HTML 產出:
|
||||
|
||||
```
|
||||
gitea-link.sh parse <url> # 解析 wiki 或議題連結;不是這兩種就 exit 3(呼叫端據此中止)
|
||||
issue.sh labels|label-ids|projects <owner>/<repo>
|
||||
issue.sh title|body|labels-of <owner>/<repo> <index>
|
||||
issue.sh show <owner>/<repo> <index> # 一次拿齊標題、標籤與正文,只打一次 API
|
||||
# 四段固定格式:title、labels、body 標記,第 4 行起是正文
|
||||
issue.sh create <owner>/<repo> <title> <body-file> [--labels <ids>] [--milestone <id>] # 會先要求確認
|
||||
html-style.sh get|set|unset|list|layouts|styles # 種類對版型與風格的設定
|
||||
html-style.sh key wiki <頁名> # 推導種類:取第一個底線前的字首
|
||||
html-style.sh key issue <owner>/<repo> <編號> [--labels <名稱>[,<名稱>]]
|
||||
# 推導種類:依序試每個標籤,第一個設定過的勝出,都沒有就回 ISSUE:DEFAULT
|
||||
# 輸出一行「{種類}<TAB>{依據}」;結束碼 1=取不到標籤
|
||||
html-render.sh --markdown <檔案> --title <標題> --out <輸出檔> [--layout] [--style] [--subtitle] [--source-url]
|
||||
```
|
||||
|
||||
## 參考資料
|
||||
|
||||
- `references/wiki-links.md`:寫 wiki 頁才需要的連結規則。同類型用 `[[顯示文字|頁名]]`(顯示文字在左),跨類型用 `wiki-url` 給的絕對網址。
|
||||
|
||||
## HTML 範本
|
||||
|
||||
版型(`templates/html/layout/*.html`)決定內容怎麼排,風格(`templates/html/style/*.css`)決定看起來長怎樣。兩者自由搭配,六乘五共三十種。
|
||||
|
||||
| 版型 | 內容排法 |
|
||||
| --- | --- |
|
||||
| `report` | 左側目錄加章節內文,長文件用 |
|
||||
| `slide` | 一個 `##` 一張投影片,鍵盤左右鍵翻頁 |
|
||||
| `dashboard` | 每個 `##` 一張卡片並排 |
|
||||
| `spec` | 表格表頭固定、程式碼區塊放大,API 文件用 |
|
||||
| `timeline` | 每個 `##` 一個節點串成一條線 |
|
||||
| `onepager` | 窄欄單頁,印出來剛好一頁 |
|
||||
|
||||
| 風格 | 視覺 |
|
||||
| --- | --- |
|
||||
| `minimal` | 白底細線、無襯線,資訊密度優先 |
|
||||
| `corporate` | 深藍主色、表頭反白,正式對外 |
|
||||
| `dark` | 深底亮字 |
|
||||
| `print` | 襯線字、A4 邊界,列印或轉 PDF |
|
||||
| `vivid` | 高彩度、圓角卡片、漸層標題 |
|
||||
|
||||
哪一種頁面套哪一組,由 `html-style.sh` 的設定決定:專案的 `./.jsc/html-styles` 優先,其次 `$JSC_HOME/html-styles.conf`,種類對不到就退 `DEFAULT`,再對不到才用內建的 `report`/`minimal`。設定的 key 是 `WIKI:{頁名前綴}`、`ISSUE:{標籤名}` 或 `DEFAULT`。
|
||||
|
||||
自訂範本:版型放進 `templates/html/layout/`,風格放進 `templates/html/style/`,檔案第一行寫一句繁中說明——那句話就是技能問使用者時顯示的選項說明。版型檔可用的佔位有 `{{TITLE}}`、`{{SUBTITLE}}`、`{{CONTENT}}`、`{{BASE}}`、`{{BASE_JS}}`、`{{STYLE}}`、`{{SOURCE}}`、`{{GENERATED}}`、`{{LAYOUT}}`、`{{STYLE_NAME}}`。
|
||||
|
||||
## Skills 目錄
|
||||
|
||||
呼叫方式:Claude / Antigravity `/jsc-gitea:{name}`;Codex `${name}`;Copilot / Kiro 描述需求自動觸發。
|
||||
@@ -47,11 +122,23 @@ check-wiki-rules.sh # 驗證 wiki repo 解析與 ha
|
||||
|
||||
### `wiki`
|
||||
|
||||
Gitea wiki 頁讀寫的統一入口:依頁面類型(QUESTION / PLAN / ANALYZE / DELIVER / MAINTAIN / REPO / LOG / LEARN / ERROR)解析 wiki 所在的 `{owner}/{repo}`,先讀對應的 `JSC_WIKI_REPO_{TYPE}`,再退回 `JSC_WIKI_REPO`,不同類型不可互相代用。頁面內容以圖表優先(mermaid 圖、markdown 表格),純文字為最後手段。
|
||||
Gitea wiki 頁讀寫的統一入口:依頁面類型(QUESTION / PLAN / ANALYZE / DELIVER / MAINTAIN / REPO / LOG / LEARN / ERROR / CHECK / REPORT / SKILLSET / TOOLING)解析 wiki 所在的 `{owner}/{repo}`,先讀對應的 `JSC_WIKI_REPO_{TYPE}`,再退回 `JSC_WIKI_REPO`,不同類型不可互相代用。頁面內容以圖表優先(mermaid 圖、markdown 表格),純文字每節最多三句。
|
||||
|
||||
### `repo-sync`
|
||||
|
||||
存取庫批次同步:列出 owner → 使用者選擇 → 逐 repo(sub agent)clone 或切 develop/master 更新;有變更就開分支 commit、push、PR。
|
||||
存取庫批次同步:列出 owner → 使用者選擇 → 各存取庫併行(一個 repo 一個 sub agent)呼叫 `tools/repo-sync.sh` clone 或更新;回報 `dirty {分支}` 的存取庫交給 `jsc-git:pr`,base 直接用腳本帶出來的那個分支;PR 依 `jsc-meta/references/pr-report.md` 集中成表。
|
||||
|
||||
### `html-export`
|
||||
|
||||
把一頁 wiki 或一筆議題輸出成單一 HTML 檔:解析連結 → 判斷種類 → 查該種類的版型與風格 → 用 Gitea 自己的 markdown 渲染出圖。CSS 與腳本全部內嵌,檔案拿到哪裡都打得開。**沒有連結就直接中止**,不猜存取庫、不猜頁名、不猜議題編號。
|
||||
|
||||
### `html-style`
|
||||
|
||||
設定「哪一種 wiki 頁或議題,出 HTML 時用哪一種版型與風格」:六種版型與五種風格全部列給使用者選,再寫進專案的 `.jsc/html-styles` 或全域的 `$JSC_HOME/html-styles.conf`。`html-export` 讀的就是這份設定。
|
||||
|
||||
### `wiki-to-issue`
|
||||
|
||||
把一頁 wiki 轉成同一個存取庫的議題:讀頁面 → sub agent 起草標題與正文(開頭附來源連結)→ 從既有標籤挑合適的 → 關聯專案看板 → 建立議題。標籤只從存取庫既有的挑,不自己發明;站台沒有看板 API 時據實回報請使用者手動拖,不假裝關聯成功。**沒有連結就直接中止**。
|
||||
|
||||
<!-- JSC-SKILLS:END -->
|
||||
|
||||
@@ -61,12 +148,16 @@ Gitea wiki 頁讀寫的統一入口:依頁面類型(QUESTION / PLAN / ANALYZ
|
||||
| --- | --- | --- |
|
||||
| `GITEA_HOST` | Gitea 站台(可省略 scheme,預設 https) | 詢問使用者 |
|
||||
| `GITEA_TOKEN` | Gitea API token;缺少或遇 401/403 時自動退回 tea CLI 登入 token | 詢問使用者 |
|
||||
| `JSC_WIKI_REPO_{TYPE}` | 各類型 wiki 頁的 `{owner}/{repo}`;TYPE = QUESTION / PLAN / ANALYZE / DELIVER / MAINTAIN / REPO / LOG / LEARN / ERROR | 退回 `JSC_WIKI_REPO`,不做跨類型代用 |
|
||||
| `JSC_WIKI_REPO_{TYPE}` | 各類型 wiki 頁的 `{owner}/{repo}`;TYPE = QUESTION / PLAN / ANALYZE / DELIVER / MAINTAIN / REPO / LOG / LEARN / ERROR / CHECK / REPORT | 退回 `JSC_WIKI_REPO`,不做跨類型代用 |
|
||||
| `JSC_WIKI_REPO_SKILLSET` | `SKILLSET_CONTENTS`、`SKILLSET_{HASH}`(技能組異動報告)的 `{owner}/{repo}` | 退回 `JSC_WIKI_REPO`,不做跨類型代用 |
|
||||
| `JSC_WIKI_REPO_TOOLING` | `TOOLING_CONTENTS`、`TOOLING_{HASH}`(技能盤點)的 `{owner}/{repo}` | 退回 `JSC_WIKI_REPO`,不做跨類型代用 |
|
||||
| `JSC_WIKI_REPO` | 共用預設的 wiki `{owner}/{repo}` | 詢問使用者 |
|
||||
| `JSC_HOME` | `pr-watch.sh` 狀態檔的根目錄 | 預設 `~/.jsc` |
|
||||
| `JSC_PR_WATCH_INTERVAL` | `pr-watch.sh` 的輪詢間隔秒數(正整數) | 預設 60 |
|
||||
|
||||
## Hash 規則
|
||||
|
||||
`{HASH}` 由 `tools/hash-id` 產生。先算 `SHA-1` 前 8 碼並轉大寫。若首碼是數字或 `A`、`B`、`C`,就改成 `H` 加上原本的前 7 碼,維持 8 碼長度。
|
||||
`{HASH}` 一律由 `tools/hash-id` 產生。長度、大小寫、`H` 前綴與各頁型的雜湊來源,唯一來源是 `jsc-meta` 的 [`references/guidelines.md`](https://gitea.jsc.idv.tw/plugins/meta/src/branch/master/references/guidelines.md)「Wiki 頁命名總表」,這裡不再複述一份。
|
||||
|
||||
## 相關 domain
|
||||
|
||||
|
||||
+10
-3
@@ -1,6 +1,13 @@
|
||||
{
|
||||
"name": "jsc-gitea",
|
||||
"version": "0.0.9",
|
||||
"description": "Gitea API 工具、Wiki 讀寫與存取庫批次同步",
|
||||
"skills": "./skills/"
|
||||
"version": "0.2.0",
|
||||
"description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步",
|
||||
"skills": "./skills/",
|
||||
"jsc": {
|
||||
"requires": {
|
||||
"jsc-ask": ">=0.0.6",
|
||||
"jsc-git": ">=0.0.9",
|
||||
"jsc-meta": ">=0.2.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# jsc-gitea 技能行為清單
|
||||
|
||||
本頁記錄 jsc-gitea 每支技能的行為基準,供技能驗證比對。技能異動時,在同一個 PR 內一起更新這一頁。
|
||||
|
||||
## html-export
|
||||
|
||||
| 項目 | 內容 |
|
||||
| --- | --- |
|
||||
| 觸發時機 | 使用者給一條 Gitea wiki 頁連結或議題連結,要把那一頁變成一份離得開 Gitea 的 HTML 檔。請求裡沒有連結就直接停手。不猜存放庫、不猜頁名、不猜議題編號,也不回頭去讀工作目錄的 remote。要改某一類頁面用哪一組範本,走 jsc-gitea:html-style。 |
|
||||
| 關鍵步驟 | 用 tools/gitea-link.sh parse 解析連結、確認 GITEA_HOST 有值、同一批平行跑三條線、開 sub agent 整理 markdown、用 tools/html-render.sh 產出檔案。三條線分別是:A 取內容,wiki 頁走 jsc-gitea:wiki 的 wiki-get 與 wiki-url,議題走 tools/issue.sh show 一次取回標題、標籤、內文;B 取範本,tools/html-style.sh key 算出 kind key,再用 get 讀出版型、風格、來源三欄;C 問輸出路徑,預設提 ./.jsc/html/{名稱}.html。sub agent 只做兩件事:把 wiki 內部連結換成絕對網址、拿掉個資。 |
|
||||
| 外部呼叫 | tools/gitea-link.sh、tools/html-style.sh、tools/html-render.sh、tools/issue.sh、jsc-gitea:wiki、jsc-ask:ask、Gitea 的 wiki API、Gitea 的議題 API、Gitea 的 markdown 渲染 API。 |
|
||||
| 完成條件 | 檔案已經寫出來。回報裡有檔案路徑、版型名稱、風格名稱,以及這一組是從哪裡來的。來源是 default 或 builtin 時,回報要多一行告訴使用者可以用 jsc-gitea:html-style 設定。渲染失敗就不留半成品檔,直接停手回報。 |
|
||||
| 可驗證跡象 | 使用者確認過的輸出路徑多一個 HTML 檔,預設落在 ./.jsc/html/ 底下。這個檔把 CSS 與 JS 內嵌,不外連任何資源,開起來就是完整的一頁。不寫 wiki 頁、不建議題、不開 PR,來源頁面本身也不動。 |
|
||||
|
||||
## html-style
|
||||
|
||||
| 項目 | 內容 |
|
||||
| --- | --- |
|
||||
| 觸發時機 | 某一類 wiki 頁或議題匯出時要換版型、換風格。或是 jsc-gitea:html-export 回報來源是 builtin 或 default,要補上這一類自己的設定。只是要出一份檔案,走 jsc-gitea:html-export。 |
|
||||
| 關鍵步驟 | 用 tools/html-style.sh list 列出目前設定、依決策樹敲定一個 kind key、用 layouts 列出全部六種版型讓使用者挑、用 styles 列出全部五種風格讓使用者挑、問這次寫專案還是寫全機、用 set 寫進設定檔、再用 get 讀回來核對來源欄。kind key 只有三種形狀:WIKI:{頁名前綴}、ISSUE:{標籤名}、DEFAULT。版型與風格一律整份列出,不先篩短清單。 |
|
||||
| 外部呼叫 | tools/html-style.sh 的 list、get、layouts、styles、set、unset 子命令、jsc-ask:ask。不打任何 Gitea API。 |
|
||||
| 完成條件 | set 回結束碼 0,並印出它寫的那個檔案。get 讀回來的來源欄與這次選的範圍一致:選 --project 就顯示 project,選 --global 就顯示 global。寫進去的版型名與風格名,都要是腳本列過的名字。 |
|
||||
| 可驗證跡象 | 設定檔多一列 {種類}={版型},{風格},例如 WIKI:PLAN=report,corporate。選 --project 改的是工作目錄的 ./.jsc/html-styles,這個檔會跟著存放庫一起提交。選 --global 改的是 $JSC_HOME/html-styles.conf,JSC_HOME 預設 ~/.jsc。兩個檔都握有同一個 key 時,專案檔贏。不產 HTML 檔、不動 Gitea。 |
|
||||
|
||||
## repo-sync
|
||||
|
||||
| 項目 | 內容 |
|
||||
| --- | --- |
|
||||
| 觸發時機 | 要把某一個 Gitea 擁有者底下讀得到的存放庫,一次全部拉到工作目錄。開新工作環境、或整批更新既有存放庫時用。只同步一個存放庫時不用這支。 |
|
||||
| 關鍵步驟 | 用 tools/gitea.sh owners 列出讀得到的擁有者、請使用者挑一個、用 tools/gitea.sh repos 列出該擁有者底下的存放庫、同一批平行開 sub agent 一個存放庫一個、每個 sub agent 跑 tools/repo-sync.sh 並依它那一行輸出分流、最後把每個存放庫的結果彙整回報。分流有四種:cloned 與 updated 就算完成,dirty {分支} 把那個分支當 base 交給 jsc-git:pr,failed {原因} 記下原因並停掉這個存放庫。複製或拉取的判斷、基準分支的優先序,都由腳本決定,不自己下 git clone、git checkout、git pull。 |
|
||||
| 外部呼叫 | tools/gitea.sh 的 owners、repos、clone-url、tools/repo-sync.sh、jsc-git:pr、jsc-ask:ask、Gitea 的擁有者與存放庫清單 API,以及腳本內部的 git clone、git fetch、git pull。 |
|
||||
| 完成條件 | 清單上的每一個存放庫都拿到一種結果:cloned、updated、一列 PR 表格,或失敗原因。一個存放庫失敗不取消其他存放庫。有多條 PR 時,全部併進同一張表。 |
|
||||
| 可驗證跡象 | 工作目錄底下多出或更新了各存放庫的目錄。新的是 git clone 的結果,既有的已經快轉到基準分支。原本有未提交變更的存放庫,會多一條推上去的分支,以及一條開在 Gitea 上的 PR,PR 網址寫在回報表格裡。不寫 wiki 頁、不建議題。 |
|
||||
|
||||
## wiki
|
||||
|
||||
| 項目 | 內容 |
|
||||
| --- | --- |
|
||||
| 觸發時機 | 技能組裡任何一次 wiki 頁的讀或寫,都經過這一支。呼叫端有 jsc-ask、jsc-sdlc、jsc-log、jsc-hooks 的 ERROR 頁、jsc-cli 的 CHECK 頁、jsc-meta 的 SKILLSET 與 TOOLING 頁。存放庫裡的程式碼檔案不歸這一支管。 |
|
||||
| 關鍵步驟 | 確認 GITEA_HOST 有值、用 tools/gitea.sh wiki-repo {TYPE} 依頁名前綴解出 {owner}/{repo}、用 tools/hash-id 算頁名要用的 HASH、再依動作跑 wiki-list、wiki-get、wiki-put 或 wiki-url、每一次呼叫都照結束碼表分流。存放庫的解法是先讀 JSC_WIKI_REPO_{TYPE}、再讀 JSC_WIKI_REPO、兩個都沒有才問使用者,而且不借用別的頁型的存放庫。寫入前一定先 wiki-get 讀回舊內容,把新內容接上去再整頁寫回;只有結束碼 4 才准用範本建新頁。 |
|
||||
| 外部呼叫 | tools/gitea.sh 的 wiki-repo、wiki-list、wiki-get、wiki-put、wiki-url、tools/hash-id、tools/write-confirm.sh、jsc-ask:ask、Gitea 的 wiki API。 |
|
||||
| 完成條件 | 讀取動作拿到頁面內容,或拿到一個講得清楚的結束碼。寫入動作通過人工確認、wiki-put 回結束碼 0,而且送出去的是舊內容加上這次的異動,不是整頁覆蓋。結束碼 7 與 8 一律中止整個動作,不建頁、不寫入、不用原參數重試。 |
|
||||
| 可驗證跡象 | 目標 wiki 存放庫多一頁或改一頁,頁名照命名表,例如 PLAN_{HASH}、LOG_{HASH}、各種 *_CONTENTS。頁面內容是 UTF-8 繁體中文,以 mermaid 圖與 markdown 表格為主,散文每節最多三句。寫入前 tools/write-confirm.sh 會留下一次人工確認。只做讀取的呼叫無寫入跡象,只有回報內容。 |
|
||||
|
||||
## wiki-to-issue
|
||||
|
||||
| 項目 | 內容 |
|
||||
| --- | --- |
|
||||
| 觸發時機 | 一頁 wiki 要變成一條追得動的議題,而且使用者已經給了那一頁的連結。請求裡沒有 wiki 連結就直接停手,不猜存放庫、不猜頁名。連結指向的是議題也停手。從零起草的議題不走這一支,已經存在的議題要同步也不走這一支。 |
|
||||
| 關鍵步驟 | 同一批檢查連結與主機、平行跑三條線、依決策樹挑標籤、處理看板、最後建議題。連結用 tools/gitea-link.sh parse 解析,要印出 kind=wiki 才算過;GITEA_HOST 要有值。三條線是:A 讀頁面並開 sub agent 起草標題與內文;B 用 tools/issue.sh labels 取這個存放庫既有的標籤清單;C 用 tools/issue.sh projects 取看板清單。內文開頭放一行「來源:{絕對網址}」,wiki 內部連結換成絕對網址,個資拿掉。標籤只能從既有清單裡挑,確認後用 label-ids 換成 id,缺的標籤不補建。看板 API 不存在時,把腳本印出的看板網址交給使用者自己拖。 |
|
||||
| 外部呼叫 | tools/gitea-link.sh、tools/issue.sh 的 labels、projects、label-ids、create、jsc-gitea:wiki、jsc-ask:ask、tools/write-confirm.sh、Gitea 的 wiki API 與議題 API。 |
|
||||
| 完成條件 | 議題已經建立,回報裡有議題網址、實際套上的標籤,以及看板狀態。建不成就明講沒有建成,並交出草稿檔的路徑,讓草稿不會白寫。標題、內文、標籤、看板都在呼叫 create 之前先跟使用者確認過。 |
|
||||
| 可驗證跡象 | 目標存放庫的議題追蹤器多一條議題,create 會印出 index= 與 url= 兩行。議題內文第一行是「來源:」加上 wiki 頁的絕對網址,內文裡沒有殘留的 wiki 內部連結語法。標籤就是這次確認過的那一組。站台有看板 API 時,看板上多一張卡;沒有時,回報裡留一行待辦說明誰要去補。來源 wiki 頁本身不動。 |
|
||||
@@ -0,0 +1,24 @@
|
||||
# Wiki 頁之間的連結
|
||||
|
||||
寫 wiki 頁才需要這份規則。兩條規則任一條寫錯,連結會指向一個不存在的頁,畫面上看不出異常。
|
||||
|
||||
## 方向:顯示文字在左,頁名在右
|
||||
|
||||
Gitea 採 GitHub/Gollum 慣例:`[[顯示文字|頁名]]`。方向與 MediaWiki 相反。Gitea 原始碼(`modules/markup/html_link.go`)寫得很清楚:
|
||||
|
||||
> MediaWiki uses [[link|text]], while GitHub uses [[text|link]] … we prefer GitHub syntax
|
||||
|
||||
所以 `[[PLAN_H1234567|我的計畫]]` 會顯示成文字 `PLAN_H1234567`,連到一個叫「我的計畫」的頁——這是壞連結。要寫 `[[我的計畫|PLAN_H1234567]]`。
|
||||
|
||||
顯示文字與頁名相同時,用不帶豎線的 `[[PLAN_H1234567]]`,這種寫法不會寫錯。
|
||||
|
||||
## 範圍:`[[...]]` 只在同一個 wiki 內解析
|
||||
|
||||
`[[...]]` 與 markdown 相對連結都只在目前這個 wiki 內解析。跨存取庫沒有 wiki 連結語法。
|
||||
|
||||
| 連結 | 同一個 wiki? | 寫法 |
|
||||
| --- | --- | --- |
|
||||
| 同頁面類型(例:`PLAN_CONTENTS` → `PLAN_{HASH}`) | 一定同一個:一個類型一個存取庫 | `[[顯示文字\|頁名]]` 或 `[[頁名]]` |
|
||||
| 不同頁面類型(例:`LOG_{HASH}` → `PLAN_{HASH}`) | **只有兩個類型解析到同一個存取庫時才同一個** | `wiki-url` 給的絕對網址:`[顯示文字](https://…/wiki/PLAN_…)` |
|
||||
|
||||
每個類型各自解析自己的 `JSC_WIKI_REPO_{TYPE}`,所以跨類型連結**一律**用絕對網址:兩個類型剛好同存取庫也照樣正確,不必分兩種寫法。網址一律取自 `tools/gitea.sh wiki-url`,不要自己組路徑。
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: html-export
|
||||
description: 'Export one Gitea wiki page or issue as a single self-contained HTML file. Parse the link with tools/gitea-link.sh, resolve that kind''s layout and style through tools/html-style.sh (kind, then DEFAULT, then the built-in report/minimal), then render with tools/html-render.sh, which puts the markdown through Gitea''s own renderer and inlines every asset. A request without a wiki or issue link stops the skill immediately: never guess the repository, the page or the issue number. Use when a page or issue has to leave Gitea as a document; not for choosing which template a kind uses - that is jsc-gitea:html-style.'
|
||||
---
|
||||
|
||||
# html-export — a wiki page or issue becomes one HTML file
|
||||
|
||||
The link is the only input. The output is one file that opens anywhere, with no external asset.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Link gate.** Run `tools/gitea-link.sh parse {url}`. Exit 3, or no link in the request: **stop and report it**. Exit 2 is a usage error — fix the arguments and call again. Never fall back to the working directory's remote or to a page name the user mentioned in passing. This gate runs first because it stops the whole skill more often than any other check, and it costs no API call. Completion condition: `kind` is `wiki` or `issue`, and `repo` plus `page` or `index` are known.
|
||||
2. **Host gate.** Confirm `GITEA_HOST` holds a value in the current shell; ask for it per the `jsc-ask:ask` rules when it does not. `GITEA_TOKEN` needs no inventory — `tools/gitea.sh` resolves it, retries once with the tea CLI login token, and exits 7 when neither works. Completion condition: `GITEA_HOST` holds a value.
|
||||
3. **Run these three tracks at the same time.** They are independent, so start them in one batch rather than one after another; only the issue branch of track B waits, and only for track A's `labels` line.
|
||||
- **Track A — content.** Wiki page: `jsc-gitea:wiki` `wiki-get {repo} {page}` for the markdown, plus `wiki-url {repo} {page}` for the source URL; route its exit codes by that skill's table (4 = no such page, 7 = the key is invalid, 8 = other API failure), and every one of them stops this skill with the page name in the report. Issue: `tools/issue.sh show {repo} {index}`, which returns title, labels and body from **one** API call — never call `title`, `body` and `labels-of` separately on the same issue. Exit 1 means the issue could not be read: stop and report the issue number; exit 2 is a usage error, so fix the arguments and call again.
|
||||
- **Track B — template.** Wiki page: `tools/html-style.sh key wiki {page}`. Issue: `tools/html-style.sh key issue {repo} {index} --labels {the names from track A's labels line}`, which spends no extra API call; the labels line arrives before the body, so this track starts well before track A finishes. The script owns both derivation rules — the page-name prefix, and trying each label in order until one is configured. It prints `{key}<TAB>{reason}`; keep the reason, it is how the report says which prefix or label produced the key. Exit 1 means the labels could not be read: **report that first**, then continue with `ISSUE:DEFAULT` and say in the report that the template was picked without labels. Exit 2 is a usage error — fix the arguments and call again. Then run `tools/html-style.sh get {key}`, which always prints `layout<TAB>style<TAB>source`. **Read the third column and report it**: `project` or `global` means the user configured this kind; `default` means it fell back to the DEFAULT row; `builtin` means nothing is configured at all and `report`/`minimal` was used. For `default` and `builtin`, tell the user in one line that `jsc-gitea:html-style` can set this kind's own template.
|
||||
- **Track C — destination.** Ask per the `jsc-ask:ask` rules where the file goes, proposing `./.jsc/html/{page-or-issue}.html`. State the impact scope: a path inside a repository gets committed unless it is ignored.
|
||||
|
||||
Completion condition: the markdown and the document title are in hand, exactly one kind key is chosen with the reason that produced it, the layout, style and source are reported, and the user has confirmed one output path.
|
||||
4. **Prepare the markdown — this step MUST run as a sub agent.** Convert `[[display|page]]` wiki links to absolute URLs from `wiki-url`; the renderer does not resolve them, so they would ship as literal brackets. Strip personal data — an exported file travels further than the page it came from. Leave everything else exactly as written; this step never rewrites the content. Completion condition: no `[[...]]` remains, and the diff against the source is limited to link conversion and personal-data removal.
|
||||
5. Render: `tools/html-render.sh --markdown {file} --title {title} --layout {layout} --style {style} --source-url {absolute URL} --out {path}`. Route every exit code: 0 → the path it printed is the finished file; 1 → Gitea's renderer or the write failed, so report it and stop, with no half-rendered file left behind; 2 → a usage error or a missing markdown file, so fix the arguments and call again; 4 → the layout or style template file is gone, so report which pair was asked for and send the user to `jsc-gitea:html-style` rather than editing the configuration by hand. Completion condition: the file exists, and the report names its path, the layout, the style and where that pair came from.
|
||||
|
||||
## Rules
|
||||
|
||||
- One link, one file. Batch export is a loop the caller runs, not something this skill decides on its own.
|
||||
- The rendered file inlines CSS and scripts on purpose: it is usually sent to someone outside Gitea, and an external asset breaks on their machine.
|
||||
- The layout and style are never chosen by inspecting the content. The configuration decides, and `jsc-gitea:html-style` owns the configuration.
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: html-style
|
||||
description: Set which HTML layout and style a kind of Gitea wiki page or issue gets when jsc-gitea:html-export renders it. Offer all six layouts from tools/html-style.sh layouts and all five styles from tools/html-style.sh styles as decision-tree options per jsc-ask, then write the pair with tools/html-style.sh set - either to the project file .jsc/html-styles or to the global $JSC_HOME/html-styles.conf. Use when a kind of page or issue should come out looking different, or when the export reported source builtin or default; not for rendering a page, which is jsc-gitea:html-export.
|
||||
---
|
||||
|
||||
# html-style — which template a kind of page gets
|
||||
|
||||
One kind of page, one layout, one style. `jsc-gitea:html-export` reads what this skill writes.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Settle the kind key.** Show the current configuration with `tools/html-style.sh list` first, then ask per `jsc-ask:ask` rules which kind this run sets. The three shapes are fixed: `WIKI:{page-name prefix}` (`WIKI:PLAN`, `WIKI:ANALYZE`, `WIKI:LOG` …), `ISSUE:{label name}` (`ISSUE:bug`), and `DEFAULT` for everything that matches nothing else. Every option states its impact scope — `DEFAULT` changes every kind that has no row of its own. Completion condition: exactly one key is agreed, and its current value from `tools/html-style.sh get {key}` has been read back with its source column.
|
||||
2. **Pick the layout — offer all six.** Run `tools/html-style.sh layouts`; it prints each name with its Traditional Chinese description, taken from the template file itself. `list`, `get`, `layouts` and `styles` exit 2 on a usage error — fix the arguments and call again. An empty listing means the template directory is missing, which stops this skill: report the path rather than offering a name the export cannot use. Present all six as options per `jsc-ask:ask` rules, each with what it does to the content (`report` builds a table of contents beside the text, `slide` turns every `##` into a keyboard-flipped page, `dashboard` turns them into cards, `spec` freezes table headers, `timeline` strings them along a line, `onepager` narrows everything into one printable page). Completion condition: the user has picked one layout name that the script listed.
|
||||
3. **Pick the style — offer all five.** Run `tools/html-style.sh styles` and present every one it prints (`minimal`, `corporate`, `dark`, `print`, `vivid`) with its description. Never trim the list to a shortlist: the point of this skill is that the user sees the whole set. Completion condition: the user has picked one style name that the script listed.
|
||||
4. **Pick the scope.** Ask per `jsc-ask:ask` rules: `--project` writes `./.jsc/html-styles`, which only applies inside this working directory and is committed with the repository; `--global` writes `$JSC_HOME/html-styles.conf`, which follows the user across every project on this machine. State that the project file wins whenever both hold the same key. Completion condition: the user has picked one scope.
|
||||
5. Write it: `tools/html-style.sh set {key} {layout} {style} [--project|--global]`. Route every exit code: 0 → the file it printed now holds the pair; 1 → the settings file's directory could not be created, so report the path and stop, since nothing was written; 2 → a usage error, such as a missing name or a scope flag that is neither `--project` nor `--global`, so fix the arguments and call again; 4 → the layout or style name has no template file, so go back to step 2 or step 3 rather than editing the settings file by hand. Completion condition: the script exits 0 and prints the file it wrote.
|
||||
6. Read it back with `tools/html-style.sh get {key}` and report the resolved layout, style and source. Completion condition: the source column shows `project` or `global`, matching the scope chosen in step 4.
|
||||
|
||||
## Rules
|
||||
|
||||
- Only names the script listed may be written. A key holding a template that does not exist fails at export time, long after the mistake was made.
|
||||
- Removing a row is `tools/html-style.sh unset {key} [--project|--global]`; after that the kind falls back to `DEFAULT`, then to the built-in `report`/`minimal`.
|
||||
- New layouts live in `templates/html/layout/{name}.html` and new styles in `templates/html/style/{name}.css`, each starting with a one-line Traditional Chinese comment — that comment is what the option list shows.
|
||||
+10
-10
@@ -1,18 +1,18 @@
|
||||
---
|
||||
name: repo-sync
|
||||
description: Batch-sync all readable repos of a chosen Gitea owner into the working directory. List owners, let the user pick, then clone or update each repo; local changes become a branch, commit, push, and PR to develop or master. Use for workspace bootstrap or bulk refresh; not for a single repo.
|
||||
description: Batch-sync all readable repos of a chosen Gitea owner into the working directory. List owners, let the user pick, then clone or update each repo through tools/repo-sync.sh; a repo reported dirty goes to jsc-git:pr against the base branch that same script reports. Use for workspace bootstrap or bulk refresh; not for a single repo.
|
||||
---
|
||||
|
||||
# repo-sync — batch-sync repositories
|
||||
|
||||
## Steps
|
||||
|
||||
1. Before calling `tools/gitea.sh`, resolve `GITEA_HOST` and `GITEA_TOKEN` from the current shell environment (also check `tea login list` for a usable login token when `GITEA_TOKEN` is unset). If `GITEA_HOST` is unresolvable, or `GITEA_TOKEN` is unset and no tea login token exists either, ask the user for the missing value per the `jsc-ask:ask` rules before proceeding to any `tools/gitea.sh` call.
|
||||
2. Run `tools/gitea.sh owners` to list every `{owner}` the user can read.
|
||||
3. Ask the user which `{owner}` to sync, per the `jsc-ask:ask` rules. Every option states the owner's repo count and impact scope.
|
||||
4. Run `tools/gitea.sh repos {owner}` to list every readable `{repo}` under that owner.
|
||||
5. Sync each `{repo}` one by one. This step **MUST run as a sub agent** (one sub agent per repo):
|
||||
1. Missing locally → `git clone` into the working directory (clone URL from `tools/gitea.sh clone-url`).
|
||||
2. Present locally → switch to `develop`, else `master` (or the result of `tools/gitea.sh default-branch`), then `git pull`.
|
||||
3. Local file changes → create a branch from develop or master, commit via `jsc-git:commit`, push, then open a PR back to develop or master via `jsc-git:pr`.
|
||||
6. Report the sync result for every repo: cloned, updated, PR created, or the failure reason.
|
||||
1. Run `tools/gitea.sh owners` to list every `{owner}` the user can read. The script reads `GITEA_HOST` and `GITEA_TOKEN` from the inherited environment and retries once with the tea CLI login token, so take no separate inventory first — a missing value surfaces here, before any work is done. Route on the result: at least one `{owner}` printed → next step; the script stops with `GITEA_HOST is required` or `GITEA_TOKEN is required` → ask the user for that one value per the `jsc-ask:ask` rules and run the command again, guessing no host; exit 7 → report that the key is invalid or lacks permission, and stop; exit 8 → report the HTTP status in the message, and stop; exit 0 with no output → report that this key can read no owner, and stop. Done when at least one `{owner}` is printed, or the run stopped with one of those reasons.
|
||||
2. Ask the user which `{owner}` to sync, per the `jsc-ask:ask` rules. Every option states the owner's repo count and impact scope. Done when the user has named exactly one `{owner}` from that list.
|
||||
3. Run `tools/gitea.sh repos {owner}` to list every readable `{repo}` under that owner. Exit 7 and exit 8 stop the run with the same report as step 1; an empty list means this owner has no readable repo and there is nothing to sync. Done when the command has printed the full `{owner}/{repo}` list for the chosen owner, or the run stopped.
|
||||
4. Sync every `{repo}` from step 3 **in parallel — one sub agent per repo, all launched in the same batch**, never one after another. Each repo has its own directory and its own remote, so nothing makes them wait for each other, and a hundred-repo owner otherwise costs a hundred sequential clones. Each sub agent does this:
|
||||
1. Run `tools/repo-sync.sh {owner}/{repo}`. The script owns the clone-versus-pull decision and the base-branch precedence, so run no `git clone`, `git checkout` or `git pull` by hand, and derive no branch name yourself. Route on its single line of output: `cloned` or `updated` (exit 0) → this repo is done; `dirty {branch}` (exit 0) → go to substep 2; `failed {reason}` (exit 1) → record that reason and stop this repo. Done when exactly one of those four outcomes is recorded for this repo.
|
||||
2. `dirty {branch}` → call `jsc-git:pr` with `{branch}` from that same output line as the base, passed through verbatim. It commits, branches, pushes and opens the PR itself, so add none of those steps. Done when `jsc-git:pr` returns the PR URL and reports it with the table format in `jsc-meta/references/pr-report.md`.
|
||||
|
||||
Done when every repo's sub agent has returned one of those outcomes; one repo failing never cancels the others.
|
||||
5. Report the sync result for every repo: cloned, updated, PR table row, or the failure reason. Done when every `{repo}` from step 3 carries one of those four results, and all PR rows share one table when more than one PR exists.
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: wiki-to-issue
|
||||
description: 'Turn one Gitea wiki page into an issue in the same repository. Parse the link with tools/gitea-link.sh, read the page through jsc-gitea:wiki, draft title and body as a sub agent, then create the issue with tools/issue.sh - labels picked from the repository''s existing set per the jsc-ask decision tree, project board attached or reported as manual when the site has no board API. A request without a wiki link stops the skill immediately: never guess the repository or the page. Use when a wiki page has to become trackable work; not for issues drafted from scratch, and not for syncing an issue that already exists.'
|
||||
---
|
||||
|
||||
# wiki-to-issue — a wiki page becomes an issue
|
||||
|
||||
The wiki link is the only input. Everything else — repository, page name, host — comes out of that link.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Input gate — the link and the host, checked in the same batch.** Neither depends on the other, so run both before anything else and report every failure found, not just the first.
|
||||
- **The link.** Run `tools/gitea-link.sh parse {url}` on the link the user gave. Exit 3, no link in the request, or `kind=issue` (this skill reads wiki pages, not issues) all mean the same thing: **stop and report which one it was**. Exit 2 is a usage error — fix the arguments and call again. Never ask for a repository name instead, and never fall back to the working directory's remote — a page written into the wrong repository's issue tracker is public and hard to take back.
|
||||
- **The host.** Confirm `GITEA_HOST` holds a value in the current shell; ask for it per the `jsc-ask:ask` rules when it does not. `GITEA_TOKEN` needs no inventory — `tools/gitea.sh` resolves it, retries once with the tea CLI login token, and exits 7 when neither works.
|
||||
|
||||
Completion condition: the parse printed `kind=wiki` with `repo`, `page` and `host` known, **and** `GITEA_HOST` holds a value — both, or the run has stopped with the reason named.
|
||||
2. **Run these three tracks at the same time.** The label list and the board list depend on the repository only, not on the page, so they start in the same batch as the read rather than queueing behind the draft.
|
||||
- **Track A — read and draft.** Read the page with `jsc-gitea:wiki` (`wiki-get {repo} {page}`) and take its absolute URL from `wiki-url` in the same pass. Route the exit codes by that skill's table: 4 means the page does not exist, 7 means the key is invalid or lacks permission, 8 is any other API failure — all three stop this skill with the page name in the report. **Drafting MUST run as a sub agent.** Title: the page's first heading, or the page name when it has none. Body: the page content in Traditional Chinese, opening with a 「來源:{絕對網址}」 line so the issue points back at the wiki. Convert `[[display|page]]` links to absolute URLs (`wiki-url`), because `[[...]]` resolves only inside a wiki. Drop personal data — an issue is read by more people than a wiki page.
|
||||
- **Track B — label list.** Run `tools/issue.sh labels {repo}`. Exit 1 means the label list could not be read: stop and report it, because the alternative is inventing labels. Exit 2 is a usage error — fix the arguments and call again.
|
||||
- **Track C — board list.** Run `tools/issue.sh projects {repo}`. Exit 3 means this Gitea has no board API — keep the board URL the script printed for step 4. Exit 1 means the call failed for another reason: report it and treat the board link as outstanding. Exit 2 is a usage error — fix the arguments and call again.
|
||||
|
||||
Completion condition: the title and body file exist with the source line and no `[[...]]` left in the body, the repository's label list is in hand or the run has stopped, and the board list is either in hand or recorded as unavailable.
|
||||
3. **Labels come from what the repository already has.** Propose the fitting ones from track B's list with a reason each, and confirm per `jsc-ask:ask` rules — every option states its impact scope (a label drives filters and board rules, so a wrong one routes the work to the wrong queue). Turn the confirmed names into ids with `tools/issue.sh label-ids {repo} {names}`. Exit 4 means a name is not in the repository: go back to the list and pick again, never create the label to make the command pass. Exit 1 means the call failed — report it and stop. An empty label list, or nothing fitting: ask whether to create the issue with no label, and record that answer. **Never invent a label that the repository does not have.** Completion condition: the user has confirmed a label set — possibly empty — and its ids are resolved.
|
||||
4. **Project board.** Track C returned a board list: let the user pick one per `jsc-ask:ask` rules, attach it, and report the failure verbatim if the attach call is refused. Track C exited 3: say plainly that this Gitea has no board API, and hand the user the board URL the script printed so they can drag the issue in themselves. Completion condition: the issue is either attached to a board, or the report states in one line that the board link is still outstanding and who has to do it.
|
||||
5. Create the issue: `tools/issue.sh create {repo} {title} {body-file} [--labels {ids}]`. The script asks for confirmation before it writes, so expect that prompt and hand the user the title, the labels and the board it is about to apply. Exit 0: report the `index=` and `url=` it prints. Exit 1 means no issue was created — report that plainly, and hand back the path of the drafted body file so the draft is not lost. Exit 2 is a usage error, usually a body file that is not there — fix the arguments and call again. Completion condition: the issue URL is reported to the user together with the labels applied and the board status from step 4, or the report states that no issue was created and where the draft is.
|
||||
|
||||
## Rules
|
||||
|
||||
- One wiki page, one issue. Splitting a page into several issues is analysis work, not conversion — hand that to `jsc-sdlc:analyze`.
|
||||
- The issue body stays Traditional Chinese per the STE100 rule, and keeps the source line at the top.
|
||||
- Creating an issue is an outward-facing action: the title, body, labels and board pick are confirmed with the user before the create call, never after.
|
||||
+28
-23
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: wiki
|
||||
description: Read or write a Gitea wiki page through tools/gitea.sh and tools/hash-id. Resolve the wiki repo per page type with JSC_WIKI_REPO_{TYPE} first, then JSC_WIKI_REPO, and ask only when neither is set. Page content is chart-first - prefer mermaid diagrams and markdown tables over plain prose. Used by jsc-ask, jsc-sdlc, and jsc-log for wiki pages, including ERROR pages, not for repo code files.
|
||||
description: Read or write a Gitea wiki page through tools/gitea.sh and tools/hash-id. Resolve the wiki repo per page type with JSC_WIKI_REPO_{TYPE} first, then JSC_WIKI_REPO, and ask only when neither is set. Page content is chart-first - prefer mermaid diagrams and markdown tables over plain prose. Callers are jsc-ask, jsc-sdlc, jsc-log, jsc-hooks for its ERROR pages, jsc-cli for its CHECK pages, and jsc-meta for its SKILLSET and TOOLING pages. Use for any wiki page in the skill set; not for repo code files.
|
||||
---
|
||||
|
||||
# wiki — read and write Gitea wiki pages
|
||||
@@ -9,41 +9,46 @@ Every wiki operation in the jsc skill set goes through this skill. One entry poi
|
||||
|
||||
## Resolve the wiki location
|
||||
|
||||
Different page types can live in different `{owner}/{repo}` repos, classified by page-name prefix: `QUESTION`, `PLAN`, `ANALYZE`, `DELIVER`, `MAINTAIN`, `REPO`, `LOG`, `LEARN`, `ERROR`.
|
||||
Different page types can live in different `{owner}/{repo}` repos, classified by the page-name prefix.
|
||||
|
||||
1. Before asking the user, inspect the current shell environment for the needed repo variables and Gitea connection variables: `JSC_WIKI_REPO_{TYPE}`, `JSC_WIKI_REPO`, `GITEA_HOST`, and `GITEA_TOKEN` (also check `tea login list` for a usable login token when `GITEA_TOKEN` is unset). Use inherited shell values first; only ask when the needed repo or connection value cannot be resolved after that check. `GITEA_HOST` and `GITEA_TOKEN` are both covered by this ask-if-unresolvable rule, the same as the wiki-repo variables below.
|
||||
2. Run `tools/gitea.sh wiki-repo {TYPE}` (TYPE = the page-name prefix). Allowed types are `QUESTION`, `PLAN`, `ANALYZE`, `DELIVER`, `MAINTAIN`, `REPO`, `LOG`, `LEARN`, and `ERROR`. Resolution order is `JSC_WIKI_REPO_{TYPE}` first, then `JSC_WIKI_REPO`. Never borrow another type's repo.
|
||||
3. On exit 3 (neither is set after env inspection), ask the user for that page type's `{owner}/{repo}` per the `jsc-ask:ask` rules, and suggest setting `JSC_WIKI_REPO_{TYPE}` (can differ per type) or `JSC_WIKI_REPO` (shared default).
|
||||
1. **Host gate.** Confirm `GITEA_HOST` holds a value in the current shell. When it is missing, ask for it per the `jsc-ask:ask` rules before any `tools/gitea.sh` call that reaches the API; otherwise the first thing the user sees is the script's `GITEA_HOST is required` line instead of a decision-tree question. `GITEA_TOKEN` needs no inventory here — the script resolves it, retries once with the tea CLI login token, and exits 7 when neither works. Done when `GITEA_HOST` holds a value.
|
||||
2. Run `tools/gitea.sh wiki-repo {TYPE}` (TYPE = the page-name prefix). Allowed types are `QUESTION`, `PLAN`, `ANALYZE`, `DELIVER`, `MAINTAIN`, `REPO`, `LOG`, `LEARN`, `ERROR`, `CHECK`, `REPORT`, `SKILLSET`, and `TOOLING`. The script reads `JSC_WIKI_REPO_{TYPE}` first and `JSC_WIKI_REPO` second, straight from the inherited environment, so take no separate inventory of those two variables. Never borrow another type's repo. Done when the command has printed exactly one `{owner}/{repo}`, or exited 3 and sent this page type to step 3, or exited 2 on a type outside the list above and stopped the run.
|
||||
3. On exit 3 (neither variable is set), ask the user for that page type's `{owner}/{repo}` per the `jsc-ask:ask` rules, and suggest setting `JSC_WIKI_REPO_{TYPE}` (can differ per type) or `JSC_WIKI_REPO` (shared default). Done when the user has supplied one `{owner}/{repo}` for that page type.
|
||||
|
||||
## Operations
|
||||
|
||||
| Action | Command |
|
||||
| --- | --- |
|
||||
| list pages | `tools/gitea.sh wiki-list {owner}/{repo}` |
|
||||
| read page | `tools/gitea.sh wiki-get {owner}/{repo} {page}` (exit 4 when missing) |
|
||||
| write page | write the content to a temp file first, then `tools/gitea.sh wiki-put {owner}/{repo} {page} {file}` (creates or updates automatically) |
|
||||
| page URL | `tools/gitea.sh wiki-url {owner}/{repo} {page}` — the page's absolute URL, taken from the API's `html_url` (exit 4 when the page is missing) |
|
||||
| read page | `tools/gitea.sh wiki-get {owner}/{repo} {page}` |
|
||||
| write page | write the content to a temp file first, then `tools/gitea.sh wiki-put {owner}/{repo} {page} {file}` (asks for confirmation first, then creates or updates) |
|
||||
| page URL | `tools/gitea.sh wiki-url {owner}/{repo} {page}` — the page's absolute URL, taken from the API's `html_url` |
|
||||
|
||||
## Linking between wiki pages
|
||||
When writing a page, link same-type pages with the `[[display|page]]` form (display text on the LEFT) and cross-type pages with the absolute URL from `wiki-url`, because `[[...]]` resolves only inside one wiki. Full rules and the direction trap: `references/wiki-links.md`.
|
||||
|
||||
Two rules, and getting either wrong produces a link that silently points at a page that does not exist.
|
||||
## Exit codes
|
||||
|
||||
**Direction**: Gitea uses the GitHub/Gollum convention — `[[display text|page name]]`, **display text on the LEFT, page name on the RIGHT**. This is the opposite of MediaWiki. Gitea's own source says so (`modules/markup/html_link.go`): *"MediaWiki uses [[link|text]], while GitHub uses [[text|link]] … we prefer GitHub syntax"*. So `[[PLAN_H1234567|我的計畫]]` renders as the text `PLAN_H1234567` linking to a page named 我的計畫 — broken. Write `[[我的計畫|PLAN_H1234567]]`. When display text and page name are the same, use the no-pipe form `[[PLAN_H1234567]]`, which cannot be got wrong.
|
||||
Route every `tools/gitea.sh` call in this skill on its exit code. A code with no branch below stops the run and gets reported as it is.
|
||||
|
||||
**Scope**: `[[...]]` and relative markdown links both resolve **only inside the current wiki**. There is no cross-repo wiki-link syntax.
|
||||
|
||||
| Link | Same wiki? | Use |
|
||||
| Code | Meaning | What this skill does |
|
||||
| --- | --- | --- |
|
||||
| Same page type (e.g. `PLAN_CONTENTS` → `PLAN_{HASH}`) | Always — one type, one repo | `[[display\|page]]` or `[[page]]` |
|
||||
| Different page type (e.g. `LOG_{HASH}` → `PLAN_{HASH}`) | **Only when both types resolve to the same repo** | Absolute URL from `wiki-url`: `[display](https://…/wiki/PLAN_…)` |
|
||||
|
||||
Because each type resolves its own `JSC_WIKI_REPO_{TYPE}`, a cross-type link **must** use the absolute URL — it stays correct whether or not the two types happen to share a repo, so never branch on that. Get the URL from `wiki-url`, never hand-assemble the path.
|
||||
| 0 | success | use the output |
|
||||
| 2 | usage error, or a page type outside the allowed list | fix the arguments, then call again; never repeat the same call unchanged |
|
||||
| 3 | `wiki-repo`: neither `JSC_WIKI_REPO_{TYPE}` nor `JSC_WIKI_REPO` is set | go to step 3 and ask |
|
||||
| 4 | HTTP 404: `wiki-get` and `wiki-url` found no such page | for a read the caller expects to succeed, stop and report the page name; this is the **only** code that opens the create path of rule 4 — write the page from the template instead of appending |
|
||||
| 5 | `wiki-url`: the page exists but the API returned no `html_url` | stop and report it. Link inside the same wiki with `[[display\|page]]`; a cross-repo link has no absolute URL to point at, so do not fabricate one |
|
||||
| 7 | HTTP 401 or 403 after the tea-token retry: the key is invalid or lacks permission | **stop the whole operation and report the key problem.** Never read this as an empty or missing page, and never take the create path of rule 4: writing a fresh page over one you could not read destroys the record that is still there |
|
||||
| 8 | any other API failure, HTTP status in the message | stop and report that status; call again only after the cause is fixed |
|
||||
|
||||
## Rules
|
||||
|
||||
1. Page names must follow the wiki naming table in the skill guidelines (see `jsc-meta/references/guidelines.md`).
|
||||
2. Use `tools/hash-id` for `{HASH}` values. It returns the first 8 uppercase SHA-1 hex chars, or `H` plus the first 7 chars when the raw hash starts with `0-9`, `A`, `B`, or `C`.
|
||||
3. To update a contents page (`*_CONTENTS`): `wiki-get` it first, apply the template to append or modify, then `wiki-put` the whole page back. Never overwrite entries owned by others.
|
||||
4. Write all wiki content in UTF-8 Traditional Chinese, per the STE100 output rule.
|
||||
5. Prefer visual forms for page content: use mermaid diagrams (flowchart, sequence, gantt, pie) and markdown tables wherever the information allows. Plain running text is the last resort, kept short.
|
||||
6. Authentication fallback is built into `tools/gitea.sh`: on a missing GITEA_TOKEN or a 401/403 response it retries with the tea CLI login token automatically. But before calling it, resolve `GITEA_HOST` and `GITEA_TOKEN` per rule 1: if `GITEA_HOST` is unresolvable, or `GITEA_TOKEN` is unset and no tea login token exists either, ask the user for the missing value per the `jsc-ask:ask` rules — same decision-tree pattern as the missing-wiki-repo case above — before calling `tools/gitea.sh`. Only report a genuine failure when the user has no answer to give or Gitea itself rejects the request (e.g. a 401/403 even after the tea fallback).
|
||||
2. Use `tools/hash-id` for `{HASH}` values. It returns the first 8 uppercase SHA-1 hex chars, or `H` plus the first 7 chars when the raw hash starts with `0-9`, `A`, `B`, or `C`. Exit 1 means this machine has neither `sha1sum` nor `shasum`: stop, report that one of them has to be installed, and compute no hash by hand — a hand-made page name lands the content on a page nobody else reads.
|
||||
3. To update a contents page (`*_CONTENTS`): `wiki-get` it first, apply the template to append or modify, then `wiki-put` the whole page back — the write asks for confirmation before it goes out. Never overwrite entries owned by others. Whether the page may be created from the template instead is decided by rule 4, and by nothing else.
|
||||
4. **Only exit 4 means the page is not there yet — this rule binds every "create it if it does not exist" path, without exception.** It is not limited to contents pages: a content page (`*_{HASH}`), a work log, an error page, a report, any page at all, follows the same branch.
|
||||
- **Correct branch.** Read the page with `wiki-get`. Exit 0 means the page exists, so append or modify the content that came back and `wiki-put` the whole page. Exit 4 (HTTP 404) is the one and only code that permits creating a new page from the template.
|
||||
- **Exit 7 and exit 8 abort.** Exit 7 (HTTP 401 or 403) and exit 8 (any other API failure) both mean the old content is unknown, never that the page is missing. Stop the operation and report the exit code with its cause. Create no page, write nothing, and do not retry the same call unchanged.
|
||||
- **Why.** Wiki writes in this skill set are append-not-overwrite, and that semantics rests entirely on reading the old page back first. Reading a 401 as a 404 makes the caller believe it holds a brand-new page and `wiki-put` a fresh template over a live one, and the whole earlier record is gone — the write carries no merge and no backup.
|
||||
5. Write all wiki content in UTF-8 Traditional Chinese, per the STE100 output rule.
|
||||
6. Prefer visual forms for page content: use mermaid diagrams (flowchart, sequence, gantt, pie) and markdown tables wherever the information allows. Prose is capped at 3 sentences per section, and a sentence stays only when neither a mermaid diagram nor a markdown table can carry the same information.
|
||||
7. `tools/gitea.sh` retries once with the tea CLI login token when `GITEA_TOKEN` is missing or the response is 401/403. Report a failure only after that retry also fails.
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/* 共用排版:所有版型與風格都吃這一份,顏色與字型一律走 CSS 變數,由風格檔決定。 */
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font-family: var(--font);
|
||||
font-size: 16px;
|
||||
line-height: 1.75;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
padding: 2.5rem 0 1.5rem;
|
||||
border-bottom: 2px solid var(--accent);
|
||||
}
|
||||
|
||||
.page-head h1 {
|
||||
margin: 0 0 .35rem;
|
||||
font-family: var(--font-head);
|
||||
font-size: 2rem;
|
||||
line-height: 1.3;
|
||||
color: var(--head-fg);
|
||||
}
|
||||
|
||||
.subtitle { margin: 0 0 .5rem; color: var(--muted); font-size: 1.05rem; }
|
||||
.subtitle:empty { display: none; }
|
||||
|
||||
.meta {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: .85rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.meta a.source { color: var(--accent); text-decoration: none; word-break: break-all; }
|
||||
.meta a.source:hover { text-decoration: underline; }
|
||||
|
||||
.content { padding: 1.5rem 0 3rem; }
|
||||
|
||||
.content h1, .content h2, .content h3, .content h4 {
|
||||
font-family: var(--font-head);
|
||||
color: var(--head-fg);
|
||||
line-height: 1.35;
|
||||
margin: 2rem 0 .75rem;
|
||||
}
|
||||
|
||||
.content h1 { font-size: 1.7rem; }
|
||||
.content h2 { font-size: 1.4rem; }
|
||||
.content h3 { font-size: 1.15rem; }
|
||||
.content h4 { font-size: 1rem; }
|
||||
|
||||
.content p { margin: 0 0 1rem; }
|
||||
.content ul, .content ol { margin: 0 0 1rem; padding-left: 1.5rem; }
|
||||
.content li { margin: .25rem 0; }
|
||||
.content li input[type="checkbox"] { margin-right: .4rem; }
|
||||
|
||||
.content a { color: var(--accent); text-decoration: none; }
|
||||
.content a:hover { text-decoration: underline; }
|
||||
|
||||
.content blockquote {
|
||||
margin: 1rem 0;
|
||||
padding: .6rem 1rem;
|
||||
border-left: 4px solid var(--accent);
|
||||
background: var(--card);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.content code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: .9em;
|
||||
background: var(--code-bg);
|
||||
padding: .15em .4em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.content pre {
|
||||
margin: 0 0 1rem;
|
||||
padding: 1rem;
|
||||
overflow-x: auto;
|
||||
background: var(--code-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.content pre code { background: none; padding: 0; }
|
||||
|
||||
/* 表格一律可橫向捲動,寬表格不會把整頁撐開 */
|
||||
.table-wrap { overflow-x: auto; margin: 0 0 1.25rem; }
|
||||
|
||||
.content table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
font-size: .95rem;
|
||||
}
|
||||
|
||||
.content th, .content td {
|
||||
border: 1px solid var(--border);
|
||||
padding: .5rem .7rem;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.content th { background: var(--table-head-bg); color: var(--head-fg); font-weight: 600; }
|
||||
.content tbody tr:nth-child(even) { background: var(--stripe); }
|
||||
|
||||
.content img { max-width: 100%; height: auto; }
|
||||
.content hr { border: 0; border-top: 1px solid var(--border); margin: 2rem 0; }
|
||||
|
||||
.page-foot {
|
||||
padding: 1.25rem 0 2rem;
|
||||
border-top: 1px solid var(--border);
|
||||
color: var(--muted);
|
||||
font-size: .8rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .75rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: .1rem .5rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
font-size: .75rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// 共用行為:表格加捲動外框、依 h2 切段、產生目錄。各版型自己決定要用哪幾個。
|
||||
(function (w) {
|
||||
'use strict';
|
||||
|
||||
// 寬表格包一層可橫捲的外框,整頁就不會被撐開。
|
||||
function wrapTables(root) {
|
||||
root.querySelectorAll('table').forEach(function (t) {
|
||||
if (t.parentElement && t.parentElement.classList.contains('table-wrap')) return;
|
||||
var box = document.createElement('div');
|
||||
box.className = 'table-wrap';
|
||||
t.parentNode.insertBefore(box, t);
|
||||
box.appendChild(t);
|
||||
});
|
||||
}
|
||||
|
||||
// 依 h2 把內容切成一段一段。h2 之前的內容自成第一段(前言)。
|
||||
function splitBySection(root) {
|
||||
var nodes = Array.prototype.slice.call(root.childNodes);
|
||||
var sections = [];
|
||||
var current = null;
|
||||
|
||||
function open(headingText) {
|
||||
current = document.createElement('section');
|
||||
current.className = 'jsc-section';
|
||||
current.dataset.title = headingText || '';
|
||||
sections.push(current);
|
||||
}
|
||||
|
||||
nodes.forEach(function (node) {
|
||||
if (node.nodeType === 1 && node.tagName === 'H2') {
|
||||
open(node.textContent.trim());
|
||||
} else if (!current) {
|
||||
if (node.nodeType === 3 && !node.textContent.trim()) return;
|
||||
open('');
|
||||
}
|
||||
current.appendChild(node);
|
||||
});
|
||||
|
||||
root.innerHTML = '';
|
||||
sections.forEach(function (s) { root.appendChild(s); });
|
||||
return sections;
|
||||
}
|
||||
|
||||
// 依 h2、h3 產生目錄,塞進指定容器。標題沒有 id 就補一個。
|
||||
function buildToc(root, target) {
|
||||
var heads = root.querySelectorAll('h2, h3');
|
||||
if (!heads.length) { target.remove(); return; }
|
||||
var list = document.createElement('ul');
|
||||
heads.forEach(function (h, i) {
|
||||
if (!h.id) h.id = 'sec-' + (i + 1);
|
||||
var li = document.createElement('li');
|
||||
li.className = 'toc-' + h.tagName.toLowerCase();
|
||||
var a = document.createElement('a');
|
||||
a.href = '#' + h.id;
|
||||
a.textContent = h.textContent.trim();
|
||||
li.appendChild(a);
|
||||
list.appendChild(li);
|
||||
});
|
||||
target.appendChild(list);
|
||||
}
|
||||
|
||||
w.jsc = { wrapTables: wrapTables, splitBySection: splitBySection, buildToc: buildToc };
|
||||
})(window);
|
||||
@@ -0,0 +1,52 @@
|
||||
<!-- 看板:每個 h2 一張卡片並排,適合進度、狀態、維護清單 -->
|
||||
<!doctype html>
|
||||
<html lang="zh-Hant" data-layout="{{LAYOUT}}" data-style="{{STYLE_NAME}}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{TITLE}}</title>
|
||||
<style>{{BASE}}</style>
|
||||
<style>{{STYLE}}</style>
|
||||
<style>
|
||||
.shell { max-width: 1280px; margin: 0 auto; padding: 0 1.25rem; }
|
||||
.content { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 1.25rem; align-items: start; }
|
||||
.jsc-section {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 1.1rem 1.25rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.jsc-section h2 { margin-top: 0; font-size: 1.15rem; }
|
||||
.jsc-section table { font-size: .88rem; background: var(--bg); }
|
||||
.jsc-section pre { background: var(--bg); }
|
||||
/* 只有一段內容時不要孤零零一張窄卡片 */
|
||||
.content.single { grid-template-columns: minmax(0, 1fr); }
|
||||
@media print { .content { display: block; } .jsc-section { page-break-inside: avoid; margin-bottom: 1rem; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<header class="page-head">
|
||||
<h1>{{TITLE}}</h1>
|
||||
<p class="subtitle">{{SUBTITLE}}</p>
|
||||
<p class="meta">{{SOURCE}}<span class="generated">產生時間:{{GENERATED}}</span></p>
|
||||
</header>
|
||||
<main class="content" id="content">{{CONTENT}}</main>
|
||||
<footer class="page-foot">
|
||||
<span class="badge">{{LAYOUT}}/{{STYLE_NAME}}</span>
|
||||
<span>本頁由 jsc-gitea:html-export 產生</span>
|
||||
</footer>
|
||||
</div>
|
||||
<script>{{BASE_JS}}</script>
|
||||
<script>
|
||||
(function () {
|
||||
var content = document.getElementById('content');
|
||||
jsc.wrapTables(content);
|
||||
var cards = jsc.splitBySection(content);
|
||||
if (cards.length < 2) content.classList.add('single');
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,46 @@
|
||||
<!-- 單頁摘要:窄欄、重點框,適合議題摘要與會議結論,印出來剛好一頁 -->
|
||||
<!doctype html>
|
||||
<html lang="zh-Hant" data-layout="{{LAYOUT}}" data-style="{{STYLE_NAME}}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{TITLE}}</title>
|
||||
<style>{{BASE}}</style>
|
||||
<style>{{STYLE}}</style>
|
||||
<style>
|
||||
.shell { max-width: 760px; margin: 0 auto; padding: 0 1.25rem; }
|
||||
.page-head { padding-top: 2rem; }
|
||||
.content { font-size: .97rem; }
|
||||
.content h2 { font-size: 1.15rem; margin-top: 1.4rem; }
|
||||
.content h3 { font-size: 1rem; }
|
||||
.content ul { padding-left: 1.2rem; }
|
||||
/* 待辦清單獨立成框,一眼看得到還有什麼沒做 */
|
||||
.content li:has(input[type="checkbox"]) { list-style: none; margin-left: -1.2rem; }
|
||||
.content blockquote { font-size: .95rem; }
|
||||
.jsc-section + .jsc-section { border-top: 1px dashed var(--border); padding-top: .75rem; }
|
||||
@media print { body { font-size: 13px; } .page-foot { display: none; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<header class="page-head">
|
||||
<h1>{{TITLE}}</h1>
|
||||
<p class="subtitle">{{SUBTITLE}}</p>
|
||||
<p class="meta">{{SOURCE}}<span class="generated">產生時間:{{GENERATED}}</span></p>
|
||||
</header>
|
||||
<main class="content" id="content">{{CONTENT}}</main>
|
||||
<footer class="page-foot">
|
||||
<span class="badge">{{LAYOUT}}/{{STYLE_NAME}}</span>
|
||||
<span>本頁由 jsc-gitea:html-export 產生</span>
|
||||
</footer>
|
||||
</div>
|
||||
<script>{{BASE_JS}}</script>
|
||||
<script>
|
||||
(function () {
|
||||
var content = document.getElementById('content');
|
||||
jsc.wrapTables(content);
|
||||
jsc.splitBySection(content);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,60 @@
|
||||
<!-- 報告書:左側目錄加章節內文,適合分析頁、交付頁這類長文件 -->
|
||||
<!doctype html>
|
||||
<html lang="zh-Hant" data-layout="{{LAYOUT}}" data-style="{{STYLE_NAME}}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{TITLE}}</title>
|
||||
<style>{{BASE}}</style>
|
||||
<style>{{STYLE}}</style>
|
||||
<style>
|
||||
.shell { max-width: 1080px; margin: 0 auto; padding: 0 1.25rem; }
|
||||
.body-grid { display: grid; grid-template-columns: 232px minmax(0, 1fr); gap: 2.5rem; }
|
||||
.toc {
|
||||
position: sticky;
|
||||
top: 1.5rem;
|
||||
align-self: start;
|
||||
max-height: calc(100vh - 3rem);
|
||||
overflow-y: auto;
|
||||
padding: 1.25rem 0;
|
||||
font-size: .9rem;
|
||||
}
|
||||
.toc h2 { margin: 0 0 .5rem; font-size: .8rem; letter-spacing: .12em; color: var(--muted); text-transform: uppercase; }
|
||||
.toc ul { list-style: none; margin: 0; padding: 0; }
|
||||
.toc li { margin: .2rem 0; }
|
||||
.toc a { color: var(--fg); text-decoration: none; display: block; padding: .15rem .5rem; border-left: 2px solid var(--border); }
|
||||
.toc a:hover { color: var(--accent); border-left-color: var(--accent); }
|
||||
.toc .toc-h3 a { padding-left: 1.25rem; color: var(--muted); }
|
||||
@media (max-width: 860px) {
|
||||
.body-grid { grid-template-columns: minmax(0, 1fr); gap: 1rem; }
|
||||
.toc { position: static; max-height: none; border-bottom: 1px solid var(--border); }
|
||||
}
|
||||
@media print { .toc { display: none; } .body-grid { display: block; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<header class="page-head">
|
||||
<h1>{{TITLE}}</h1>
|
||||
<p class="subtitle">{{SUBTITLE}}</p>
|
||||
<p class="meta">{{SOURCE}}<span class="generated">產生時間:{{GENERATED}}</span></p>
|
||||
</header>
|
||||
<div class="body-grid">
|
||||
<nav class="toc" id="toc"><h2>目錄</h2></nav>
|
||||
<main class="content" id="content">{{CONTENT}}</main>
|
||||
</div>
|
||||
<footer class="page-foot">
|
||||
<span class="badge">{{LAYOUT}}/{{STYLE_NAME}}</span>
|
||||
<span>本頁由 jsc-gitea:html-export 產生</span>
|
||||
</footer>
|
||||
</div>
|
||||
<script>{{BASE_JS}}</script>
|
||||
<script>
|
||||
(function () {
|
||||
var content = document.getElementById('content');
|
||||
jsc.wrapTables(content);
|
||||
jsc.buildToc(content, document.getElementById('toc'));
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,90 @@
|
||||
<!-- 投影片:一個 h2 一張,鍵盤左右鍵翻頁,適合簡報與匯報 -->
|
||||
<!doctype html>
|
||||
<html lang="zh-Hant" data-layout="{{LAYOUT}}" data-style="{{STYLE_NAME}}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{TITLE}}</title>
|
||||
<style>{{BASE}}</style>
|
||||
<style>{{STYLE}}</style>
|
||||
<style>
|
||||
body { display: flex; flex-direction: column; min-height: 100vh; }
|
||||
.shell { flex: 1; display: flex; flex-direction: column; max-width: 1200px; width: 100%; margin: 0 auto; padding: 0 1.5rem; }
|
||||
.page-head { padding: 1.5rem 0 1rem; }
|
||||
.page-head h1 { font-size: 1.5rem; }
|
||||
.content { flex: 1; padding: 0; }
|
||||
.jsc-section { display: none; padding: 1.5rem 0; }
|
||||
.jsc-section.is-active { display: block; animation: fade .18s ease-out; }
|
||||
@keyframes fade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
|
||||
.jsc-section h2 { margin-top: 0; font-size: 2rem; }
|
||||
.jsc-section p, .jsc-section li { font-size: 1.15rem; }
|
||||
.deck-bar {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .75rem;
|
||||
padding: .75rem 0 1.25rem;
|
||||
background: var(--bg);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.deck-bar button {
|
||||
font: inherit;
|
||||
color: var(--fg);
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: .35rem .9rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.deck-bar button:hover { border-color: var(--accent); color: var(--accent); }
|
||||
.deck-pos { color: var(--muted); font-size: .9rem; margin-left: auto; }
|
||||
@media print {
|
||||
.jsc-section, .jsc-section.is-active { display: block; page-break-after: always; }
|
||||
.deck-bar { display: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<header class="page-head">
|
||||
<h1>{{TITLE}}</h1>
|
||||
<p class="subtitle">{{SUBTITLE}}</p>
|
||||
<p class="meta">{{SOURCE}}<span class="generated">產生時間:{{GENERATED}}</span></p>
|
||||
</header>
|
||||
<main class="content" id="content">{{CONTENT}}</main>
|
||||
<div class="deck-bar">
|
||||
<button type="button" id="prev">← 上一頁</button>
|
||||
<button type="button" id="next">下一頁 →</button>
|
||||
<span class="badge">{{LAYOUT}}/{{STYLE_NAME}}</span>
|
||||
<span class="deck-pos" id="pos"></span>
|
||||
</div>
|
||||
</div>
|
||||
<script>{{BASE_JS}}</script>
|
||||
<script>
|
||||
(function () {
|
||||
var content = document.getElementById('content');
|
||||
jsc.wrapTables(content);
|
||||
var slides = jsc.splitBySection(content);
|
||||
var at = 0;
|
||||
var pos = document.getElementById('pos');
|
||||
|
||||
function show(i) {
|
||||
if (!slides.length) return;
|
||||
at = Math.max(0, Math.min(slides.length - 1, i));
|
||||
slides.forEach(function (s, n) { s.classList.toggle('is-active', n === at); });
|
||||
pos.textContent = (at + 1) + ' / ' + slides.length;
|
||||
window.scrollTo({ top: 0 });
|
||||
}
|
||||
|
||||
document.getElementById('prev').addEventListener('click', function () { show(at - 1); });
|
||||
document.getElementById('next').addEventListener('click', function () { show(at + 1); });
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'ArrowLeft' || e.key === 'PageUp') show(at - 1);
|
||||
if (e.key === 'ArrowRight' || e.key === 'PageDown' || e.key === ' ') show(at + 1);
|
||||
});
|
||||
show(0);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,58 @@
|
||||
<!-- 規格書:表格與程式碼區塊放大,表頭固定,適合 API 文件與交付規格 -->
|
||||
<!doctype html>
|
||||
<html lang="zh-Hant" data-layout="{{LAYOUT}}" data-style="{{STYLE_NAME}}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{TITLE}}</title>
|
||||
<style>{{BASE}}</style>
|
||||
<style>{{STYLE}}</style>
|
||||
<style>
|
||||
.shell { max-width: 1120px; margin: 0 auto; padding: 0 1.25rem; }
|
||||
.body-grid { display: grid; grid-template-columns: 210px minmax(0, 1fr); gap: 2rem; }
|
||||
.toc { position: sticky; top: 1.5rem; align-self: start; padding: 1.25rem 0; font-size: .88rem; }
|
||||
.toc h2 { margin: 0 0 .5rem; font-size: .78rem; letter-spacing: .12em; color: var(--muted); text-transform: uppercase; }
|
||||
.toc ul { list-style: none; margin: 0; padding: 0; }
|
||||
.toc a { color: var(--fg); text-decoration: none; display: block; padding: .15rem .4rem; }
|
||||
.toc a:hover { color: var(--accent); }
|
||||
.toc .toc-h3 a { padding-left: 1.1rem; color: var(--muted); }
|
||||
.jsc-section { border-bottom: 1px solid var(--border); padding-bottom: 1.25rem; margin-bottom: 1.5rem; }
|
||||
.jsc-section:last-child { border-bottom: 0; }
|
||||
.content .table-wrap { max-height: 70vh; overflow: auto; border: 1px solid var(--border); border-radius: var(--radius); }
|
||||
.content .table-wrap table { border: 0; }
|
||||
.content .table-wrap th { position: sticky; top: 0; z-index: 1; }
|
||||
.content code { font-size: .92em; }
|
||||
.content pre { font-size: .95rem; line-height: 1.6; }
|
||||
/* 欄位表第一欄是欄位名,等寬字比較好對 */
|
||||
.content td:first-child code { white-space: nowrap; }
|
||||
@media (max-width: 860px) { .body-grid { grid-template-columns: minmax(0, 1fr); } .toc { position: static; } }
|
||||
@media print { .toc { display: none; } .body-grid { display: block; } .content .table-wrap { max-height: none; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<header class="page-head">
|
||||
<h1>{{TITLE}}</h1>
|
||||
<p class="subtitle">{{SUBTITLE}}</p>
|
||||
<p class="meta">{{SOURCE}}<span class="generated">產生時間:{{GENERATED}}</span></p>
|
||||
</header>
|
||||
<div class="body-grid">
|
||||
<nav class="toc" id="toc"><h2>章節</h2></nav>
|
||||
<main class="content" id="content">{{CONTENT}}</main>
|
||||
</div>
|
||||
<footer class="page-foot">
|
||||
<span class="badge">{{LAYOUT}}/{{STYLE_NAME}}</span>
|
||||
<span>本頁由 jsc-gitea:html-export 產生</span>
|
||||
</footer>
|
||||
</div>
|
||||
<script>{{BASE_JS}}</script>
|
||||
<script>
|
||||
(function () {
|
||||
var content = document.getElementById('content');
|
||||
jsc.wrapTables(content);
|
||||
jsc.buildToc(content, document.getElementById('toc'));
|
||||
jsc.splitBySection(content);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,61 @@
|
||||
<!-- 時間軸:每個 h2 一個節點串成一條線,適合工作日誌與里程碑 -->
|
||||
<!doctype html>
|
||||
<html lang="zh-Hant" data-layout="{{LAYOUT}}" data-style="{{STYLE_NAME}}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{TITLE}}</title>
|
||||
<style>{{BASE}}</style>
|
||||
<style>{{STYLE}}</style>
|
||||
<style>
|
||||
.shell { max-width: 900px; margin: 0 auto; padding: 0 1.25rem; }
|
||||
.content { position: relative; padding-left: 2rem; }
|
||||
.content::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: .45rem;
|
||||
top: .75rem;
|
||||
bottom: .75rem;
|
||||
width: 2px;
|
||||
background: var(--border);
|
||||
}
|
||||
.jsc-section { position: relative; padding: 0 0 1.75rem; }
|
||||
.jsc-section::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -1.9rem;
|
||||
top: .95rem;
|
||||
width: .85rem;
|
||||
height: .85rem;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
border: 3px solid var(--bg);
|
||||
}
|
||||
.jsc-section h2 { margin-top: .5rem; font-size: 1.2rem; }
|
||||
.jsc-section:last-child { padding-bottom: .5rem; }
|
||||
@media print { .jsc-section { page-break-inside: avoid; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<header class="page-head">
|
||||
<h1>{{TITLE}}</h1>
|
||||
<p class="subtitle">{{SUBTITLE}}</p>
|
||||
<p class="meta">{{SOURCE}}<span class="generated">產生時間:{{GENERATED}}</span></p>
|
||||
</header>
|
||||
<main class="content" id="content">{{CONTENT}}</main>
|
||||
<footer class="page-foot">
|
||||
<span class="badge">{{LAYOUT}}/{{STYLE_NAME}}</span>
|
||||
<span>本頁由 jsc-gitea:html-export 產生</span>
|
||||
</footer>
|
||||
</div>
|
||||
<script>{{BASE_JS}}</script>
|
||||
<script>
|
||||
(function () {
|
||||
var content = document.getElementById('content');
|
||||
jsc.wrapTables(content);
|
||||
jsc.splitBySection(content);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
/* 商務:深藍主色、表頭反白、正式對外用 */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--fg: #22272e;
|
||||
--head-fg: #0b3358;
|
||||
--muted: #5c6b7a;
|
||||
--accent: #0b5fa5;
|
||||
--border: #c9d6e2;
|
||||
--card: #eef4fa;
|
||||
--code-bg: #eef2f6;
|
||||
--table-head-bg: #0b3358;
|
||||
--stripe: #f4f8fc;
|
||||
--radius: 6px;
|
||||
--shadow: 0 1px 2px rgba(11, 51, 88, .12);
|
||||
--font: "Noto Sans TC", "PingFang TC", "Microsoft JhengHei", system-ui, sans-serif;
|
||||
--font-head: var(--font);
|
||||
--font-mono: "JetBrains Mono", "Cascadia Mono", Consolas, monospace;
|
||||
}
|
||||
|
||||
.page-head { border-bottom-width: 3px; }
|
||||
.content th { color: #ffffff; letter-spacing: .03em; }
|
||||
.content h2 { border-left: 5px solid var(--accent); padding-left: .6rem; }
|
||||
.content table { box-shadow: var(--shadow); }
|
||||
@@ -0,0 +1,21 @@
|
||||
/* 深色:深底亮字,長時間閱讀與投影機環境 */
|
||||
:root {
|
||||
--bg: #11161c;
|
||||
--fg: #d7dee6;
|
||||
--head-fg: #f2f6fa;
|
||||
--muted: #8b98a6;
|
||||
--accent: #56a8f5;
|
||||
--border: #2b3540;
|
||||
--card: #1a2129;
|
||||
--code-bg: #1c242d;
|
||||
--table-head-bg: #1f2932;
|
||||
--stripe: #161d24;
|
||||
--radius: 6px;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, .5);
|
||||
--font: "Noto Sans TC", "PingFang TC", "Microsoft JhengHei", system-ui, sans-serif;
|
||||
--font-head: var(--font);
|
||||
--font-mono: "JetBrains Mono", "Cascadia Mono", Consolas, monospace;
|
||||
}
|
||||
|
||||
.content pre { border-color: #2b3540; }
|
||||
.content h2 { border-bottom: 1px solid var(--border); padding-bottom: .3rem; }
|
||||
@@ -0,0 +1,21 @@
|
||||
/* 極簡:白底、細線、無襯線,資訊密度優先 */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--fg: #1f2328;
|
||||
--head-fg: #0d1117;
|
||||
--muted: #6a737d;
|
||||
--accent: #2f6f9f;
|
||||
--border: #d8dee4;
|
||||
--card: #f6f8fa;
|
||||
--code-bg: #f2f4f7;
|
||||
--table-head-bg: #f6f8fa;
|
||||
--stripe: #fbfcfd;
|
||||
--radius: 4px;
|
||||
--shadow: none;
|
||||
--font: "Noto Sans TC", "PingFang TC", "Microsoft JhengHei", system-ui, sans-serif;
|
||||
--font-head: var(--font);
|
||||
--font-mono: "JetBrains Mono", "Cascadia Mono", Consolas, monospace;
|
||||
}
|
||||
|
||||
.page-head { border-bottom-width: 1px; }
|
||||
.content h2 { border-bottom: 1px solid var(--border); padding-bottom: .3rem; }
|
||||
@@ -0,0 +1,30 @@
|
||||
/* 印刷:襯線字、A4 邊界、去掉陰影,列印或轉 PDF 用 */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--fg: #1b1b1b;
|
||||
--head-fg: #000000;
|
||||
--muted: #55555f;
|
||||
--accent: #4a4a4a;
|
||||
--border: #b8b8b8;
|
||||
--card: #f4f4f2;
|
||||
--code-bg: #f2f2f0;
|
||||
--table-head-bg: #ececeb;
|
||||
--stripe: #f9f9f8;
|
||||
--radius: 0;
|
||||
--shadow: none;
|
||||
--font: "Noto Serif TC", "Songti TC", "PMingLiU", Georgia, serif;
|
||||
--font-head: var(--font);
|
||||
--font-mono: "JetBrains Mono", Consolas, monospace;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm 18mm; }
|
||||
|
||||
body { font-size: 15px; line-height: 1.85; }
|
||||
.page-head { border-bottom: 1px solid var(--border); }
|
||||
.content h2 { page-break-after: avoid; }
|
||||
.content table, .content pre, .content blockquote { page-break-inside: avoid; }
|
||||
|
||||
@media print {
|
||||
.meta a.source { color: var(--fg); }
|
||||
.page-foot { border-top: 1px solid var(--border); }
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/* 明亮:高彩度、圓角卡片、漸層標題,簡報與對內宣達用 */
|
||||
:root {
|
||||
--bg: #fdfbff;
|
||||
--fg: #241f2e;
|
||||
--head-fg: #4c1d95;
|
||||
--muted: #6d6480;
|
||||
--accent: #7c3aed;
|
||||
--border: #e2d9f5;
|
||||
--card: #f6f1ff;
|
||||
--code-bg: #f1ecfd;
|
||||
--table-head-bg: #ede4ff;
|
||||
--stripe: #faf7ff;
|
||||
--radius: 12px;
|
||||
--shadow: 0 2px 10px rgba(124, 58, 237, .12);
|
||||
--font: "Noto Sans TC", "PingFang TC", "Microsoft JhengHei", system-ui, sans-serif;
|
||||
--font-head: var(--font);
|
||||
--font-mono: "JetBrains Mono", "Cascadia Mono", Consolas, monospace;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
border-bottom: 0;
|
||||
background: linear-gradient(135deg, #7c3aed 0%, #e0378f 100%);
|
||||
color: #ffffff;
|
||||
border-radius: var(--radius);
|
||||
padding: 2rem 1.5rem;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.page-head h1, .page-head .subtitle, .page-head .meta { color: #ffffff; }
|
||||
.page-head .meta a.source { color: #ffffff; text-decoration: underline; }
|
||||
|
||||
.content h2 { border-left: 6px solid var(--accent); padding-left: .6rem; }
|
||||
.content table { border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow); }
|
||||
.content blockquote { border-radius: var(--radius); }
|
||||
+49
-34
@@ -1,11 +1,18 @@
|
||||
#!/usr/bin/env sh
|
||||
# check-wiki-rules — 驗證 wiki repo 解析與 hash-id 規則。
|
||||
# 涵蓋 TYPES 列的每一種頁面類型,每種三項:專用變數優先、退回共用變數、不得跨類型代用。
|
||||
# 全部通過印 OK 並 exit 0;任一項不符印出差異並 exit 1。
|
||||
# 結束碼: 0=全部通過,stdout 印 OK
|
||||
# 1=有一項不符,stderr 印出 {項目}: want=… got=… 之後立刻停住,不續跑其餘項目。
|
||||
# 只有 0 與 1 兩種;這支不吃參數,也沒有用法錯誤那條路。
|
||||
set -eu
|
||||
|
||||
dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
gitea="$dir/gitea.sh"
|
||||
hash_id="$dir/hash-id"
|
||||
|
||||
TYPES='QUESTION PLAN ANALYZE DELIVER MAINTAIN REPO LOG LEARN ERROR CHECK REPORT SKILLSET TOOLING'
|
||||
|
||||
fail() {
|
||||
printf '%s\n' "$1" >&2
|
||||
exit 1
|
||||
@@ -18,7 +25,7 @@ expect_eq() {
|
||||
[ "$got" = "$want" ] || fail "$label: want=$want got=$got"
|
||||
}
|
||||
|
||||
check_repo() {
|
||||
check_repo() { # type label want [VAR=值...]
|
||||
type=$1
|
||||
label=$2
|
||||
want=$3
|
||||
@@ -27,6 +34,19 @@ check_repo() {
|
||||
expect_eq "$got" "$want" "$label"
|
||||
}
|
||||
|
||||
check_no_repo() { # type label [VAR=值...]:期望 exit 3 且不印出任何 {owner}/{repo}
|
||||
type=$1
|
||||
label=$2
|
||||
shift 2
|
||||
if got=$(env -i PATH="${PATH:-/usr/bin:/bin}" "$@" "$gitea" wiki-repo "$type" 2>/dev/null); then
|
||||
code=0
|
||||
else
|
||||
code=$?
|
||||
fi
|
||||
[ "$code" -eq 3 ] || fail "$label: want exit=3 got exit=$code output=$got"
|
||||
[ -z "$got" ] || fail "$label: want no output got=$got"
|
||||
}
|
||||
|
||||
check_hash() {
|
||||
input=$1
|
||||
want=$2
|
||||
@@ -34,47 +54,42 @@ check_hash() {
|
||||
expect_eq "$got" "$want" "hash-id $input"
|
||||
}
|
||||
|
||||
check_repo REPO 'REPO specific wins' 'records/REPO' \
|
||||
JSC_WIKI_REPO_REPO='records/REPO' \
|
||||
JSC_WIKI_REPO_ANALYZE='knowledges/ANALYZE' \
|
||||
JSC_WIKI_REPO='shared/wiki'
|
||||
# 其餘每一個類型的誘餌值。跨類型代用一旦發生,回傳的就會是 decoy/{別的類型}。
|
||||
decoys() { # $1=要排除的類型
|
||||
for t in $TYPES; do
|
||||
[ "$t" = "$1" ] || printf 'JSC_WIKI_REPO_%s=decoy/%s ' "$t" "$t"
|
||||
done
|
||||
}
|
||||
|
||||
check_repo REPO 'REPO falls back to shared only' 'shared/wiki' \
|
||||
JSC_WIKI_REPO_ANALYZE='knowledges/ANALYZE' \
|
||||
JSC_WIKI_REPO='shared/wiki'
|
||||
for ty in $TYPES; do
|
||||
# 1. 自己的變數贏過共用變數,也贏過其他類型的誘餌。
|
||||
check_repo "$ty" "$ty specific wins" "own/$ty" \
|
||||
$(decoys "$ty") "JSC_WIKI_REPO_$ty=own/$ty" JSC_WIKI_REPO='shared/wiki'
|
||||
|
||||
check_repo ANALYZE 'ANALYZE specific wins' 'knowledges/ANALYZE' \
|
||||
JSC_WIKI_REPO_REPO='records/REPO' \
|
||||
JSC_WIKI_REPO_ANALYZE='knowledges/ANALYZE' \
|
||||
JSC_WIKI_REPO='shared/wiki'
|
||||
# 2. 自己的變數未設定時,只退回共用變數。
|
||||
check_repo "$ty" "$ty falls back to shared only" 'shared/wiki' \
|
||||
$(decoys "$ty") JSC_WIKI_REPO='shared/wiki'
|
||||
|
||||
check_repo ANALYZE 'ANALYZE falls back to shared only' 'shared/wiki' \
|
||||
JSC_WIKI_REPO_REPO='records/REPO' \
|
||||
JSC_WIKI_REPO='shared/wiki'
|
||||
# 3. 自己的變數與共用變數都沒有時,exit 3,不借用別的類型。
|
||||
check_no_repo "$ty" "$ty never borrows another type" $(decoys "$ty")
|
||||
done
|
||||
|
||||
check_repo LEARN 'LEARN specific wins' 'lessons/LEARN' \
|
||||
JSC_WIKI_REPO_LEARN='lessons/LEARN' \
|
||||
JSC_WIKI_REPO='shared/wiki'
|
||||
|
||||
check_repo LEARN 'LEARN falls back to shared only' 'shared/wiki' \
|
||||
JSC_WIKI_REPO_LOG='records/LOG' \
|
||||
JSC_WIKI_REPO='shared/wiki'
|
||||
|
||||
check_repo DELIVER 'DELIVER specific wins' 'handover/DELIVER' \
|
||||
JSC_WIKI_REPO_DELIVER='handover/DELIVER' \
|
||||
JSC_WIKI_REPO='shared/wiki'
|
||||
|
||||
check_repo DELIVER 'DELIVER falls back to shared only' 'shared/wiki' \
|
||||
JSC_WIKI_REPO_ANALYZE='knowledges/ANALYZE' \
|
||||
JSC_WIKI_REPO='shared/wiki'
|
||||
|
||||
check_repo ERROR 'ERROR uses its own repo' 'errors/wiki' \
|
||||
JSC_WIKI_REPO_ERROR='errors/wiki' \
|
||||
JSC_WIKI_REPO='shared/wiki'
|
||||
# 未知類型 exit 2,與「設定不足」的 exit 3 分開。
|
||||
if got=$(env -i PATH="${PATH:-/usr/bin:/bin}" JSC_WIKI_REPO='shared/wiki' \
|
||||
"$gitea" wiki-repo NOSUCH 2>/dev/null); then
|
||||
code=0
|
||||
else
|
||||
code=$?
|
||||
fi
|
||||
[ "$code" -eq 2 ] || fail "unknown type: want exit=2 got exit=$code"
|
||||
[ -z "$got" ] || fail "unknown type: want no output got=$got"
|
||||
|
||||
# hash-id:首碼 0-9/A/B/C 改成 H 加前 7 碼;其餘首碼原樣輸出 8 碼大寫。
|
||||
check_hash 'case-2' 'H5172CB7'
|
||||
check_hash 'case-11' 'HA9A6662'
|
||||
check_hash 'case-1' 'HB6EC7FD'
|
||||
check_hash 'case-12' 'HCCA8D42'
|
||||
check_hash 'case-3' 'D3E5AA27'
|
||||
check_hash 'case-7' 'D794C002'
|
||||
|
||||
printf '%s\n' 'OK'
|
||||
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env sh
|
||||
# gitea-link.sh — 解析 Gitea 的 wiki 頁或議題連結(供 jsc-gitea:wiki-to-issue、html-export 使用)。
|
||||
#
|
||||
# 為什麼要有這支腳本:兩支技能都以「一條連結」為唯一入口,沒有連結就中斷。連結長什麼樣、
|
||||
# 哪一段是存取庫、哪一段是頁名,是固定的字串規則,交給模型每次自己拆,拆錯就寫到別的存取庫去。
|
||||
#
|
||||
# 用法:
|
||||
# gitea-link.sh parse <url> 解析連結,印出可直接 eval 的欄位
|
||||
#
|
||||
# parse 輸出(每行一個 key=value,值已加單引號):
|
||||
# kind='wiki' repo='owner/repo' page='PAGE_NAME' host='https://gitea.example'
|
||||
# kind='issue' repo='owner/repo' index='12' host='https://gitea.example'
|
||||
#
|
||||
# 認得的形式:
|
||||
# https://host/{owner}/{repo}/wiki/{page} (page 可含 %XX 編碼,會還原)
|
||||
# https://host/{owner}/{repo}/wiki/{page}/_edit (尾巴的 _edit、_new 會去掉)
|
||||
# https://host/{owner}/{repo}/issues/{index}
|
||||
# https://host/{owner}/{repo}/issues/{index}#issuecomment-123
|
||||
#
|
||||
# 結束碼: 0=解析成功 2=用法錯誤 3=不是認得的 wiki 或議題連結
|
||||
#
|
||||
# 陷阱:
|
||||
# - 結束碼 3 由呼叫端當成「沒有連結」處理,直接中斷流程,不要退回去猜存取庫或頁名。
|
||||
# - 只解析連結本身,不打網路,也不檢查頁面存不存在;那是 wiki-get 與 issue.sh 的事。
|
||||
set -eu
|
||||
|
||||
usage() {
|
||||
echo 'usage: gitea-link.sh parse <url>' >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
[ "$#" -eq 2 ] || usage
|
||||
[ "$1" = parse ] || usage
|
||||
url="$2"
|
||||
[ -n "$url" ] || usage
|
||||
|
||||
python3 - "$url" <<'PY'
|
||||
import re, sys
|
||||
from urllib.parse import urlsplit, unquote
|
||||
|
||||
url = sys.argv[1].strip()
|
||||
if not re.match(r'^https?://', url):
|
||||
print('[jsc][連結解析][ERR]:連結要以 http:// 或 https:// 開頭。', file=sys.stderr)
|
||||
sys.exit(3)
|
||||
|
||||
u = urlsplit(url)
|
||||
host = f'{u.scheme}://{u.netloc}'
|
||||
parts = [p for p in u.path.split('/') if p]
|
||||
if len(parts) < 4:
|
||||
print('[jsc][連結解析][ERR]:認不出存取庫與頁面,需要 /{owner}/{repo}/wiki/{page} 或 /{owner}/{repo}/issues/{index}。', file=sys.stderr)
|
||||
sys.exit(3)
|
||||
|
||||
owner, repo, kind = parts[0], parts[1], parts[2]
|
||||
rest = parts[3:]
|
||||
|
||||
def out(**kv):
|
||||
for k, v in kv.items():
|
||||
print(f"{k}='{str(v)}'")
|
||||
|
||||
if kind == 'wiki':
|
||||
while rest and rest[-1] in ('_edit', '_new', '_pages'):
|
||||
rest.pop()
|
||||
if not rest:
|
||||
print('[jsc][連結解析][ERR]:wiki 連結少了頁名。', file=sys.stderr)
|
||||
sys.exit(3)
|
||||
page = unquote('/'.join(rest))
|
||||
out(kind='wiki', repo=f'{owner}/{repo}', page=page, host=host)
|
||||
elif kind == 'issues':
|
||||
idx = rest[0].split('#')[0]
|
||||
if not idx.isdigit():
|
||||
print('[jsc][連結解析][ERR]:議題編號不是數字。', file=sys.stderr)
|
||||
sys.exit(3)
|
||||
out(kind='issue', repo=f'{owner}/{repo}', index=idx, host=host)
|
||||
else:
|
||||
print(f'[jsc][連結解析][ERR]:只認得 wiki 與 issues 連結,收到「{kind}」。', file=sys.stderr)
|
||||
sys.exit(3)
|
||||
PY
|
||||
+295
-18
@@ -7,27 +7,59 @@
|
||||
# gitea.sh clone-url <owner>/<repo> # 印出 clone URL
|
||||
# gitea.sh hash-id <text> # 產生 8 碼大寫 SHA-1 hash;首碼為 0-9/A/B/C 時改成 Hxxxxxxx
|
||||
# gitea.sh wiki-repo <TYPE> # 解析頁面類型的 wiki 位置:
|
||||
# TYPE = QUESTION|PLAN|ANALYZE|DELIVER|MAINTAIN|REPO|LOG|LEARN|ERROR(即頁名前綴)
|
||||
# TYPE = QUESTION|PLAN|ANALYZE|DELIVER|MAINTAIN|REPO|LOG|LEARN|ERROR|CHECK|REPORT|SKILLSET|TOOLING(即頁名前綴)
|
||||
# 依序取 JSC_WIKI_REPO_{TYPE} > JSC_WIKI_REPO;不得跨類型代用;都未設定 exit 3
|
||||
# gitea.sh wiki-list <owner>/<repo> # 列出 wiki 頁名
|
||||
# gitea.sh wiki-get <owner>/<repo> <page> # 印出 wiki 頁 markdown;不存在時 exit 4
|
||||
# gitea.sh wiki-put <owner>/<repo> <page> <file> # 建立或更新 wiki 頁(內容取自檔案)
|
||||
# gitea.sh wiki-url <owner>/<repo> <page> # 印出 wiki 頁絕對網址(取自 API 的 html_url);跨存取庫連結用
|
||||
# gitea.sh pr-create <owner>/<repo> <head> <base> <title> <body-file> # 建立 PR,印出 PR URL
|
||||
# gitea.sh pr-status <owner>/<repo> <pr-index> # 印出「{state} {merged} {mergeable}」
|
||||
# gitea.sh pr-get <owner>/<repo> <pr-index> # 印出 PR 的標題、base 分支與描述,供比對用
|
||||
# gitea.sh pr-of-branch <owner>/<repo> <branch> # 印出該分支目前開啟中的 PR(比對 head.ref)
|
||||
# 輸出格式固定四段,描述放最後,因為只有它會多行:
|
||||
# 第 1 行 number<TAB>{PR 編號}
|
||||
# 第 2 行 title<TAB>{標題}
|
||||
# 第 3 行 base<TAB>{base 分支}
|
||||
# 第 4 行 body (單獨一個字,當描述的起始標記)
|
||||
# 第 5 行起 描述原文,一直到檔尾
|
||||
# 後三段與 pr-get 完全一致,只在最前面多一行 number。呼叫端取編號用 head -n1 | cut -f2-,
|
||||
# 取描述用 tail -n +5,全程不必解析 JSON,也不必再打一次 pr-get。
|
||||
# 結束碼: 0=找到 2=用法錯誤 3=該分支沒有開啟中的 PR 4=API 失敗
|
||||
# 「沒有 PR」必須是 3,不能借用通用的 API 失敗碼:兩者混用會把金鑰失效讀成
|
||||
# 「這個分支還沒開 PR」,呼叫端接著就開出第二支重複的 PR。
|
||||
# gitea.sh pr-edit <owner>/<repo> <pr-index> <title> <body-file> # 更新 PR 的標題與描述
|
||||
# gitea.sh pr-comments <owner>/<repo> <pr-index> # 印出所有留言(issue 留言、審查評語、行內留言),第三欄帶 #id
|
||||
# gitea.sh comment-reply <owner>/<repo> <pr-index> <issue|review|inline> <comment-id> <body-file>
|
||||
# 回覆本輪處理過的 PR 留言;inline 走 review comment reply,其餘補一則 PR 留言
|
||||
# gitea.sh pr-depend <owner>/<repo> <pr-index> <dep-owner>/<dep-repo> <dep-index>
|
||||
# 把 PR 掛上前置 PR 依賴;依賴未關閉前 Gitea 會阻擋合併
|
||||
# gitea.sh repo-set <owner>/<repo> <description> [website] # 設定 repo 描述與網頁
|
||||
# gitea.sh markdown <file> # markdown 檔渲染成 HTML 片段(走 /markdown/raw)
|
||||
# gitea.sh api <METHOD> <path> [json-file] # 原始 API 呼叫(path 以 /repos/... 起始)
|
||||
# 環境變數: GITEA_HOST(例 https://gitea.jsc.idv.tw)、GITEA_TOKEN
|
||||
# GITEA_TOKEN 未設定,或請求遇 401/403 時,自動退回 tea CLI 的登入 token
|
||||
# (~/.config/tea/config.yml,優先取 default: true 的登入);兩者皆無才失敗。
|
||||
# (~/.config/tea/config.yml,優先取 url 與 GITEA_HOST 同主機的登入,其次 default: true);兩者皆無才失敗。
|
||||
# 結束碼: 0=成功 1=api 子命令的請求失敗 2=用法錯誤或不認得的指令
|
||||
# 3=wiki-repo 的該類型沒有設定存取庫、pr-of-branch 的該分支沒有開啟中的 PR
|
||||
# 4=找不到(HTTP 404,含 wiki 頁不存在)、PR 系列子命令的 API 失敗,
|
||||
# 以及 pr-of-branch 翻過 50 頁上限仍沒結束(分頁沒有前進)
|
||||
# 5=wiki 頁沒有 html_url
|
||||
# 7=Gitea 金鑰失效或權限不足(HTTP 401/403)
|
||||
# 8=其他 API 失敗,訊息帶 HTTP 狀態
|
||||
# 7 與 8 是 2026-08 實測補上的:原本認證失敗、伺服器錯誤全部被歸成「頁面不存在」,
|
||||
# 而 wiki 寫入的「附加不覆蓋」判斷就靠讀得到舊頁,誤判會直接蓋掉舊紀錄。
|
||||
set -eu
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
confirm_write() {
|
||||
sh "$script_dir/write-confirm.sh" "$1" "$2"
|
||||
}
|
||||
|
||||
resolve_wiki_repo() { # TYPE -> JSC_WIKI_REPO_{TYPE} -> JSC_WIKI_REPO
|
||||
type=$(printf '%s' "${1:?TYPE required}" | tr a-z A-Z)
|
||||
case "$type" in
|
||||
QUESTION|PLAN|ANALYZE|DELIVER|MAINTAIN|REPO|LOG|LEARN|ERROR) ;;
|
||||
QUESTION|PLAN|ANALYZE|DELIVER|MAINTAIN|REPO|LOG|LEARN|ERROR|CHECK|REPORT|SKILLSET|TOOLING) ;;
|
||||
*) echo "unknown wiki type: $type" >&2; exit 2 ;;
|
||||
esac
|
||||
eval "v=\${JSC_WIKI_REPO_${type}:-}"
|
||||
@@ -92,13 +124,40 @@ fi
|
||||
case "$GITEA_HOST" in http://*|https://*) HOST="$GITEA_HOST" ;; *) HOST="https://$GITEA_HOST" ;; esac
|
||||
API="${HOST%/}/api/v1"
|
||||
|
||||
req() { # METHOD path [json-file] -> body(HTTP >= 400 時 exit 4;401/403 以 tea token 重試一次)
|
||||
# 最後一次請求的 HTTP 狀態碼寫進檔案,不是變數:req 幾乎都在 $(...) 裡跑,
|
||||
# 子行程設的變數回不到主行程,狀態碼會在回來的路上不見。
|
||||
REQ_CODE_FILE=$(mktemp)
|
||||
trap 'rm -f "$REQ_CODE_FILE"' EXIT
|
||||
|
||||
req_code() { cat "$REQ_CODE_FILE" 2>/dev/null || true; }
|
||||
|
||||
api_fail() { # $1=情境說明 -> 依最後一次的 HTTP 狀態分流退出
|
||||
# 失敗成因一定要分開回報。全部歸成「找不到」是最危險的一種簡化:
|
||||
# 認證失敗看起來就會像頁面不存在,呼叫端接著就用新頁的邏輯往上蓋。
|
||||
_c=$(req_code)
|
||||
case "$_c" in
|
||||
401|403)
|
||||
echo "[jsc][gitea][ERR]:$1 —— Gitea 金鑰失效或權限不足(HTTP $_c)。請換一支有效的 GITEA_TOKEN,或重新 tea login 之後再跑一次。" >&2
|
||||
exit 7 ;;
|
||||
404)
|
||||
echo "[jsc][gitea][ERR]:$1 —— 找不到(HTTP 404)。" >&2
|
||||
exit 4 ;;
|
||||
*)
|
||||
echo "[jsc][gitea][ERR]:$1 —— API 失敗(HTTP ${_c:-無回應})。" >&2
|
||||
exit 8 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
req() { # METHOD path [body-file] -> body(HTTP >= 400 時回非 0;401/403 以 tea token 重試一次)
|
||||
# 送出的 Content-Type 由 REQ_CONTENT_TYPE 決定,預設 application/json;
|
||||
# /markdown/raw 這種吃純文字的端點要先改成 text/plain 再呼叫。
|
||||
method="$1"; path="$2"; body_file="${3:-}"
|
||||
ctype="${REQ_CONTENT_TYPE:-application/json}"
|
||||
retried=0
|
||||
while :; do
|
||||
if [ -n "$body_file" ]; then
|
||||
out=$(curl -sS -w '\n%{http_code}' -X "$method" \
|
||||
-H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \
|
||||
-H "Authorization: token $GITEA_TOKEN" -H "Content-Type: $ctype" \
|
||||
--data-binary "@$body_file" "$API$path")
|
||||
else
|
||||
out=$(curl -sS -w '\n%{http_code}' -X "$method" \
|
||||
@@ -111,6 +170,7 @@ req() { # METHOD path [json-file] -> body(HTTP >= 400 時 exit 4;401/403 以
|
||||
fi
|
||||
break
|
||||
done
|
||||
printf '%s' "$code" > "$REQ_CODE_FILE"
|
||||
printf '%s\n' "$out" | sed '$d'
|
||||
[ "$code" -lt 400 ]
|
||||
}
|
||||
@@ -128,15 +188,21 @@ for i in items:
|
||||
|
||||
case "$cmd" in
|
||||
owners)
|
||||
{ req GET "/user" | json_field login
|
||||
req GET "/user/orgs?limit=50" | json_field username; } | sort -u ;;
|
||||
# req 不直接接管線:管線的結束狀態取自 json_field,會把 req 的失敗整個吃掉,
|
||||
# 金鑰失效看起來就變成「查詢成功,一個 owner 都沒有」。
|
||||
me=$(req GET "/user") || api_fail "讀不到目前登入的使用者"
|
||||
orgs=$(req GET "/user/orgs?limit=50") || api_fail "讀不到組織清單"
|
||||
{ printf '%s' "$me" | json_field login
|
||||
printf '%s' "$orgs" | json_field username; } | sort -u ;;
|
||||
repos)
|
||||
owner="${1:?owner required}"
|
||||
# 分頁抓 owner 的 repo(org 與 user 端點擇一成功)
|
||||
page=1
|
||||
while :; do
|
||||
out=$(req GET "/orgs/$owner/repos?limit=50&page=$page" 2>/dev/null) \
|
||||
|| out=$(req GET "/users/$owner/repos?limit=50&page=$page")
|
||||
if ! out=$(req GET "/orgs/$owner/repos?limit=50&page=$page" 2>/dev/null); then
|
||||
out=$(req GET "/users/$owner/repos?limit=50&page=$page") \
|
||||
|| api_fail "列不出 $owner 的存取庫"
|
||||
fi
|
||||
names=$(printf '%s' "$out" | json_field full_name)
|
||||
[ -n "$names" ] || break
|
||||
printf '%s\n' "$names"
|
||||
@@ -144,17 +210,30 @@ case "$cmd" in
|
||||
done ;;
|
||||
default-branch)
|
||||
or="${1:?owner/repo required}"
|
||||
req GET "/repos/$or" | json_field default_branch ;;
|
||||
out=$(req GET "/repos/$or") || api_fail "讀不到存取庫 $or"
|
||||
printf '%s' "$out" | json_field default_branch ;;
|
||||
clone-url)
|
||||
or="${1:?owner/repo required}"
|
||||
req GET "/repos/$or" | json_field clone_url ;;
|
||||
out=$(req GET "/repos/$or") || api_fail "讀不到存取庫 $or"
|
||||
printf '%s' "$out" | json_field clone_url ;;
|
||||
wiki-list)
|
||||
or="${1:?owner/repo required}"
|
||||
req GET "/repos/$or/wiki/pages?limit=200" | json_field title ;;
|
||||
# 先接變數、先看 req 自己的結束碼,再餵給 json_field。直接接管線的話,
|
||||
# 結束狀態會變成 json_field 的:金鑰失效回 401 時,這裡看起來像「列出成功,
|
||||
# 一頁都沒有」,呼叫端就把整個 wiki 判成空的。
|
||||
out=$(req GET "/repos/$or/wiki/pages?limit=200") || api_fail "列不出 $or 的 wiki 頁"
|
||||
printf '%s' "$out" | json_field title ;;
|
||||
wiki-get)
|
||||
# 失敗成因一定要分開:技能組寫 wiki 的語意是「附加一節、不覆蓋舊紀錄」,
|
||||
# 判斷依據就是先把舊內容讀回來。金鑰失效回 401 若被翻譯成「頁面不存在」,
|
||||
# 呼叫端會把它當成一張新頁整份蓋上去,舊紀錄就沒了。
|
||||
# 只有真的 404 才回 4;401/403 回 7,其他失敗回 8 並帶 HTTP 狀態。
|
||||
or="${1:?owner/repo required}"; page="${2:?page required}"
|
||||
if ! out=$(req GET "/repos/$or/wiki/page/$page" 2>/dev/null); then
|
||||
echo "wiki page not found: $page" >&2; exit 4
|
||||
case "$(req_code)" in
|
||||
404) echo "wiki page not found: $page" >&2; exit 4 ;;
|
||||
*) api_fail "讀不到 wiki 頁 $or/$page" ;;
|
||||
esac
|
||||
fi
|
||||
printf '%s' "$out" | python3 -c '
|
||||
import json,sys,base64
|
||||
@@ -167,7 +246,10 @@ sys.stdout.write(base64.b64decode(d.get("content_base64","")).decode("utf-8"))
|
||||
# 只有絕對網址會通:[[頁名]] 與 markdown 相對連結都只在同一個 wiki 內解析。
|
||||
or="${1:?owner/repo required}"; page="${2:?page required}"
|
||||
if ! out=$(req GET "/repos/$or/wiki/page/$page" 2>/dev/null); then
|
||||
echo "wiki page not found: $page" >&2; exit 4
|
||||
case "$(req_code)" in
|
||||
404) echo "wiki page not found: $page" >&2; exit 4 ;;
|
||||
*) api_fail "讀不到 wiki 頁 $or/$page" ;;
|
||||
esac
|
||||
fi
|
||||
url=$(printf '%s' "$out" | python3 -c '
|
||||
import json,sys
|
||||
@@ -179,6 +261,7 @@ sys.stdout.write(json.load(sys.stdin).get("html_url") or "")
|
||||
printf '%s\n' "$url" ;;
|
||||
wiki-put)
|
||||
or="${1:?owner/repo required}"; page="${2:?page required}"; file="${3:?content file required}"
|
||||
confirm_write "寫入 wiki 頁" "$page"
|
||||
tmp=$(mktemp)
|
||||
python3 -c '
|
||||
import json,sys,base64
|
||||
@@ -186,27 +269,211 @@ title,path=sys.argv[1],sys.argv[2]
|
||||
content=open(path,"rb").read()
|
||||
print(json.dumps({"title":title,"content_base64":base64.b64encode(content).decode()}))
|
||||
' "$page" "$file" > "$tmp"
|
||||
# 先探路只為了決定新建或更新。探路失敗的成因若是認證問題,接下來的 POST 也會失敗,
|
||||
# 由 api_fail 照實回報,不會靜靜蓋掉既有頁面。
|
||||
if req GET "/repos/$or/wiki/page/$page" >/dev/null 2>&1; then
|
||||
req PATCH "/repos/$or/wiki/page/$page" "$tmp" >/dev/null
|
||||
req PATCH "/repos/$or/wiki/page/$page" "$tmp" >/dev/null \
|
||||
|| { rm -f "$tmp"; api_fail "更新 wiki 頁 $or/$page 失敗"; }
|
||||
else
|
||||
req POST "/repos/$or/wiki/new" "$tmp" >/dev/null
|
||||
req POST "/repos/$or/wiki/new" "$tmp" >/dev/null \
|
||||
|| { rm -f "$tmp"; api_fail "建立 wiki 頁 $or/$page 失敗"; }
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
echo "OK $page" ;;
|
||||
pr-create)
|
||||
or="${1:?owner/repo required}"; head="${2:?head required}"; base="${3:?base required}"
|
||||
title="${4:?title required}"; body_file="${5:?body file required}"
|
||||
# 描述檔缺席要回 2「用法錯誤」,跟 pr-edit、comment-reply 一致。少了這道檢查,
|
||||
# 缺檔會變成 python3 的 open() 例外加 exit 1,呼叫端讀成「API 失敗」而去重試。
|
||||
# 這道檢查排在確認之前:先擋掉自己打錯的參數,才不會問完使用者又失敗。
|
||||
[ -f "$body_file" ] || { echo "找不到描述檔: $body_file" >&2; exit 2; }
|
||||
confirm_write "建立 PR" "$or#$title"
|
||||
tmp=$(mktemp)
|
||||
python3 -c '
|
||||
import json,sys
|
||||
print(json.dumps({"head":sys.argv[1],"base":sys.argv[2],"title":sys.argv[3],
|
||||
"body":open(sys.argv[4],encoding="utf-8").read()}))
|
||||
' "$head" "$base" "$title" "$body_file" > "$tmp"
|
||||
req POST "/repos/$or/pulls" "$tmp" | json_field html_url
|
||||
rm -f "$tmp" ;;
|
||||
if ! out=$(req POST "/repos/$or/pulls" "$tmp"); then
|
||||
rm -f "$tmp"; printf '%s\n' "$out" >&2; exit 4
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
printf '%s' "$out" | json_field html_url ;;
|
||||
pr-status)
|
||||
# 印出「{state} {merged} {mergeable}」,供呼叫端判斷 PR 是否已合併。
|
||||
# 查不到 PR(404)維持印「? none none」並 exit 0:pr-watch.sh 的白名單靠這個字串
|
||||
# 判成 exit 3「查不到該 PR」。認證或連線失敗改回非 0,不再混進同一個字串裡。
|
||||
or="${1:?owner/repo required}"; idx="${2:?pr index required}"
|
||||
if ! out=$(req GET "/repos/$or/pulls/$idx" 2>/dev/null); then
|
||||
case "$(req_code)" in
|
||||
404) echo '? none none'; exit 0 ;;
|
||||
*) api_fail "讀不到 PR $or#$idx" ;;
|
||||
esac
|
||||
fi
|
||||
printf '%s' "$out" | python3 -c '
|
||||
import json,sys
|
||||
p=json.load(sys.stdin)
|
||||
print("%s %s %s" % (p.get("state","?"), str(p.get("merged")).lower(), str(p.get("mergeable")).lower()))
|
||||
' ;;
|
||||
pr-get)
|
||||
# 印出 PR 的標題、base 分支與描述,供呼叫端比對本機資料是否已經跟 PR 不一致。
|
||||
# 格式固定三段,描述放最後一段,因為只有它會多行:
|
||||
# 第 1 行 title<TAB>{標題}
|
||||
# 第 2 行 base<TAB>{base 分支}
|
||||
# 第 3 行 body (單獨一個字,當描述的起始標記)
|
||||
# 第 4 行起 描述原文,一直到檔尾
|
||||
# 呼叫端取標題用 head -n1 | cut -f2-,取 base 用 sed -n 2p | cut -f2-,
|
||||
# 取描述用 tail -n +4,全程不必解析 JSON。
|
||||
or="${1:?owner/repo required}"; idx="${2:?pr index required}"
|
||||
if ! out=$(req GET "/repos/$or/pulls/$idx"); then
|
||||
printf '%s\n' "$out" >&2; exit 4
|
||||
fi
|
||||
printf '%s' "$out" | python3 -c '
|
||||
import json,sys
|
||||
p=json.load(sys.stdin)
|
||||
print("title\t%s" % (p.get("title") or ""))
|
||||
print("base\t%s" % ((p.get("base") or {}).get("ref") or ""))
|
||||
print("body")
|
||||
sys.stdout.write(p.get("body") or "")
|
||||
' ;;
|
||||
pr-of-branch)
|
||||
# 找出某條分支目前開啟中的 PR。比對的是 head.ref,不是分支名的字串包含:
|
||||
# feat/報表 與 feat/報表/main 只差一段,用包含比對會回錯的那一支。
|
||||
# 輸出格式與 pr-get 對齊並多帶 index 與 url,呼叫端(jsc-git:commit、jsc-git:pr)
|
||||
# 拿到就能直接比對標題與描述,不必再打一次 pr-get。
|
||||
or="${1:?owner/repo required}"; branch="${2:?branch required}"
|
||||
# 頁數上限。只靠「回空陣列」收尾的迴圈,遇上忽略 page 參數的站台或代理會一直
|
||||
# 拿到同一頁而永遠停不下來——沒有輸出、也沒有結束碼,呼叫端只看得到卡住。
|
||||
page=1
|
||||
max_page=50
|
||||
while :; do
|
||||
if [ "$page" -gt "$max_page" ]; then
|
||||
echo "[jsc][gitea][ERR]:列 $or 的開啟中 PR 超過 $max_page 頁仍沒有結束,分頁沒有前進(站台或代理可能忽略 page 參數)。" >&2
|
||||
exit 4
|
||||
fi
|
||||
# req 的輸出先接進變數再解析。寫成 req ... | python3 的話,管線的結束狀態
|
||||
# 取自 python,401 會變成「解不到相符的 PR」而回 3,正好踩中重複開 PR 那個坑。
|
||||
if ! out=$(req GET "/repos/$or/pulls?state=open&limit=50&page=$page"); then
|
||||
echo "[jsc][gitea][ERR]:列不出 $or 的開啟中 PR(HTTP $(req_code))。" >&2
|
||||
exit 4
|
||||
fi
|
||||
res=$(printf '%s' "$out" | python3 -c '
|
||||
import json,sys
|
||||
want=sys.argv[1]
|
||||
d=json.load(sys.stdin)
|
||||
if not isinstance(d,list) or not d:
|
||||
print("__EMPTY__"); raise SystemExit
|
||||
for p in d:
|
||||
if ((p.get("head") or {}).get("ref") or "") == want:
|
||||
print("number\t%s" % (p.get("number") or p.get("index") or ""))
|
||||
print("title\t%s" % (p.get("title") or ""))
|
||||
print("base\t%s" % ((p.get("base") or {}).get("ref") or ""))
|
||||
print("body")
|
||||
sys.stdout.write(p.get("body") or "")
|
||||
raise SystemExit
|
||||
print("__NONE__")
|
||||
' "$branch") || { echo "[jsc][gitea][ERR]:$or 的 PR 清單解不開。" >&2; exit 4; }
|
||||
case "$res" in
|
||||
__NONE__) page=$((page+1)); continue ;;
|
||||
__EMPTY__) break ;;
|
||||
esac
|
||||
printf '%s\n' "$res"
|
||||
exit 0
|
||||
done
|
||||
echo "[jsc][gitea][ERR]:分支 $branch 沒有開啟中的 PR。" >&2
|
||||
exit 3 ;;
|
||||
pr-edit)
|
||||
# 更新 PR 的標題與描述。描述從檔案讀,才裝得下多行內容與全形標點。
|
||||
or="${1:?owner/repo required}"; idx="${2:?pr index required}"
|
||||
title="${3:?title required}"; body_file="${4:?body file required}"
|
||||
[ -f "$body_file" ] || { echo "找不到描述檔: $body_file" >&2; exit 2; }
|
||||
confirm_write "更新 PR" "$or#$idx"
|
||||
tmp=$(mktemp)
|
||||
python3 -c '
|
||||
import json,sys
|
||||
print(json.dumps({"title":sys.argv[1],"body":open(sys.argv[2],encoding="utf-8").read()}))
|
||||
' "$title" "$body_file" > "$tmp"
|
||||
if ! out=$(req PATCH "/repos/$or/pulls/$idx" "$tmp"); then
|
||||
rm -f "$tmp"; printf '%s\n' "$out" >&2; exit 4
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
echo "OK $or#$idx" ;;
|
||||
pr-comments)
|
||||
# 印出 PR 的所有留言,每行「{時間}<TAB>{作者}<TAB>{類型}#{id}<TAB>{內容單行化}」。
|
||||
# 三個來源都要讀:issue 留言、review 本體的評語、review 內逐行的程式碼留言。
|
||||
# 只讀 issue 留言會漏掉真正的審查意見,那正是需要修正的部分。
|
||||
# 類型欄保留在第三欄,僅追加 id;呼叫端用第一欄做 since 比對時不受影響。
|
||||
# req 一律先接進變數、先看它自己的結束碼,再餵給 python3。寫成 req … | python3
|
||||
# 的話,管線的結束狀態取自 python:金鑰失效回 401 時,這裡會印出一份空的留言清單
|
||||
# 並回 0,呼叫端(jsc-git:pr)就判成「這支 PR 沒有任何審查意見」,整輪留言修正被跳過。
|
||||
or="${1:?owner/repo required}"; idx="${2:?pr index required}"
|
||||
issues=$(req GET "/repos/$or/issues/$idx/comments?limit=100") \
|
||||
|| api_fail "讀不到 PR $or#$idx 的留言"
|
||||
reviews=$(req GET "/repos/$or/pulls/$idx/reviews?limit=100") \
|
||||
|| api_fail "讀不到 PR $or#$idx 的審查清單"
|
||||
rids=$(printf '%s' "$reviews" | python3 -c '
|
||||
import json,sys
|
||||
for r in json.load(sys.stdin):
|
||||
if r.get("comments_count", 0) or r.get("id"): print(r["id"])
|
||||
')
|
||||
# 行內留言逐則抓,抓失敗一樣要當成失敗。收集在 $lines 裡最後才排序:
|
||||
# 把 api_fail 放進管線的話,它只結束子行程,主流程照樣往下印出殘缺清單。
|
||||
lines=$(mktemp)
|
||||
{ printf '%s' "$issues" | python3 -c '
|
||||
import json,sys
|
||||
for c in json.load(sys.stdin):
|
||||
body=" ".join((c.get("body") or "").split())
|
||||
if body: print("%s\t%s\t留言#%s\t%s" % (c.get("created_at",""), (c.get("user") or {}).get("login","?"), c.get("id","?"), body))
|
||||
'
|
||||
printf '%s' "$reviews" | python3 -c '
|
||||
import json,sys
|
||||
for r in json.load(sys.stdin):
|
||||
body=" ".join((r.get("body") or "").split())
|
||||
st=r.get("state","")
|
||||
# 沒有評語的審查照樣要印:APPROVED 代表可以合併,REQUEST_CHANGES 代表被要求修改,
|
||||
# 兩者都是呼叫端要據以決策的事實,過濾掉會看不見 PR 真正的狀態。
|
||||
print("%s\t%s\t審查#%s(%s)\t%s" % (r.get("submitted_at",""), (r.get("user") or {}).get("login","?"), r.get("id","?"), st, body or "(無評語)"))
|
||||
'
|
||||
} > "$lines"
|
||||
for rid in $rids; do
|
||||
rc=$(req GET "/repos/$or/pulls/$idx/reviews/$rid/comments") \
|
||||
|| { rm -f "$lines"; api_fail "讀不到 PR $or#$idx 審查 $rid 的行內留言"; }
|
||||
printf '%s' "$rc" | python3 -c '
|
||||
import json,sys
|
||||
d=json.load(sys.stdin)
|
||||
for c in d:
|
||||
body=" ".join((c.get("body") or "").split())
|
||||
if body: print("%s\t%s\t行內#%s(%s:%s)\t%s" % (c.get("created_at",""), (c.get("user") or {}).get("login","?"), c.get("id","?"), c.get("path",""), c.get("original_position") or c.get("position") or "", body))
|
||||
' >> "$lines"
|
||||
done
|
||||
sort "$lines"
|
||||
rm -f "$lines" ;;
|
||||
comment-reply)
|
||||
or="${1:?owner/repo required}"; idx="${2:?pr index required}"
|
||||
kind="${3:?kind required}"; cid="${4:?comment id required}"; body_file="${5:?body file required}"
|
||||
[ -f "$body_file" ] || { echo "找不到回覆檔: $body_file" >&2; exit 2; }
|
||||
case "$kind" in issue|review|inline) ;; *) echo "kind must be issue, review, or inline" >&2; exit 2 ;; esac
|
||||
confirm_write "回覆留言" "$or#$idx/$kind#$cid"
|
||||
tmp=$(mktemp)
|
||||
python3 -c '
|
||||
import json,sys
|
||||
print(json.dumps({"body":open(sys.argv[1],encoding="utf-8").read()}))
|
||||
' "$body_file" > "$tmp"
|
||||
case "$kind" in
|
||||
inline)
|
||||
path="/repos/$or/pulls/$idx/comments/$cid/replies" ;;
|
||||
issue|review)
|
||||
path="/repos/$or/issues/$idx/comments" ;;
|
||||
esac
|
||||
if ! out=$(req POST "$path" "$tmp"); then
|
||||
rm -f "$tmp"; printf '%s\n' "$out" >&2; exit 4
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
printf '%s' "$out" | json_field html_url ;;
|
||||
pr-depend)
|
||||
or="${1:?owner/repo required}"; idx="${2:?pr index required}"
|
||||
dep_or="${3:?dep owner/repo required}"; dep_idx="${4:?dep index required}"
|
||||
confirm_write "設定 PR 依賴" "$or#$idx -> $dep_or#$dep_idx"
|
||||
tmp=$(mktemp)
|
||||
python3 -c '
|
||||
import json,sys
|
||||
@@ -220,6 +487,7 @@ print(json.dumps({"owner":o,"repo":r,"index":int(sys.argv[2])}))
|
||||
echo "OK $or#$idx depends on $dep_or#$dep_idx" ;;
|
||||
repo-set)
|
||||
or="${1:?owner/repo required}"; desc="${2:?description required}"; site="${3:-}"
|
||||
confirm_write "更新 repo 資訊" "$or"
|
||||
tmp=$(mktemp)
|
||||
python3 -c '
|
||||
import json,sys
|
||||
@@ -232,6 +500,15 @@ print(json.dumps(b))
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
echo "OK $or" ;;
|
||||
markdown)
|
||||
# markdown 檔 -> HTML 片段。走 /markdown/raw(吃純文字)而不是 /markdown:
|
||||
# 後者在 Gitea 1.27 回 200 但內容是空的,看起來像成功,其實什麼都沒渲染。
|
||||
# 代價:raw 端點不吃 context,wiki 的 [[頁名]] 內部連結不會變成連結,
|
||||
# 呼叫端要先把它換成絕對網址。
|
||||
file="${1:?markdown file}"
|
||||
[ -f "$file" ] || { echo "找不到 markdown 檔: $file" >&2; exit 2; }
|
||||
REQ_CONTENT_TYPE='text/plain'
|
||||
req POST "/markdown/raw" "$file" || api_fail "markdown 渲染失敗($file)" ;;
|
||||
api)
|
||||
method="${1:?METHOD}"; path="${2:?path}"; body="${3:-}"
|
||||
req "$method" "$path" $body ;;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
# 規則:
|
||||
# 先取 SHA-1 前 8 碼大寫。
|
||||
# 若首碼為 0-9、A、B、C,改成 H 加上原 SHA-1 前 7 碼。
|
||||
# 結束碼: 0=成功 1=這台機器沒有 sha1sum 也沒有 shasum,算不出雜湊
|
||||
set -eu
|
||||
|
||||
input=''
|
||||
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env sh
|
||||
# html-render.sh — 把 markdown 套上版型與風格,輸出單一 HTML 檔(供 jsc-gitea:html-export 使用)。
|
||||
#
|
||||
# 為什麼要有這支腳本:markdown 轉 HTML 交給 Gitea 自己渲染(gitea.sh markdown),出來的排版才跟
|
||||
# wiki、議題頁看到的一致;版型與風格則是固定的字串替換。兩件事都有標準輸入輸出,不必每次重寫。
|
||||
#
|
||||
# 用法:
|
||||
# html-render.sh --markdown <檔案> --title <標題> --out <輸出檔>
|
||||
# [--subtitle <副標>] [--layout <版型>] [--style <風格>]
|
||||
# [--source-url <來源網址>]
|
||||
#
|
||||
# --layout 預設 report,--style 預設 minimal。可用清單見 html-style.sh layouts / styles。
|
||||
#
|
||||
# 輸出: 寫出 --out 指定的 HTML 檔,並在標準輸出印出該檔路徑。
|
||||
# 結束碼: 0=成功 1=渲染或寫檔失敗 2=用法錯誤 4=找不到版型或風格範本
|
||||
#
|
||||
# 陷阱:
|
||||
# - HTML 是單一檔案,CSS 直接內嵌,不外連任何資源;產出物常常是寄給別人看的,外連在對方那裡會破圖。
|
||||
# - markdown 渲染走 Gitea API。連不上就失敗收場,不自己拼一套半套的轉換——半套轉換出來的表格
|
||||
# 跟 wiki 上看到的不一樣,比失敗更難發現。
|
||||
# - 渲染端點不吃 wiki 情境,`[[頁名]]` 這種 wiki 內部連結會原樣留著。呼叫端要先換成絕對網址,
|
||||
# 產出的 HTML 才連得回去。
|
||||
set -eu
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
plugin_root="${CLAUDE_PLUGIN_ROOT:-$script_dir/..}"
|
||||
TPL="$plugin_root/templates/html"
|
||||
GITEA="$script_dir/gitea.sh"
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
用法:
|
||||
html-render.sh --markdown <檔案> --title <標題> --out <輸出檔>
|
||||
[--subtitle <副標>] [--layout <版型>] [--style <風格>]
|
||||
[--source-url <來源網址>]
|
||||
結束碼: 0=成功 1=渲染或寫檔失敗 2=用法錯誤 4=找不到版型或風格範本
|
||||
EOF
|
||||
exit 2
|
||||
}
|
||||
|
||||
md=''; title=''; out=''; subtitle=''; layout=report; style=minimal; source_url=''
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--markdown) [ "$#" -ge 2 ] || usage; md="$2"; shift 2 ;;
|
||||
--title) [ "$#" -ge 2 ] || usage; title="$2"; shift 2 ;;
|
||||
--out) [ "$#" -ge 2 ] || usage; out="$2"; shift 2 ;;
|
||||
--subtitle) [ "$#" -ge 2 ] || usage; subtitle="$2"; shift 2 ;;
|
||||
--layout) [ "$#" -ge 2 ] || usage; layout="$2"; shift 2 ;;
|
||||
--style) [ "$#" -ge 2 ] || usage; style="$2"; shift 2 ;;
|
||||
--source-url) [ "$#" -ge 2 ] || usage; source_url="$2"; shift 2 ;;
|
||||
*) echo "[jsc][HTML 產生][ERR]:不認得的選項「$1」。" >&2; usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ -n "$md" ] && [ -n "$title" ] && [ -n "$out" ] || usage
|
||||
[ -f "$md" ] || { echo "[jsc][HTML 產生][ERR]:找不到 markdown 檔「$md」。" >&2; exit 2; }
|
||||
|
||||
layout_file="$TPL/layout/$layout.html"
|
||||
style_file="$TPL/style/$style.css"
|
||||
[ -f "$layout_file" ] || { echo "[jsc][HTML 產生][ERR]:找不到版型範本「$layout_file」。" >&2; exit 4; }
|
||||
[ -f "$style_file" ] || { echo "[jsc][HTML 產生][ERR]:找不到風格範本「$style_file」。" >&2; exit 4; }
|
||||
|
||||
# markdown -> HTML 片段:交給 Gitea 自己渲染,排版才跟站上一致。
|
||||
fragment=$(mktemp)
|
||||
trap 'rm -f "$fragment"' EXIT
|
||||
|
||||
if ! "$GITEA" markdown "$md" > "$fragment" 2>/dev/null || [ ! -s "$fragment" ]; then
|
||||
echo '[jsc][HTML 產生][ERR]:Gitea 的 markdown 渲染失敗,這次不出檔。請確認 GITEA_HOST 與權杖後重跑。' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 - "$layout_file" "$style_file" "$fragment" "$out" "$title" "$subtitle" "$source_url" "$layout" "$style" "$TPL" <<'PY'
|
||||
import html, sys, datetime, os
|
||||
|
||||
layout_file, style_file, frag_file, out_file, title, subtitle, source_url, layout, style, tpl_dir = sys.argv[1:11]
|
||||
|
||||
def read(p):
|
||||
with open(p, encoding='utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
page = read(layout_file)
|
||||
css = read(style_file)
|
||||
base_css = read(os.path.join(tpl_dir, 'base.css'))
|
||||
base_js = read(os.path.join(tpl_dir, 'base.js'))
|
||||
content = read(frag_file)
|
||||
generated = datetime.datetime.now().astimezone().strftime('%Y-%m-%d %H:%M')
|
||||
|
||||
source_html = ''
|
||||
if source_url:
|
||||
safe = html.escape(source_url, quote=True)
|
||||
source_html = '<a class="source" href="%s" rel="noreferrer">來源:%s</a>' % (safe, safe)
|
||||
|
||||
for key, value in (
|
||||
('{{TITLE}}', html.escape(title)),
|
||||
('{{SUBTITLE}}', html.escape(subtitle)),
|
||||
('{{BASE}}', base_css),
|
||||
('{{BASE_JS}}', base_js),
|
||||
('{{STYLE}}', css),
|
||||
('{{CONTENT}}', content),
|
||||
('{{SOURCE}}', source_html),
|
||||
('{{GENERATED}}', generated),
|
||||
('{{LAYOUT}}', html.escape(layout)),
|
||||
('{{STYLE_NAME}}', html.escape(style)),
|
||||
):
|
||||
page = page.replace(key, value)
|
||||
|
||||
with open(out_file, 'w', encoding='utf-8') as f:
|
||||
f.write(page)
|
||||
PY
|
||||
|
||||
printf '%s\n' "$out"
|
||||
Executable
+227
@@ -0,0 +1,227 @@
|
||||
#!/usr/bin/env sh
|
||||
# html-style.sh — 「哪一種 wiki 頁或議題,用哪一種版型與風格出 HTML」的設定(供 jsc-gitea:html-style、html-export 使用)。
|
||||
#
|
||||
# 設定格式(一行一筆):{種類}={版型},{風格}
|
||||
# 種類:WIKI:{頁名前綴}(例 WIKI:PLAN)、ISSUE:{標籤名}(例 ISSUE:bug)、DEFAULT(都對不到時用)
|
||||
# 「#」開頭為註解,空白行忽略。同一種類出現多行時取最後一行。
|
||||
# 解析順序:
|
||||
# 1. 目前工作目錄的 ./.jsc/html-styles(專案覆寫)
|
||||
# 2. $JSC_HOME/html-styles.conf(JSC_HOME 預設 ~/.jsc)
|
||||
# 3. 種類對不到就退 DEFAULT,DEFAULT 也沒有才用內建預設 report,minimal
|
||||
#
|
||||
# 用法:
|
||||
# html-style.sh get <種類> 印出 版型<TAB>風格<TAB>來源(project/global/default/builtin)
|
||||
# html-style.sh set <種類> <版型> <風格> [--project|--global] 寫入設定(預設 --global)
|
||||
# html-style.sh unset <種類> [--project|--global] 移除設定
|
||||
# html-style.sh list 印出合併後的所有設定:種類<TAB>版型<TAB>風格<TAB>來源
|
||||
# html-style.sh layouts 列出可用版型:名稱<TAB>繁中說明
|
||||
# html-style.sh styles 列出可用風格:名稱<TAB>繁中說明
|
||||
# html-style.sh key wiki <頁名> 推導 wiki 頁的種類:取第一個底線前的字首
|
||||
# html-style.sh key issue <owner>/<repo> <編號> [--labels <名稱>[,<名稱>]]
|
||||
# 推導議題的種類:依序試每個標籤,第一個讓 get 回 project 或 global 的標籤勝出;
|
||||
# 都對不到就回 ISSUE:DEFAULT。已經有標籤名單時用 --labels 帶進來,省掉一次 API 呼叫。
|
||||
# key 的輸出固定一行:{種類}<TAB>{依據}
|
||||
# 依據 = prefix:{字首}、label:{標籤名}、fallback:no-labels、fallback:no-configured-label
|
||||
#
|
||||
# 結束碼: 0=成功 1=取不到議題標籤,或設定檔的目錄建不出來 2=用法錯誤 4=版型或風格沒有對應範本檔
|
||||
#
|
||||
# 陷阱:
|
||||
# - get 永遠印得出一組值:對不到就退 DEFAULT,再對不到就退內建預設。呼叫端不必自己準備退路,
|
||||
# 但要看第三欄,才知道這組值是使用者設的還是撿來的。
|
||||
# - key 的推導規則放在腳本裡,不放技能內文:字首怎麼切、標籤怎麼試,是固定的輸入輸出,
|
||||
# 每次由模型重推就會有人切錯底線或跳過標籤順序。
|
||||
# - set 會先確認範本檔真的存在,擋掉打錯字的版型或風格;設定寫得進去、出圖卻失敗最難查。
|
||||
set -u
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
plugin_root="${CLAUDE_PLUGIN_ROOT:-$script_dir/..}"
|
||||
TPL="$plugin_root/templates/html"
|
||||
JSC_HOME="${JSC_HOME:-$HOME/.jsc}"
|
||||
PROJECT_FILE=./.jsc/html-styles
|
||||
GLOBAL_FILE="$JSC_HOME/html-styles.conf"
|
||||
BUILTIN_LAYOUT=report
|
||||
BUILTIN_STYLE=minimal
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
用法:
|
||||
html-style.sh get <種類>
|
||||
html-style.sh set <種類> <版型> <風格> [--project|--global]
|
||||
html-style.sh unset <種類> [--project|--global]
|
||||
html-style.sh list | layouts | styles
|
||||
html-style.sh key wiki <頁名>
|
||||
html-style.sh key issue <owner>/<repo> <編號> [--labels <名稱>[,<名稱>]]
|
||||
種類: WIKI:{頁名前綴}、ISSUE:{標籤名}、DEFAULT
|
||||
結束碼: 0=成功 1=取不到議題標籤或建不出目錄 2=用法錯誤 4=版型或風格沒有對應範本檔
|
||||
EOF
|
||||
exit 2
|
||||
}
|
||||
|
||||
read_value() { # $1=設定檔 $2=種類 -> 「版型,風格」
|
||||
[ -f "$1" ] || return 0
|
||||
awk -F= -v want="$2" '
|
||||
{ sub(/\r$/, "") }
|
||||
/^[ \t]*#/ { next }
|
||||
/^[ \t]*$/ { next }
|
||||
index($0, "=") == 0 { next }
|
||||
{
|
||||
key = $1
|
||||
gsub(/[ \t]/, "", key)
|
||||
if (key != want) next
|
||||
val = substr($0, index($0, "=") + 1)
|
||||
gsub(/[ \t]/, "", val)
|
||||
if (val != "") v = val
|
||||
}
|
||||
END { if (v != "") print v }
|
||||
' "$1"
|
||||
}
|
||||
|
||||
resolve() { # $1=種類 -> 版型<TAB>風格<TAB>來源
|
||||
for _f in "$PROJECT_FILE:project" "$GLOBAL_FILE:global"; do
|
||||
_file=${_f%:*}; _src=${_f##*:}
|
||||
_v=$(read_value "$_file" "$1")
|
||||
if [ -n "$_v" ]; then
|
||||
printf '%s\t%s\t%s\n' "${_v%%,*}" "${_v##*,}" "$_src"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
if [ "$1" != DEFAULT ]; then
|
||||
_d=$(resolve DEFAULT)
|
||||
_src=$(printf '%s' "$_d" | cut -f3)
|
||||
# DEFAULT 自己也沒設定時,來源照實說是 builtin,不要蓋成 default
|
||||
[ "$_src" = builtin ] || _src=default
|
||||
printf '%s\t%s\n' "$(printf '%s' "$_d" | cut -f1,2)" "$_src"
|
||||
return 0
|
||||
fi
|
||||
printf '%s\t%s\tbuiltin\n' "$BUILTIN_LAYOUT" "$BUILTIN_STYLE"
|
||||
}
|
||||
|
||||
# 範本檔第一行註解就是繁中說明:版型放在 <!-- 說明 -->,風格放在 /* 說明 */。
|
||||
describe() { # $1=檔案
|
||||
head -n1 "$1" 2>/dev/null | sed 's/<!--[[:space:]]*//; s/[[:space:]]*-->//; s|/\*[[:space:]]*||; s|[[:space:]]*\*/||'
|
||||
}
|
||||
|
||||
list_layouts() {
|
||||
for f in "$TPL"/layout/*.html; do
|
||||
[ -f "$f" ] || continue
|
||||
name=$(basename "$f" .html)
|
||||
printf '%s\t%s\n' "$name" "$(describe "$f")"
|
||||
done
|
||||
}
|
||||
|
||||
list_styles() {
|
||||
for f in "$TPL"/style/*.css; do
|
||||
[ -f "$f" ] || continue
|
||||
name=$(basename "$f" .css)
|
||||
printf '%s\t%s\n' "$name" "$(describe "$f")"
|
||||
done
|
||||
}
|
||||
|
||||
write_kv() { # $1=檔案 $2=種類 $3=值
|
||||
dir=$(dirname "$1")
|
||||
mkdir -p "$dir" || { echo "[jsc][HTML 設定][ERR]:建不出目錄「$dir」。" >&2; exit 1; }
|
||||
tmp="$1.tmp.$$"
|
||||
{ [ -f "$1" ] && grep -v "^[[:space:]]*$2[[:space:]]*=" "$1" || true; } > "$tmp"
|
||||
[ -n "$3" ] && printf '%s=%s\n' "$2" "$3" >> "$tmp"
|
||||
mv "$tmp" "$1"
|
||||
}
|
||||
|
||||
cmd="${1:-}"; [ -n "$cmd" ] || usage
|
||||
shift || true
|
||||
|
||||
case "$cmd" in
|
||||
layouts) list_layouts; exit 0 ;;
|
||||
styles) list_styles; exit 0 ;;
|
||||
list)
|
||||
keys=$( { [ -f "$PROJECT_FILE" ] && cut -d= -f1 "$PROJECT_FILE" || true
|
||||
[ -f "$GLOBAL_FILE" ] && cut -d= -f1 "$GLOBAL_FILE" || true; } \
|
||||
| sed 's/^[[:space:]]*//; s/[[:space:]]*$//' | grep -v '^#' | grep -v '^$' | sort -u)
|
||||
[ -n "$keys" ] || { printf 'DEFAULT\t%s\t%s\tbuiltin\n' "$BUILTIN_LAYOUT" "$BUILTIN_STYLE"; exit 0; }
|
||||
printf '%s\n' "$keys" | while IFS= read -r k; do
|
||||
printf '%s\t%s\n' "$k" "$(resolve "$k")"
|
||||
done
|
||||
exit 0 ;;
|
||||
get)
|
||||
key="${1:-}"; [ -n "$key" ] || usage
|
||||
resolve "$key"
|
||||
exit 0 ;;
|
||||
key)
|
||||
kind="${1:-}"; [ -n "$kind" ] || usage
|
||||
shift
|
||||
case "$kind" in
|
||||
wiki)
|
||||
page="${1:-}"; [ -n "$page" ] || usage
|
||||
# 頁名前綴 = 第一個底線之前那一段;沒有底線就是整個頁名。
|
||||
prefix=${page%%_*}
|
||||
printf 'WIKI:%s\tprefix:%s\n' "$prefix" "$prefix"
|
||||
exit 0 ;;
|
||||
issue)
|
||||
repo="${1:-}"; idx="${2:-}"
|
||||
[ -n "$repo" ] && [ -n "$idx" ] || usage
|
||||
shift 2
|
||||
labels=''
|
||||
have_labels=0
|
||||
case "${1:-}" in
|
||||
--labels) [ "$#" -ge 2 ] || usage; labels="$2"; have_labels=1 ;;
|
||||
'') ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
if [ "$have_labels" = 0 ]; then
|
||||
raw=$("$script_dir/issue.sh" labels-of "$repo" "$idx") || {
|
||||
echo "[jsc][HTML 設定][ERR]:取不到議題 $repo#$idx 的標籤。" >&2; exit 1; }
|
||||
labels=$(printf '%s' "$raw" | tr '\n' ',')
|
||||
fi
|
||||
matched=''
|
||||
old_ifs=$IFS
|
||||
IFS=','
|
||||
for l in $labels; do
|
||||
[ -n "$l" ] || continue
|
||||
# 標籤順序就是優先序:第一個使用者真的設過的標籤勝出,
|
||||
# 撿來的 DEFAULT 不算數,否則第一個標籤永遠贏,後面的設定形同虛設。
|
||||
case "$(resolve "ISSUE:$l" | cut -f3)" in
|
||||
project|global) matched="$l"; break ;;
|
||||
esac
|
||||
done
|
||||
IFS=$old_ifs
|
||||
if [ -n "$matched" ]; then
|
||||
printf 'ISSUE:%s\tlabel:%s\n' "$matched" "$matched"
|
||||
elif [ -z "$(printf '%s' "$labels" | tr -d ',')" ]; then
|
||||
printf 'ISSUE:DEFAULT\tfallback:no-labels\n'
|
||||
else
|
||||
printf 'ISSUE:DEFAULT\tfallback:no-configured-label\n'
|
||||
fi
|
||||
exit 0 ;;
|
||||
*) usage ;;
|
||||
esac ;;
|
||||
set)
|
||||
key="${1:-}"; layout="${2:-}"; style="${3:-}"
|
||||
[ -n "$key" ] && [ -n "$layout" ] && [ -n "$style" ] || usage
|
||||
shift 3
|
||||
target="$GLOBAL_FILE"; scope=global
|
||||
case "${1:-}" in
|
||||
--project) target="$PROJECT_FILE"; scope=project ;;
|
||||
--global|'') ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
[ -f "$TPL/layout/$layout.html" ] || {
|
||||
echo "[jsc][HTML 設定][ERR]:沒有版型「$layout」。可用:$(list_layouts | cut -f1 | tr '\n' ' ')" >&2; exit 4; }
|
||||
[ -f "$TPL/style/$style.css" ] || {
|
||||
echo "[jsc][HTML 設定][ERR]:沒有風格「$style」。可用:$(list_styles | cut -f1 | tr '\n' ' ')" >&2; exit 4; }
|
||||
write_kv "$target" "$key" "$layout,$style"
|
||||
printf '已寫入 %s:%s=%s,%s(%s)\n' "$target" "$key" "$layout" "$style" "$scope"
|
||||
exit 0 ;;
|
||||
unset)
|
||||
key="${1:-}"; [ -n "$key" ] || usage
|
||||
shift
|
||||
target="$GLOBAL_FILE"
|
||||
case "${1:-}" in
|
||||
--project) target="$PROJECT_FILE" ;;
|
||||
--global|'') ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
[ -f "$target" ] || { echo "找不到設定檔:$target" >&2; exit 0; }
|
||||
write_kv "$target" "$key" ''
|
||||
printf '已移除 %s 的 %s\n' "$target" "$key"
|
||||
exit 0 ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
Executable
+219
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env sh
|
||||
# issue.sh — Gitea 議題的讀取與建立(供 jsc-gitea:wiki-to-issue、html-export 使用)。
|
||||
#
|
||||
# 為什麼要有這支腳本:建議題要組 JSON、要把標籤名換成標籤 id、要把回應裡的編號與網址挑出來,
|
||||
# 全是固定的輸入輸出。交給模型每次自己拼 curl,跳脫字元一錯就把議題內容寫壞,而議題是對外的。
|
||||
#
|
||||
# 用法:
|
||||
# issue.sh labels <owner>/<repo> 列出既有標籤:id<TAB>name
|
||||
# issue.sh label-ids <owner>/<repo> <name>[,<name>] 標籤名換成 id(逗號分隔);有名字對不到就 exit 4
|
||||
# issue.sh projects <owner>/<repo> 列出專案看板:id<TAB>title;站台沒有這個 API 就 exit 3
|
||||
# issue.sh title <owner>/<repo> <index> 印出議題標題
|
||||
# issue.sh body <owner>/<repo> <index> 印出議題正文(markdown)
|
||||
# issue.sh labels-of <owner>/<repo> <index> 印出議題目前的標籤名,一行一個
|
||||
# issue.sh show <owner>/<repo> <index> 一次印出標題、標籤與正文(只打一次 API)
|
||||
# 輸出格式固定四段,正文放最後,因為只有它會多行:
|
||||
# 第 1 行 title<TAB>{標題}
|
||||
# 第 2 行 labels<TAB>{標籤名,逗號分隔;沒有標籤時這一欄留空}
|
||||
# 第 3 行 body (單獨一個字,當正文的起始標記)
|
||||
# 第 4 行起 正文原文,一直到檔尾
|
||||
# 呼叫端取標題用 head -n1 | cut -f2-,取標籤用 sed -n 2p | cut -f2-,取正文用 tail -n +4。
|
||||
# 同一筆議題要標題、正文又要標籤時一律用 show,不要連打 title、body、labels-of 三次。
|
||||
# issue.sh create <owner>/<repo> <title> <body-file> [--labels <id>[,<id>]] [--milestone <id>]
|
||||
# 建立議題。輸出兩行: index=<編號>、url=<議題網址>
|
||||
#
|
||||
# 環境變數: 同 gitea.sh(GITEA_HOST、GITEA_TOKEN;token 缺或被拒時退回 tea 登入金鑰)。
|
||||
# 結束碼: 0=成功 1=API 失敗 2=用法錯誤 3=站台不支援該 API 4=名稱對不到 id
|
||||
#
|
||||
# 陷阱:
|
||||
# - 正文一律用檔案傳進來,內容維持 UTF-8 與真實換行;不要在參數裡塞 \n。
|
||||
# - Gitea 1.27 沒有公開的專案看板 API,projects 會 exit 3。呼叫端要據實回報「請手動把議題拖到看板」,
|
||||
# 不要假裝已經關聯好——關聯不上跟關聯成功看起來一樣,是最容易被當成完成的一種失敗。
|
||||
# exit 3 的兩條路徑(端點回錯、端點回了但不是清單)都會印出看板網址,呼叫端一定拿得到那條連結。
|
||||
set -eu
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
GITEA="$script_dir/gitea.sh"
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
用法:
|
||||
issue.sh labels <owner>/<repo> 列出既有標籤:id<TAB>name
|
||||
issue.sh label-ids <owner>/<repo> <name>[,<name>] 標籤名換成 id
|
||||
issue.sh projects <owner>/<repo> 列出專案看板;站台不支援就 exit 3
|
||||
issue.sh title|body|labels-of|show <owner>/<repo> <index>
|
||||
issue.sh create <owner>/<repo> <title> <body-file> [--labels <id>[,<id>]] [--milestone <id>]
|
||||
結束碼: 0=成功 1=API 失敗 2=用法錯誤 3=站台不支援 4=名稱對不到 id
|
||||
EOF
|
||||
exit 2
|
||||
}
|
||||
|
||||
[ -f "$GITEA" ] || { echo "[jsc][議題][ERR]:找不到 $GITEA。" >&2; exit 1; }
|
||||
|
||||
valid_repo() {
|
||||
case "${1:-}" in
|
||||
*/*/*|/*|*/) return 1 ;;
|
||||
*/*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
board_url() { # 看板頁的人工網址。站台沒有看板 API 時,呼叫端要靠這條連結手動處理
|
||||
_host="${GITEA_HOST:-}"
|
||||
case "$_host" in http://*|https://*) : ;; *) _host="https://$_host" ;; esac
|
||||
printf '%s/%s/projects\n' "${_host%/}" "$repo"
|
||||
}
|
||||
|
||||
cmd="${1:-}"; [ -n "$cmd" ] || usage
|
||||
shift || true
|
||||
repo="${1:-}"
|
||||
valid_repo "$repo" || { echo "[jsc][議題][ERR]:存取庫須為 {owner}/{repo},收到「${repo:-空值}」。" >&2; exit 2; }
|
||||
shift
|
||||
|
||||
# gitea.sh 的輸出一律先接進變數再解析,不要寫成 "$GITEA" api ... | python3。
|
||||
# 管線的結束狀態取自 python,會把 gitea.sh 的失敗整個吃掉:金鑰失效時,回應是一份
|
||||
# 錯誤 JSON,python 從裡面挑不到 labels 就印出空清單並回 0,看起來像「這個議題沒有標籤」。
|
||||
api_get() { # $1=API 路徑 -> 回應內容;失敗回 1
|
||||
"$GITEA" api GET "$1"
|
||||
}
|
||||
|
||||
case "$cmd" in
|
||||
labels)
|
||||
out=$(api_get "/repos/$repo/labels?limit=100") \
|
||||
|| { echo "[jsc][議題][ERR]:讀不到 $repo 的標籤清單。" >&2; exit 1; }
|
||||
printf '%s' "$out" | python3 -c '
|
||||
import json,sys
|
||||
for l in json.load(sys.stdin):
|
||||
print("%s\t%s" % (l["id"], l["name"]))
|
||||
' ;;
|
||||
|
||||
label-ids)
|
||||
names="${1:-}"; [ -n "$names" ] || usage
|
||||
out=$(api_get "/repos/$repo/labels?limit=100") \
|
||||
|| { echo "[jsc][議題][ERR]:讀不到 $repo 的標籤清單。" >&2; exit 1; }
|
||||
printf '%s' "$out" | python3 -c '
|
||||
import json,sys
|
||||
want=[n.strip() for n in sys.argv[1].split(",") if n.strip()]
|
||||
have={l["name"]: l["id"] for l in json.load(sys.stdin)}
|
||||
miss=[n for n in want if n not in have]
|
||||
if miss:
|
||||
sys.stderr.write("[jsc][議題][ERR]:這些標籤在存取庫裡找不到:" + "、".join(miss) + "\n")
|
||||
sys.exit(4)
|
||||
print(",".join(str(have[n]) for n in want))
|
||||
' "$names" ;;
|
||||
|
||||
projects)
|
||||
manual=0
|
||||
if out=$(api_get "/repos/$repo/projects" 2>/dev/null); then
|
||||
printf '%s' "$out" | python3 -c '
|
||||
import json,sys
|
||||
try:
|
||||
d=json.load(sys.stdin)
|
||||
except Exception:
|
||||
sys.exit(3)
|
||||
if not isinstance(d, list):
|
||||
sys.exit(3)
|
||||
for p in d:
|
||||
print("%s\t%s" % (p.get("id",""), p.get("title") or p.get("name","")))
|
||||
' || manual=1
|
||||
else
|
||||
manual=1
|
||||
fi
|
||||
if [ "$manual" = 1 ]; then
|
||||
# 兩條 exit 3 的路徑都要印出看板網址:呼叫端的技能被要求把這條連結交給使用者,
|
||||
# 少印一次,使用者就只收到「關聯不上」而不知道要去哪裡手動拖。
|
||||
echo "[jsc][議題][WARN]:這個 Gitea 站台沒有專案看板 API,程式關聯不了。請開 $(board_url) 手動把議題拖進看板。" >&2
|
||||
exit 3
|
||||
fi ;;
|
||||
|
||||
title)
|
||||
idx="${1:-}"; [ -n "$idx" ] || usage
|
||||
out=$(api_get "/repos/$repo/issues/$idx") \
|
||||
|| { echo "[jsc][議題][ERR]:讀不到議題 $repo#$idx。" >&2; exit 1; }
|
||||
printf '%s' "$out" | python3 -c '
|
||||
import json,sys
|
||||
print(json.load(sys.stdin).get("title",""))
|
||||
' ;;
|
||||
|
||||
body)
|
||||
idx="${1:-}"; [ -n "$idx" ] || usage
|
||||
out=$(api_get "/repos/$repo/issues/$idx") \
|
||||
|| { echo "[jsc][議題][ERR]:讀不到議題 $repo#$idx。" >&2; exit 1; }
|
||||
printf '%s' "$out" | python3 -c '
|
||||
import json,sys
|
||||
sys.stdout.write(json.load(sys.stdin).get("body","") or "")
|
||||
' ;;
|
||||
|
||||
labels-of)
|
||||
idx="${1:-}"; [ -n "$idx" ] || usage
|
||||
out=$(api_get "/repos/$repo/issues/$idx") \
|
||||
|| { echo "[jsc][議題][ERR]:讀不到議題 $repo#$idx 的標籤。" >&2; exit 1; }
|
||||
printf '%s' "$out" | python3 -c '
|
||||
import json,sys
|
||||
for l in json.load(sys.stdin).get("labels") or []:
|
||||
print(l.get("name",""))
|
||||
' ;;
|
||||
|
||||
show)
|
||||
# 標題、標籤、正文都在同一份 API 回應裡。分成 title、body、labels-of 三次呼叫,
|
||||
# 打的是同一支端點三遍,還可能取到三個不同時間點的版本。
|
||||
idx="${1:-}"; [ -n "$idx" ] || usage
|
||||
out=$(api_get "/repos/$repo/issues/$idx") \
|
||||
|| { echo "[jsc][議題][ERR]:讀不到議題 $repo#$idx。" >&2; exit 1; }
|
||||
printf '%s' "$out" | python3 -c '
|
||||
import json,sys
|
||||
try:
|
||||
d=json.load(sys.stdin)
|
||||
except Exception:
|
||||
sys.exit(1)
|
||||
if not isinstance(d, dict) or not d.get("title"):
|
||||
sys.exit(1)
|
||||
print("title\t%s" % d.get("title"))
|
||||
print("labels\t%s" % ",".join((l.get("name") or "") for l in (d.get("labels") or [])))
|
||||
print("body")
|
||||
sys.stdout.write(d.get("body") or "")
|
||||
' || { echo "[jsc][議題][ERR]:議題 $repo#$idx 的回應解不出標題。" >&2; exit 1; } ;;
|
||||
|
||||
create)
|
||||
title="${1:-}"; body_file="${2:-}"
|
||||
[ -n "$title" ] || usage
|
||||
[ -n "$body_file" ] || usage
|
||||
[ -f "$body_file" ] || { echo "[jsc][議題][ERR]:找不到正文檔「$body_file」。" >&2; exit 2; }
|
||||
shift 2
|
||||
labels=''
|
||||
milestone=''
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--labels) [ "$#" -ge 2 ] || usage; labels="$2"; shift 2 ;;
|
||||
--milestone) [ "$#" -ge 2 ] || usage; milestone="$2"; shift 2 ;;
|
||||
*) echo "[jsc][議題][ERR]:不認得的選項「$1」。" >&2; usage ;;
|
||||
esac
|
||||
done
|
||||
confirm_target="議題「$title」"
|
||||
[ -n "$labels" ] && confirm_target="$confirm_target,標籤:$labels"
|
||||
[ -n "$milestone" ] && confirm_target="$confirm_target,里程碑:$milestone"
|
||||
sh "$script_dir/write-confirm.sh" "建立議題" "$confirm_target"
|
||||
payload=$(mktemp)
|
||||
trap 'rm -f "$payload"' EXIT
|
||||
python3 -c '
|
||||
import json,sys
|
||||
title, body_file, labels, milestone, out = sys.argv[1:6]
|
||||
d={"title": title, "body": open(body_file, encoding="utf-8").read()}
|
||||
if labels:
|
||||
d["labels"]=[int(x) for x in labels.split(",") if x.strip()]
|
||||
if milestone:
|
||||
d["milestone"]=int(milestone)
|
||||
with open(out, "w", encoding="utf-8") as f:
|
||||
json.dump(d, f, ensure_ascii=False)
|
||||
' "$title" "$body_file" "$labels" "$milestone" "$payload"
|
||||
out=$("$GITEA" api POST "/repos/$repo/issues" "$payload") \
|
||||
|| { echo "[jsc][議題][ERR]:建不出議題到 $repo。" >&2; exit 1; }
|
||||
printf '%s' "$out" | python3 -c '
|
||||
import json,sys
|
||||
d=json.load(sys.stdin)
|
||||
print("index=%s" % (d.get("number") or d.get("index") or ""))
|
||||
print("url=%s" % (d.get("html_url") or ""))
|
||||
' ;;
|
||||
|
||||
*) usage ;;
|
||||
esac
|
||||
Executable
+134
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env sh
|
||||
# pr-watch.sh — 盯著一支 PR,直到它合併或關閉。
|
||||
# 用法: pr-watch.sh <owner>/<repo> <pr-index> [state-file]
|
||||
# 規則: 資料一律經同目錄的 gitea.sh(pr-status、pr-comments),不自行拼 API。
|
||||
# 輪詢間隔預設 60 秒,JSC_PR_WATCH_INTERVAL 可覆寫,單位是秒的正整數。
|
||||
# 不自動逾時退場,盯到 PR 合併或關閉為止。
|
||||
# 已回報過的留言不重複回報:最後處理過的留言時間戳寫進狀態檔,
|
||||
# 預設 $JSC_HOME/pr-watch/{owner}-{repo}-{index}.seen,JSC_HOME 未設定時用 ~/.jsc。
|
||||
# 狀態檔還不存在時,把既有留言整批當成新留言回報一次。寧可重複回報,
|
||||
# 也不要漏掉開始盯之前就留下的審查意見。
|
||||
# 每輪先看 PR 狀態,再看留言。PR 收尾那一輪若還有沒回報的留言,
|
||||
# 照樣印出來再退出,退出碼仍是 0:留言不會因為 PR 剛好合併就消失。
|
||||
# 輸出: 全部繁中,留言行維持 gitea.sh pr-comments 的原格式
|
||||
# 「{時間}<TAB>{作者}<TAB>{類型}<TAB>{內容}」,呼叫端可直接用 cut -f 取欄位。
|
||||
# 結束碼: 0=PR 已合併或關閉,最終狀態印在 stdout。盯完了,不用再盯一次。
|
||||
# 2=用法或環境問題:參數個數不對、第一個參數不是 {owner}/{repo}、PR 編號不是數字、
|
||||
# JSC_PR_WATCH_INTERVAL 不是正整數秒、建不出暫存檔,或第一輪就連不上 Gitea。
|
||||
# 照 stderr 的訊息修參數或 GITEA_HOST、GITEA_TOKEN,再重新盯。
|
||||
# 3=查不到該 PR。確認存取庫與 PR 編號再重新盯,不要原樣重試——
|
||||
# 編號打錯會變成永遠輪詢一支不存在的 PR。
|
||||
# 10=有新留言,內容印在 stdout。接手跑決策樹處理完,再重新盯同一支 PR。
|
||||
# 130=收到 INT 或 TERM 訊號而中止。狀態檔留著,重新盯會從上次那則留言接下去。
|
||||
# 護欄: gitea.sh pr-status 對不存在的 PR 會印「? none none」並且 exit 0。
|
||||
# 所以 state 只認 open 與 closed、merged 只認 true 與 false,對不上就回 3。
|
||||
# 少了這道白名單,PR 編號打錯會變成永遠輪詢一支不存在的 PR。
|
||||
# 第一輪查詢失敗算環境沒接好,直接回 2;之後才失敗算連線抖動,警告一句繼續輪詢。
|
||||
set -u
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
GITEA="$script_dir/gitea.sh"
|
||||
|
||||
usage() {
|
||||
echo "用法: pr-watch.sh <owner>/<repo> <pr-index> [state-file]" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
[ "$#" -ge 2 ] && [ "$#" -le 3 ] || usage
|
||||
REPO="$1"; INDEX="$2"; STATE="${3:-}"
|
||||
|
||||
case "$REPO" in
|
||||
*/*) ;;
|
||||
*) echo "錯誤: 第一個參數要寫成 {owner}/{repo},收到「$REPO」。" >&2; exit 2 ;;
|
||||
esac
|
||||
case "$INDEX" in
|
||||
''|*[!0-9]*) echo "錯誤: PR 編號要是數字,收到「$INDEX」。" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
INTERVAL="${JSC_PR_WATCH_INTERVAL:-60}"
|
||||
case "$INTERVAL" in
|
||||
''|*[!0-9]*) echo "錯誤: JSC_PR_WATCH_INTERVAL 要是正整數秒,收到「$INTERVAL」。" >&2; exit 2 ;;
|
||||
esac
|
||||
[ "$INTERVAL" -gt 0 ] || { echo "錯誤: JSC_PR_WATCH_INTERVAL 要大於 0。" >&2; exit 2; }
|
||||
|
||||
if [ -z "$STATE" ]; then
|
||||
jsc_home="${JSC_HOME:-$HOME/.jsc}"
|
||||
owner=${REPO%%/*}; name=${REPO#*/}
|
||||
STATE="$jsc_home/pr-watch/$owner-$name-$INDEX.seen"
|
||||
fi
|
||||
|
||||
TMP_NEW=$(mktemp) || { echo "錯誤: 無法建立暫存檔。" >&2; exit 2; }
|
||||
trap 'rm -f "$TMP_NEW"' EXIT
|
||||
trap 'rm -f "$TMP_NEW"; exit 130' INT TERM
|
||||
|
||||
SEEN=""
|
||||
[ -f "$STATE" ] && SEEN=$(head -n1 "$STATE" 2>/dev/null)
|
||||
|
||||
save_seen() { # 時間戳 -> 寫回狀態檔
|
||||
ts="$1"
|
||||
[ -n "$ts" ] || return 0
|
||||
dir=$(dirname -- "$STATE")
|
||||
mkdir -p "$dir" 2>/dev/null || { echo "警告: 建不出狀態檔目錄 $dir,這批留言下次會重複回報。" >&2; return 0; }
|
||||
printf '%s\n' "$ts" > "$STATE" 2>/dev/null \
|
||||
|| echo "警告: 寫不進狀態檔 $STATE,這批留言下次會重複回報。" >&2
|
||||
SEEN="$ts"
|
||||
}
|
||||
|
||||
collect_new() { # 新留言寫進 $TMP_NEW;有東西才回 0
|
||||
# 時間戳一律由同一台 Gitea 產生,格式與時區都一致,所以字串比大小就夠用;
|
||||
# pr-comments 自己也是照這個字串排序的。
|
||||
"$GITEA" pr-comments "$REPO" "$INDEX" 2>/dev/null \
|
||||
| awk -F '\t' -v seen="$SEEN" 'NF >= 4 && ($1 "") > (seen "")' > "$TMP_NEW"
|
||||
[ -s "$TMP_NEW" ]
|
||||
}
|
||||
|
||||
flush_new() { # 印出新留言並記住最後一筆的時間戳
|
||||
cat "$TMP_NEW"
|
||||
save_seen "$(awk -F '\t' 'END { print $1 }' "$TMP_NEW")"
|
||||
}
|
||||
|
||||
first_round=1
|
||||
while :; do
|
||||
if ! status=$("$GITEA" pr-status "$REPO" "$INDEX" 2>/dev/null); then
|
||||
if [ "$first_round" -eq 1 ]; then
|
||||
echo "錯誤: 連不上 Gitea 或 gitea.sh 執行失敗。請確認 GITEA_HOST 與 GITEA_TOKEN,再重新盯。" >&2
|
||||
exit 2
|
||||
fi
|
||||
echo "警告: 這一輪查不到 PR 狀態,$INTERVAL 秒後再試。" >&2
|
||||
sleep "$INTERVAL"
|
||||
continue
|
||||
fi
|
||||
first_round=0
|
||||
|
||||
state=$(printf '%s\n' "$status" | awk '{ print $1 }')
|
||||
merged=$(printf '%s\n' "$status" | awk '{ print $2 }')
|
||||
case "$state" in
|
||||
open|closed) ;;
|
||||
*) echo "錯誤: 查不到 PR $REPO#$INDEX。請確認存取庫與 PR 編號。" >&2; exit 3 ;;
|
||||
esac
|
||||
case "$merged" in
|
||||
true|false) ;;
|
||||
*) echo "錯誤: 查不到 PR $REPO#$INDEX。請確認存取庫與 PR 編號。" >&2; exit 3 ;;
|
||||
esac
|
||||
|
||||
if [ "$state" = closed ]; then
|
||||
if [ "$merged" = true ]; then
|
||||
printf '最終狀態: PR %s#%s 已合併。\n' "$REPO" "$INDEX"
|
||||
else
|
||||
printf '最終狀態: PR %s#%s 已關閉,沒有合併。\n' "$REPO" "$INDEX"
|
||||
fi
|
||||
if collect_new; then
|
||||
echo "收尾前還有沒回報的留言:"
|
||||
flush_new
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if collect_new; then
|
||||
printf '新留言 %s 則(%s#%s),請接手跑決策樹:\n' "$(wc -l < "$TMP_NEW" | tr -d ' ')" "$REPO" "$INDEX"
|
||||
flush_new
|
||||
exit 10
|
||||
fi
|
||||
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
Executable
+83
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env sh
|
||||
# repo-sync.sh — 把單一存取庫同步到本機。
|
||||
# 用法: repo-sync.sh <owner>/<repo> [target-dir] # target-dir 預設為 <repo>
|
||||
# 目錄不存在 → git clone(clone URL 取自 gitea.sh clone-url)
|
||||
# 目錄已存在 → git fetch、切到基準分支、git pull --ff-only
|
||||
# 有未提交變更 → 只回報 dirty 與基準分支,不動分支也不 pull
|
||||
# 基準分支的唯一優先序(呼叫端不必再判斷,也不要自己再推導一次):
|
||||
# 1. gitea.sh default-branch <owner>/<repo>(該分支要在遠端存在)
|
||||
# 2. 查不到時依序取遠端的 develop、main、master
|
||||
# 3. 都沒有 → failed no default branch
|
||||
# 輸出: 恰好一行,cloned、updated、dirty {分支} 或 failed {原因}。
|
||||
# 前三種 exit 0;failed exit 1。
|
||||
# dirty 會把解析好的基準分支一起帶出來,呼叫端直接拿去當 PR 的 base。
|
||||
# 結束碼: 0=同步完成,輸出是 cloned、updated 或 dirty {分支}
|
||||
# 1=失敗,輸出是 failed {原因};參數錯誤、clone-url 取不到、fetch/checkout/pull 失敗、
|
||||
# 以及找不到基準分支都走這一碼。只有 0 與 1 兩種。
|
||||
set -u
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
gitea="$script_dir/gitea.sh"
|
||||
|
||||
fail() { # 原因壓成一行,避免呼叫端解析多行輸出
|
||||
printf 'failed %s\n' "$(printf '%s' "$1" | tr '\n\r\t' ' ')"
|
||||
exit 1
|
||||
}
|
||||
|
||||
full="${1:-}"
|
||||
[ -n "$full" ] || fail "usage: repo-sync.sh <owner>/<repo> [target-dir]"
|
||||
case "$full" in
|
||||
*/*/*|/*|*/) fail "not owner/repo: $full" ;;
|
||||
*/*) ;;
|
||||
*) fail "not owner/repo: $full" ;;
|
||||
esac
|
||||
repo="${full#*/}"
|
||||
target="${2:-$repo}"
|
||||
|
||||
remote_has() { # 遠端是否有這個分支
|
||||
git -C "$target" rev-parse --verify --quiet "refs/remotes/origin/$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
if [ ! -e "$target" ]; then
|
||||
# clone URL 只收 stdout:把 stderr 併進來會讓任何雜訊變成網址的一部分
|
||||
err=$(mktemp)
|
||||
url=$("$gitea" clone-url "$full" 2>"$err") || url=''
|
||||
if [ -z "$url" ]; then
|
||||
reason=$(cat "$err"); rm -f "$err"
|
||||
fail "clone-url failed for $full: ${reason:-no clone URL}"
|
||||
fi
|
||||
rm -f "$err"
|
||||
out=$(git clone "$url" "$target" 2>&1) || fail "clone: $out"
|
||||
printf 'cloned\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git -C "$target" rev-parse --git-dir >/dev/null 2>&1 || fail "$target is not a git repo"
|
||||
|
||||
dirty=0
|
||||
[ -z "$(git -C "$target" status --porcelain 2>/dev/null)" ] || dirty=1
|
||||
|
||||
# dirty 的存取庫也要解析基準分支:呼叫端拿它當 PR 的 base,自己再推導一次就會
|
||||
# 出現第二套優先序。fetch 只更新 refs,不動工作目錄,dirty 時照樣安全;但 dirty
|
||||
# 時網路失敗不算致命,還是要把 dirty 回報出去,改用本機既有的 remote refs 判斷。
|
||||
if ! out=$(git -C "$target" fetch --prune origin 2>&1); then
|
||||
[ "$dirty" -eq 1 ] || fail "fetch: $out"
|
||||
fi
|
||||
|
||||
branch=$("$gitea" default-branch "$full" 2>/dev/null) || branch=''
|
||||
if [ -z "$branch" ] || ! remote_has "$branch"; then
|
||||
branch=''
|
||||
for b in develop main master; do
|
||||
if remote_has "$b"; then branch="$b"; break; fi
|
||||
done
|
||||
fi
|
||||
[ -n "$branch" ] || fail "no default branch"
|
||||
|
||||
if [ "$dirty" -eq 1 ]; then
|
||||
printf 'dirty %s\n' "$branch"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
out=$(git -C "$target" checkout "$branch" 2>&1) || fail "checkout $branch: $out"
|
||||
out=$(git -C "$target" pull --ff-only origin "$branch" 2>&1) || fail "pull $branch: $out"
|
||||
printf 'updated\n'
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env sh
|
||||
# write-confirm.sh — Gitea 寫入前的人工確認。
|
||||
#
|
||||
# 為什麼要有這支腳本:wiki 與議題都是對外寫入。先確認,才能避免把
|
||||
# 錯頁、錯內容、錯標籤直接送出去。
|
||||
#
|
||||
# 用法:
|
||||
# write-confirm.sh <動作> <目標>
|
||||
#
|
||||
# 規則:
|
||||
# - JSC_GITEA_CONFIRM=yes:直接放行。
|
||||
# - JSC_GITEA_CONFIRM=no:直接拒絕。
|
||||
# - 互動式執行:要求輸入「確認」。
|
||||
# - 沒有互動終端:拒絕,避免默默寫出錯資料。
|
||||
# 結束碼: 0=已確認,呼叫端接著送出這次寫入。
|
||||
# 2=不要寫入,原因印在 stderr:缺動作名稱、缺目標名稱、JSC_GITEA_CONFIRM=no、
|
||||
# 輸入的不是「確認」、讀不到輸入,以及沒有互動終端這五種都走這一碼。
|
||||
# 呼叫端一律中止這次寫入,不要重試,也不要繞過確認直接呼叫 API。
|
||||
# 只有 0 與 2 兩種。用法錯誤與人工拒絕共用 2,因為兩者的處置一樣是「不寫」;
|
||||
# 拆成兩碼只會誘使呼叫端把用法錯誤當成可重試,反而寫出沒經過確認的資料。
|
||||
set -eu
|
||||
|
||||
action="${1:-}"
|
||||
target="${2:-}"
|
||||
|
||||
[ -n "$action" ] || { echo '[jsc][Gitea][ERR]:缺少動作名稱。' >&2; exit 2; }
|
||||
[ -n "$target" ] || { echo '[jsc][Gitea][ERR]:缺少目標名稱。' >&2; exit 2; }
|
||||
|
||||
case "${JSC_GITEA_CONFIRM:-}" in
|
||||
yes|YES|1|true|TRUE)
|
||||
exit 0 ;;
|
||||
no|NO|0|false|FALSE)
|
||||
echo "[jsc][Gitea][ERR]:已取消寫入「$target」。" >&2
|
||||
exit 2 ;;
|
||||
esac
|
||||
|
||||
if [ -t 0 ]; then
|
||||
printf '[jsc][Gitea] 即將%s「%s」。輸入「確認」繼續:' "$action" "$target" >&2
|
||||
IFS= read -r answer || exit 2
|
||||
case "$answer" in
|
||||
確認)
|
||||
exit 0 ;;
|
||||
*)
|
||||
echo "[jsc][Gitea][ERR]:已取消寫入「$target」。" >&2
|
||||
exit 2 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
echo "[jsc][Gitea][ERR]:寫入「$target」需要先確認;請改用互動式執行,或先設 JSC_GITEA_CONFIRM=yes。" >&2
|
||||
exit 2
|
||||
Reference in New Issue
Block a user