mosage 0.1.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -0
- package/README.md +16 -217
- package/bin.js +2 -0
- package/dist/build-C7NW_3Pk.js +14 -0
- package/dist/check-CP4873Wx.js +41 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +228 -0
- package/dist/config-DPm1BBAb.js +2619 -0
- package/dist/config-TlTe7Ona.d.ts +24 -0
- package/dist/context-BqsdSrAQ.js +1084 -0
- package/dist/dev-Biz42qlu.js +17 -0
- package/dist/diagram-xlVDekYk.js +763 -0
- package/dist/export-Bi6nuxjT.js +31 -0
- package/dist/import-D2jNB07F.js +25 -0
- package/dist/index.d.ts +455 -0
- package/dist/index.js +693 -0
- package/dist/init-Bbtj2pxF.js +262 -0
- package/dist/preview-CLm51aRt.js +19 -0
- package/dist/sdk-DjpX6mCv.js +51 -0
- package/dist/vite/index.d.ts +25 -0
- package/dist/vite/index.js +2 -0
- package/env.d.ts +83 -0
- package/package.json +84 -64
- package/skills/apply-comments/SKILL.md +43 -56
- package/skills/create-doc/SKILL.md +106 -0
- package/skills/create-theme/SKILL.md +184 -0
- package/skills/current-doc/SKILL.md +120 -0
- package/skills/doc-authoring/SKILL.md +434 -0
- package/skills/doc-authoring/references/assets.md +47 -0
- package/skills/doc-authoring/references/design-system.md +81 -0
- package/skills/doc-authoring/references/long-form.md +131 -0
- package/skills/doc-authoring/references/pagination.md +118 -0
- package/skills/doc-authoring/references/tables-and-charts.md +161 -0
- package/src/app/app.tsx +42 -0
- package/src/app/components/data-table.tsx +196 -0
- package/src/app/components/design-panel/design-panel.tsx +318 -0
- package/src/app/components/design-panel/design-provider.tsx +121 -0
- package/src/app/components/design-panel/use-design.ts +85 -0
- package/src/app/components/diagram.tsx +76 -0
- package/src/app/components/doc-assets.tsx +129 -0
- package/src/app/components/doc-search.tsx +248 -0
- package/src/app/components/doc-sidebar.tsx +162 -0
- package/src/app/components/flow-page.tsx +93 -0
- package/src/app/components/footnote.tsx +204 -0
- package/src/app/components/image-placeholder.tsx +50 -0
- package/src/app/components/inspector/inspector.tsx +518 -0
- package/src/app/components/numbering.tsx +224 -0
- package/src/app/components/page-frame.tsx +70 -0
- package/src/app/components/sidebar/folder-item.tsx +212 -0
- package/src/app/components/sidebar/icon-picker.tsx +99 -0
- package/src/app/components/sidebar/sidebar.tsx +252 -0
- package/src/app/components/table-of-contents.tsx +93 -0
- package/src/app/components/theme-toggle.tsx +50 -0
- package/src/app/components/themes/markdown.tsx +249 -0
- package/src/app/components/themes/theme-preview.tsx +74 -0
- package/src/app/components/ui/menu.tsx +143 -0
- package/src/app/index.html +12 -0
- package/src/app/lib/agent-bridge.ts +140 -0
- package/src/app/lib/assets.ts +151 -0
- package/src/app/lib/design-presets.ts +109 -0
- package/src/app/lib/design.ts +88 -0
- package/src/app/lib/diagnostics.ts +282 -0
- package/src/app/lib/doc-preview.tsx +29 -0
- package/src/app/lib/docs.ts +26 -0
- package/src/app/lib/docx/extract.ts +1623 -0
- package/src/app/lib/docx/fonts.test.ts +136 -0
- package/src/app/lib/docx/fonts.ts +166 -0
- package/src/app/lib/docx/media.ts +102 -0
- package/src/app/lib/docx/model.ts +206 -0
- package/src/app/lib/docx/paragraph.test.ts +92 -0
- package/src/app/lib/docx/paragraph.ts +107 -0
- package/src/app/lib/docx/props.ts +187 -0
- package/src/app/lib/docx/styles.ts +306 -0
- package/src/app/lib/docx/units.ts +35 -0
- package/src/app/lib/docx/write.test.ts +507 -0
- package/src/app/lib/docx/write.ts +581 -0
- package/src/app/lib/docx/xml.ts +39 -0
- package/src/app/lib/export-docx.ts +289 -0
- package/src/app/lib/export-dom.ts +318 -0
- package/src/app/lib/export-html.ts +156 -0
- package/src/app/lib/export-image.ts +70 -0
- package/src/app/lib/export-pdf.ts +165 -0
- package/src/app/lib/flow-measure.test.ts +31 -0
- package/src/app/lib/flow-measure.ts +183 -0
- package/src/app/lib/flow.test.ts +110 -0
- package/src/app/lib/flow.ts +136 -0
- package/src/app/lib/folders.ts +192 -0
- package/src/app/lib/footnotes.test.tsx +102 -0
- package/src/app/lib/footnotes.ts +94 -0
- package/src/app/lib/inspector/fiber.ts +99 -0
- package/src/app/lib/labels.test.ts +18 -0
- package/src/app/lib/labels.ts +181 -0
- package/src/app/lib/outline.ts +118 -0
- package/src/app/lib/page-context.tsx +43 -0
- package/src/app/lib/page-range.test.ts +95 -0
- package/src/app/lib/page-range.ts +90 -0
- package/src/app/lib/print-ready.ts +69 -0
- package/src/app/lib/rasterize.ts +173 -0
- package/src/app/lib/scan.ts +26 -0
- package/src/app/lib/sdk.test.ts +32 -0
- package/src/app/lib/sdk.ts +115 -0
- package/src/app/lib/themes.ts +31 -0
- package/src/app/lib/use-doc-module.ts +53 -0
- package/src/app/lib/use-doc-pages.ts +147 -0
- package/src/app/lib/utils.ts +6 -0
- package/src/app/lib/view-mode.test.ts +91 -0
- package/src/app/lib/view-mode.ts +104 -0
- package/src/app/main.tsx +14 -0
- package/src/app/routes/assets.tsx +257 -0
- package/src/app/routes/doc.tsx +877 -0
- package/src/app/routes/home-shell.tsx +203 -0
- package/src/app/routes/home.tsx +269 -0
- package/src/app/routes/themes.tsx +121 -0
- package/src/app/styles.css +97 -0
- package/src/app/virtual.d.ts +30 -0
- package/template/AGENTS.md +27 -0
- package/template/README.md +39 -0
- package/template/docs/getting-started/index.tsx +230 -0
- package/template/mosage.config.ts +5 -0
- package/template/package.json +24 -0
- package/template/tsconfig.json +17 -0
- package/README.en.md +0 -57
- package/dist/cli.js +0 -4545
- package/dist/web/assets/index-Czg2WeHe.js +0 -182
- package/dist/web/assets/index-DKAyt92W.css +0 -1
- package/dist/web/index.html +0 -15
- package/skills/current-position/SKILL.md +0 -65
- package/skills/kickoff/SKILL.md +0 -105
- package/skills/mosage-reference/SKILL.md +0 -164
- package/skills/outline/SKILL.md +0 -79
- package/skills/review/SKILL.md +0 -64
- package/skills/write-chapter/SKILL.md +0 -58
- package/template/book/STYLE.md +0 -6
- package/template/book/assets/.gitkeep +0 -0
- package/template/book/book.yaml +0 -42
- package/template/book/brief.md +0 -6
- package/template/book/chapters/.gitkeep +0 -0
- package/template/book/notes/README.md +0 -7
- package/template/project/AGENTS.md +0 -82
- package/template/project/CLAUDE.md +0 -1
- package/template/project/README.md +0 -48
- package/template/project/books/.gitkeep +0 -0
- package/template/project/gitignore +0 -5
- package/template/project/mosage.yaml +0 -23
- package/template/project/notes/README.md +0 -8
- package/template/project/package.json +0 -14
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,230 +1,29 @@
|
|
|
1
|
-
#
|
|
1
|
+
# mosage
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
[](LICENSE)
|
|
6
|
-
|
|
7
|
-
**繁體中文** · [English](README.en.md)
|
|
8
|
-
|
|
9
|
-
**和 AI 一起寫書、寫論文的本機寫作平台。** 用 Claude Code、Codex 等 AI 工具打開專案資料夾,
|
|
10
|
-
跟 AI 對話完成立項、訂書名、排大綱、撰寫與修訂;你在瀏覽器裡用「書本排版」閱讀成果、
|
|
11
|
-
選取文字留言給 AI、一鍵接受或拒絕 AI 的修改建議 —— 最後再匯出成出版社或學校要的 Word 檔。
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
npx mosage init my-writing
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
不需要資料庫,不需要帳號。所有內容都是專案資料夾裡的純文字檔(Markdown + YAML),圖片和參考資料直接複製進資料夾即可,
|
|
18
|
-
可以用 git 管理版本,也可以用任何編輯器打開。
|
|
19
|
-
|
|
20
|
-
## 為什麼
|
|
21
|
-
|
|
22
|
-
直接在 Word 裡寫長篇稿件,格式和內容總是攪在一起:一邊構思,一邊處理目錄頁碼、標題樣式和「追蹤修訂」。
|
|
23
|
-
AI 很擅長寫文字,但不擅長操作 Word。
|
|
24
|
-
|
|
25
|
-
MoSage 把兩件事分開:
|
|
26
|
-
|
|
27
|
-
- **寫作階段**:內容是 Markdown,AI 讀寫起來又快又準;你在瀏覽器裡看到的是排好版的書頁,而不是原始碼。
|
|
28
|
-
- **交稿階段**:一鍵輸出 Word,標題樣式、目錄、頁碼、註腳、圖說都幫你處理好。
|
|
29
|
-
|
|
30
|
-
## 特色
|
|
31
|
-
|
|
32
|
-
- 📚 **一個專案,很多本書**:`books/` 底下每個資料夾就是一本書或一篇論文,各自有設定、進度與風格。
|
|
33
|
-
- 🤖 **AI 帶你從零開始**:全新專案時,AI 會主動訪談你的寫作目的、讀者、風格與篇幅,再提出書名候選與章節架構。
|
|
34
|
-
- 🗂️ **大綱編排**:在瀏覽器裡拖曳章節順序、直接修改章名與摘要、設定每章狀態(構想/草稿/修訂中/完成)。
|
|
35
|
-
- 📖 **書本排版預覽**:首行縮排、行距、字型沿用 Word 匯出設定;單章閱讀或全書預覽。
|
|
36
|
-
- 💬 **留言給 AI**:選取任何文字 → 留言。留言以標記存進 Markdown,對 AI 說「處理留言」就會處理。
|
|
37
|
-
- ✍️ **修改建議,先看再決定**:AI 修改你寫好的文字時,會以「新舊對照」呈現,你按「接受」才會生效。
|
|
38
|
-
- ⚡ **即時同步**:AI 改檔案的同時,瀏覽器畫面自動更新 —— 看著 AI 一節一節寫。
|
|
39
|
-
- 🕰️ **自動版本紀錄**:每次內容改變前自動保存上一版,可比較差異、一鍵還原。
|
|
40
|
-
- 📍 **AI 知道你在看哪裡**:說「這段改短一點」,AI 會讀取你在瀏覽器裡正在看的段落與選取的文字。
|
|
41
|
-
- 📄 **匯出 Word**:真正的 Word 樣式(標題 1–4、內文、引文、圖說)、自動目錄、頁碼、註腳、表格、圖片;也能匯出 HTML(可列印成 PDF)與合併的 Markdown。
|
|
42
|
-
- 📥 **匯入舊稿**:`mosage import 書稿.docx` 依標題樣式把 Word 稿切成章節,圖片與註腳一併帶入。
|
|
43
|
-
- 🎓 **書與論文兩種預設**:論文預設標楷體+Times New Roman 12pt、左邊界 3 公分,符合台灣常見的論文格式。
|
|
44
|
-
|
|
45
|
-
## 快速開始
|
|
46
|
-
|
|
47
|
-
需要 [Node.js](https://nodejs.org/) 20.19 以上。
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
npx mosage init my-writing # 建立寫作專案
|
|
51
|
-
cd my-writing
|
|
52
|
-
npm run dev # 開啟寫作介面 → http://localhost:5280
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
另開一個終端機,在同一個資料夾啟動 AI 工具:
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
claude # Claude Code
|
|
59
|
-
codex # 或 OpenAI Codex
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
然後對 AI 說「**開始**」(Claude Code 也可以輸入 `/kickoff`)。
|
|
63
|
-
|
|
64
|
-
## 寫作流程
|
|
65
|
-
|
|
66
|
-
| 階段 | 你對 AI 說 | 發生什麼事 |
|
|
67
|
-
| --- | --- | --- |
|
|
68
|
-
| 1. 立項 | 「開始」 | AI 一次問一兩題:目的、類型、讀者、核心主張、風格、篇幅、既有素材。確認後建立 `books/<代號>/`、寫作企劃 `brief.md` 與風格指南 `STYLE.md` |
|
|
69
|
-
| 2. 大綱 | 「幫我訂書名和大綱」 | 提出 5 個書名與 2–3 種章節架構;定案後建立每章骨架(小節+要點) |
|
|
70
|
-
| 3. 編排 | —— | 你在介面「大綱」頁拖曳排序、修改章名與摘要、新增或移出章節 |
|
|
71
|
-
| 4. 撰寫 | 「寫第一章」 | AI 讀企劃、風格與前後章,一節一節寫,畫面即時更新;需要你補充資料的地方會留言 |
|
|
72
|
-
| 5. 修訂 | 「處理留言」「幫我審這章」 | 你選取文字留言;AI 回以修改建議,你逐一接受或拒絕 |
|
|
73
|
-
| 6. 匯出 | —— | 介面右上角「匯出 → Word」,或 `npx mosage export <書> docx` |
|
|
74
|
-
|
|
75
|
-
寫到一半想改大綱?直接說,AI 會調整章節並保留已寫好的內容。想寫第二本?說「我想寫一本新書」。
|
|
76
|
-
|
|
77
|
-
## 專案結構
|
|
78
|
-
|
|
79
|
-
```
|
|
80
|
-
my-writing/
|
|
81
|
-
├── mosage.yaml 專案預設值:作者、AI 修改模式、共用匯出格式
|
|
82
|
-
├── AGENTS.md 給 AI 的協作守則(CLAUDE.md 會引用它)
|
|
83
|
-
├── .agents/skills/ AI skills(Codex 等工具讀這裡)
|
|
84
|
-
├── .claude/skills/ → 連到 .agents/skills(Claude Code 讀這裡)
|
|
85
|
-
├── notes/ 所有書共用的素材(作者簡介…)
|
|
86
|
-
├── books/
|
|
87
|
-
│ └── <代號>/
|
|
88
|
-
│ ├── book.yaml 類型、階段、書名、章節順序、Word 格式
|
|
89
|
-
│ ├── brief.md 寫作企劃
|
|
90
|
-
│ ├── STYLE.md 風格指南
|
|
91
|
-
│ ├── chapters/ 01-xxx.md、02-xxx.md …
|
|
92
|
-
│ ├── assets/ 圖片
|
|
93
|
-
│ └── notes/ 這本書的參考資料(不會匯出)
|
|
94
|
-
├── output/<代號>/ 匯出的 Word / HTML / Markdown
|
|
95
|
-
└── .mosage/ 目前閱讀位置、自動版本紀錄(不進 git)
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
章節檔就是一般的 Markdown:
|
|
99
|
-
|
|
100
|
-
```markdown
|
|
101
|
-
---
|
|
102
|
-
status: draft
|
|
103
|
-
summary: 這一章讓讀者理解……
|
|
104
|
-
---
|
|
105
|
-
|
|
106
|
-
# 第一章 出發
|
|
107
|
-
|
|
108
|
-
## 1.1 小節
|
|
109
|
-
|
|
110
|
-
正文,支援**粗體**、註腳[^1]、表格、圖片 、
|
|
111
|
-
提示框 `> [!TIP]`、場景分隔 `---`、強制換頁 `<!-- pagebreak -->`。
|
|
112
|
-
|
|
113
|
-
[^1]: 註腳內容。
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
## AI Skills
|
|
117
|
-
|
|
118
|
-
| Skill | 用途 |
|
|
119
|
-
| --- | --- |
|
|
120
|
-
| `kickoff` | 立項訪談,建立新書、寫作企劃與風格指南 |
|
|
121
|
-
| `outline` | 書名候選、章節架構、章節骨架;寫作途中調整大綱 |
|
|
122
|
-
| `write-chapter` | 撰寫、續寫、擴寫章節 |
|
|
123
|
-
| `apply-comments` | 處理你在介面上的留言 |
|
|
124
|
-
| `review` | 以編輯角度審稿,只留意見與修改建議,不直接改稿 |
|
|
125
|
-
| `current-position` | 解讀「這段」「這章」—— 你在瀏覽器中正在看的位置 |
|
|
126
|
-
| `mosage-reference` | 檔案格式、標記語法、Markdown 支援範圍、指令 |
|
|
127
|
-
|
|
128
|
-
升級 mosage 後執行 `npx mosage sync-skills` 更新專案裡的 skills。
|
|
129
|
-
|
|
130
|
-
### 留言與修改建議的格式
|
|
131
|
-
|
|
132
|
-
兩者都是 HTML 註解,任何 Markdown 工具都看不見,也不會被匯出:
|
|
133
|
-
|
|
134
|
-
```markdown
|
|
135
|
-
<!-- mosage:comment id=c-1a2b3c4d by=human quote="被選取的文字"
|
|
136
|
-
這段太長,拆成兩段並舉個例子
|
|
137
|
-
-->
|
|
138
|
-
被留言的段落。
|
|
139
|
-
|
|
140
|
-
<!-- mosage:suggest by=ai note="刪除贅字"
|
|
141
|
-
AI 提議的新段落。按「接受」後取代下方這一段。
|
|
142
|
-
-->
|
|
143
|
-
原本的段落。
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
`mosage.yaml`(或單本的 `book.yaml`)的 `ai.editMode` 決定 AI 修改既有文字的方式:`suggest`(預設,修改建議)或 `direct`(直接改,仍有版本紀錄)。
|
|
147
|
-
|
|
148
|
-
## 指令
|
|
149
|
-
|
|
150
|
-
| 指令 | 說明 |
|
|
151
|
-
| --- | --- |
|
|
152
|
-
| `npx mosage init <資料夾>` | 建立寫作專案(`--no-install`、`--no-git`、`--author`) |
|
|
153
|
-
| `mosage dev` | 開啟寫作介面(`--port`、`--host`、`--no-open`) |
|
|
154
|
-
| `mosage new <代號>` | 新增一本書(`--title`、`--type book\|thesis\|other`) |
|
|
155
|
-
| `mosage status [書]` | 書架總覽,或單本書的章節、字數、待處理標記(`--json`) |
|
|
156
|
-
| `mosage export [書] [docx\|html\|md]` | 匯出到 `output/<書>/`(`--out`) |
|
|
157
|
-
| `mosage import <檔案.docx\|.md>` | 匯入舊稿(`--book`、`--title`、`--no-split`、`--unlisted`) |
|
|
158
|
-
| `mosage sync-skills` | 更新專案裡的 AI skills |
|
|
159
|
-
|
|
160
|
-
## Word 匯出設定
|
|
161
|
-
|
|
162
|
-
在 `book.yaml`(單本)或 `mosage.yaml`(全部的書)的 `export:` 調整:
|
|
163
|
-
|
|
164
|
-
```yaml
|
|
165
|
-
export:
|
|
166
|
-
pageSize: A4 # A4 | A5 | B5 | Letter | { width: 170, height: 230 }(公釐)
|
|
167
|
-
margins: { top: 25, bottom: 25, left: 30, right: 25 }
|
|
168
|
-
fonts: { body: 標楷體, latin: Times New Roman, heading: 標楷體, code: Consolas }
|
|
169
|
-
fontSize: 12 # pt
|
|
170
|
-
lineSpacing: 1.5
|
|
171
|
-
firstLineIndent: 2 # 首行縮排字數
|
|
172
|
-
titlePage: true
|
|
173
|
-
toc: true # Word 開啟時會詢問是否更新目錄頁碼,選「是」
|
|
174
|
-
header: title # none | title | chapter
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
匯出的是使用 Word 內建樣式的文件,要改整體字型或段落,直接在 Word 裡修改「標題 1」「內文」等樣式即可。
|
|
178
|
-
|
|
179
|
-
## 常見問題
|
|
180
|
-
|
|
181
|
-
**需要資料庫或網路嗎?** 不需要。MoSage 只是在你的電腦上開一個讀寫專案資料夾的小伺服器(只接受本機連線);AI 工具本身才需要網路。
|
|
182
|
-
|
|
183
|
-
**支援哪些 AI 工具?** Claude Code 與 OpenAI Codex 會自動讀取 skills。其他會讀 `AGENTS.md` 的工具也能照著守則工作。
|
|
184
|
-
|
|
185
|
-
**可以不用 AI 嗎?** 可以。介面裡能直接修改段落、編輯 Markdown 原始碼、編排大綱與匯出。
|
|
186
|
-
|
|
187
|
-
**我的稿子會不會被 AI 改壞?** 預設是「修改建議」模式,AI 的修改要你按接受才生效;另外每次變動前都會自動保存版本,可以在「版本紀錄」還原。建議同時用 git。
|
|
188
|
-
|
|
189
|
-
## 開發
|
|
3
|
+
讓 AI coding agent 幫你做出要印出來、要交出去的文件:報告、企劃書、白皮書、手冊。
|
|
4
|
+
Agent 把內容寫成頁面,MoSage 負責紙張尺寸、分頁、目錄與頁碼,並匯出 PDF、可編輯的 Word 或 HTML。
|
|
190
5
|
|
|
191
6
|
```bash
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
npm
|
|
195
|
-
npm run test:e2e # 打包 → npx init → CLI → 伺服器;MOSAGE_E2E_BROWSER=1 再加上瀏覽器測試
|
|
196
|
-
npm run check # Biome(格式與 lint)
|
|
197
|
-
npm run typecheck
|
|
7
|
+
npx mosage init my-docs
|
|
8
|
+
cd my-docs
|
|
9
|
+
npm run dev # http://localhost:5273
|
|
198
10
|
```
|
|
199
11
|
|
|
200
|
-
|
|
201
|
-
再於本 repo 執行 `npm run dev:web`(Vite 會把 `/api` 轉給 5281)。
|
|
12
|
+
接著在同一個資料夾開啟 Claude Code 或 Codex,描述你要的文件即可。
|
|
202
13
|
|
|
203
|
-
|
|
204
|
-
| --- | --- |
|
|
205
|
-
| `src/shared/` | 瀏覽器與 Node 共用:設定、Markdown 解析、標記格式、字數、差異比對、原始碼編輯 |
|
|
206
|
-
| `src/node/` | 專案與書籍檔案、版本紀錄、HTTP 伺服器、Word/HTML/Markdown 匯出、匯入 |
|
|
207
|
-
| `src/cli/` | `mosage` 指令 |
|
|
208
|
-
| `web/` | 瀏覽器介面(React) |
|
|
209
|
-
| `template/` | `init` 與 `new` 使用的專案、書籍範本 |
|
|
210
|
-
| `skills/` | 安裝到專案裡的 AI skills |
|
|
14
|
+
## 這個套件包含
|
|
211
15
|
|
|
212
|
-
|
|
16
|
+
- **CLI**:`mosage init`(建立專案)、`dev`、`check`(版面檢查)、`export`(PDF/Word/HTML/PNG)、`import`(Markdown 轉文件)、`build`、`preview`、`sync:skills`
|
|
17
|
+
- **檢視器**:真實尺寸的頁面、縮圖與大綱、單頁/雙頁/格狀檢視、Inspect(直接改字或留言給 AI)、Design 面板
|
|
18
|
+
- **文件元件**:`flow()` 自動分頁、`TableOfContents`、`Footnote`、`Figure`/`Ref` 編號與交互參照、`DataTable`(讀 CSV)、頁碼 hooks
|
|
19
|
+
- **AI skills**:`create-doc`、`doc-authoring`、`current-doc`、`apply-comments`、`create-theme`
|
|
213
20
|
|
|
214
|
-
|
|
21
|
+
`mosage check` 與 `mosage export` 會用無頭 Chromium 渲染,第一次使用前請安裝 Playwright:
|
|
215
22
|
|
|
216
23
|
```bash
|
|
217
|
-
npm
|
|
218
|
-
git push --follow-tags
|
|
24
|
+
npm i -D playwright && npx playwright install chromium
|
|
219
25
|
```
|
|
220
26
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
MoSage 的概念來自 [open-slide](https://github.com/open-slide/open-slide)(Yiwei Ho)與
|
|
224
|
-
[open-doc](https://github.com/simonliu-ai-product/open-doc)(Simon Liu),兩者皆為 MIT 授權:
|
|
225
|
-
用 `npx` 建立工作區、把 AI skills 放進專案、在瀏覽器裡留言再由 AI 套用、由開發伺服器發布目前閱讀位置。
|
|
226
|
-
MoSage 是針對長篇寫作與 Word 交稿重新設計的獨立實作(Markdown 章節、修改建議、Word 匯出),未複製上述專案的程式碼。
|
|
227
|
-
|
|
228
|
-
## 授權
|
|
27
|
+
完整說明與範例:<https://github.com/nickchen1998/MoSage>
|
|
229
28
|
|
|
230
|
-
[
|
|
29
|
+
MoSage 以 [open-doc](https://github.com/simonliu-ai-product/open-doc)(Simon Liu,MIT)為基礎開發。授權:MIT。
|
package/bin.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { t as createViteConfig } from "./config-DPm1BBAb.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { build as build$1, mergeConfig } from "vite";
|
|
4
|
+
//#region src/cli/build.ts
|
|
5
|
+
async function build(opts = {}) {
|
|
6
|
+
const base = await createViteConfig({
|
|
7
|
+
userCwd: process.cwd(),
|
|
8
|
+
mode: "build"
|
|
9
|
+
});
|
|
10
|
+
const config = mergeConfig(base, { build: { ...opts.outDir !== void 0 ? { outDir: path.resolve(process.cwd(), opts.outDir) } : {} } });
|
|
11
|
+
await build$1(config);
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { build };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { i as listDocIds } from "./config-DPm1BBAb.js";
|
|
2
|
+
import { n as checkLayout, r as closeRenderSession, t as cliContext } from "./context-BqsdSrAQ.js";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
//#region src/cli/check.ts
|
|
5
|
+
function line(finding) {
|
|
6
|
+
const where = finding.page > 0 ? `p.${finding.page}` : "doc";
|
|
7
|
+
const mark = finding.severity === "error" ? chalk.red("✗") : chalk.yellow("!");
|
|
8
|
+
const detail = [finding.element, finding.loc && chalk.dim(`@ ${finding.loc}`)].filter(Boolean).join(" ");
|
|
9
|
+
return ` ${mark} ${chalk.dim(where.padEnd(5))} ${finding.message}${detail ? `\n ${chalk.dim(detail)}` : ""}`;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Renders every sheet at true page size and reports the layout faults an agent
|
|
13
|
+
* writing React cannot see: content clipped by the page edge, an empty sheet, a
|
|
14
|
+
* heading stranded at the foot of a page, type too small to print.
|
|
15
|
+
*
|
|
16
|
+
* Exits non-zero when anything is an error, so it works as a CI gate.
|
|
17
|
+
*/
|
|
18
|
+
async function checkDocs(docIds, opts = {}) {
|
|
19
|
+
const ctx = await cliContext();
|
|
20
|
+
const targets = docIds.length > 0 ? docIds : await listDocIds(ctx);
|
|
21
|
+
if (targets.length === 0) throw new Error("No documents found under docs/.");
|
|
22
|
+
let errors = 0;
|
|
23
|
+
const reports = [];
|
|
24
|
+
try {
|
|
25
|
+
for (const docId of targets) {
|
|
26
|
+
const report = await checkLayout(ctx, docId);
|
|
27
|
+
reports.push(report);
|
|
28
|
+
errors += report.errors;
|
|
29
|
+
if (opts.json) continue;
|
|
30
|
+
const summary = report.errors === 0 && report.warnings === 0 ? chalk.green("clean") : [report.errors > 0 ? chalk.red(`${report.errors} error(s)`) : "", report.warnings > 0 ? chalk.yellow(`${report.warnings} warning(s)`) : ""].filter(Boolean).join(", ");
|
|
31
|
+
process.stdout.write(`${chalk.bold(docId)} ${chalk.dim(`${report.pageCount} pages`)} — ${summary}\n`);
|
|
32
|
+
for (const finding of report.findings) process.stdout.write(`${line(finding)}\n`);
|
|
33
|
+
}
|
|
34
|
+
} finally {
|
|
35
|
+
await closeRenderSession();
|
|
36
|
+
}
|
|
37
|
+
if (opts.json) process.stdout.write(`${JSON.stringify(reports, null, 2)}\n`);
|
|
38
|
+
if (errors > 0) process.exitCode = 1;
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { checkDocs };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
package/dist/cli/bin.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { a as PAGE_SIZE_NAMES, r as ORIENTATIONS } from "../sdk-DjpX6mCv.js";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import * as readline from "node:readline/promises";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { Command, Option } from "commander";
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import fs, { readFile } from "node:fs/promises";
|
|
10
|
+
//#region \0rolldown/runtime.js
|
|
11
|
+
var __defProp = Object.defineProperty;
|
|
12
|
+
var __exportAll = (all, no_symbols) => {
|
|
13
|
+
let target = {};
|
|
14
|
+
for (var name in all) __defProp(target, name, {
|
|
15
|
+
get: all[name],
|
|
16
|
+
enumerable: true
|
|
17
|
+
});
|
|
18
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
19
|
+
return target;
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/ops/formats.ts
|
|
23
|
+
/** Kept apart from `layout.ts`, so the CLI can list the choices without loading the renderer. */
|
|
24
|
+
const EXPORT_FORMATS = [
|
|
25
|
+
"pdf",
|
|
26
|
+
"html",
|
|
27
|
+
"docx",
|
|
28
|
+
"png"
|
|
29
|
+
];
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/cli/package-version.ts
|
|
32
|
+
const PACKAGE_NAME = "mosage";
|
|
33
|
+
/**
|
|
34
|
+
* This package's root folder (the one holding its own `package.json`, the
|
|
35
|
+
* `template/` and `skills/`). Walks up instead of assuming a fixed depth: the
|
|
36
|
+
* bundler decides which chunk a module lands in, so `../../` is right from
|
|
37
|
+
* `dist/cli/` and wrong from `dist/`.
|
|
38
|
+
*/
|
|
39
|
+
function packageRoot() {
|
|
40
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
41
|
+
for (let up = 0; up < 6; up += 1) {
|
|
42
|
+
const file = path.join(dir, "package.json");
|
|
43
|
+
if (existsSync(file)) try {
|
|
44
|
+
if (JSON.parse(readFileSync(file, "utf8")).name === PACKAGE_NAME) return dir;
|
|
45
|
+
} catch {}
|
|
46
|
+
const parent = path.dirname(dir);
|
|
47
|
+
if (parent === dir) break;
|
|
48
|
+
dir = parent;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/** Getting the root wrong is silent — it reports `0.0.0` rather than failing. */
|
|
53
|
+
async function readCoreVersion() {
|
|
54
|
+
const root = packageRoot();
|
|
55
|
+
if (!root) return "0.0.0";
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(await readFile(path.join(root, "package.json"), "utf8")).version ?? "0.0.0";
|
|
58
|
+
} catch {
|
|
59
|
+
return "0.0.0";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/cli/sync.ts
|
|
64
|
+
var sync_exports = /* @__PURE__ */ __exportAll({
|
|
65
|
+
detectSkillsDrift: () => detectSkillsDrift,
|
|
66
|
+
syncSkills: () => syncSkills
|
|
67
|
+
});
|
|
68
|
+
const SKILL_TARGET_DIRS = [".agents/skills", ".claude/skills"];
|
|
69
|
+
async function listFiles(dir) {
|
|
70
|
+
const out = [];
|
|
71
|
+
const walk = async (current) => {
|
|
72
|
+
const entries = await fs.readdir(current, { withFileTypes: true });
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
const abs = path.join(current, entry.name);
|
|
75
|
+
if (entry.isDirectory()) await walk(abs);
|
|
76
|
+
else out.push(path.relative(dir, abs));
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
await walk(dir);
|
|
80
|
+
return out.sort();
|
|
81
|
+
}
|
|
82
|
+
async function sameTree(src, dst) {
|
|
83
|
+
if (!existsSync(dst)) return false;
|
|
84
|
+
const [srcFiles, dstFiles] = await Promise.all([listFiles(src), listFiles(dst)]);
|
|
85
|
+
if (srcFiles.length !== dstFiles.length) return false;
|
|
86
|
+
if (srcFiles.some((f, i) => f !== dstFiles[i])) return false;
|
|
87
|
+
for (const rel of srcFiles) {
|
|
88
|
+
const [a, b] = await Promise.all([fs.readFile(path.join(src, rel), "utf8"), fs.readFile(path.join(dst, rel), "utf8")]);
|
|
89
|
+
if (a !== b) return false;
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
async function skillNames(builtinDir) {
|
|
94
|
+
if (!existsSync(builtinDir)) return [];
|
|
95
|
+
return (await fs.readdir(builtinDir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
96
|
+
}
|
|
97
|
+
async function detectSkillsDrift(builtinDir, userCwd = process.cwd()) {
|
|
98
|
+
const names = await skillNames(builtinDir);
|
|
99
|
+
const primaryTarget = path.join(userCwd, SKILL_TARGET_DIRS[0]);
|
|
100
|
+
const drift = [];
|
|
101
|
+
for (const name of names) {
|
|
102
|
+
const src = path.join(builtinDir, name);
|
|
103
|
+
const dst = path.join(primaryTarget, name);
|
|
104
|
+
if (!existsSync(dst)) drift.push({
|
|
105
|
+
name,
|
|
106
|
+
status: "added"
|
|
107
|
+
});
|
|
108
|
+
else if (!await sameTree(src, dst)) drift.push({
|
|
109
|
+
name,
|
|
110
|
+
status: "updated"
|
|
111
|
+
});
|
|
112
|
+
else drift.push({
|
|
113
|
+
name,
|
|
114
|
+
status: "unchanged"
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return drift;
|
|
118
|
+
}
|
|
119
|
+
async function syncSkills(builtinDir, opts = {}, userCwd = process.cwd()) {
|
|
120
|
+
const stale = (await detectSkillsDrift(builtinDir, userCwd)).filter((d) => d.status !== "unchanged");
|
|
121
|
+
if (stale.length === 0) {
|
|
122
|
+
if (!opts.quiet) process.stdout.write(`${chalk.green("✔")} Skills already up to date.\n`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
for (const { name, status } of stale) {
|
|
126
|
+
if (opts.dryRun) {
|
|
127
|
+
process.stdout.write(`${chalk.yellow("~")} ${name} ${chalk.dim(`(${status})`)}\n`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
for (const targetDir of SKILL_TARGET_DIRS) {
|
|
131
|
+
const dst = path.join(userCwd, targetDir, name);
|
|
132
|
+
await fs.rm(dst, {
|
|
133
|
+
recursive: true,
|
|
134
|
+
force: true
|
|
135
|
+
});
|
|
136
|
+
await fs.mkdir(path.dirname(dst), { recursive: true });
|
|
137
|
+
await fs.cp(path.join(builtinDir, name), dst, { recursive: true });
|
|
138
|
+
}
|
|
139
|
+
if (!opts.quiet) process.stdout.write(`${chalk.green("✔")} ${name} ${chalk.dim(`(${status})`)}\n`);
|
|
140
|
+
}
|
|
141
|
+
if (opts.dryRun) process.stdout.write(chalk.dim("\nDry run — nothing written.\n"));
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/cli/run.ts
|
|
145
|
+
function parsePort(value) {
|
|
146
|
+
const n = Number(value);
|
|
147
|
+
if (!Number.isInteger(n) || n < 0 || n > 65535) throw new Error(`Invalid port: ${value}`);
|
|
148
|
+
return n;
|
|
149
|
+
}
|
|
150
|
+
function resolveBuiltinSkillsDir() {
|
|
151
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
152
|
+
return path.resolve(here, "..", "..", "skills");
|
|
153
|
+
}
|
|
154
|
+
async function runSkillsDriftCheck(skillsDir) {
|
|
155
|
+
if (process.env.OPEN_DOC_SKIP_SKILLS_CHECK === "1") return;
|
|
156
|
+
let drift;
|
|
157
|
+
try {
|
|
158
|
+
drift = await detectSkillsDrift(skillsDir);
|
|
159
|
+
} catch {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const stale = drift.filter((d) => d.status !== "unchanged");
|
|
163
|
+
if (stale.length === 0) return;
|
|
164
|
+
const names = stale.map((d) => d.name).join(", ");
|
|
165
|
+
if (!Boolean(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
166
|
+
process.stderr.write(`${chalk.yellow("!")} Skills out of date (${names}). Run \`mosage sync:skills\` to update.\n`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const rl = readline.createInterface({
|
|
170
|
+
input: process.stdin,
|
|
171
|
+
output: process.stdout
|
|
172
|
+
});
|
|
173
|
+
try {
|
|
174
|
+
const answer = (await rl.question(`${chalk.yellow("!")} Skills out of date: ${chalk.bold(names)}. Sync now? ${chalk.dim("(Y/n) ")}`)).trim().toLowerCase();
|
|
175
|
+
if (answer === "" || answer === "y" || answer === "yes") await syncSkills(skillsDir);
|
|
176
|
+
else process.stdout.write(chalk.dim("Skipped. Run `mosage sync:skills` later to update.\n"));
|
|
177
|
+
} finally {
|
|
178
|
+
rl.close();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async function run(argv) {
|
|
182
|
+
const version = await readCoreVersion();
|
|
183
|
+
const program = new Command();
|
|
184
|
+
program.name("mosage").description("Author documents — we handle the Vite/React stack, pagination, and export.").version(version, "-v, --version", "print version").helpOption("-h, --help", "show help").showHelpAfterError(chalk.dim("(run `mosage --help` for usage)"));
|
|
185
|
+
program.command("init").description("Create a new MoSage workspace").argument("[dir]", "target directory", void 0).option("-f, --force", "overwrite non-empty target directory", false).option("-n, --name <name>", "override package name (defaults to folder name)").option("--use-npm", "use npm to install dependencies").option("--use-pnpm", "use pnpm to install dependencies").option("--use-yarn", "use yarn to install dependencies").option("--use-bun", "use bun to install dependencies").option("--no-install", "skip dependency installation").option("--no-git", "skip git init and initial commit").action(async (dir, flags) => {
|
|
186
|
+
const { runInit } = await import("../init-Bbtj2pxF.js");
|
|
187
|
+
await runInit(dir, flags);
|
|
188
|
+
});
|
|
189
|
+
program.command("dev").description("Start the dev server").addOption(new Option("-p, --port <port>", "port to listen on").argParser(parsePort)).addOption(new Option("--host [host]", "expose on the network (optional host)")).option("--open", "open the browser on start").option("--no-skills-check", "skip the built-in skills drift check").action(async (flags) => {
|
|
190
|
+
if (flags.skillsCheck !== false) await runSkillsDriftCheck(resolveBuiltinSkillsDir());
|
|
191
|
+
const { dev } = await import("../dev-Biz42qlu.js");
|
|
192
|
+
await dev(flags);
|
|
193
|
+
});
|
|
194
|
+
program.command("build").description("Build a static site").option("--out-dir <dir>", "output directory (defaults to `dist`)").action(async (flags) => {
|
|
195
|
+
const { build } = await import("../build-C7NW_3Pk.js");
|
|
196
|
+
await build(flags);
|
|
197
|
+
});
|
|
198
|
+
program.command("preview").description("Preview the production build").addOption(new Option("-p, --port <port>", "port to listen on").argParser(parsePort)).addOption(new Option("--host [host]", "expose on the network (optional host)")).option("--open", "open the browser on start").action(async (flags) => {
|
|
199
|
+
const { preview } = await import("../preview-CLm51aRt.js");
|
|
200
|
+
await preview(flags);
|
|
201
|
+
});
|
|
202
|
+
program.command("export [docIds...]").description("Render documents headlessly to PDF, HTML, DOCX, or PNG").addOption(new Option("-f, --format <format>", "output format").choices(EXPORT_FORMATS)).option("-o, --out-dir <dir>", "directory to write into (defaults to `out`)").option("--all", "export every document under docs/").action(async (docIds, flags) => {
|
|
203
|
+
const { exportDocs } = await import("../export-Bi6nuxjT.js");
|
|
204
|
+
await exportDocs(docIds, flags);
|
|
205
|
+
});
|
|
206
|
+
program.command("check [docIds...]").description("Report layout faults — clipped content, blank sheets, stranded headings").option("--json", "print the full report as JSON").action(async (docIds, flags) => {
|
|
207
|
+
const { checkDocs } = await import("../check-CP4873Wx.js");
|
|
208
|
+
await checkDocs(docIds, flags);
|
|
209
|
+
});
|
|
210
|
+
program.command("import <file>").description("Turn a Markdown file into a document under docs/").option("--id <id>", "document id (defaults to a slug of the title)").option("--title <title>", "override the title").option("--subtitle <subtitle>", "subtitle, also used as the cover eyebrow").option("--author <author>", "author line on the cover").option("--theme <theme>", "theme id to back-link from meta.theme").addOption(new Option("--page-size <size>", "page size").choices(PAGE_SIZE_NAMES)).addOption(new Option("--orientation <orientation>", "page orientation").choices(ORIENTATIONS)).option("--no-cover", "skip the title page").option("--contents", "add a self-filling contents page").action(async (file, flags) => {
|
|
211
|
+
const { importDoc } = await import("../import-D2jNB07F.js");
|
|
212
|
+
await importDoc(file, flags);
|
|
213
|
+
});
|
|
214
|
+
program.command("sync:skills").description("Sync built-in skills from mosage into this workspace").option("--dry-run", "show what would change without writing").action(async (flags) => {
|
|
215
|
+
const { syncSkills } = await Promise.resolve().then(() => sync_exports);
|
|
216
|
+
await syncSkills(resolveBuiltinSkillsDir(), flags);
|
|
217
|
+
});
|
|
218
|
+
await program.parseAsync(argv, { from: "user" });
|
|
219
|
+
}
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/cli/bin.ts
|
|
222
|
+
run(process.argv.slice(2)).catch((err) => {
|
|
223
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
224
|
+
process.stderr.write(`${chalk.red("error:")} ${message}\n`);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
});
|
|
227
|
+
//#endregion
|
|
228
|
+
export { EXPORT_FORMATS as i, packageRoot as n, readCoreVersion as r, syncSkills as t };
|