@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.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +69 -0
  3. package/package.json +73 -0
  4. package/src/cli/commands/attachments.ts +113 -0
  5. package/src/cli/commands/clone.ts +188 -0
  6. package/src/cli/commands/comments.ts +56 -0
  7. package/src/cli/commands/create.ts +58 -0
  8. package/src/cli/commands/delete.ts +46 -0
  9. package/src/cli/commands/doctor.ts +161 -0
  10. package/src/cli/commands/duplicate-check.ts +89 -0
  11. package/src/cli/commands/file-rename.ts +113 -0
  12. package/src/cli/commands/folder-hierarchy.ts +241 -0
  13. package/src/cli/commands/info.ts +56 -0
  14. package/src/cli/commands/labels.ts +53 -0
  15. package/src/cli/commands/move.ts +23 -0
  16. package/src/cli/commands/open.ts +145 -0
  17. package/src/cli/commands/pull.ts +241 -0
  18. package/src/cli/commands/push-errors.ts +40 -0
  19. package/src/cli/commands/push.ts +699 -0
  20. package/src/cli/commands/search.ts +62 -0
  21. package/src/cli/commands/setup.ts +124 -0
  22. package/src/cli/commands/spaces.ts +42 -0
  23. package/src/cli/commands/status.ts +88 -0
  24. package/src/cli/commands/tree.ts +190 -0
  25. package/src/cli/help.ts +425 -0
  26. package/src/cli/index.ts +413 -0
  27. package/src/cli/utils/browser.ts +34 -0
  28. package/src/cli/utils/progress-reporter.ts +49 -0
  29. package/src/cli.ts +6 -0
  30. package/src/lib/config.ts +156 -0
  31. package/src/lib/confluence-client/attachment-operations.ts +221 -0
  32. package/src/lib/confluence-client/client.ts +653 -0
  33. package/src/lib/confluence-client/comment-operations.ts +60 -0
  34. package/src/lib/confluence-client/folder-operations.ts +203 -0
  35. package/src/lib/confluence-client/index.ts +47 -0
  36. package/src/lib/confluence-client/label-operations.ts +102 -0
  37. package/src/lib/confluence-client/page-operations.ts +270 -0
  38. package/src/lib/confluence-client/search-operations.ts +60 -0
  39. package/src/lib/confluence-client/types.ts +329 -0
  40. package/src/lib/confluence-client/user-operations.ts +58 -0
  41. package/src/lib/dependency-sorter.ts +233 -0
  42. package/src/lib/errors.ts +237 -0
  43. package/src/lib/file-scanner.ts +195 -0
  44. package/src/lib/formatters.ts +314 -0
  45. package/src/lib/health-check.ts +204 -0
  46. package/src/lib/markdown/converter.ts +427 -0
  47. package/src/lib/markdown/frontmatter.ts +116 -0
  48. package/src/lib/markdown/html-converter.ts +398 -0
  49. package/src/lib/markdown/index.ts +21 -0
  50. package/src/lib/markdown/link-converter.ts +189 -0
  51. package/src/lib/markdown/reference-updater.ts +251 -0
  52. package/src/lib/markdown/slugify.ts +32 -0
  53. package/src/lib/page-state.ts +195 -0
  54. package/src/lib/resolve-page-target.ts +33 -0
  55. package/src/lib/space-config.ts +264 -0
  56. package/src/lib/sync/cleanup.ts +50 -0
  57. package/src/lib/sync/folder-path.ts +61 -0
  58. package/src/lib/sync/index.ts +2 -0
  59. package/src/lib/sync/link-resolution-pass.ts +139 -0
  60. package/src/lib/sync/sync-engine.ts +681 -0
  61. package/src/lib/sync/sync-specific.ts +221 -0
  62. package/src/lib/sync/types.ts +42 -0
  63. package/src/test/attachments.test.ts +68 -0
  64. package/src/test/clone.test.ts +373 -0
  65. package/src/test/comments.test.ts +53 -0
  66. package/src/test/config.test.ts +209 -0
  67. package/src/test/confluence-client.test.ts +535 -0
  68. package/src/test/delete.test.ts +39 -0
  69. package/src/test/dependency-sorter.test.ts +384 -0
  70. package/src/test/errors.test.ts +199 -0
  71. package/src/test/file-rename.test.ts +305 -0
  72. package/src/test/file-scanner.test.ts +331 -0
  73. package/src/test/folder-hierarchy.test.ts +337 -0
  74. package/src/test/formatters.test.ts +213 -0
  75. package/src/test/html-converter.test.ts +399 -0
  76. package/src/test/info.test.ts +56 -0
  77. package/src/test/labels.test.ts +70 -0
  78. package/src/test/link-conversion-integration.test.ts +189 -0
  79. package/src/test/link-converter.test.ts +413 -0
  80. package/src/test/link-resolution-pass.test.ts +368 -0
  81. package/src/test/markdown.test.ts +443 -0
  82. package/src/test/mocks/handlers.ts +228 -0
  83. package/src/test/move.test.ts +53 -0
  84. package/src/test/msw-schema-validation.ts +151 -0
  85. package/src/test/page-state.test.ts +542 -0
  86. package/src/test/push.test.ts +551 -0
  87. package/src/test/reference-updater.test.ts +293 -0
  88. package/src/test/resolve-page-target.test.ts +55 -0
  89. package/src/test/search.test.ts +64 -0
  90. package/src/test/setup-msw.ts +75 -0
  91. package/src/test/space-config.test.ts +516 -0
  92. package/src/test/spaces.test.ts +53 -0
  93. package/src/test/sync-engine.test.ts +486 -0
  94. package/src/types/turndown-plugin-gfm.d.ts +9 -0
