@aaronshaf/confluence-cli 0.1.15 → 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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/cli/commands/folder.ts +189 -0
- package/src/cli/help.ts +30 -36
- package/src/cli/index.ts +12 -14
- package/src/lib/confluence-client/client.ts +19 -3
- package/src/lib/confluence-client/folder-operations.ts +41 -0
- package/src/lib/confluence-client/search-operations.ts +2 -1
- package/src/test/folder-command.test.ts +182 -0
- package/src/cli/commands/duplicate-check.ts +0 -89
- package/src/cli/commands/file-rename.ts +0 -113
- package/src/cli/commands/folder-hierarchy.ts +0 -241
- package/src/cli/commands/push-errors.ts +0 -40
- package/src/cli/commands/push.ts +0 -699
- package/src/lib/dependency-sorter.ts +0 -233
- package/src/test/dependency-sorter.test.ts +0 -384
- package/src/test/file-rename.test.ts +0 -305
- package/src/test/folder-hierarchy.test.ts +0 -337
- package/src/test/push.test.ts +0 -551
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { confirm } from '@inquirer/prompts';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import {
|
|
4
|
-
checkFileForDuplicates,
|
|
5
|
-
findBestDuplicate,
|
|
6
|
-
findDuplicatePageIds,
|
|
7
|
-
scanFilesForHealthCheck,
|
|
8
|
-
type DuplicatePageId,
|
|
9
|
-
} from '../../lib/health-check.js';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Display duplicate page_id information
|
|
13
|
-
*/
|
|
14
|
-
export function displayDuplicates(duplicates: DuplicatePageId[]): void {
|
|
15
|
-
console.log(chalk.red('Duplicate page_ids detected:'));
|
|
16
|
-
for (const dup of duplicates) {
|
|
17
|
-
const best = findBestDuplicate(dup.files);
|
|
18
|
-
console.log(chalk.yellow(`\n page_id: ${dup.pageId}`));
|
|
19
|
-
for (const file of dup.files) {
|
|
20
|
-
const isBest = file.path === best.path;
|
|
21
|
-
const marker = isBest ? chalk.green(' (keep)') : chalk.red(' (stale)');
|
|
22
|
-
const version = file.version ? `v${file.version}` : 'v?';
|
|
23
|
-
console.log(` ${isBest ? chalk.green('*') : chalk.red('x')} ${file.path}${marker} - ${version}`);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
console.log('');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Check for duplicate page_ids before push and prompt for confirmation
|
|
31
|
-
* Returns true if push should continue, false if user cancelled
|
|
32
|
-
*/
|
|
33
|
-
export async function checkDuplicatesBeforePush(directory: string): Promise<boolean> {
|
|
34
|
-
const allFiles = scanFilesForHealthCheck(directory);
|
|
35
|
-
const duplicates = findDuplicatePageIds(allFiles);
|
|
36
|
-
|
|
37
|
-
if (duplicates.length === 0) {
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
displayDuplicates(duplicates);
|
|
42
|
-
|
|
43
|
-
const shouldContinue = await confirm({
|
|
44
|
-
message: 'Continue with push? (Stale files may cause version conflicts)',
|
|
45
|
-
default: false,
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
if (!shouldContinue) {
|
|
49
|
-
console.log(chalk.gray('Run "cn doctor" to fix duplicate page_ids.'));
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
console.log('');
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Display version conflict guidance, checking for duplicates that may explain the conflict
|
|
58
|
-
* This is called when a version mismatch is detected during push
|
|
59
|
-
*/
|
|
60
|
-
export function displayVersionConflictGuidance(directory: string, relativePath: string, remoteVersion: number): void {
|
|
61
|
-
const { hasDuplicates, duplicates } = checkFileForDuplicates(directory, relativePath);
|
|
62
|
-
|
|
63
|
-
if (hasDuplicates) {
|
|
64
|
-
const newerDuplicate = duplicates.find((d) => (d.version || 0) >= remoteVersion);
|
|
65
|
-
if (newerDuplicate) {
|
|
66
|
-
console.log(chalk.yellow('Found another file with the same page_id:'));
|
|
67
|
-
console.log(chalk.yellow(` ${newerDuplicate.path} (v${newerDuplicate.version || '?'})`));
|
|
68
|
-
console.log('');
|
|
69
|
-
console.log(chalk.gray('This usually happens when a page was moved on Confluence.'));
|
|
70
|
-
console.log(chalk.gray(`The file at ${relativePath} appears to be stale.`));
|
|
71
|
-
console.log('');
|
|
72
|
-
console.log(chalk.gray('Recommended actions:'));
|
|
73
|
-
console.log(chalk.gray(` 1. Delete the stale file: rm "${relativePath}"`));
|
|
74
|
-
console.log(chalk.gray(` 2. Or run "cn doctor" to fix all duplicates`));
|
|
75
|
-
} else {
|
|
76
|
-
console.log(chalk.yellow('Found duplicate files with the same page_id:'));
|
|
77
|
-
for (const dup of duplicates) {
|
|
78
|
-
console.log(chalk.yellow(` ${dup.path} (v${dup.version || '?'})`));
|
|
79
|
-
}
|
|
80
|
-
console.log('');
|
|
81
|
-
console.log(chalk.gray('Run "cn doctor" to identify and fix duplicates.'));
|
|
82
|
-
}
|
|
83
|
-
} else {
|
|
84
|
-
console.log(chalk.yellow('The page has been modified on Confluence since your last pull.'));
|
|
85
|
-
console.log(chalk.gray('Options:'));
|
|
86
|
-
console.log(chalk.gray(` - Run "cn pull --page ${relativePath}" to get the latest version`));
|
|
87
|
-
console.log(chalk.gray(` - Run "cn push ${relativePath} --force" to overwrite remote changes`));
|
|
88
|
-
}
|
|
89
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* File rename utilities for push operations
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { existsSync, mkdtempSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
6
|
-
import { tmpdir } from 'node:os';
|
|
7
|
-
import { basename, dirname, join } from 'node:path';
|
|
8
|
-
import chalk from 'chalk';
|
|
9
|
-
import { slugify, updateReferencesAfterRename } from '../../lib/markdown/index.js';
|
|
10
|
-
|
|
11
|
-
// Index files that should not be renamed based on title (case-insensitive check)
|
|
12
|
-
const INDEX_FILES = new Set(['index.md', 'readme.md']);
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Result of file rename operation
|
|
16
|
-
*/
|
|
17
|
-
export interface RenameResult {
|
|
18
|
-
finalPath: string;
|
|
19
|
-
wasRenamed: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Handle file renaming when title changes
|
|
24
|
-
* Returns the final local path for updating sync state
|
|
25
|
-
* Uses atomic operations: writes to temp file first, then renames
|
|
26
|
-
* Also updates references in other markdown files when a rename occurs
|
|
27
|
-
*/
|
|
28
|
-
export function handleFileRename(
|
|
29
|
-
filePath: string,
|
|
30
|
-
originalRelativePath: string,
|
|
31
|
-
expectedTitle: string,
|
|
32
|
-
updatedMarkdown: string,
|
|
33
|
-
spaceRoot?: string,
|
|
34
|
-
): RenameResult {
|
|
35
|
-
const currentFilename = basename(filePath);
|
|
36
|
-
const currentDir = dirname(filePath);
|
|
37
|
-
const expectedSlug = slugify(expectedTitle);
|
|
38
|
-
const expectedFilename = `${expectedSlug}.md`;
|
|
39
|
-
// Track the current relative path (will be updated if file is renamed)
|
|
40
|
-
const currentRelativePath = originalRelativePath.replace(/^\.\//, '');
|
|
41
|
-
let finalLocalPath = currentRelativePath;
|
|
42
|
-
|
|
43
|
-
const isIndexFile = INDEX_FILES.has(currentFilename.toLowerCase());
|
|
44
|
-
|
|
45
|
-
// Write to temp file first for atomicity
|
|
46
|
-
const tempDir = mkdtempSync(join(tmpdir(), 'cn-push-'));
|
|
47
|
-
const tempFile = join(tempDir, 'temp.md');
|
|
48
|
-
writeFileSync(tempFile, updatedMarkdown, 'utf-8');
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
if (!isIndexFile && expectedFilename !== currentFilename && expectedSlug) {
|
|
52
|
-
const newFilePath = join(currentDir, expectedFilename);
|
|
53
|
-
|
|
54
|
-
if (existsSync(newFilePath)) {
|
|
55
|
-
console.log(chalk.yellow(` Note: Keeping filename "${currentFilename}" (${expectedFilename} already exists)`));
|
|
56
|
-
// Atomic rename: temp file -> original file
|
|
57
|
-
renameSync(tempFile, filePath);
|
|
58
|
-
return { finalPath: finalLocalPath, wasRenamed: false };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Warn user about automatic rename
|
|
62
|
-
console.log(chalk.cyan(` Note: File will be renamed to match page title`));
|
|
63
|
-
|
|
64
|
-
// Atomic operations: remove old file, move temp to new location
|
|
65
|
-
const backupPath = `${filePath}.bak`;
|
|
66
|
-
renameSync(filePath, backupPath);
|
|
67
|
-
try {
|
|
68
|
-
renameSync(tempFile, newFilePath);
|
|
69
|
-
// Clean up backup only after successful rename
|
|
70
|
-
try {
|
|
71
|
-
unlinkSync(backupPath);
|
|
72
|
-
} catch {
|
|
73
|
-
// Ignore cleanup errors
|
|
74
|
-
}
|
|
75
|
-
} catch (error) {
|
|
76
|
-
// Restore from backup if rename fails
|
|
77
|
-
try {
|
|
78
|
-
renameSync(backupPath, filePath);
|
|
79
|
-
} catch {
|
|
80
|
-
// If restore fails, log the backup location
|
|
81
|
-
console.error(chalk.red(` Error: Failed to rename file. Backup available at: ${backupPath}`));
|
|
82
|
-
}
|
|
83
|
-
throw error;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const relativeDir = dirname(currentRelativePath);
|
|
87
|
-
const newRelativePath = relativeDir === '.' ? expectedFilename : join(relativeDir, expectedFilename);
|
|
88
|
-
finalLocalPath = newRelativePath;
|
|
89
|
-
console.log(chalk.cyan(` Renamed: ${currentFilename} → ${expectedFilename}`));
|
|
90
|
-
|
|
91
|
-
// Update references in other markdown files that link to the old filename
|
|
92
|
-
if (spaceRoot) {
|
|
93
|
-
const updatedFiles = updateReferencesAfterRename(spaceRoot, currentRelativePath, newRelativePath);
|
|
94
|
-
if (updatedFiles.length > 0) {
|
|
95
|
-
console.log(chalk.cyan(` Updated ${updatedFiles.length} file(s) with new link path`));
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return { finalPath: finalLocalPath, wasRenamed: true };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Atomic rename: temp file -> original file
|
|
103
|
-
renameSync(tempFile, filePath);
|
|
104
|
-
return { finalPath: finalLocalPath, wasRenamed: false };
|
|
105
|
-
} finally {
|
|
106
|
-
// Always clean up temp directory
|
|
107
|
-
try {
|
|
108
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
109
|
-
} catch {
|
|
110
|
-
// Ignore cleanup errors
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Folder hierarchy management for push operations
|
|
3
|
-
* Per ADR-0023: Folder push workflow support
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { confirm } from '@inquirer/prompts';
|
|
7
|
-
import chalk from 'chalk';
|
|
8
|
-
import { dirname } from 'node:path';
|
|
9
|
-
import type { ConfluenceClient, CreateFolderRequest } from '../../lib/confluence-client/index.js';
|
|
10
|
-
import { ApiError, EXIT_CODES } from '../../lib/errors.js';
|
|
11
|
-
import {
|
|
12
|
-
getFolderByPath,
|
|
13
|
-
updateFolderSyncInfo,
|
|
14
|
-
writeSpaceConfig,
|
|
15
|
-
type FolderSyncInfo,
|
|
16
|
-
type SpaceConfigWithState,
|
|
17
|
-
} from '../../lib/space-config.js';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Result of ensureFolderHierarchy operation
|
|
21
|
-
*/
|
|
22
|
-
export interface FolderHierarchyResult {
|
|
23
|
-
parentId: string | undefined;
|
|
24
|
-
updatedConfig: SpaceConfigWithState;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Maximum folder hierarchy depth to prevent hitting Confluence limits
|
|
28
|
-
const MAX_FOLDER_DEPTH = 10;
|
|
29
|
-
|
|
30
|
-
// Characters not allowed in Confluence page/folder titles
|
|
31
|
-
const INVALID_TITLE_CHARS = /[|\\/:*?"<>]/g;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Error thrown during folder hierarchy operations
|
|
35
|
-
*/
|
|
36
|
-
export class FolderHierarchyError extends Error {
|
|
37
|
-
constructor(
|
|
38
|
-
message: string,
|
|
39
|
-
public readonly exitCode: number,
|
|
40
|
-
) {
|
|
41
|
-
super(message);
|
|
42
|
-
this.name = 'FolderHierarchyError';
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Sanitize folder title for Confluence
|
|
48
|
-
* Removes invalid characters and returns sanitized title
|
|
49
|
-
*/
|
|
50
|
-
export function sanitizeFolderTitle(title: string): { sanitized: string; wasModified: boolean } {
|
|
51
|
-
const sanitized = title.replace(INVALID_TITLE_CHARS, '-').trim();
|
|
52
|
-
return { sanitized, wasModified: sanitized !== title };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Ensure folder hierarchy exists for a file path
|
|
57
|
-
* Per ADR-0023: Creates Confluence folders matching local directory structure
|
|
58
|
-
*
|
|
59
|
-
* @param client - Confluence client
|
|
60
|
-
* @param spaceConfig - Current space configuration
|
|
61
|
-
* @param directory - Base directory of the space
|
|
62
|
-
* @param filePath - Relative path to the file (e.g., "docs/api/endpoints.md")
|
|
63
|
-
* @param dryRun - If true, don't actually create folders
|
|
64
|
-
* @returns The leaf folder ID as parentId and updated config
|
|
65
|
-
*/
|
|
66
|
-
export async function ensureFolderHierarchy(
|
|
67
|
-
client: ConfluenceClient,
|
|
68
|
-
spaceConfig: SpaceConfigWithState,
|
|
69
|
-
directory: string,
|
|
70
|
-
filePath: string,
|
|
71
|
-
dryRun = false,
|
|
72
|
-
): Promise<FolderHierarchyResult> {
|
|
73
|
-
// Extract directory path from file path
|
|
74
|
-
const normalizedPath = filePath.replace(/^\.\//, '');
|
|
75
|
-
const dirPath = dirname(normalizedPath);
|
|
76
|
-
|
|
77
|
-
// Root level file - no folder needed
|
|
78
|
-
if (dirPath === '.') {
|
|
79
|
-
return { parentId: undefined, updatedConfig: spaceConfig };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Split directory path into segments
|
|
83
|
-
const segments = dirPath.split('/').filter((s) => s.length > 0);
|
|
84
|
-
if (segments.length === 0) {
|
|
85
|
-
return { parentId: undefined, updatedConfig: spaceConfig };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Path traversal validation - reject paths with ..
|
|
89
|
-
if (segments.some((s) => s === '..')) {
|
|
90
|
-
throw new FolderHierarchyError(
|
|
91
|
-
`Invalid path: "${dirPath}" contains path traversal sequences`,
|
|
92
|
-
EXIT_CODES.INVALID_ARGUMENTS,
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Folder depth guard - prevent hitting Confluence hierarchy limits
|
|
97
|
-
if (segments.length > MAX_FOLDER_DEPTH) {
|
|
98
|
-
throw new FolderHierarchyError(
|
|
99
|
-
`Folder hierarchy too deep: ${segments.length} levels (max: ${MAX_FOLDER_DEPTH})`,
|
|
100
|
-
EXIT_CODES.INVALID_ARGUMENTS,
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
let config = spaceConfig;
|
|
105
|
-
let currentParentId: string | undefined;
|
|
106
|
-
let currentPath = '';
|
|
107
|
-
|
|
108
|
-
// Iterate through each directory segment
|
|
109
|
-
for (const segment of segments) {
|
|
110
|
-
currentPath = currentPath ? `${currentPath}/${segment}` : segment;
|
|
111
|
-
|
|
112
|
-
// Check if folder already exists in local config
|
|
113
|
-
const existingLocalFolder = getFolderByPath(config, currentPath);
|
|
114
|
-
if (existingLocalFolder) {
|
|
115
|
-
currentParentId = existingLocalFolder.folderId;
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Sanitize folder title for Confluence
|
|
120
|
-
const { sanitized: folderTitle, wasModified } = sanitizeFolderTitle(segment);
|
|
121
|
-
if (wasModified) {
|
|
122
|
-
console.log(chalk.yellow(` Note: Folder title sanitized: "${segment}" → "${folderTitle}"`));
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Check if folder already exists on Confluence (not in local config)
|
|
126
|
-
const existingRemoteFolder = await client.findFolderByTitle(config.spaceKey, folderTitle, currentParentId);
|
|
127
|
-
if (existingRemoteFolder) {
|
|
128
|
-
console.log(
|
|
129
|
-
chalk.gray(
|
|
130
|
-
` Found existing folder on Confluence: ${existingRemoteFolder.title} (id: ${existingRemoteFolder.id})`,
|
|
131
|
-
),
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
// Track folder in config
|
|
135
|
-
const folderInfo: FolderSyncInfo = {
|
|
136
|
-
folderId: existingRemoteFolder.id,
|
|
137
|
-
title: existingRemoteFolder.title,
|
|
138
|
-
parentId: currentParentId,
|
|
139
|
-
localPath: currentPath,
|
|
140
|
-
};
|
|
141
|
-
config = updateFolderSyncInfo(config, folderInfo);
|
|
142
|
-
writeSpaceConfig(directory, config);
|
|
143
|
-
|
|
144
|
-
currentParentId = existingRemoteFolder.id;
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (dryRun) {
|
|
149
|
-
console.log(chalk.gray(` Would create folder: ${currentPath}`));
|
|
150
|
-
// In dry run, we can't continue because we don't have a real folder ID
|
|
151
|
-
return { parentId: undefined, updatedConfig: config };
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Prompt user for confirmation before creating folder
|
|
155
|
-
const shouldCreate = await confirm({
|
|
156
|
-
message: `Create folder "${folderTitle}" on Confluence?`,
|
|
157
|
-
default: true,
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
if (!shouldCreate) {
|
|
161
|
-
console.log(chalk.yellow(` Skipping folder creation for "${folderTitle}"`));
|
|
162
|
-
throw new FolderHierarchyError(
|
|
163
|
-
`Cannot push to subdirectory without creating folder hierarchy`,
|
|
164
|
-
EXIT_CODES.GENERAL_ERROR,
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
console.log(chalk.gray(` Creating folder: ${folderTitle}...`));
|
|
169
|
-
|
|
170
|
-
// Create folder on Confluence
|
|
171
|
-
const createRequest: CreateFolderRequest = {
|
|
172
|
-
spaceId: config.spaceId,
|
|
173
|
-
title: folderTitle,
|
|
174
|
-
parentId: currentParentId,
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
try {
|
|
178
|
-
const folder = await client.createFolder(createRequest);
|
|
179
|
-
console.log(chalk.green(` Created folder: ${folder.title} (id: ${folder.id})`));
|
|
180
|
-
|
|
181
|
-
// Track folder in config
|
|
182
|
-
const folderInfo: FolderSyncInfo = {
|
|
183
|
-
folderId: folder.id,
|
|
184
|
-
title: folder.title,
|
|
185
|
-
parentId: currentParentId,
|
|
186
|
-
localPath: currentPath,
|
|
187
|
-
};
|
|
188
|
-
config = updateFolderSyncInfo(config, folderInfo);
|
|
189
|
-
|
|
190
|
-
// Save config immediately so we don't lose folder tracking on failure
|
|
191
|
-
writeSpaceConfig(directory, config);
|
|
192
|
-
|
|
193
|
-
currentParentId = folder.id;
|
|
194
|
-
} catch (error) {
|
|
195
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
196
|
-
console.error(chalk.red(` Failed to create folder "${folderTitle}": ${message}`));
|
|
197
|
-
throw new FolderHierarchyError(`Failed to create folder: ${folderTitle}`, EXIT_CODES.GENERAL_ERROR);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Return the leaf folder ID - page will be created with this as parentId
|
|
202
|
-
// The v2 API supports creating pages directly under folders
|
|
203
|
-
return { parentId: currentParentId, updatedConfig: config };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Determine expected parent ID based on file's folder location.
|
|
208
|
-
*
|
|
209
|
-
* This is a semantic wrapper around ensureFolderHierarchy for use when updating
|
|
210
|
-
* existing pages. It answers: "What parent should this page have based on its
|
|
211
|
-
* current local file path?" - creating any missing folders in the process.
|
|
212
|
-
*
|
|
213
|
-
* For new pages, use ensureFolderHierarchy directly.
|
|
214
|
-
* For existing pages being moved, use this function to determine target parent.
|
|
215
|
-
*
|
|
216
|
-
* @param client - Confluence client
|
|
217
|
-
* @param spaceConfig - Current space configuration
|
|
218
|
-
* @param directory - Base directory of the space
|
|
219
|
-
* @param filePath - Relative path to the file
|
|
220
|
-
* @param dryRun - If true, don't create folders
|
|
221
|
-
* @returns The expected parent ID and updated config
|
|
222
|
-
*/
|
|
223
|
-
export async function determineExpectedParent(
|
|
224
|
-
client: ConfluenceClient,
|
|
225
|
-
spaceConfig: SpaceConfigWithState,
|
|
226
|
-
directory: string,
|
|
227
|
-
filePath: string,
|
|
228
|
-
dryRun = false,
|
|
229
|
-
): Promise<FolderHierarchyResult> {
|
|
230
|
-
// Get directory path
|
|
231
|
-
const normalizedPath = filePath.replace(/^\.\//, '');
|
|
232
|
-
const dirPath = dirname(normalizedPath);
|
|
233
|
-
|
|
234
|
-
// Root level file - no parent
|
|
235
|
-
if (dirPath === '.') {
|
|
236
|
-
return { parentId: undefined, updatedConfig: spaceConfig };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Use ensureFolderHierarchy to determine (and create if needed) the parent
|
|
240
|
-
return ensureFolderHierarchy(client, spaceConfig, directory, filePath, dryRun);
|
|
241
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
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
|
-
}
|