@aaronshaf/confluence-cli 0.1.15
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 +21 -0
- package/README.md +69 -0
- package/package.json +73 -0
- package/src/cli/commands/attachments.ts +113 -0
- package/src/cli/commands/clone.ts +188 -0
- package/src/cli/commands/comments.ts +56 -0
- package/src/cli/commands/create.ts +58 -0
- package/src/cli/commands/delete.ts +46 -0
- package/src/cli/commands/doctor.ts +161 -0
- package/src/cli/commands/duplicate-check.ts +89 -0
- package/src/cli/commands/file-rename.ts +113 -0
- package/src/cli/commands/folder-hierarchy.ts +241 -0
- package/src/cli/commands/info.ts +56 -0
- package/src/cli/commands/labels.ts +53 -0
- package/src/cli/commands/move.ts +23 -0
- package/src/cli/commands/open.ts +145 -0
- package/src/cli/commands/pull.ts +241 -0
- package/src/cli/commands/push-errors.ts +40 -0
- package/src/cli/commands/push.ts +699 -0
- package/src/cli/commands/search.ts +62 -0
- package/src/cli/commands/setup.ts +124 -0
- package/src/cli/commands/spaces.ts +42 -0
- package/src/cli/commands/status.ts +88 -0
- package/src/cli/commands/tree.ts +190 -0
- package/src/cli/help.ts +425 -0
- package/src/cli/index.ts +413 -0
- package/src/cli/utils/browser.ts +34 -0
- package/src/cli/utils/progress-reporter.ts +49 -0
- package/src/cli.ts +6 -0
- package/src/lib/config.ts +156 -0
- package/src/lib/confluence-client/attachment-operations.ts +221 -0
- package/src/lib/confluence-client/client.ts +653 -0
- package/src/lib/confluence-client/comment-operations.ts +60 -0
- package/src/lib/confluence-client/folder-operations.ts +203 -0
- package/src/lib/confluence-client/index.ts +47 -0
- package/src/lib/confluence-client/label-operations.ts +102 -0
- package/src/lib/confluence-client/page-operations.ts +270 -0
- package/src/lib/confluence-client/search-operations.ts +60 -0
- package/src/lib/confluence-client/types.ts +329 -0
- package/src/lib/confluence-client/user-operations.ts +58 -0
- package/src/lib/dependency-sorter.ts +233 -0
- package/src/lib/errors.ts +237 -0
- package/src/lib/file-scanner.ts +195 -0
- package/src/lib/formatters.ts +314 -0
- package/src/lib/health-check.ts +204 -0
- package/src/lib/markdown/converter.ts +427 -0
- package/src/lib/markdown/frontmatter.ts +116 -0
- package/src/lib/markdown/html-converter.ts +398 -0
- package/src/lib/markdown/index.ts +21 -0
- package/src/lib/markdown/link-converter.ts +189 -0
- package/src/lib/markdown/reference-updater.ts +251 -0
- package/src/lib/markdown/slugify.ts +32 -0
- package/src/lib/page-state.ts +195 -0
- package/src/lib/resolve-page-target.ts +33 -0
- package/src/lib/space-config.ts +264 -0
- package/src/lib/sync/cleanup.ts +50 -0
- package/src/lib/sync/folder-path.ts +61 -0
- package/src/lib/sync/index.ts +2 -0
- package/src/lib/sync/link-resolution-pass.ts +139 -0
- package/src/lib/sync/sync-engine.ts +681 -0
- package/src/lib/sync/sync-specific.ts +221 -0
- package/src/lib/sync/types.ts +42 -0
- package/src/test/attachments.test.ts +68 -0
- package/src/test/clone.test.ts +373 -0
- package/src/test/comments.test.ts +53 -0
- package/src/test/config.test.ts +209 -0
- package/src/test/confluence-client.test.ts +535 -0
- package/src/test/delete.test.ts +39 -0
- package/src/test/dependency-sorter.test.ts +384 -0
- package/src/test/errors.test.ts +199 -0
- package/src/test/file-rename.test.ts +305 -0
- package/src/test/file-scanner.test.ts +331 -0
- package/src/test/folder-hierarchy.test.ts +337 -0
- package/src/test/formatters.test.ts +213 -0
- package/src/test/html-converter.test.ts +399 -0
- package/src/test/info.test.ts +56 -0
- package/src/test/labels.test.ts +70 -0
- package/src/test/link-conversion-integration.test.ts +189 -0
- package/src/test/link-converter.test.ts +413 -0
- package/src/test/link-resolution-pass.test.ts +368 -0
- package/src/test/markdown.test.ts +443 -0
- package/src/test/mocks/handlers.ts +228 -0
- package/src/test/move.test.ts +53 -0
- package/src/test/msw-schema-validation.ts +151 -0
- package/src/test/page-state.test.ts +542 -0
- package/src/test/push.test.ts +551 -0
- package/src/test/reference-updater.test.ts +293 -0
- package/src/test/resolve-page-target.test.ts +55 -0
- package/src/test/search.test.ts +64 -0
- package/src/test/setup-msw.ts +75 -0
- package/src/test/space-config.test.ts +516 -0
- package/src/test/spaces.test.ts +53 -0
- package/src/test/sync-engine.test.ts +486 -0
- package/src/types/turndown-plugin-gfm.d.ts +9 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { buildPageStateFromFiles, getPageInfoByPath, scanDirectoryForPages } from '../lib/page-state.js';
|
|
6
|
+
|
|
7
|
+
describe('page-state', () => {
|
|
8
|
+
let testDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
testDir = mkdtempSync(join(tmpdir(), 'cn-page-state-test-'));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
if (existsSync(testDir)) {
|
|
16
|
+
rmSync(testDir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('buildPageStateFromFiles', () => {
|
|
21
|
+
test('builds page state from valid markdown files', () => {
|
|
22
|
+
// Create test files with frontmatter
|
|
23
|
+
const docsDir = join(testDir, 'docs');
|
|
24
|
+
mkdirSync(docsDir, { recursive: true });
|
|
25
|
+
|
|
26
|
+
writeFileSync(
|
|
27
|
+
join(testDir, 'home.md'),
|
|
28
|
+
`---
|
|
29
|
+
page_id: "page-1"
|
|
30
|
+
title: "Home Page"
|
|
31
|
+
version: 3
|
|
32
|
+
updated_at: "2024-01-15T10:00:00Z"
|
|
33
|
+
synced_at: "2024-01-16T08:00:00Z"
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
# Home Page
|
|
37
|
+
|
|
38
|
+
Welcome!
|
|
39
|
+
`,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
writeFileSync(
|
|
43
|
+
join(docsDir, 'guide.md'),
|
|
44
|
+
`---
|
|
45
|
+
page_id: "page-2"
|
|
46
|
+
title: "User Guide"
|
|
47
|
+
version: 5
|
|
48
|
+
updated_at: "2024-01-20T14:30:00Z"
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
# User Guide
|
|
52
|
+
|
|
53
|
+
Content here.
|
|
54
|
+
`,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const pageMappings = {
|
|
58
|
+
'page-1': 'home.md',
|
|
59
|
+
'page-2': 'docs/guide.md',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const result = buildPageStateFromFiles(testDir, pageMappings);
|
|
63
|
+
|
|
64
|
+
// Check pages map
|
|
65
|
+
expect(result.pages.size).toBe(2);
|
|
66
|
+
|
|
67
|
+
const page1 = result.pages.get('page-1');
|
|
68
|
+
expect(page1).toBeDefined();
|
|
69
|
+
expect(page1?.pageId).toBe('page-1');
|
|
70
|
+
expect(page1?.localPath).toBe('home.md');
|
|
71
|
+
expect(page1?.title).toBe('Home Page');
|
|
72
|
+
expect(page1?.version).toBe(3);
|
|
73
|
+
expect(page1?.updatedAt).toBe('2024-01-15T10:00:00Z');
|
|
74
|
+
expect(page1?.syncedAt).toBe('2024-01-16T08:00:00Z');
|
|
75
|
+
|
|
76
|
+
const page2 = result.pages.get('page-2');
|
|
77
|
+
expect(page2).toBeDefined();
|
|
78
|
+
expect(page2?.title).toBe('User Guide');
|
|
79
|
+
expect(page2?.version).toBe(5);
|
|
80
|
+
|
|
81
|
+
// Check pathToPageId map
|
|
82
|
+
expect(result.pathToPageId.size).toBe(2);
|
|
83
|
+
expect(result.pathToPageId.get('home.md')).toBe('page-1');
|
|
84
|
+
expect(result.pathToPageId.get('docs/guide.md')).toBe('page-2');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('skips files that do not exist and reports warnings', () => {
|
|
88
|
+
writeFileSync(
|
|
89
|
+
join(testDir, 'exists.md'),
|
|
90
|
+
`---
|
|
91
|
+
page_id: "page-1"
|
|
92
|
+
title: "Existing Page"
|
|
93
|
+
version: 1
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
Content
|
|
97
|
+
`,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const pageMappings = {
|
|
101
|
+
'page-1': 'exists.md',
|
|
102
|
+
'page-2': 'does-not-exist.md',
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const result = buildPageStateFromFiles(testDir, pageMappings);
|
|
106
|
+
|
|
107
|
+
// Only the existing file should be in pages map
|
|
108
|
+
expect(result.pages.size).toBe(1);
|
|
109
|
+
expect(result.pages.has('page-1')).toBe(true);
|
|
110
|
+
expect(result.pages.has('page-2')).toBe(false);
|
|
111
|
+
|
|
112
|
+
// pathToPageId should only contain successfully parsed pages
|
|
113
|
+
expect(result.pathToPageId.size).toBe(1);
|
|
114
|
+
expect(result.pathToPageId.get('exists.md')).toBe('page-1');
|
|
115
|
+
|
|
116
|
+
// Should have a warning about the missing file
|
|
117
|
+
expect(result.warnings.length).toBe(1);
|
|
118
|
+
expect(result.warnings[0]).toContain('File not found');
|
|
119
|
+
expect(result.warnings[0]).toContain('page-2');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('handles files with malformed frontmatter and reports warnings', () => {
|
|
123
|
+
writeFileSync(
|
|
124
|
+
join(testDir, 'valid.md'),
|
|
125
|
+
`---
|
|
126
|
+
page_id: "page-1"
|
|
127
|
+
title: "Valid Page"
|
|
128
|
+
version: 1
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
Content
|
|
132
|
+
`,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
writeFileSync(
|
|
136
|
+
join(testDir, 'malformed.md'),
|
|
137
|
+
`---
|
|
138
|
+
this is not valid yaml: [
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
Content
|
|
142
|
+
`,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const pageMappings = {
|
|
146
|
+
'page-1': 'valid.md',
|
|
147
|
+
'page-2': 'malformed.md',
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const result = buildPageStateFromFiles(testDir, pageMappings);
|
|
151
|
+
|
|
152
|
+
// Only the valid file should be in pages map
|
|
153
|
+
expect(result.pages.size).toBe(1);
|
|
154
|
+
expect(result.pages.has('page-1')).toBe(true);
|
|
155
|
+
expect(result.pages.has('page-2')).toBe(false);
|
|
156
|
+
|
|
157
|
+
// Should have a warning about the malformed file
|
|
158
|
+
expect(result.warnings.length).toBe(1);
|
|
159
|
+
expect(result.warnings[0]).toContain('Failed to parse frontmatter');
|
|
160
|
+
expect(result.warnings[0]).toContain('malformed.md');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('handles empty page mappings', () => {
|
|
164
|
+
const result = buildPageStateFromFiles(testDir, {});
|
|
165
|
+
|
|
166
|
+
expect(result.pages.size).toBe(0);
|
|
167
|
+
expect(result.pathToPageId.size).toBe(0);
|
|
168
|
+
expect(result.warnings.length).toBe(0);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('warns when frontmatter page_id does not match mapping key', () => {
|
|
172
|
+
writeFileSync(
|
|
173
|
+
join(testDir, 'mismatched.md'),
|
|
174
|
+
`---
|
|
175
|
+
page_id: "actual-page-id"
|
|
176
|
+
title: "Mismatched Page"
|
|
177
|
+
version: 1
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
Content
|
|
181
|
+
`,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const pageMappings = {
|
|
185
|
+
'mapping-page-id': 'mismatched.md',
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const result = buildPageStateFromFiles(testDir, pageMappings);
|
|
189
|
+
|
|
190
|
+
// Page should still be added (using mapping key)
|
|
191
|
+
expect(result.pages.size).toBe(1);
|
|
192
|
+
expect(result.pages.has('mapping-page-id')).toBe(true);
|
|
193
|
+
|
|
194
|
+
// Should have a warning about the mismatch
|
|
195
|
+
expect(result.warnings.length).toBe(1);
|
|
196
|
+
expect(result.warnings[0]).toContain('Page ID mismatch');
|
|
197
|
+
expect(result.warnings[0]).toContain('mapping-page-id');
|
|
198
|
+
expect(result.warnings[0]).toContain('actual-page-id');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('skips paths that attempt directory traversal', () => {
|
|
202
|
+
writeFileSync(
|
|
203
|
+
join(testDir, 'safe.md'),
|
|
204
|
+
`---
|
|
205
|
+
page_id: "safe-page"
|
|
206
|
+
title: "Safe Page"
|
|
207
|
+
version: 1
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
Content
|
|
211
|
+
`,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const pageMappings = {
|
|
215
|
+
'safe-page': 'safe.md',
|
|
216
|
+
'malicious-page': '../../../etc/passwd',
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const result = buildPageStateFromFiles(testDir, pageMappings);
|
|
220
|
+
|
|
221
|
+
// Only the safe page should be added
|
|
222
|
+
expect(result.pages.size).toBe(1);
|
|
223
|
+
expect(result.pages.has('safe-page')).toBe(true);
|
|
224
|
+
expect(result.pages.has('malicious-page')).toBe(false);
|
|
225
|
+
|
|
226
|
+
// Should have a warning about the traversal attempt
|
|
227
|
+
expect(result.warnings.length).toBe(1);
|
|
228
|
+
expect(result.warnings[0]).toContain('Skipping path outside directory');
|
|
229
|
+
expect(result.warnings[0]).toContain('malicious-page');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('uses default values for missing frontmatter fields', () => {
|
|
233
|
+
writeFileSync(
|
|
234
|
+
join(testDir, 'minimal.md'),
|
|
235
|
+
`---
|
|
236
|
+
page_id: "page-1"
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
Minimal content
|
|
240
|
+
`,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const pageMappings = {
|
|
244
|
+
'page-1': 'minimal.md',
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const result = buildPageStateFromFiles(testDir, pageMappings);
|
|
248
|
+
|
|
249
|
+
const page = result.pages.get('page-1');
|
|
250
|
+
expect(page).toBeDefined();
|
|
251
|
+
expect(page?.title).toBe(''); // Default empty string
|
|
252
|
+
expect(page?.version).toBe(1); // Default version 1
|
|
253
|
+
expect(page?.updatedAt).toBeUndefined();
|
|
254
|
+
expect(page?.syncedAt).toBeUndefined();
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('getPageInfoByPath', () => {
|
|
259
|
+
test('returns page info for valid file', () => {
|
|
260
|
+
writeFileSync(
|
|
261
|
+
join(testDir, 'page.md'),
|
|
262
|
+
`---
|
|
263
|
+
page_id: "page-123"
|
|
264
|
+
title: "Test Page"
|
|
265
|
+
version: 7
|
|
266
|
+
updated_at: "2024-02-01T12:00:00Z"
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
Content
|
|
270
|
+
`,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
const result = getPageInfoByPath(testDir, 'page.md');
|
|
274
|
+
|
|
275
|
+
expect(result).not.toBeNull();
|
|
276
|
+
expect(result?.pageId).toBe('page-123');
|
|
277
|
+
expect(result?.localPath).toBe('page.md');
|
|
278
|
+
expect(result?.title).toBe('Test Page');
|
|
279
|
+
expect(result?.version).toBe(7);
|
|
280
|
+
expect(result?.updatedAt).toBe('2024-02-01T12:00:00Z');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test('returns null for non-existent file', () => {
|
|
284
|
+
const result = getPageInfoByPath(testDir, 'does-not-exist.md');
|
|
285
|
+
|
|
286
|
+
expect(result).toBeNull();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('returns null for file without page_id', () => {
|
|
290
|
+
writeFileSync(
|
|
291
|
+
join(testDir, 'no-id.md'),
|
|
292
|
+
`---
|
|
293
|
+
title: "Page Without ID"
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
Content
|
|
297
|
+
`,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
const result = getPageInfoByPath(testDir, 'no-id.md');
|
|
301
|
+
|
|
302
|
+
expect(result).toBeNull();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test('returns null for file with malformed frontmatter', () => {
|
|
306
|
+
writeFileSync(
|
|
307
|
+
join(testDir, 'bad.md'),
|
|
308
|
+
`---
|
|
309
|
+
invalid: yaml: content: [
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
Content
|
|
313
|
+
`,
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
const result = getPageInfoByPath(testDir, 'bad.md');
|
|
317
|
+
|
|
318
|
+
expect(result).toBeNull();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('handles nested paths', () => {
|
|
322
|
+
const nestedDir = join(testDir, 'deeply', 'nested', 'path');
|
|
323
|
+
mkdirSync(nestedDir, { recursive: true });
|
|
324
|
+
|
|
325
|
+
writeFileSync(
|
|
326
|
+
join(nestedDir, 'page.md'),
|
|
327
|
+
`---
|
|
328
|
+
page_id: "nested-page"
|
|
329
|
+
title: "Nested Page"
|
|
330
|
+
version: 2
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
Nested content
|
|
334
|
+
`,
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
const result = getPageInfoByPath(testDir, 'deeply/nested/path/page.md');
|
|
338
|
+
|
|
339
|
+
expect(result).not.toBeNull();
|
|
340
|
+
expect(result?.pageId).toBe('nested-page');
|
|
341
|
+
expect(result?.localPath).toBe('deeply/nested/path/page.md');
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('scanDirectoryForPages', () => {
|
|
346
|
+
test('discovers all markdown files with page_id', () => {
|
|
347
|
+
// Create directory structure
|
|
348
|
+
const docsDir = join(testDir, 'docs');
|
|
349
|
+
const apiDir = join(testDir, 'docs', 'api');
|
|
350
|
+
mkdirSync(apiDir, { recursive: true });
|
|
351
|
+
|
|
352
|
+
writeFileSync(
|
|
353
|
+
join(testDir, 'README.md'),
|
|
354
|
+
`---
|
|
355
|
+
page_id: "home"
|
|
356
|
+
title: "Home"
|
|
357
|
+
version: 1
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
Home content
|
|
361
|
+
`,
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
writeFileSync(
|
|
365
|
+
join(docsDir, 'guide.md'),
|
|
366
|
+
`---
|
|
367
|
+
page_id: "guide"
|
|
368
|
+
title: "Guide"
|
|
369
|
+
version: 2
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
Guide content
|
|
373
|
+
`,
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
writeFileSync(
|
|
377
|
+
join(apiDir, 'endpoints.md'),
|
|
378
|
+
`---
|
|
379
|
+
page_id: "api-endpoints"
|
|
380
|
+
title: "API Endpoints"
|
|
381
|
+
version: 3
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
API content
|
|
385
|
+
`,
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
// File without page_id should be skipped
|
|
389
|
+
writeFileSync(
|
|
390
|
+
join(testDir, 'untracked.md'),
|
|
391
|
+
`---
|
|
392
|
+
title: "Untracked File"
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
Not a Confluence page
|
|
396
|
+
`,
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
const result = scanDirectoryForPages(testDir);
|
|
400
|
+
|
|
401
|
+
expect(result.pages.size).toBe(3);
|
|
402
|
+
expect(result.pages.has('home')).toBe(true);
|
|
403
|
+
expect(result.pages.has('guide')).toBe(true);
|
|
404
|
+
expect(result.pages.has('api-endpoints')).toBe(true);
|
|
405
|
+
|
|
406
|
+
expect(result.pathToPageId.get('README.md')).toBe('home');
|
|
407
|
+
expect(result.pathToPageId.get('docs/guide.md')).toBe('guide');
|
|
408
|
+
expect(result.pathToPageId.get('docs/api/endpoints.md')).toBe('api-endpoints');
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test('skips hidden directories', () => {
|
|
412
|
+
const hiddenDir = join(testDir, '.hidden');
|
|
413
|
+
mkdirSync(hiddenDir, { recursive: true });
|
|
414
|
+
|
|
415
|
+
writeFileSync(
|
|
416
|
+
join(hiddenDir, 'secret.md'),
|
|
417
|
+
`---
|
|
418
|
+
page_id: "secret"
|
|
419
|
+
title: "Secret Page"
|
|
420
|
+
version: 1
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
Should be skipped
|
|
424
|
+
`,
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
writeFileSync(
|
|
428
|
+
join(testDir, 'visible.md'),
|
|
429
|
+
`---
|
|
430
|
+
page_id: "visible"
|
|
431
|
+
title: "Visible Page"
|
|
432
|
+
version: 1
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
Should be included
|
|
436
|
+
`,
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
const result = scanDirectoryForPages(testDir);
|
|
440
|
+
|
|
441
|
+
expect(result.pages.size).toBe(1);
|
|
442
|
+
expect(result.pages.has('visible')).toBe(true);
|
|
443
|
+
expect(result.pages.has('secret')).toBe(false);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test('skips node_modules directory', () => {
|
|
447
|
+
const nodeModulesDir = join(testDir, 'node_modules', 'some-package');
|
|
448
|
+
mkdirSync(nodeModulesDir, { recursive: true });
|
|
449
|
+
|
|
450
|
+
writeFileSync(
|
|
451
|
+
join(nodeModulesDir, 'readme.md'),
|
|
452
|
+
`---
|
|
453
|
+
page_id: "npm-page"
|
|
454
|
+
title: "NPM Package"
|
|
455
|
+
version: 1
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
Should be skipped
|
|
459
|
+
`,
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
writeFileSync(
|
|
463
|
+
join(testDir, 'app.md'),
|
|
464
|
+
`---
|
|
465
|
+
page_id: "app"
|
|
466
|
+
title: "App"
|
|
467
|
+
version: 1
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
Should be included
|
|
471
|
+
`,
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
const result = scanDirectoryForPages(testDir);
|
|
475
|
+
|
|
476
|
+
expect(result.pages.size).toBe(1);
|
|
477
|
+
expect(result.pages.has('app')).toBe(true);
|
|
478
|
+
expect(result.pages.has('npm-page')).toBe(false);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test('skips reserved filenames (claude.md, agents.md)', () => {
|
|
482
|
+
writeFileSync(
|
|
483
|
+
join(testDir, 'CLAUDE.md'),
|
|
484
|
+
`---
|
|
485
|
+
page_id: "claude-page"
|
|
486
|
+
title: "Claude Instructions"
|
|
487
|
+
version: 1
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
Should be skipped
|
|
491
|
+
`,
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
writeFileSync(
|
|
495
|
+
join(testDir, 'agents.md'),
|
|
496
|
+
`---
|
|
497
|
+
page_id: "agents-page"
|
|
498
|
+
title: "Agents Config"
|
|
499
|
+
version: 1
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
Should be skipped
|
|
503
|
+
`,
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
writeFileSync(
|
|
507
|
+
join(testDir, 'readme.md'),
|
|
508
|
+
`---
|
|
509
|
+
page_id: "readme"
|
|
510
|
+
title: "Readme"
|
|
511
|
+
version: 1
|
|
512
|
+
---
|
|
513
|
+
|
|
514
|
+
Should be included
|
|
515
|
+
`,
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
const result = scanDirectoryForPages(testDir);
|
|
519
|
+
|
|
520
|
+
expect(result.pages.size).toBe(1);
|
|
521
|
+
expect(result.pages.has('readme')).toBe(true);
|
|
522
|
+
expect(result.pages.has('claude-page')).toBe(false);
|
|
523
|
+
expect(result.pages.has('agents-page')).toBe(false);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test('handles empty directory', () => {
|
|
527
|
+
const result = scanDirectoryForPages(testDir);
|
|
528
|
+
|
|
529
|
+
expect(result.pages.size).toBe(0);
|
|
530
|
+
expect(result.pathToPageId.size).toBe(0);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test('handles directory with only non-markdown files', () => {
|
|
534
|
+
writeFileSync(join(testDir, 'config.json'), '{}');
|
|
535
|
+
writeFileSync(join(testDir, 'script.ts'), 'console.log("hello")');
|
|
536
|
+
|
|
537
|
+
const result = scanDirectoryForPages(testDir);
|
|
538
|
+
|
|
539
|
+
expect(result.pages.size).toBe(0);
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
});
|