@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,53 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { HttpResponse, http } from 'msw';
|
|
3
|
+
import { ConfluenceClient } from '../lib/confluence-client/client.js';
|
|
4
|
+
import { server } from './setup-msw.js';
|
|
5
|
+
|
|
6
|
+
const testConfig = {
|
|
7
|
+
confluenceUrl: 'https://test.atlassian.net',
|
|
8
|
+
email: 'test@example.com',
|
|
9
|
+
apiToken: 'test-token',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe('ConfluenceClient - getFooterComments', () => {
|
|
13
|
+
test('returns empty comments by default', async () => {
|
|
14
|
+
const client = new ConfluenceClient(testConfig);
|
|
15
|
+
const response = await client.getFooterComments('page-123');
|
|
16
|
+
expect(response.results).toBeArray();
|
|
17
|
+
expect(response.results.length).toBe(0);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('returns comments when present', async () => {
|
|
21
|
+
server.use(
|
|
22
|
+
http.get('*/wiki/api/v2/pages/:pageId/footer-comments', () => {
|
|
23
|
+
return HttpResponse.json({
|
|
24
|
+
results: [
|
|
25
|
+
{
|
|
26
|
+
id: 'comment-1',
|
|
27
|
+
body: { storage: { value: '<p>Test comment</p>', representation: 'storage' } },
|
|
28
|
+
authorId: 'user-123',
|
|
29
|
+
createdAt: new Date().toISOString(),
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const client = new ConfluenceClient(testConfig);
|
|
37
|
+
const response = await client.getFooterComments('page-123');
|
|
38
|
+
expect(response.results.length).toBe(1);
|
|
39
|
+
expect(response.results[0].id).toBe('comment-1');
|
|
40
|
+
expect(response.results[0].body?.storage?.value).toContain('Test comment');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('throws on API error', async () => {
|
|
44
|
+
server.use(
|
|
45
|
+
http.get('*/wiki/api/v2/pages/:pageId/footer-comments', () => {
|
|
46
|
+
return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const client = new ConfluenceClient(testConfig);
|
|
51
|
+
await expect(client.getFooterComments('page-123')).rejects.toThrow();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { Effect } from 'effect';
|
|
6
|
+
import { ConfigManager } from '../lib/config.js';
|
|
7
|
+
import { ConfigError, FileSystemError, ValidationError } from '../lib/errors.js';
|
|
8
|
+
|
|
9
|
+
describe('ConfigManager', () => {
|
|
10
|
+
let testDir: string;
|
|
11
|
+
let originalEnv: string | undefined;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
// Create a temporary directory for test config
|
|
15
|
+
testDir = join(tmpdir(), `cn-test-${Date.now()}`);
|
|
16
|
+
mkdirSync(testDir, { recursive: true });
|
|
17
|
+
|
|
18
|
+
// Override config path
|
|
19
|
+
originalEnv = process.env.CN_CONFIG_PATH;
|
|
20
|
+
process.env.CN_CONFIG_PATH = testDir;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
// Cleanup
|
|
25
|
+
if (existsSync(testDir)) {
|
|
26
|
+
rmSync(testDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Restore environment
|
|
30
|
+
if (originalEnv !== undefined) {
|
|
31
|
+
process.env.CN_CONFIG_PATH = originalEnv;
|
|
32
|
+
} else {
|
|
33
|
+
delete process.env.CN_CONFIG_PATH;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('validateUrl', () => {
|
|
38
|
+
test('accepts valid Confluence Cloud URLs', () => {
|
|
39
|
+
expect(ConfigManager.validateUrl('https://example.atlassian.net')).toBe(true);
|
|
40
|
+
expect(ConfigManager.validateUrl('https://my-company.atlassian.net')).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('rejects invalid URLs', () => {
|
|
44
|
+
expect(ConfigManager.validateUrl('http://example.atlassian.net')).toBe(false);
|
|
45
|
+
expect(ConfigManager.validateUrl('https://example.com')).toBe(false);
|
|
46
|
+
expect(ConfigManager.validateUrl('https://atlassian.net')).toBe(false);
|
|
47
|
+
expect(ConfigManager.validateUrl('example.atlassian.net')).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('validateEmail', () => {
|
|
52
|
+
test('accepts valid email addresses', () => {
|
|
53
|
+
expect(ConfigManager.validateEmail('user@example.com')).toBe(true);
|
|
54
|
+
expect(ConfigManager.validateEmail('test.user@company.co.uk')).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('rejects invalid email addresses', () => {
|
|
58
|
+
expect(ConfigManager.validateEmail('invalid')).toBe(false);
|
|
59
|
+
expect(ConfigManager.validateEmail('user@')).toBe(false);
|
|
60
|
+
expect(ConfigManager.validateEmail('@example.com')).toBe(false);
|
|
61
|
+
expect(ConfigManager.validateEmail('user @example.com')).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('hasConfig', () => {
|
|
66
|
+
test('returns false when no config exists', () => {
|
|
67
|
+
const manager = new ConfigManager();
|
|
68
|
+
expect(manager.hasConfig()).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('returns true when config exists', () => {
|
|
72
|
+
const configPath = join(testDir, 'config.json');
|
|
73
|
+
writeFileSync(
|
|
74
|
+
configPath,
|
|
75
|
+
JSON.stringify({ confluenceUrl: 'https://test.atlassian.net', email: 'test@example.com', apiToken: 'token' }),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const manager = new ConfigManager();
|
|
79
|
+
expect(manager.hasConfig()).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('getConfig', () => {
|
|
84
|
+
test('returns null when no config exists', async () => {
|
|
85
|
+
const manager = new ConfigManager();
|
|
86
|
+
const config = await manager.getConfig();
|
|
87
|
+
expect(config).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('returns config when it exists', async () => {
|
|
91
|
+
const configPath = join(testDir, 'config.json');
|
|
92
|
+
const testConfig = {
|
|
93
|
+
confluenceUrl: 'https://test.atlassian.net',
|
|
94
|
+
email: 'test@example.com',
|
|
95
|
+
apiToken: 'test-token',
|
|
96
|
+
};
|
|
97
|
+
writeFileSync(configPath, JSON.stringify(testConfig));
|
|
98
|
+
|
|
99
|
+
const manager = new ConfigManager();
|
|
100
|
+
const config = await manager.getConfig();
|
|
101
|
+
|
|
102
|
+
expect(config).not.toBeNull();
|
|
103
|
+
expect(config?.confluenceUrl).toBe(testConfig.confluenceUrl);
|
|
104
|
+
expect(config?.email).toBe(testConfig.email);
|
|
105
|
+
expect(config?.apiToken).toBe(testConfig.apiToken);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('returns null for invalid JSON', async () => {
|
|
109
|
+
const configPath = join(testDir, 'config.json');
|
|
110
|
+
writeFileSync(configPath, 'invalid json');
|
|
111
|
+
|
|
112
|
+
const manager = new ConfigManager();
|
|
113
|
+
const config = await manager.getConfig();
|
|
114
|
+
expect(config).toBeNull();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('getConfigEffect', () => {
|
|
119
|
+
test('fails with ConfigError when no config exists', async () => {
|
|
120
|
+
const manager = new ConfigManager();
|
|
121
|
+
|
|
122
|
+
const result = await Effect.runPromise(Effect.either(manager.getConfigEffect()));
|
|
123
|
+
|
|
124
|
+
expect(result._tag).toBe('Left');
|
|
125
|
+
if (result._tag === 'Left') {
|
|
126
|
+
expect(result.left).toBeInstanceOf(ConfigError);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('succeeds with config when it exists', async () => {
|
|
131
|
+
const configPath = join(testDir, 'config.json');
|
|
132
|
+
const testConfig = {
|
|
133
|
+
confluenceUrl: 'https://test.atlassian.net',
|
|
134
|
+
email: 'test@example.com',
|
|
135
|
+
apiToken: 'test-token',
|
|
136
|
+
};
|
|
137
|
+
writeFileSync(configPath, JSON.stringify(testConfig));
|
|
138
|
+
|
|
139
|
+
const manager = new ConfigManager();
|
|
140
|
+
const result = await Effect.runPromise(Effect.either(manager.getConfigEffect()));
|
|
141
|
+
|
|
142
|
+
expect(result._tag).toBe('Right');
|
|
143
|
+
if (result._tag === 'Right') {
|
|
144
|
+
expect(result.right.confluenceUrl).toBe(testConfig.confluenceUrl);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('setConfig', () => {
|
|
150
|
+
test('saves valid config', async () => {
|
|
151
|
+
const manager = new ConfigManager();
|
|
152
|
+
const testConfig = {
|
|
153
|
+
confluenceUrl: 'https://test.atlassian.net',
|
|
154
|
+
email: 'test@example.com',
|
|
155
|
+
apiToken: 'test-token',
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
await manager.setConfig(testConfig);
|
|
159
|
+
|
|
160
|
+
const config = await manager.getConfig();
|
|
161
|
+
expect(config?.confluenceUrl).toBe(testConfig.confluenceUrl);
|
|
162
|
+
expect(config?.email).toBe(testConfig.email);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('throws for invalid config', async () => {
|
|
166
|
+
const manager = new ConfigManager();
|
|
167
|
+
const invalidConfig = {
|
|
168
|
+
confluenceUrl: 'invalid-url',
|
|
169
|
+
email: 'test@example.com',
|
|
170
|
+
apiToken: 'test-token',
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
expect(async () => {
|
|
174
|
+
await manager.setConfig(invalidConfig as any);
|
|
175
|
+
}).toThrow();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('setConfigEffect', () => {
|
|
180
|
+
test('succeeds with valid config', async () => {
|
|
181
|
+
const manager = new ConfigManager();
|
|
182
|
+
const testConfig = {
|
|
183
|
+
confluenceUrl: 'https://test.atlassian.net',
|
|
184
|
+
email: 'test@example.com',
|
|
185
|
+
apiToken: 'test-token',
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const result = await Effect.runPromise(Effect.either(manager.setConfigEffect(testConfig)));
|
|
189
|
+
|
|
190
|
+
expect(result._tag).toBe('Right');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('fails with ValidationError for invalid config', async () => {
|
|
194
|
+
const manager = new ConfigManager();
|
|
195
|
+
const invalidConfig = {
|
|
196
|
+
confluenceUrl: 'invalid-url',
|
|
197
|
+
email: 'test@example.com',
|
|
198
|
+
apiToken: 'test-token',
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const result = await Effect.runPromise(Effect.either(manager.setConfigEffect(invalidConfig as any)));
|
|
202
|
+
|
|
203
|
+
expect(result._tag).toBe('Left');
|
|
204
|
+
if (result._tag === 'Left') {
|
|
205
|
+
expect(result.left).toBeInstanceOf(ValidationError);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|