@onozaty/growi-uploader 1.4.0 → 1.6.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/README.ja.md ADDED
@@ -0,0 +1,341 @@
1
+ # growi-uploader
2
+
3
+ 日本語 | [English](README.md)
4
+
5
+ [![Test](https://github.com/onozaty/growi-uploader/actions/workflows/test.yaml/badge.svg)](https://github.com/onozaty/growi-uploader/actions/workflows/test.yaml)
6
+ [![codecov](https://codecov.io/gh/onozaty/growi-uploader/graph/badge.svg?token=X0YN1OP5PB)](https://codecov.io/gh/onozaty/growi-uploader)
7
+ [![npm version](https://badge.fury.io/js/@onozaty%2Fgrowi-uploader.svg)](https://www.npmjs.com/package/@onozaty/growi-uploader)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
+
10
+ ローカルのMarkdownファイルと添付ファイルを[GROWI](https://growi.org/) Wikiに一括アップロードするCLIツールです。
11
+
12
+ ## 機能
13
+
14
+ - 📁 **ディレクトリ構造の維持** - ローカルのフォルダ階層がGROWIのページ階層になります
15
+ - 📝 **Markdownファイルのアップロード** - `.md`ファイルからGROWIページを作成・更新
16
+ - 📎 **添付ファイルの自動検出** - `<ページ名>_attachment_<ファイル名>`パターンのファイルを自動的に添付ファイルとしてアップロード
17
+ - 🔗 **リンクの自動置換** - ローカルの添付ファイルへのリンクをGROWI形式(`/attachment/{id}`)に自動変換
18
+ - 🖼️ **画像の埋め込み** - 画像リンク(`![alt](image.png)`)を自動変換
19
+ - ⚙️ **柔軟な設定** - ベースパス、更新動作などを制御可能
20
+
21
+ ## クイックスタート
22
+
23
+ 1. 設定ファイル`growi-uploader.json`を作成:
24
+
25
+ ```json
26
+ {
27
+ "url": "https://your-growi-instance.com",
28
+ "token": "your-api-token",
29
+ "basePath": "/",
30
+ "update": false
31
+ }
32
+ ```
33
+
34
+ 2. npxで実行(インストール不要):
35
+
36
+ ```bash
37
+ npx @onozaty/growi-uploader ./docs
38
+ ```
39
+
40
+ これだけです!ローカルの`./docs`ディレクトリがGROWIにアップロードされます。
41
+
42
+ ## インストール
43
+
44
+ ### npxを使用(推奨)
45
+
46
+ インストール不要で実行できます:
47
+
48
+ ```bash
49
+ npx @onozaty/growi-uploader <source-dir>
50
+ ```
51
+
52
+ ### グローバルインストール
53
+
54
+ 頻繁に使用する場合は、グローバルインストールできます:
55
+
56
+ ```bash
57
+ npm install -g @onozaty/growi-uploader
58
+ growi-uploader <source-dir>
59
+ ```
60
+
61
+ ## 使い方
62
+
63
+ ### 基本コマンド
64
+
65
+ ```bash
66
+ growi-uploader <source-dir> [options]
67
+ ```
68
+
69
+ **引数:**
70
+ - `<source-dir>`: Markdownファイルを含むディレクトリのパス
71
+
72
+ **オプション:**
73
+ - `-c, --config <path>`: 設定ファイルのパス(デフォルト: `growi-uploader.json`)
74
+ - `-v, --verbose`: 詳細なエラー出力を有効化
75
+ - `-V, --version`: バージョン番号を表示
76
+ - `-h, --help`: ヘルプ情報を表示
77
+
78
+ ### 実行例
79
+
80
+ ```bash
81
+ # Upload with default config file
82
+ npx @onozaty/growi-uploader ./docs
83
+
84
+ # Upload with custom config file
85
+ npx @onozaty/growi-uploader ./docs -c my-config.json
86
+
87
+ # Upload with verbose error output
88
+ npx @onozaty/growi-uploader ./docs --verbose
89
+ ```
90
+
91
+ ## ディレクトリ構造の例
92
+
93
+ ### ローカルディレクトリ
94
+
95
+ ```
96
+ docs/
97
+ guide.md
98
+ guide_attachment_diagram.svg
99
+ guide_attachment_sample.txt
100
+ api/
101
+ overview.md
102
+ overview_attachment_example.json
103
+ authentication.md
104
+ ```
105
+
106
+ ### アップロード後のGROWIページ
107
+
108
+ ```
109
+ /docs/guide (from guide.md)
110
+ └─ diagram.svg (attachment)
111
+ └─ sample.txt (attachment)
112
+ /docs/api/overview (from api/overview.md)
113
+ └─ example.json (attachment)
114
+ /docs/api/authentication (from api/authentication.md)
115
+ ```
116
+
117
+ ## ページ名の正規化
118
+
119
+ APIエラーを防ぐため、ページ名は以下のルールで自動的に正規化されます:
120
+
121
+ ### 正規化ルール
122
+
123
+ 1. **スラッシュ前後のスペース** → アンダースコアに置換
124
+ - `a / b.md` → `/a_/_b`
125
+
126
+ 2. **特殊文字** → 安全な文字列に置換:
127
+ - `+` → `-plus-`
128
+ - `?` → `-question-`
129
+ - `*` → `-asterisk-`
130
+ - `$` → `-dollar-`
131
+ - `^` → `-caret-`
132
+ - `%` → `-percent-`
133
+
134
+ 3. **予約済みページ名** → アンダースコアを末尾に追加:
135
+ - `edit` → `edit_` (パスの最後のセグメントの場合のみ)
136
+
137
+ ### 例
138
+
139
+ ```
140
+ Local file GROWI page path
141
+ ──────────────────────────────────────────────────
142
+ C++.md → /C-plus--plus-
143
+ What?.md → /What-question-
144
+ C++ / Python?.md → /C-plus--plus-_/_Python-question-
145
+ edit.md → /edit_
146
+ docs/edit.md → /docs/edit_
147
+ docs/normal-page.md → /docs/normal-page (no change)
148
+ ```
149
+
150
+ この正規化により、GROWIのページ名要件との互換性を確保しつつ、ファイル名の可読性を維持します。
151
+
152
+ ## 設定ファイル
153
+
154
+ プロジェクトルートに`growi-uploader.json`ファイルを作成:
155
+
156
+ ```json
157
+ {
158
+ "url": "https://your-growi-instance.com",
159
+ "token": "your-api-token",
160
+ "basePath": "/imported",
161
+ "update": true,
162
+ "verbose": false
163
+ }
164
+ ```
165
+
166
+ ### 設定オプション
167
+
168
+ | オプション | 型 | 必須 | デフォルト | 説明 |
169
+ |--------|------|----------|---------|-------------|
170
+ | `url` | string | ✅ | - | GROWIインスタンスのURL |
171
+ | `token` | string | ✅ | - | GROWI APIアクセストークン |
172
+ | `basePath` | string | ❌ | `/` | インポートされるページのベースパス |
173
+ | `update` | boolean | ❌ | `false` | trueの場合は既存ページを更新、falseの場合はスキップ |
174
+ | `verbose` | boolean | ❌ | `false` | 詳細なエラー出力を有効化 |
175
+
176
+ ### APIトークンの取得方法
177
+
178
+ 1. GROWIインスタンスにログイン
179
+ 2. **ユーザー設定** → **API設定** に移動
180
+ 3. **新しいトークンを発行** をクリック
181
+ 4. 生成されたトークンを設定ファイルにコピー
182
+
183
+ ## 添付ファイル
184
+
185
+ 添付ファイルは2つの方法で自動検出されます:
186
+
187
+ ### 方法1: 命名規則
188
+
189
+ 以下の命名パターンに従うファイルが添付ファイルとして検出されます:
190
+
191
+ ```
192
+ <ページ名>_attachment_<ファイル名>
193
+ ```
194
+
195
+ **例:**
196
+ ```
197
+ guide.md → GROWI page: /guide
198
+ guide_attachment_image.png → Attached to /guide
199
+ guide_attachment_document.pdf → Attached to /guide
200
+ ```
201
+
202
+ ### 方法2: リンクベースの検出
203
+
204
+ Markdownリンクで参照されているファイルが自動的に添付ファイルとして検出されます:
205
+
206
+ **ローカルディレクトリ:**
207
+ ```
208
+ guide.md
209
+ assets/
210
+ banner.png
211
+ images/
212
+ logo.png
213
+ screenshot.png
214
+ ```
215
+
216
+ **guide.mdの内容:**
217
+ ```markdown
218
+ ![Logo](./images/logo.png)
219
+ ![Screenshot](images/screenshot.png)
220
+ ![Banner](/assets/banner.png)
221
+ ```
222
+
223
+ 参照されているすべてのファイル(`logo.png`、`screenshot.png`、`banner.png`)は、`_attachment_`命名規則に従っていなくても、`/guide`ページに添付ファイルとしてアップロードされます。
224
+
225
+ **パスの解決:**
226
+ - Markdownエスケープシーケンスは解除されます(`\(` → `(`)
227
+ - URLエンコーディング(パーセントエンコーディング)はデコードされます(`%20` → スペース、`%E7%94%BB%E5%83%8F` → `画像`)
228
+ - 相対パス(`./`、`../`、またはプレフィックスなし): Markdownファイルのディレクトリからの相対パス
229
+ - 絶対パス(`/`で始まる): ソースディレクトリのルートからの絶対パス
230
+ - 例: `/assets/banner.png` → `<source-dir>/assets/banner.png`
231
+
232
+ **サポートされるリンク形式:**
233
+ ```markdown
234
+ ![Logo](./images/logo.png) # Standard relative path
235
+ ![Logo](images/logo.png) # Relative path without ./
236
+ ![Image](./images/%E7%94%BB%E5%83%8F.png) # URL-encoded Japanese filename
237
+ [File](./docs/my%20file.pdf) # URL-encoded space
238
+ [File](<./path/file (1).png>) # Special chars with angle brackets
239
+ ![Image](./path/file\\(1\\).png) # Special chars with escaping
240
+ <img src="./images/logo.png" alt="Logo"> # HTML img tag (double quotes)
241
+ <img src='./images/logo.png' alt='Logo'> # HTML img tag (single quotes)
242
+ ```
243
+
244
+ **検出から除外されるもの:**
245
+ - `.md`ファイル(ページリンクとして扱われます)
246
+ - 外部URL(`http://`、`https://`)
247
+ - 存在しないファイル
248
+
249
+ ### リンクの自動置換
250
+
251
+ 添付ファイルへのMarkdownリンクは自動的にGROWI形式(`/attachment/{id}`)に変換されます。
252
+
253
+ **例(命名規則):**
254
+
255
+ ```markdown
256
+ # Before upload
257
+ ![Diagram](./guide_attachment_diagram.png)
258
+ Download the [documentation](guide_attachment_document.pdf).
259
+
260
+ # After upload (on GROWI)
261
+ ![Diagram](/attachment/68f3a41c794f665ad2c0d322)
262
+ Download the [documentation](/attachment/68f3a3fa794f665ad2c0d2b3).
263
+ ```
264
+
265
+ **例(リンクベース):**
266
+
267
+ ```markdown
268
+ # Before upload
269
+ ![Logo](./images/logo.png)
270
+
271
+ # After upload (on GROWI)
272
+ ![Logo](/attachment/68f3a41c794f665ad2c0d322)
273
+ ```
274
+
275
+ 両方の検出方法で、複数のリンク形式(`./`あり・なし)がサポートされています。
276
+
277
+ ## 高度な使い方
278
+
279
+ ### 既存ページの更新
280
+
281
+ 設定ファイルで`update: true`を設定すると、既存ページを更新できます:
282
+
283
+ ```json
284
+ {
285
+ "url": "https://your-growi-instance.com",
286
+ "token": "your-api-token",
287
+ "update": true
288
+ }
289
+ ```
290
+
291
+ ### 特定のパスへのインポート
292
+
293
+ `basePath`を使用して、すべてのページを特定のパス配下にインポートできます:
294
+
295
+ ```json
296
+ {
297
+ "url": "https://your-growi-instance.com",
298
+ "token": "your-api-token",
299
+ "basePath": "/imported"
300
+ }
301
+ ```
302
+
303
+ **結果:**
304
+ ```
305
+ docs/guide.md → /imported/docs/guide
306
+ ```
307
+
308
+ ### 出力例
309
+
310
+ ```
311
+ Found 5 Markdown file(s) and 3 attachment(s)
312
+
313
+ [SUCCESS] docs/guide.md → /docs/guide (created)
314
+ [SUCCESS] docs/guide_attachment_diagram.svg → /docs/guide (attachment)
315
+ [SUCCESS] docs/guide.md → /docs/guide (attachment links replaced)
316
+ [SUCCESS] docs/api/overview.md → /docs/api/overview (created)
317
+ [SKIP] docs/api/auth.md → /docs/api/auth (page already exists)
318
+
319
+ Completed:
320
+ - Pages created: 2
321
+ - Pages updated: 0
322
+ - Pages skipped: 1
323
+ - Page errors: 0
324
+ - Attachments uploaded: 2
325
+ - Attachments skipped: 0
326
+ - Attachment errors: 0
327
+ - Link replacement errors: 0
328
+ ```
329
+
330
+ ## 必要環境
331
+
332
+ - Node.js 18以降
333
+ - REST API v3をサポートするGROWIインスタンス
334
+
335
+ ## ライセンス
336
+
337
+ MIT
338
+
339
+ ## 作者
340
+
341
+ [onozaty](https://github.com/onozaty)
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # growi-uploader
2
2
 
3
+ English | [日本語](README.ja.md)
4
+
3
5
  [![Test](https://github.com/onozaty/growi-uploader/actions/workflows/test.yaml/badge.svg)](https://github.com/onozaty/growi-uploader/actions/workflows/test.yaml)
4
6
  [![codecov](https://codecov.io/gh/onozaty/growi-uploader/graph/badge.svg?token=X0YN1OP5PB)](https://codecov.io/gh/onozaty/growi-uploader)
5
7
  [![npm version](https://badge.fury.io/js/@onozaty%2Fgrowi-uploader.svg)](https://www.npmjs.com/package/@onozaty/growi-uploader)
@@ -69,6 +71,7 @@ growi-uploader <source-dir> [options]
69
71
 
70
72
  **Options:**
71
73
  - `-c, --config <path>`: Path to config file (default: `growi-uploader.json`)
74
+ - `-v, --verbose`: Enable verbose error output with detailed information
72
75
  - `-V, --version`: Output the version number
73
76
  - `-h, --help`: Display help information
74
77
 
@@ -80,6 +83,9 @@ npx @onozaty/growi-uploader ./docs
80
83
 
81
84
  # Upload with custom config file
82
85
  npx @onozaty/growi-uploader ./docs -c my-config.json
86
+
87
+ # Upload with verbose error output
88
+ npx @onozaty/growi-uploader ./docs --verbose
83
89
  ```
84
90
 
85
91
  ## Directory Structure Example
@@ -108,6 +114,41 @@ docs/
108
114
  /docs/api/authentication (from api/authentication.md)
109
115
  ```
110
116
 
117
+ ## Page Name Normalization
118
+
119
+ To prevent API errors, page names are automatically normalized using the following rules:
120
+
121
+ ### Normalization Rules
122
+
123
+ 1. **Spaces around slashes** → Replaced with underscores
124
+ - `a / b.md` → `/a_/_b`
125
+
126
+ 2. **Special characters** → Replaced with safe alternatives:
127
+ - `+` → `-plus-`
128
+ - `?` → `-question-`
129
+ - `*` → `-asterisk-`
130
+ - `$` → `-dollar-`
131
+ - `^` → `-caret-`
132
+ - `%` → `-percent-`
133
+
134
+ 3. **Reserved page names** → Suffixed with underscore:
135
+ - `edit` → `edit_` (only when it's the last path segment)
136
+
137
+ ### Examples
138
+
139
+ ```
140
+ Local file GROWI page path
141
+ ──────────────────────────────────────────────────
142
+ C++.md → /C-plus--plus-
143
+ What?.md → /What-question-
144
+ C++ / Python?.md → /C-plus--plus-_/_Python-question-
145
+ edit.md → /edit_
146
+ docs/edit.md → /docs/edit_
147
+ docs/normal-page.md → /docs/normal-page (no change)
148
+ ```
149
+
150
+ This normalization ensures compatibility with GROWI's page naming requirements while preserving the readability of your file names.
151
+
111
152
  ## Configuration File
112
153
 
113
154
  Create a `growi-uploader.json` file in your project root:
@@ -117,7 +158,8 @@ Create a `growi-uploader.json` file in your project root:
117
158
  "url": "https://your-growi-instance.com",
118
159
  "token": "your-api-token",
119
160
  "basePath": "/imported",
120
- "update": true
161
+ "update": true,
162
+ "verbose": false
121
163
  }
122
164
  ```
123
165
 
@@ -129,6 +171,7 @@ Create a `growi-uploader.json` file in your project root:
129
171
  | `token` | string | ✅ | - | GROWI API access token |
130
172
  | `basePath` | string | ❌ | `/` | Base path for imported pages |
131
173
  | `update` | boolean | ❌ | `false` | Update existing pages if true, skip if false |
174
+ | `verbose` | boolean | ❌ | `false` | Enable verbose error output with detailed information |
132
175
 
133
176
  ### Getting an API Token
134
177
 
package/dist/index.mjs CHANGED
@@ -8,7 +8,7 @@ import { lookup } from "mime-types";
8
8
  import { glob } from "glob";
9
9
 
10
10
  //#region package.json
11
- var version = "1.4.0";
11
+ var version = "1.6.0";
12
12
 
13
13
  //#endregion
14
14
  //#region src/config.ts
@@ -19,6 +19,7 @@ var version = "1.4.0";
19
19
  * @returns Configuration object with defaults applied:
20
20
  * - basePath defaults to "/" if not specified or empty
21
21
  * - update defaults to false if not specified
22
+ * - verbose defaults to false if not specified
22
23
  * @throws Error if config file is not found or required fields (url, token) are missing
23
24
  */
24
25
  const loadConfig = (configPath) => {
@@ -32,7 +33,8 @@ const loadConfig = (configPath) => {
32
33
  url: input.url,
33
34
  token: input.token,
34
35
  basePath: input.basePath || "/",
35
- update: input.update ?? false
36
+ update: input.update ?? false,
37
+ verbose: input.verbose ?? false
36
38
  };
37
39
  } catch (error) {
38
40
  if (error.code === "ENOENT") throw new Error(`Config file not found: ${fullPath}`);
@@ -85,7 +87,8 @@ const putPage = (putPageBody, options) => {
85
87
  * @param token API token for authentication
86
88
  */
87
89
  const configureAxios = (growiUrl, token) => {
88
- axios.defaults.baseURL = `${growiUrl}/_api/v3`;
90
+ const baseUrl = growiUrl.replace(/\/+$/, "");
91
+ axios.defaults.baseURL = `${baseUrl}/_api/v3`;
89
92
  axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;
90
93
  };
91
94
  /**
@@ -95,11 +98,59 @@ const configureAxios = (growiUrl, token) => {
95
98
  * @returns Formatted error message string
96
99
  */
97
100
  const formatErrorMessage = (error) => {
98
- if (axios.isAxiosError(error)) return `${error.response?.status} ${error.response?.data?.message || error.message}`;
101
+ if (axios.isAxiosError(error)) {
102
+ const status = error.response?.status;
103
+ const statusText = error.response?.statusText;
104
+ const data = error.response?.data;
105
+ const parts = [];
106
+ if (status) parts.push(`HTTP ${status}${statusText ? ` ${statusText}` : ""}`);
107
+ if (data && typeof data === "object") {
108
+ const message = data.message || data.error || data.errors;
109
+ if (message) parts.push(message);
110
+ else if (Object.keys(data).length > 0) parts.push(JSON.stringify(data));
111
+ }
112
+ if (error.code) parts.push(`(${error.code})`);
113
+ if (parts.length === 0) return error.message;
114
+ return parts.join(" ");
115
+ }
99
116
  if (error instanceof Error) return error.message;
100
117
  return String(error);
101
118
  };
102
119
  /**
120
+ * Format error into a detailed error message for verbose mode
121
+ *
122
+ * @param error Error object from catch block
123
+ * @returns Detailed multi-line error message with all available information
124
+ */
125
+ const formatDetailedError = (error) => {
126
+ const lines = [];
127
+ if (axios.isAxiosError(error)) {
128
+ const status = error.response?.status;
129
+ const statusText = error.response?.statusText;
130
+ const data = error.response?.data;
131
+ if (status) lines.push(` HTTP Status: ${status}${statusText ? ` ${statusText}` : ""}`);
132
+ if (data) try {
133
+ const responseStr = typeof data === "object" ? JSON.stringify(data, null, 2).split("\n").map((line) => ` ${line}`).join("\n") : String(data);
134
+ lines.push(` Response Body:\n${responseStr}`);
135
+ } catch {
136
+ lines.push(` Response Body: ${String(data)}`);
137
+ }
138
+ if (error.code) lines.push(` Error Code: ${error.code}`);
139
+ if (error.config) lines.push(` Request: ${error.config.method?.toUpperCase()} ${error.config.url}`);
140
+ if (error.stack) {
141
+ const stackLines = error.stack.split("\n").slice(0, 5);
142
+ lines.push(` Stack Trace:\n${stackLines.map((line) => ` ${line}`).join("\n")}`);
143
+ }
144
+ } else if (error instanceof Error) {
145
+ lines.push(` Error: ${error.message}`);
146
+ if (error.stack) {
147
+ const stackLines = error.stack.split("\n").slice(0, 5);
148
+ lines.push(` Stack Trace:\n${stackLines.map((line) => ` ${line}`).join("\n")}`);
149
+ }
150
+ } else lines.push(` Error: ${String(error)}`);
151
+ return lines.length > 0 ? ` Details:\n${lines.join("\n")}` : "";
152
+ };
153
+ /**
103
154
  * Create a new page or update an existing page in GROWI
104
155
  *
105
156
  * @param file Markdown file to upload
@@ -133,7 +184,8 @@ const createOrUpdatePage = async (file, content, shouldUpdate) => {
133
184
  pageId: void 0,
134
185
  revisionId: void 0,
135
186
  action: "error",
136
- errorMessage: formatErrorMessage(error)
187
+ errorMessage: formatErrorMessage(error),
188
+ error
137
189
  };
138
190
  }
139
191
  try {
@@ -151,7 +203,8 @@ const createOrUpdatePage = async (file, content, shouldUpdate) => {
151
203
  pageId: void 0,
152
204
  revisionId: void 0,
153
205
  action: "error",
154
- errorMessage: formatErrorMessage(error)
206
+ errorMessage: formatErrorMessage(error),
207
+ error
155
208
  };
156
209
  }
157
210
  };
@@ -177,7 +230,8 @@ const updatePageContent = async (pageId, revisionId, content) => {
177
230
  } catch (error) {
178
231
  return {
179
232
  success: false,
180
- errorMessage: formatErrorMessage(error)
233
+ errorMessage: formatErrorMessage(error),
234
+ error
181
235
  };
182
236
  }
183
237
  };
@@ -207,7 +261,8 @@ const uploadAttachment = async (attachment, pageId, sourceDir) => {
207
261
  } catch (error) {
208
262
  return {
209
263
  success: false,
210
- errorMessage: formatErrorMessage(error)
264
+ errorMessage: formatErrorMessage(error),
265
+ error
211
266
  };
212
267
  }
213
268
  };
@@ -308,6 +363,19 @@ const mergeAttachments = (namingAttachments, linkAttachments) => {
308
363
  }
309
364
  return Array.from(map.values());
310
365
  };
366
+ /**
367
+ * Normalize GROWI page path to avoid API errors
368
+ *
369
+ * @param path Raw GROWI page path
370
+ * @returns Normalized page path
371
+ */
372
+ const normalizeGrowiPath = (path) => {
373
+ let normalized = path;
374
+ normalized = normalized.replace(/\s+\/\s+/g, "_/_");
375
+ normalized = normalized.replace(/\+/g, "-plus-").replace(/\?/g, "-question-").replace(/\*/g, "-asterisk-").replace(/\$/g, "-dollar-").replace(/\^/g, "-caret-").replace(/%/g, "-percent-");
376
+ normalized = normalized.replace(/\/edit$/g, "/edit_");
377
+ return normalized;
378
+ };
311
379
  const scanMarkdownFiles = async (sourceDir, basePath = "/") => {
312
380
  const pageFiles = (await glob("**/*.md", {
313
381
  cwd: sourceDir,
@@ -316,7 +384,8 @@ const scanMarkdownFiles = async (sourceDir, basePath = "/") => {
316
384
  pageFiles.sort();
317
385
  return await Promise.all(pageFiles.map(async (file) => {
318
386
  const content = readFileSync(join(sourceDir, file), "utf-8");
319
- const growiPath = join(basePath, file.replace(/\.md$/, "")).replace(/\\/g, "/");
387
+ let growiPath = join(basePath, file.replace(/\.md$/, "")).replace(/\\/g, "/");
388
+ growiPath = normalizeGrowiPath(growiPath);
320
389
  const dir = dirname(file);
321
390
  const pageName = basename(file, ".md");
322
391
  const attachments = mergeAttachments((await glob(dir === "." ? `${pageName}_attachment_*` : `${dir}/${pageName}_attachment_*`, {
@@ -469,6 +538,7 @@ const uploadFiles = async (files, sourceDir, config) => {
469
538
  } else if (result.action === "error") {
470
539
  stats.pageErrors++;
471
540
  console.error(`[ERROR] ${file.localPath} → ${file.growiPath} (${result.errorMessage || "unknown error"})`);
541
+ if (config.verbose && result.error) console.error(formatDetailedError(result.error));
472
542
  }
473
543
  if (result.pageId && (result.action === "created" || result.action === "updated")) {
474
544
  let currentContent = content;
@@ -489,6 +559,7 @@ const uploadFiles = async (files, sourceDir, config) => {
489
559
  } else {
490
560
  stats.attachmentErrors++;
491
561
  console.error(`[ERROR] ${attachment.localPath} → ${file.growiPath} (${attachmentResult.errorMessage || "failed to upload attachment"})`);
562
+ if (config.verbose && attachmentResult.error) console.error(formatDetailedError(attachmentResult.error));
492
563
  }
493
564
  }
494
565
  if (hasAttachments && latestRevisionId) {
@@ -503,6 +574,7 @@ const uploadFiles = async (files, sourceDir, config) => {
503
574
  currentRevisionId = updateResult.revisionId;
504
575
  } else {
505
576
  console.error(`[ERROR] ${file.localPath} → ${file.growiPath} (failed to update attachment links: ${updateResult.errorMessage || "unknown error"})`);
577
+ if (config.verbose && updateResult.error) console.error(formatDetailedError(updateResult.error));
506
578
  stats.linkReplacementErrors++;
507
579
  }
508
580
  }
@@ -517,6 +589,7 @@ const uploadFiles = async (files, sourceDir, config) => {
517
589
  currentRevisionId = updateResult.revisionId;
518
590
  } else {
519
591
  console.error(`[ERROR] ${file.localPath} → ${file.growiPath} (failed to update page links: ${updateResult.errorMessage || "unknown error"})`);
592
+ if (config.verbose && updateResult.error) console.error(formatDetailedError(updateResult.error));
520
593
  stats.linkReplacementErrors++;
521
594
  }
522
595
  }
@@ -532,8 +605,9 @@ const uploadFiles = async (files, sourceDir, config) => {
532
605
  //#endregion
533
606
  //#region src/index.ts
534
607
  const program = new Command();
535
- const main = async (sourceDir, configPath) => {
608
+ const main = async (sourceDir, configPath, verboseOverride) => {
536
609
  const config = loadConfig(configPath);
610
+ if (verboseOverride !== void 0) config.verbose = verboseOverride;
537
611
  const sourceDirPath = resolve(sourceDir);
538
612
  configureAxios(config.url, config.token);
539
613
  const files = await scanMarkdownFiles(sourceDirPath, config.basePath);
@@ -550,9 +624,9 @@ const main = async (sourceDir, configPath) => {
550
624
  console.log(`- Attachment errors: ${stats.attachmentErrors}`);
551
625
  console.log(`- Link replacement errors: ${stats.linkReplacementErrors}`);
552
626
  };
553
- program.name("growi-uploader").description("A content uploader for GROWI").version(version).argument("<source-dir>", "Source directory containing Markdown files").option("-c, --config <path>", "Path to config file", "growi-uploader.json").action(async (sourceDir, options) => {
627
+ program.name("growi-uploader").description("A content uploader for GROWI").version(version).argument("<source-dir>", "Source directory containing Markdown files").option("-c, --config <path>", "Path to config file", "growi-uploader.json").option("-v, --verbose", "Enable verbose error output with detailed information").action(async (sourceDir, options) => {
554
628
  try {
555
- await main(sourceDir, options.config);
629
+ await main(sourceDir, options.config, options.verbose);
556
630
  } catch (error) {
557
631
  console.error("Error:", error instanceof Error ? error.message : error);
558
632
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onozaty/growi-uploader",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "A content uploader for GROWI",
5
5
  "type": "module",
6
6
  "bin": {