@onozaty/growi-uploader 1.0.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +247 -0
  3. package/dist/index.mjs +397 -0
  4. package/package.json +57 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 onozaty
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,247 @@
1
+ # growi-uploader
2
+
3
+ [![npm version](https://badge.fury.io/js/@onozaty%2Fgrowi-uploader.svg)](https://www.npmjs.com/package/@onozaty/growi-uploader)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ A CLI tool to batch upload local Markdown files and attachments to [GROWI](https://growi.org/) Wiki.
7
+
8
+ ## Features
9
+
10
+ - 📁 **Preserves directory structure** - Local folder hierarchy becomes GROWI page hierarchy
11
+ - 📝 **Markdown file upload** - Creates or updates GROWI pages from `.md` files
12
+ - 📎 **Automatic attachment detection** - Files matching `<page>_attachment_<file>` pattern are uploaded as attachments
13
+ - 🔗 **Link replacement** - Automatically converts local attachment links to GROWI format (`/attachment/{id}`)
14
+ - 🖼️ **Image embedding** - Supports image links (`![alt](image.png)`) with automatic conversion
15
+ - ⚙️ **Flexible configuration** - Control base path, update behavior, and more
16
+
17
+ ## Quick Start
18
+
19
+ 1. Create a configuration file `growi-uploader.json`:
20
+
21
+ ```json
22
+ {
23
+ "url": "https://your-growi-instance.com",
24
+ "token": "your-api-token",
25
+ "basePath": "/",
26
+ "update": false
27
+ }
28
+ ```
29
+
30
+ 2. Run with npx (no installation required):
31
+
32
+ ```bash
33
+ npx @onozaty/growi-uploader ./docs
34
+ ```
35
+
36
+ That's it! Your local `./docs` directory will be uploaded to GROWI.
37
+
38
+ ## Installation
39
+
40
+ ### Using npx (Recommended)
41
+
42
+ No installation required. Just run:
43
+
44
+ ```bash
45
+ npx @onozaty/growi-uploader <source-dir>
46
+ ```
47
+
48
+ ### Global Installation
49
+
50
+ For frequent use, you can install globally:
51
+
52
+ ```bash
53
+ npm install -g @onozaty/growi-uploader
54
+ growi-uploader <source-dir>
55
+ ```
56
+
57
+ ## Usage
58
+
59
+ ### Basic Command
60
+
61
+ ```bash
62
+ growi-uploader <source-dir> [options]
63
+ ```
64
+
65
+ **Arguments:**
66
+ - `<source-dir>`: Path to the directory containing Markdown files
67
+
68
+ **Options:**
69
+ - `-c, --config <path>`: Path to config file (default: `growi-uploader.json`)
70
+ - `-V, --version`: Output the version number
71
+ - `-h, --help`: Display help information
72
+
73
+ ### Examples
74
+
75
+ ```bash
76
+ # Upload with default config file
77
+ npx @onozaty/growi-uploader ./docs
78
+
79
+ # Upload with custom config file
80
+ npx @onozaty/growi-uploader ./docs -c my-config.json
81
+ ```
82
+
83
+ ## Directory Structure Example
84
+
85
+ ### Local Directory
86
+
87
+ ```
88
+ docs/
89
+ guide.md
90
+ guide_attachment_diagram.svg
91
+ guide_attachment_sample.txt
92
+ api/
93
+ overview.md
94
+ overview_attachment_example.json
95
+ authentication.md
96
+ ```
97
+
98
+ ### Resulting GROWI Pages
99
+
100
+ ```
101
+ /docs/guide (from guide.md)
102
+ └─ diagram.svg (attachment)
103
+ └─ sample.txt (attachment)
104
+ /docs/api/overview (from api/overview.md)
105
+ └─ example.json (attachment)
106
+ /docs/api/authentication (from api/authentication.md)
107
+ ```
108
+
109
+ ## Configuration File
110
+
111
+ Create a `growi-uploader.json` file in your project root:
112
+
113
+ ```json
114
+ {
115
+ "url": "https://your-growi-instance.com",
116
+ "token": "your-api-token",
117
+ "basePath": "/imported",
118
+ "update": true
119
+ }
120
+ ```
121
+
122
+ ### Configuration Options
123
+
124
+ | Option | Type | Required | Default | Description |
125
+ |--------|------|----------|---------|-------------|
126
+ | `url` | string | ✅ | - | GROWI instance URL |
127
+ | `token` | string | ✅ | - | GROWI API access token |
128
+ | `basePath` | string | ❌ | `/` | Base path for imported pages |
129
+ | `update` | boolean | ❌ | `false` | Update existing pages if true, skip if false |
130
+
131
+ ### Getting an API Token
132
+
133
+ 1. Log in to your GROWI instance
134
+ 2. Go to **User Settings** → **API Settings**
135
+ 3. Click **Issue new token**
136
+ 4. Copy the generated token to your config file
137
+
138
+ ## Attachment Files
139
+
140
+ ### Naming Convention
141
+
142
+ Attachment files must follow this naming pattern:
143
+
144
+ ```
145
+ <page-name>_attachment_<filename>
146
+ ```
147
+
148
+ **Example:**
149
+ ```
150
+ guide.md → GROWI page: /guide
151
+ guide_attachment_image.png → Attached to /guide
152
+ guide_attachment_document.pdf → Attached to /guide
153
+ ```
154
+
155
+ ### Automatic Link Replacement
156
+
157
+ Markdown links to attachments are automatically converted to GROWI format.
158
+
159
+ **Local Markdown (before upload):**
160
+ ```markdown
161
+ # User Guide
162
+
163
+ ![Diagram](./guide_attachment_diagram.png)
164
+
165
+ Download the [documentation](guide_attachment_document.pdf).
166
+ ```
167
+
168
+ **GROWI Page (after upload):**
169
+ ```markdown
170
+ # User Guide
171
+
172
+ ![Diagram](/attachment/68f3a41c794f665ad2c0d322)
173
+
174
+ Download the [documentation](/attachment/68f3a3fa794f665ad2c0d2b3).
175
+ ```
176
+
177
+ ### Supported Link Formats
178
+
179
+ Both formats are automatically detected and converted:
180
+
181
+ 1. **Filename only**: `![alt](guide_attachment_image.png)`
182
+ 2. **Relative path**: `![alt](./guide_attachment_image.png)`
183
+
184
+ ## Advanced Usage
185
+
186
+ ### Update Existing Pages
187
+
188
+ Set `update: true` in your config file to update existing pages:
189
+
190
+ ```json
191
+ {
192
+ "url": "https://your-growi-instance.com",
193
+ "token": "your-api-token",
194
+ "update": true
195
+ }
196
+ ```
197
+
198
+ ### Import to Specific Path
199
+
200
+ Use `basePath` to import all pages under a specific path:
201
+
202
+ ```json
203
+ {
204
+ "url": "https://your-growi-instance.com",
205
+ "token": "your-api-token",
206
+ "basePath": "/imported"
207
+ }
208
+ ```
209
+
210
+ **Result:**
211
+ ```
212
+ docs/guide.md → /imported/docs/guide
213
+ ```
214
+
215
+ ### Output Example
216
+
217
+ ```
218
+ Found 5 Markdown file(s) and 3 attachment(s)
219
+
220
+ [SUCCESS] docs/guide.md → /docs/guide (created)
221
+ [SUCCESS] docs/guide_attachment_diagram.svg → /docs/guide (attachment)
222
+ [SUCCESS] docs/guide.md → /docs/guide (attachment links replaced)
223
+ [SUCCESS] docs/api/overview.md → /docs/api/overview (created)
224
+ [SKIP] docs/api/auth.md → /docs/api/auth (page already exists)
225
+
226
+ Completed:
227
+ - Pages created: 2
228
+ - Pages updated: 0
229
+ - Pages skipped: 1
230
+ - Page errors: 0
231
+ - Attachments uploaded: 2
232
+ - Attachments skipped: 0
233
+ - Attachment errors: 0
234
+ ```
235
+
236
+ ## Requirements
237
+
238
+ - Node.js 18 or later
239
+ - GROWI instance with REST API v3 support
240
+
241
+ ## License
242
+
243
+ MIT
244
+
245
+ ## Author
246
+
247
+ [onozaty](https://github.com/onozaty)
package/dist/index.mjs ADDED
@@ -0,0 +1,397 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { readFileSync } from "node:fs";
5
+ import { glob } from "glob";
6
+ import * as axios$1 from "axios";
7
+ import axios from "axios";
8
+ import { lookup } from "mime-types";
9
+
10
+ //#region src/config.ts
11
+ const loadConfig = (configPath) => {
12
+ const fullPath = resolve(configPath);
13
+ try {
14
+ const content = readFileSync(fullPath, "utf-8");
15
+ const config = JSON.parse(content);
16
+ if (!config.url) throw new Error("Missing required field: url");
17
+ if (!config.token) throw new Error("Missing required field: token");
18
+ if (config.basePath === void 0) config.basePath = "/";
19
+ if (config.update === void 0) config.update = false;
20
+ return config;
21
+ } catch (error) {
22
+ if (error.code === "ENOENT") throw new Error(`Config file not found: ${fullPath}`);
23
+ throw error;
24
+ }
25
+ };
26
+
27
+ //#endregion
28
+ //#region src/scanner.ts
29
+ const scanMarkdownFiles = async (sourceDir, basePath = "/") => {
30
+ const mdFiles = await glob("**/*.md", {
31
+ cwd: sourceDir,
32
+ absolute: false
33
+ });
34
+ mdFiles.sort();
35
+ return await Promise.all(mdFiles.map(async (file) => {
36
+ const content = readFileSync(join(sourceDir, file), "utf-8");
37
+ const growiPath = join(basePath, file.replace(/\.md$/, "")).replace(/\\/g, "/");
38
+ const dir = dirname(file);
39
+ const pageName = basename(file, ".md");
40
+ const attachments = (await glob(dir === "." ? `${pageName}_attachment_*` : `${dir}/${pageName}_attachment_*`, {
41
+ cwd: sourceDir,
42
+ absolute: false
43
+ })).map((attachFile) => ({
44
+ localPath: attachFile,
45
+ fileName: basename(attachFile).replace(`${pageName}_attachment_`, "")
46
+ }));
47
+ return {
48
+ localPath: file,
49
+ growiPath: growiPath.startsWith("/") ? growiPath : `/${growiPath}`,
50
+ content,
51
+ attachments
52
+ };
53
+ }));
54
+ };
55
+
56
+ //#endregion
57
+ //#region src/growi.ts
58
+ /**
59
+ * Add attachment to the page
60
+ * @summary /attachment
61
+ */
62
+ const postAttachment = (postAttachmentBody, options) => {
63
+ return axios$1.default.post(`/attachment`, postAttachmentBody, options);
64
+ };
65
+ /**
66
+ * get page by pagePath or pageId
67
+ * @summary Get page
68
+ */
69
+ const getPage = (params, options) => {
70
+ return axios$1.default.get(`/page`, {
71
+ ...options,
72
+ params: {
73
+ ...params,
74
+ ...options?.params
75
+ }
76
+ });
77
+ };
78
+ /**
79
+ * Create page
80
+ * @summary Create page
81
+ */
82
+ const postPage = (postPageBody, options) => {
83
+ return axios$1.default.post(`/page`, postPageBody, options);
84
+ };
85
+ /**
86
+ * Update page
87
+ */
88
+ const putPage = (putPageBody, options) => {
89
+ return axios$1.default.put(`/page`, putPageBody, options);
90
+ };
91
+
92
+ //#endregion
93
+ //#region src/uploader.ts
94
+ const configureAxios = (growiUrl, token) => {
95
+ axios.defaults.baseURL = `${growiUrl}/_api/v3`;
96
+ axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;
97
+ };
98
+ const createOrUpdatePage = async (file, config) => {
99
+ try {
100
+ const actualResponse = (await getPage({ path: file.growiPath })).data;
101
+ if (actualResponse.page) {
102
+ const pageId = actualResponse.page._id;
103
+ const revisionId = actualResponse.page.revision?._id;
104
+ if (!config.update) {
105
+ console.log(`[SKIP] ${file.localPath} → ${file.growiPath} (page already exists)`);
106
+ return {
107
+ pageId,
108
+ revisionId,
109
+ action: "skipped"
110
+ };
111
+ }
112
+ const newRevisionId = (await putPage({
113
+ body: file.content,
114
+ pageId,
115
+ revisionId
116
+ })).data.page?.revision;
117
+ console.log(`[SUCCESS] ${file.localPath} → ${file.growiPath} (updated)`);
118
+ return {
119
+ pageId,
120
+ revisionId: newRevisionId || revisionId,
121
+ action: "updated"
122
+ };
123
+ }
124
+ } catch (error) {
125
+ if (!axios.isAxiosError(error) || error.response?.status !== 404) {
126
+ if (axios.isAxiosError(error)) {
127
+ const status = error.response?.status;
128
+ const message = error.response?.data?.message || error.message;
129
+ console.error(`[ERROR] ${file.localPath} → ${file.growiPath} (${status} ${message})`);
130
+ } else console.error(`[ERROR] ${file.localPath} → ${file.growiPath} (${error})`);
131
+ return {
132
+ pageId: void 0,
133
+ revisionId: void 0,
134
+ action: "error"
135
+ };
136
+ }
137
+ }
138
+ try {
139
+ const response = await postPage({
140
+ path: file.growiPath,
141
+ body: file.content
142
+ });
143
+ const pageId = response.data.page?._id;
144
+ const revisionId = response.data.page?.revision;
145
+ console.log(`[SUCCESS] ${file.localPath} → ${file.growiPath} (created)`);
146
+ return {
147
+ pageId,
148
+ revisionId,
149
+ action: "created"
150
+ };
151
+ } catch (error) {
152
+ if (axios.isAxiosError(error)) {
153
+ const status = error.response?.status;
154
+ const message = error.response?.data?.message || error.message;
155
+ console.error(`[ERROR] ${file.localPath} → ${file.growiPath} (${status} ${message})`);
156
+ } else console.error(`[ERROR] ${file.localPath} → ${file.growiPath} (${error})`);
157
+ return {
158
+ pageId: void 0,
159
+ revisionId: void 0,
160
+ action: "error"
161
+ };
162
+ }
163
+ };
164
+ /**
165
+ * Replace attachment links in Markdown content with GROWI format
166
+ *
167
+ * Supports:
168
+ * - Filename only: guide_attachment_file.png
169
+ * - Relative path: ./guide_attachment_file.png
170
+ *
171
+ * @param markdown Original Markdown content
172
+ * @param attachments List of attachments with their IDs
173
+ * @param pageName Page name (without .md extension)
174
+ * @returns Object with replaced content and whether any replacement occurred
175
+ */
176
+ const replaceAttachmentLinks = (markdown, attachments, pageName) => {
177
+ let result = markdown;
178
+ let replaced = false;
179
+ for (const attachment of attachments) {
180
+ if (!attachment.attachmentId) continue;
181
+ const localFileName = `${pageName}_attachment_${attachment.fileName}`;
182
+ const growiPath = `/attachment/${attachment.attachmentId}`;
183
+ const escapedFileName = localFileName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
184
+ const patterns = [escapedFileName, `\\./${escapedFileName}`];
185
+ for (const pattern of patterns) {
186
+ const imgRegex = new RegExp(`!\\[([^\\]]*)\\]\\(${pattern}\\)`, "g");
187
+ if (imgRegex.test(result)) {
188
+ replaced = true;
189
+ result = result.replace(imgRegex, `![$1](${growiPath})`);
190
+ }
191
+ const linkRegex = new RegExp(`(?<!!)\\[([^\\]]*)\\]\\(${pattern}\\)`, "g");
192
+ if (linkRegex.test(result)) {
193
+ replaced = true;
194
+ result = result.replace(linkRegex, `[$1](${growiPath})`);
195
+ }
196
+ }
197
+ }
198
+ return {
199
+ content: result,
200
+ replaced
201
+ };
202
+ };
203
+ /**
204
+ * Update page content only (for re-updating after attachment link replacement)
205
+ *
206
+ * @param pageId Page ID
207
+ * @param revisionId Current revision ID
208
+ * @param content New Markdown content
209
+ * @param growiPath GROWI page path (for logging)
210
+ * @returns True if update succeeded
211
+ */
212
+ const updatePageContent = async (pageId, revisionId, content, growiPath) => {
213
+ try {
214
+ await putPage({
215
+ body: content,
216
+ pageId,
217
+ revisionId
218
+ });
219
+ return true;
220
+ } catch (error) {
221
+ if (axios.isAxiosError(error)) {
222
+ const status = error.response?.status;
223
+ const message = error.response?.data?.message || error.message;
224
+ console.error(`[WARN] Failed to update attachment links for ${growiPath} (${status} ${message})`);
225
+ } else console.error(`[WARN] Failed to update attachment links for ${growiPath}`);
226
+ return false;
227
+ }
228
+ };
229
+ const uploadAttachment = async (attachment, pageId, growiPath, sourceDir) => {
230
+ try {
231
+ const fileBuffer = readFileSync(join(sourceDir, attachment.localPath));
232
+ const mimeType = lookup(attachment.fileName) || "application/octet-stream";
233
+ const formData = new FormData();
234
+ formData.append("page_id", pageId);
235
+ formData.append("file", new Blob([fileBuffer], { type: mimeType }), attachment.fileName);
236
+ const response = await postAttachment(formData);
237
+ const attachmentId = response.data.attachment?._id;
238
+ const revisionId = typeof response.data.revision === "string" ? response.data.revision : void 0;
239
+ console.log(`[SUCCESS] ${attachment.localPath} → ${growiPath} (attachment)`);
240
+ if (attachmentId && revisionId) return {
241
+ success: true,
242
+ attachmentId,
243
+ revisionId
244
+ };
245
+ else if (attachmentId) return {
246
+ success: true,
247
+ attachmentId
248
+ };
249
+ else if (revisionId) return {
250
+ success: true,
251
+ revisionId
252
+ };
253
+ else return { success: true };
254
+ } catch (error) {
255
+ if (axios.isAxiosError(error)) {
256
+ const status = error.response?.status;
257
+ const message = error.response?.data?.message || error.message;
258
+ console.error(`[ERROR] ${attachment.localPath} → ${growiPath} (${status} ${message})`);
259
+ } else console.error(`[ERROR] ${attachment.localPath} → ${growiPath} (${error})`);
260
+ return { success: false };
261
+ }
262
+ };
263
+
264
+ //#endregion
265
+ //#region package.json
266
+ var name = "@onozaty/growi-uploader";
267
+ var version = "1.0.0";
268
+ var description = "A content uploader for GROWI";
269
+ var type = "module";
270
+ var bin = { "growi-uploader": "./dist/index.mjs" };
271
+ var files = ["dist/**/*"];
272
+ var scripts = {
273
+ "build": "pnpm gen && tsdown",
274
+ "prepublishOnly": "pnpm run build",
275
+ "dev": "tsx src/index.ts",
276
+ "gen": "orval",
277
+ "lint": "eslint .",
278
+ "typecheck": "tsc --noEmit",
279
+ "check": "pnpm run lint && pnpm run typecheck",
280
+ "format": "prettier --write src/"
281
+ };
282
+ var repository = {
283
+ "type": "git",
284
+ "url": "git+https://github.com/onozaty/growi-uploader.git"
285
+ };
286
+ var keywords = ["growi", "uploader"];
287
+ var author = "onozaty";
288
+ var license = "MIT";
289
+ var bugs = { "url": "https://github.com/onozaty/growi-uploader/issues" };
290
+ var homepage = "https://github.com/onozaty/growi-uploader#readme";
291
+ var packageManager = "pnpm@10.12.1";
292
+ var devDependencies = {
293
+ "@eslint/js": "^9.37.0",
294
+ "@types/mime-types": "^3.0.1",
295
+ "@types/node": "^24.7.2",
296
+ "eslint": "^9.37.0",
297
+ "globals": "^16.4.0",
298
+ "jiti": "^2.6.1",
299
+ "orval": "^7.13.2",
300
+ "prettier": "^3.6.2",
301
+ "tsdown": "^0.15.7",
302
+ "tsx": "^4.20.6",
303
+ "typescript": "^5.9.3",
304
+ "typescript-eslint": "^8.46.1"
305
+ };
306
+ var dependencies = {
307
+ "axios": "^1.12.2",
308
+ "commander": "^14.0.1",
309
+ "glob": "^11.0.3",
310
+ "mime-types": "^3.0.1"
311
+ };
312
+ var package_default = {
313
+ name,
314
+ version,
315
+ description,
316
+ type,
317
+ bin,
318
+ files,
319
+ scripts,
320
+ repository,
321
+ keywords,
322
+ author,
323
+ license,
324
+ bugs,
325
+ homepage,
326
+ packageManager,
327
+ devDependencies,
328
+ dependencies
329
+ };
330
+
331
+ //#endregion
332
+ //#region src/index.ts
333
+ const program = new Command();
334
+ program.name("growi-uploader").description("A content uploader for GROWI").version(package_default.version).argument("<source-dir>", "Source directory containing Markdown files").option("-c, --config <path>", "Path to config file", "growi-uploader.json").action(async (sourceDir, options) => {
335
+ try {
336
+ const config = loadConfig(options.config);
337
+ const sourceDirPath = resolve(sourceDir);
338
+ configureAxios(config.url, config.token);
339
+ const files$1 = await scanMarkdownFiles(sourceDirPath, config.basePath);
340
+ const totalAttachments = files$1.reduce((sum, file) => sum + file.attachments.length, 0);
341
+ console.log(`Found ${files$1.length} Markdown file(s) and ${totalAttachments} attachment(s)\n`);
342
+ let pagesCreated = 0;
343
+ let pagesUpdated = 0;
344
+ let pagesSkipped = 0;
345
+ let pageErrors = 0;
346
+ let attachmentsUploaded = 0;
347
+ let attachmentsSkipped = 0;
348
+ let attachmentErrors = 0;
349
+ for (const file of files$1) {
350
+ const result = await createOrUpdatePage(file, config);
351
+ if (result.action === "created") pagesCreated++;
352
+ else if (result.action === "updated") pagesUpdated++;
353
+ else if (result.action === "skipped") pagesSkipped++;
354
+ else if (result.action === "error") pageErrors++;
355
+ if (result.pageId && (result.action === "created" || result.action === "updated") && file.attachments.length > 0) {
356
+ let hasAttachments = false;
357
+ let latestRevisionId = result.revisionId;
358
+ for (const attachment of file.attachments) {
359
+ const attachmentResult = await uploadAttachment(attachment, result.pageId, file.growiPath, sourceDirPath);
360
+ if (attachmentResult.success) {
361
+ attachmentsUploaded++;
362
+ if (attachmentResult.attachmentId) {
363
+ attachment.attachmentId = attachmentResult.attachmentId;
364
+ hasAttachments = true;
365
+ }
366
+ if (attachmentResult.revisionId) latestRevisionId = attachmentResult.revisionId;
367
+ } else attachmentErrors++;
368
+ }
369
+ if (hasAttachments && latestRevisionId) {
370
+ const pageName = basename(file.localPath, ".md");
371
+ const { content: replacedContent, replaced } = replaceAttachmentLinks(file.content, file.attachments, pageName);
372
+ if (replaced) {
373
+ if (await updatePageContent(result.pageId, latestRevisionId, replacedContent, file.growiPath)) console.log(`[SUCCESS] ${file.localPath} → ${file.growiPath} (attachment links replaced)`);
374
+ }
375
+ }
376
+ } else if (file.attachments.length > 0) for (const attachment of file.attachments) {
377
+ console.log(`[SKIP] ${attachment.localPath} → ${file.growiPath} (attachment skipped)`);
378
+ attachmentsSkipped++;
379
+ }
380
+ }
381
+ console.log("\nCompleted:");
382
+ console.log(`- Pages created: ${pagesCreated}`);
383
+ console.log(`- Pages updated: ${pagesUpdated}`);
384
+ console.log(`- Pages skipped: ${pagesSkipped}`);
385
+ console.log(`- Page errors: ${pageErrors}`);
386
+ console.log(`- Attachments uploaded: ${attachmentsUploaded}`);
387
+ console.log(`- Attachments skipped: ${attachmentsSkipped}`);
388
+ console.log(`- Attachment errors: ${attachmentErrors}`);
389
+ } catch (error) {
390
+ console.error("Error:", error instanceof Error ? error.message : error);
391
+ process.exit(1);
392
+ }
393
+ });
394
+ program.parse();
395
+
396
+ //#endregion
397
+ export { };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@onozaty/growi-uploader",
3
+ "version": "1.0.0",
4
+ "description": "A content uploader for GROWI",
5
+ "type": "module",
6
+ "bin": {
7
+ "growi-uploader": "./dist/index.mjs"
8
+ },
9
+ "files": [
10
+ "dist/**/*"
11
+ ],
12
+ "scripts": {
13
+ "build": "pnpm gen && tsdown",
14
+ "prepublishOnly": "pnpm run build",
15
+ "dev": "tsx src/index.ts",
16
+ "gen": "orval",
17
+ "lint": "eslint .",
18
+ "typecheck": "tsc --noEmit",
19
+ "check": "pnpm run lint && pnpm run typecheck",
20
+ "format": "prettier --write src/"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/onozaty/growi-uploader.git"
25
+ },
26
+ "keywords": [
27
+ "growi",
28
+ "uploader"
29
+ ],
30
+ "author": "onozaty",
31
+ "license": "MIT",
32
+ "bugs": {
33
+ "url": "https://github.com/onozaty/growi-uploader/issues"
34
+ },
35
+ "homepage": "https://github.com/onozaty/growi-uploader#readme",
36
+ "packageManager": "pnpm@10.12.1",
37
+ "devDependencies": {
38
+ "@eslint/js": "^9.37.0",
39
+ "@types/mime-types": "^3.0.1",
40
+ "@types/node": "^24.7.2",
41
+ "eslint": "^9.37.0",
42
+ "globals": "^16.4.0",
43
+ "jiti": "^2.6.1",
44
+ "orval": "^7.13.2",
45
+ "prettier": "^3.6.2",
46
+ "tsdown": "^0.15.7",
47
+ "tsx": "^4.20.6",
48
+ "typescript": "^5.9.3",
49
+ "typescript-eslint": "^8.46.1"
50
+ },
51
+ "dependencies": {
52
+ "axios": "^1.12.2",
53
+ "commander": "^14.0.1",
54
+ "glob": "^11.0.3",
55
+ "mime-types": "^3.0.1"
56
+ }
57
+ }