@@ -0,0 +1,53 @@
1
+ import chalk from 'chalk';
2
+ import { ConfluenceClient } from '../../lib/confluence-client/index.js';
3
+ import { ConfigManager } from '../../lib/config.js';
4
+ import { EXIT_CODES } from '../../lib/errors.js';
5
+ import { escapeXml } from '../../lib/formatters.js';
6
+ import { resolvePageTarget } from '../../lib/resolve-page-target.js';
7
+
8
+ export interface LabelsCommandOptions {
9
+ add?: string;
10
+ remove?: string;
11
+ xml?: boolean;
12
+ }
13
+
14
+ export async function labelsCommand(target: string, options: LabelsCommandOptions = {}): Promise<void> {
15
+ const configManager = new ConfigManager();
16
+ const config = await configManager.getConfig();
17
+
18
+ if (!config) {
19
+ console.error(chalk.red('Not configured. Run: cn setup'));
20
+ process.exit(EXIT_CODES.CONFIG_ERROR);
21
+ }
22
+
23
+ const pageId = resolvePageTarget(target);
24
+ const client = new ConfluenceClient(config);
25
+
26
+ if (options.add) {
27
+ await client.addLabel(pageId, options.add);
28
+ console.log(`${chalk.green('Added label:')} ${options.add}`);
29
+ }
30
+
31
+ if (options.remove) {
32
+ await client.removeLabel(pageId, options.remove);
33
+ console.log(`${chalk.green('Removed label:')} ${options.remove}`);
34
+ }
35
+
36
+ const labels = await client.getAllLabels(pageId);
37
+
38
+ if (options.xml) {
39
+ console.log('<labels>');
40
+ for (const label of labels) {
41
+ console.log(` <label>${escapeXml(label.name)}</label>`);
42
+ }
43
+ console.log('</labels>');
44
+ return;
45
+ }
46
+
47
+ if (labels.length === 0) {
48
+ console.log('No labels.');
49
+ return;
50
+ }
51
+
52
+ console.log(labels.map((l) => chalk.cyan(l.name)).join(', '));
53
+ }
@@ -0,0 +1,23 @@
1
+ import chalk from 'chalk';
2
+ import { ConfluenceClient } from '../../lib/confluence-client/index.js';
3
+ import { ConfigManager } from '../../lib/config.js';
4
+ import { EXIT_CODES } from '../../lib/errors.js';
5
+ import { resolvePageTarget } from '../../lib/resolve-page-target.js';
6
+
7
+ export async function moveCommand(target: string, parentId: string): Promise<void> {
8
+ const configManager = new ConfigManager();
9
+ const config = await configManager.getConfig();
10
+
11
+ if (!config) {
12
+ console.error(chalk.red('Not configured. Run: cn setup'));
13
+ process.exit(EXIT_CODES.CONFIG_ERROR);
14
+ }
15
+
16
+ const pageId = resolvePageTarget(target);
17
+ const client = new ConfluenceClient(config);
18
+
19
+ const [page, parent] = await Promise.all([client.getPage(pageId, false), client.getPage(parentId, false)]);
20
+
21
+ await client.movePage(pageId, parentId);
22
+ console.log(`${chalk.green('Moved:')} "${page.title}" under "${parent.title}"`);
23
+ }
@@ -0,0 +1,145 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import chalk from 'chalk';
4
+ import { ConfluenceClient } from '../../lib/confluence-client/index.js';
5
+ import { ConfigManager } from '../../lib/config.js';
6
+ import { EXIT_CODES } from '../../lib/errors.js';
7
+ import { extractPageId } from '../../lib/markdown/index.js';
8
+ import { readSpaceConfig } from '../../lib/space-config.js';
9
+ import { openUrl } from '../utils/browser.js';
10
+
11
+ export interface OpenCommandOptions {
12
+ page?: string;
13
+ spaceKey?: string;
14
+ }
15
+
16
+ /**
17
+ * Open command - opens a page in the browser
18
+ */
19
+ export async function openCommand(options: OpenCommandOptions = {}): Promise<void> {
20
+ const configManager = new ConfigManager();
21
+ const config = await configManager.getConfig();
22
+
23
+ if (!config) {
24
+ console.error(chalk.red('Not configured. Please run "cn setup" first.'));
25
+ process.exit(EXIT_CODES.CONFIG_ERROR);
26
+ }
27
+
28
+ const client = new ConfluenceClient(config);
29
+ const directory = process.cwd();
30
+
31
+ // Get space info
32
+ let spaceKey: string | undefined = options.spaceKey;
33
+ let spaceId: string | undefined;
34
+
35
+ if (!spaceKey) {
36
+ const spaceConfig = readSpaceConfig(directory);
37
+ if (spaceConfig) {
38
+ spaceKey = spaceConfig.spaceKey;
39
+ spaceId = spaceConfig.spaceId;
40
+ }
41
+ }
42
+
43
+ // If no page specified, open space home
44
+ if (!options.page) {
45
+ if (!spaceKey) {
46
+ console.error(chalk.red('No space specified and no space configured in this directory.'));
47
+ console.log(chalk.gray('Specify a page to open or run "cn sync --init <SPACE_KEY>" first.'));
48
+ process.exit(EXIT_CODES.CONFIG_ERROR);
49
+ }
50
+
51
+ const url = `${config.confluenceUrl}/wiki/spaces/${spaceKey}`;
52
+ console.log(chalk.gray(`Opening space: ${url}`));
53
+ openUrl(url);
54
+ return;
55
+ }
56
+
57
+ const pageArg = options.page;
58
+
59
+ // Check if it's a file path
60
+ if (pageArg.endsWith('.md') || pageArg.includes('/')) {
61
+ const filePath = pageArg.startsWith('/') ? pageArg : join(directory, pageArg);
62
+
63
+ if (existsSync(filePath)) {
64
+ try {
65
+ const content = readFileSync(filePath, 'utf-8');
66
+ const pageId = extractPageId(content);
67
+
68
+ if (pageId) {
69
+ const page = await client.getPage(pageId, false);
70
+ const webui = page._links?.webui;
71
+ if (webui) {
72
+ const url = `${config.confluenceUrl}/wiki${webui}`;
73
+ console.log(chalk.gray(`Opening page: ${url}`));
74
+ openUrl(url);
75
+ return;
76
+ }
77
+ }
78
+ } catch (_error) {
79
+ // Fall through to other methods
80
+ }
81
+ }
82
+ }
83
+
84
+ // Check if it's a page ID (numeric string)
85
+ if (/^\d+$/.test(pageArg)) {
86
+ try {
87
+ const page = await client.getPage(pageArg, false);
88
+ const webui = page._links?.webui;
89
+ if (webui) {
90
+ const url = `${config.confluenceUrl}/wiki${webui}`;
91
+ console.log(chalk.gray(`Opening page: ${url}`));
92
+ openUrl(url);
93
+ return;
94
+ }
95
+ } catch (_error) {
96
+ // Fall through to title search
97
+ }
98
+ }
99
+
100
+ // Search by title
101
+ if (!spaceId && spaceKey) {
102
+ try {
103
+ const space = await client.getSpaceByKey(spaceKey);
104
+ spaceId = space.id;
105
+ } catch (_error) {
106
+ console.error(chalk.red(`Space "${spaceKey}" not found.`));
107
+ process.exit(EXIT_CODES.SPACE_NOT_FOUND);
108
+ }
109
+ }
110
+
111
+ if (!spaceId) {
112
+ console.error(chalk.red('No space specified. Use --space or configure a space in this directory.'));
113
+ process.exit(EXIT_CODES.CONFIG_ERROR);
114
+ }
115
+
116
+ // Search for page by title
117
+ try {
118
+ const pages = await client.getAllPagesInSpace(spaceId);
119
+ const matchingPage = pages.find(
120
+ (p) => p.title.toLowerCase() === pageArg.toLowerCase() || p.title.toLowerCase().includes(pageArg.toLowerCase()),
121
+ );
122
+
123
+ if (matchingPage) {
124
+ const webui = matchingPage._links?.webui;
125
+ if (webui) {
126
+ const url = `${config.confluenceUrl}/wiki${webui}`;
127
+ console.log(chalk.gray(`Opening page: ${url}`));
128
+ openUrl(url);
129
+ return;
130
+ }
131
+
132
+ // Fallback to constructing URL
133
+ const url = `${config.confluenceUrl}/wiki/spaces/${spaceKey}/pages/${matchingPage.id}`;
134
+ console.log(chalk.gray(`Opening page: ${url}`));
135
+ openUrl(url);
136
+ return;
137
+ }
138
+
139
+ console.error(chalk.red(`Page "${pageArg}" not found in space ${spaceKey}.`));
140
+ process.exit(EXIT_CODES.GENERAL_ERROR);
141
+ } catch (error) {
142
+ console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
143
+ process.exit(EXIT_CODES.GENERAL_ERROR);
144
+ }
145
+ }
@@ -0,0 +1,241 @@
1
+ import { confirm } from '@inquirer/prompts';
2
+ import chalk from 'chalk';
3
+ import { unlinkSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { ConfigManager } from '../../lib/config.js';
6
+ import { EXIT_CODES } from '../../lib/errors.js';
7
+ import { getFormatter } from '../../lib/formatters.js';
8
+ import {
9
+ findBestDuplicate,
10
+ findDuplicatePageIds,
11
+ findStaleDuplicates,
12
+ scanFilesForHealthCheck,
13
+ } from '../../lib/health-check.js';
14
+ import { readSpaceConfig, hasSpaceConfig } from '../../lib/space-config.js';
15
+ import { SyncEngine } from '../../lib/sync/index.js';
16
+ import { createProgressReporter } from '../utils/progress-reporter.js';
17
+
18
+ export interface PullCommandOptions {
19
+ dryRun?: boolean;
20
+ force?: boolean;
21
+ depth?: number;
22
+ pages?: string[]; // Page IDs or local paths to force resync
23
+ }
24
+
25
+ interface CleanupContext {
26
+ sigintHandler: () => void;
27
+ stdinHandler: (data: Buffer | string) => void;
28
+ restoreRawMode: boolean | undefined;
29
+ }
30
+
31
+ /**
32
+ * Clean up signal handlers and restore stdin state
33
+ */
34
+ function cleanupHandlers(ctx: CleanupContext): void {
35
+ process.off('SIGINT', ctx.sigintHandler);
36
+ process.off('SIGTERM', ctx.sigintHandler);
37
+ if (process.stdin.isTTY) {
38
+ process.stdin.off('data', ctx.stdinHandler);
39
+ process.stdin.pause();
40
+ }
41
+ if (ctx.restoreRawMode !== undefined && process.stdin.setRawMode) {
42
+ try {
43
+ process.stdin.setRawMode(ctx.restoreRawMode);
44
+ } catch {
45
+ // Ignore errors restoring raw mode
46
+ }
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Pull command - pulls a Confluence space to the local directory
52
+ */
53
+ export async function pullCommand(options: PullCommandOptions): Promise<void> {
54
+ const configManager = new ConfigManager();
55
+ const config = await configManager.getConfig();
56
+
57
+ if (!config) {
58
+ console.error(chalk.red('Not configured. Please run "cn setup" first.'));
59
+ process.exit(EXIT_CODES.CONFIG_ERROR);
60
+ }
61
+
62
+ const syncEngine = new SyncEngine(config);
63
+ const directory = process.cwd();
64
+ const formatter = getFormatter(false);
65
+
66
+ // Check if space is configured
67
+ if (!hasSpaceConfig(directory)) {
68
+ console.error(chalk.red('No space configured in this directory.'));
69
+ console.log(chalk.gray('Run "cn clone <SPACE_KEY>" to clone a space.'));
70
+ process.exit(EXIT_CODES.CONFIG_ERROR);
71
+ }
72
+
73
+ // Get space info for display
74
+ const spaceConfig = readSpaceConfig(directory);
75
+ if (spaceConfig) {
76
+ console.log(chalk.bold(`Pulling space: ${spaceConfig.spaceName} (${spaceConfig.spaceKey})`));
77
+ }
78
+
79
+ // Check for duplicate page_ids before proceeding
80
+ const allFiles = scanFilesForHealthCheck(directory);
81
+ const duplicates = findDuplicatePageIds(allFiles);
82
+
83
+ if (duplicates.length > 0) {
84
+ console.log('');
85
+ console.log(chalk.red('Duplicate page_ids detected:'));
86
+ for (const dup of duplicates) {
87
+ const best = findBestDuplicate(dup.files);
88
+ console.log(chalk.yellow(`\n page_id: ${dup.pageId}`));
89
+ for (const file of dup.files) {
90
+ const isBest = file.path === best.path;
91
+ const marker = isBest ? chalk.green(' (keep)') : chalk.red(' (stale)');
92
+ const version = file.version ? `v${file.version}` : 'v?';
93
+ console.log(` ${isBest ? chalk.green('*') : chalk.red('x')} ${file.path}${marker} - ${version}`);
94
+ }
95
+ }
96
+ console.log('');
97
+
98
+ // Offer to auto-fix
99
+ const shouldFix = await confirm({
100
+ message: 'Delete stale files before pulling?',
101
+ default: true,
102
+ });
103
+
104
+ if (shouldFix) {
105
+ for (const dup of duplicates) {
106
+ const stale = findStaleDuplicates(dup);
107
+ for (const file of stale) {
108
+ try {
109
+ unlinkSync(join(directory, file.path));
110
+ console.log(chalk.green(` Deleted: ${file.path}`));
111
+ } catch (error) {
112
+ console.log(
113
+ chalk.red(` Failed to delete ${file.path}: ${error instanceof Error ? error.message : 'Unknown error'}`),
114
+ );
115
+ }
116
+ }
117
+ }
118
+ console.log('');
119
+ }
120
+ }
121
+
122
+ // Cancellation signal - shared between handler and sync engine
123
+ const signal = { cancelled: false };
124
+
125
+ // Force raw mode to capture Ctrl+C as data when Bun doesn't deliver SIGINT.
126
+ let restoreRawMode: boolean | undefined;
127
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
128
+ try {
129
+ restoreRawMode = process.stdin.isRaw ?? false;
130
+ process.stdin.setRawMode(true);
131
+ } catch {
132
+ restoreRawMode = undefined;
133
+ }
134
+ }
135
+
136
+ // Handle Ctrl+C via SIGINT
137
+ const sigintHandler = (): void => {
138
+ if (signal.cancelled) return;
139
+ signal.cancelled = true;
140
+ console.log('');
141
+ console.log(chalk.yellow('Cancelling pull...'));
142
+ };
143
+ process.on('SIGINT', sigintHandler);
144
+ process.on('SIGTERM', sigintHandler);
145
+
146
+ // Fallback: Handle Ctrl+C as raw byte (0x03/ETX) when terminal is in raw mode
147
+ // This catches Ctrl+C even when SIGINT isn't generated
148
+ const stdinHandler = (data: Buffer | string): void => {
149
+ const isCtrlC = typeof data === 'string' ? data.includes('\u0003') : data.includes(0x03);
150
+ if (isCtrlC) {
151
+ // ETX byte = Ctrl+C
152
+ if (signal.cancelled) return;
153
+ signal.cancelled = true;
154
+ console.log('');
155
+ console.log(chalk.yellow('Cancelling pull...'));
156
+ }
157
+ };
158
+ if (process.stdin.isTTY) {
159
+ process.stdin.on('data', stdinHandler);
160
+ process.stdin.resume();
161
+ }
162
+
163
+ // Perform sync
164
+ const progressReporter = options.dryRun ? undefined : createProgressReporter();
165
+
166
+ try {
167
+ const result = await syncEngine.sync(directory, {
168
+ dryRun: options.dryRun,
169
+ force: options.force,
170
+ forcePages: options.pages,
171
+ depth: options.depth,
172
+ progress: progressReporter,
173
+ signal,
174
+ });
175
+
176
+ // Clean up handlers
177
+ cleanupHandlers({ sigintHandler, stdinHandler, restoreRawMode });
178
+
179
+ // For dry run, show diff
180
+ if (options.dryRun) {
181
+ console.log('');
182
+ console.log(formatter.formatSyncDiff(result.changes));
183
+ }
184
+
185
+ // Show warnings
186
+ if (result.warnings.length > 0) {
187
+ console.log('');
188
+ console.log(chalk.yellow('Warnings:'));
189
+ for (const warning of result.warnings) {
190
+ console.log(chalk.yellow(` ! ${warning}`));
191
+ }
192
+ }
193
+
194
+ // Show errors
195
+ if (result.errors.length > 0) {
196
+ console.log('');
197
+ console.log(chalk.red('Errors:'));
198
+ for (const error of result.errors) {
199
+ console.log(chalk.red(` x ${error}`));
200
+ }
201
+ }
202
+
203
+ // Handle cancellation
204
+ if (result.cancelled) {
205
+ console.log('');
206
+ console.log(chalk.yellow('Pull cancelled. Run "cn pull" again to resume.'));
207
+ process.exit(130);
208
+ }
209
+
210
+ // If dry run, offer to sync
211
+ if (options.dryRun) {
212
+ const totalChanges = result.changes.added.length + result.changes.modified.length + result.changes.deleted.length;
213
+
214
+ if (totalChanges > 0) {
215
+ console.log('');
216
+ console.log(chalk.gray('This was a dry run. No changes were made.'));
217
+ }
218
+ } else if (!result.success) {
219
+ console.log('');
220
+ console.error(chalk.red('Pull completed with errors.'));
221
+ process.exit(EXIT_CODES.GENERAL_ERROR);
222
+ } else {
223
+ const { added, modified, deleted } = result.changes;
224
+ const total = added.length + modified.length + deleted.length;
225
+ if (total > 0) {
226
+ console.log('');
227
+ const parts = [];
228
+ if (added.length > 0) parts.push(`${added.length} added`);
229
+ if (modified.length > 0) parts.push(`${modified.length} modified`);
230
+ if (deleted.length > 0) parts.push(`${deleted.length} deleted`);
231
+ console.log(chalk.green(`✓ Pull complete: ${parts.join(', ')}`));
232
+ }
233
+ }
234
+ } catch (error) {
235
+ // Clean up handlers
236
+ cleanupHandlers({ sigintHandler, stdinHandler, restoreRawMode });
237
+ console.error(chalk.red('Pull failed'));
238
+ console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
239
+ process.exit(EXIT_CODES.GENERAL_ERROR);
240
+ }
241
+ }
@@ -0,0 +1,40 @@
1
+ import chalk from 'chalk';
2
+ import { EXIT_CODES, PageNotFoundError, VersionConflictError } from '../../lib/errors.js';
3
+
4
+ /**
5
+ * Error thrown during push operations
6
+ */
7
+ export class PushError extends Error {
8
+ constructor(
9
+ message: string,
10
+ public readonly exitCode: number,
11
+ ) {
12
+ super(message);
13
+ this.name = 'PushError';
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Handle push errors - formats error and throws PushError
19
+ */
20
+ export function handlePushError(error: unknown, filePath: string): never {
21
+ if (error instanceof PageNotFoundError) {
22
+ console.error('');
23
+ console.error(chalk.red(`Page not found on Confluence (ID: ${error.pageId}).`));
24
+ console.log(chalk.gray('The page may have been deleted.'));
25
+ throw new PushError(`Page not found: ${error.pageId}`, EXIT_CODES.PAGE_NOT_FOUND);
26
+ }
27
+
28
+ if (error instanceof VersionConflictError) {
29
+ console.error('');
30
+ console.error(chalk.red('Version conflict: remote version has changed.'));
31
+ console.log(chalk.gray(`Run "cn pull --page ${filePath}" to get the latest version.`));
32
+ throw new PushError('Version conflict', EXIT_CODES.VERSION_CONFLICT);
33
+ }
34
+
35
+ console.error('');
36
+ console.error(chalk.red('Push failed'));
37
+ const message = error instanceof Error ? error.message : 'Unknown error';
38
+ console.error(chalk.red(message));
39
+ throw new PushError(message, EXIT_CODES.GENERAL_ERROR);
40
+ }