@abdess76/i18nkit 1.0.3 → 1.0.5

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.
@@ -0,0 +1,173 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview Gitignore management for backup directories.
5
+ * @module backup/gitignore
6
+ */
7
+
8
+ const path = require('path');
9
+ const fs = require('../fs-adapter');
10
+ const { BACKUP_ROOT, GITIGNORE_FILE } = require('./constants');
11
+ const { atomicWriteSync, ensureDirSync } = require('./file-ops');
12
+
13
+ const I18NKIT_GITIGNORE_CONTENT = `# i18nkit generated - do not edit
14
+ backups/
15
+ report.json
16
+ reports/
17
+ cache/
18
+ *.log
19
+ *.tmp
20
+ `;
21
+
22
+ const PROJECT_GITIGNORE_ENTRY = '.i18nkit/';
23
+ const GITIGNORE_MARKER = '# i18nkit';
24
+ const REQUIRED_ENTRIES = ['backups/', 'report.json', 'reports/'];
25
+
26
+ function getMissingEntries(content) {
27
+ return REQUIRED_ENTRIES.filter(entry => !content.includes(entry));
28
+ }
29
+
30
+ function createGitignore(gitignorePath) {
31
+ atomicWriteSync(gitignorePath, I18NKIT_GITIGNORE_CONTENT);
32
+ return { created: true, updated: false, path: gitignorePath };
33
+ }
34
+
35
+ function updateGitignoreContent(gitignorePath, content, missing) {
36
+ const additions = missing.join('\n');
37
+ const separator = content.endsWith('\n') ? '' : '\n';
38
+ const updated = `${content}${separator}${additions}\n`;
39
+ atomicWriteSync(gitignorePath, updated);
40
+ return { created: false, updated: true, added: missing, path: gitignorePath };
41
+ }
42
+
43
+ /**
44
+ * Creates or updates .i18nkit/.gitignore
45
+ * @param {string} cwd
46
+ * @returns {{created: boolean, updated: boolean, path: string}}
47
+ */
48
+ function ensureI18nkitGitignore(cwd) {
49
+ const i18nkitDir = path.join(cwd, BACKUP_ROOT);
50
+ const gitignorePath = path.join(i18nkitDir, GITIGNORE_FILE);
51
+ ensureDirSync(i18nkitDir);
52
+ if (!fs.existsSync(gitignorePath)) {
53
+ return createGitignore(gitignorePath);
54
+ }
55
+ const content = fs.readFileSync(gitignorePath, 'utf-8');
56
+ const missing = getMissingEntries(content);
57
+ if (missing.length > 0) {
58
+ return updateGitignoreContent(gitignorePath, content, missing);
59
+ }
60
+ return { created: false, updated: false, path: gitignorePath };
61
+ }
62
+
63
+ /**
64
+ * @param {string} cwd
65
+ * @returns {{exists: boolean, hasEntry: boolean, path: string}}
66
+ */
67
+ function checkProjectGitignore(cwd) {
68
+ const gitignorePath = path.join(cwd, '.gitignore');
69
+ if (!fs.existsSync(gitignorePath)) {
70
+ return { exists: false, hasEntry: false, path: gitignorePath };
71
+ }
72
+ const content = fs.readFileSync(gitignorePath, 'utf-8');
73
+ const hasEntry = content.includes(PROJECT_GITIGNORE_ENTRY) || content.includes(BACKUP_ROOT);
74
+ return { exists: true, hasEntry, path: gitignorePath };
75
+ }
76
+
77
+ /**
78
+ * Returns suggestion if .i18nkit/ not in project .gitignore
79
+ * @param {string} cwd
80
+ * @returns {{message: string, entry: string, path: string}|null}
81
+ */
82
+ function suggestGitignoreUpdate(cwd) {
83
+ const check = checkProjectGitignore(cwd);
84
+ if (check.hasEntry) {
85
+ return null;
86
+ }
87
+ return {
88
+ message: `Add "${PROJECT_GITIGNORE_ENTRY}" to your .gitignore`,
89
+ entry: PROJECT_GITIGNORE_ENTRY,
90
+ path: check.path,
91
+ };
92
+ }
93
+
94
+ function buildGitignoreEntry() {
95
+ return `\n${GITIGNORE_MARKER}\n${PROJECT_GITIGNORE_ENTRY}\n`;
96
+ }
97
+
98
+ function appendToExistingGitignore(gitignorePath, content) {
99
+ const newEntry = buildGitignoreEntry();
100
+ const separator = content.endsWith('\n') ? '' : '\n';
101
+ atomicWriteSync(gitignorePath, `${content}${separator}${newEntry}`);
102
+ }
103
+
104
+ function createNewGitignore(gitignorePath) {
105
+ const newEntry = buildGitignoreEntry();
106
+ atomicWriteSync(gitignorePath, `${newEntry.trim()}\n`);
107
+ }
108
+
109
+ /**
110
+ * Adds .i18nkit/ to project .gitignore
111
+ * @param {string} cwd
112
+ * @returns {Object} Result with added, reason, path properties
113
+ */
114
+ function addToProjectGitignore(cwd) {
115
+ const check = checkProjectGitignore(cwd);
116
+ if (check.hasEntry) {
117
+ return { added: false, reason: 'Already present' };
118
+ }
119
+ if (check.exists) {
120
+ const content = fs.readFileSync(check.path, 'utf-8');
121
+ appendToExistingGitignore(check.path, content);
122
+ } else {
123
+ createNewGitignore(check.path);
124
+ }
125
+ return { added: true, path: check.path };
126
+ }
127
+
128
+ function handleAutoGitignore(cwd, verbose, log) {
129
+ const result = addToProjectGitignore(cwd);
130
+ if (result.added && verbose) {
131
+ log(`Added ${PROJECT_GITIGNORE_ENTRY} to ${result.path}`);
132
+ }
133
+ }
134
+
135
+ function logCreation(result, verbose, log) {
136
+ if (result.created && verbose) {
137
+ log(`Created ${result.path}`);
138
+ }
139
+ }
140
+
141
+ function processSuggestion(ctx) {
142
+ const { cwd, suggestion, autoAddGitignore, verbose, log } = ctx;
143
+ if (suggestion && autoAddGitignore) {
144
+ handleAutoGitignore(cwd, verbose, log);
145
+ return null;
146
+ }
147
+ return suggestion;
148
+ }
149
+
150
+ /**
151
+ * Initializes .i18nkit directory structure and gitignore
152
+ * @param {string} cwd
153
+ * @param {Object} [options]
154
+ * @returns {{initialized: boolean, suggestion: Object|null}}
155
+ */
156
+ function initializeBackupStructure(cwd, options = {}) {
157
+ const { autoAddGitignore = false, log = console.log, verbose = false } = options;
158
+ const i18nkitResult = ensureI18nkitGitignore(cwd);
159
+ logCreation(i18nkitResult, verbose, log);
160
+ const suggestion = suggestGitignoreUpdate(cwd);
161
+ const finalSuggestion = processSuggestion({ cwd, suggestion, autoAddGitignore, verbose, log });
162
+ return { initialized: true, suggestion: finalSuggestion };
163
+ }
164
+
165
+ module.exports = {
166
+ ensureI18nkitGitignore,
167
+ checkProjectGitignore,
168
+ suggestGitignoreUpdate,
169
+ addToProjectGitignore,
170
+ initializeBackupStructure,
171
+ I18NKIT_GITIGNORE_CONTENT,
172
+ PROJECT_GITIGNORE_ENTRY,
173
+ };
@@ -0,0 +1,189 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview Backup system public API.
5
+ * @module backup
6
+ */
7
+
8
+ const { Session, generateSessionId, parseSessionId } = require('./session');
9
+ const { validateManifest, formatManifestForDisplay } = require('./manifest');
10
+ const {
11
+ restoreSession,
12
+ restoreLatest,
13
+ autoRollback,
14
+ checkIncompleteSession,
15
+ formatRestorePreview,
16
+ getRecoveryInstructions,
17
+ } = require('./restore');
18
+ const { cleanupOldSessions, getCleanupPreview, autoCleanupIfEnabled } = require('./cleanup');
19
+ const {
20
+ ensureI18nkitGitignore,
21
+ suggestGitignoreUpdate,
22
+ addToProjectGitignore,
23
+ initializeBackupStructure,
24
+ } = require('./gitignore');
25
+ const {
26
+ SessionStatus,
27
+ DEFAULT_CONFIG,
28
+ BACKUP_ROOT,
29
+ getBackupRoot,
30
+ getBackupsDir,
31
+ getSessionDir,
32
+ } = require('./constants');
33
+
34
+ /**
35
+ * Creates and starts a new backup session
36
+ * @param {string} cwd
37
+ * @param {string} command
38
+ * @returns {Session}
39
+ */
40
+ function createBackupSession(cwd, command) {
41
+ const session = new Session(cwd, command);
42
+ initializeBackupStructure(cwd, { verbose: false });
43
+ return session.start();
44
+ }
45
+
46
+ function logIncompleteWarning(instructions, log) {
47
+ log(`\nWarning: ${instructions.message}`);
48
+ log(` ${instructions.suggestion}\n`);
49
+ }
50
+
51
+ function handleIncompleteSession(cwd, log) {
52
+ const incomplete = checkIncompleteSession(cwd);
53
+ if (!incomplete) {
54
+ return;
55
+ }
56
+ const instructions = getRecoveryInstructions(incomplete);
57
+ if (instructions?.severity === 'warning') {
58
+ logIncompleteWarning(instructions, log);
59
+ }
60
+ }
61
+
62
+ function executeWithSession(session, operation) {
63
+ const result = operation({
64
+ backupFile: filePath => session.backupFile(filePath),
65
+ markReady: () => session.markReady(),
66
+ beginModifications: () => session.beginModifications(),
67
+ saveReport: reportPath => session.saveReport(reportPath),
68
+ });
69
+ session.complete({ filesModified: session.files.length });
70
+ return result;
71
+ }
72
+
73
+ function logBackupSuccess(session, verbose, log) {
74
+ if (verbose) {
75
+ log(`Backup session: ${session.id}`);
76
+ log(`Files backed up: ${session.files.length}`);
77
+ }
78
+ }
79
+
80
+ function handleSuggestion(cwd, log) {
81
+ const suggestion = suggestGitignoreUpdate(cwd);
82
+ if (suggestion) {
83
+ log(`\nTip: ${suggestion.message}`);
84
+ }
85
+ }
86
+
87
+ function handleSessionError(session, error) {
88
+ if (session.status === SessionStatus.IN_PROGRESS) {
89
+ autoRollback(session.cwd, session, { log: console.log, verbose: false });
90
+ } else {
91
+ session.fail(error);
92
+ }
93
+ }
94
+
95
+ function shouldSkipBackup(backup, dryRun) {
96
+ return !backup || dryRun;
97
+ }
98
+
99
+ function handleBackupSuccess(ctx) {
100
+ const { session, cwd, options } = ctx;
101
+ const { log = console.log, verbose = false } = options;
102
+ logBackupSuccess(session, verbose, log);
103
+ handleSuggestion(cwd, log);
104
+ autoCleanupIfEnabled(cwd, { ...DEFAULT_CONFIG, ...options });
105
+ }
106
+
107
+ function runWithBackup(ctx) {
108
+ const { cwd, command, operation, options } = ctx;
109
+ handleIncompleteSession(cwd, options.log || console.log);
110
+ const session = createBackupSession(cwd, command);
111
+ try {
112
+ const result = executeWithSession(session, operation);
113
+ handleBackupSuccess({ session, cwd, options });
114
+ return result;
115
+ } catch (error) {
116
+ handleSessionError(session, error);
117
+ throw error;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Wraps operation with backup session lifecycle
123
+ * @param {Object} ctx - Context with cwd, command, operation, options
124
+ * @returns {*} Operation result
125
+ */
126
+ function withBackup(ctx) {
127
+ const { cwd, command, operation, options = {} } = ctx;
128
+ const { backup = true, dryRun = false } = options;
129
+ if (shouldSkipBackup(backup, dryRun)) {
130
+ return operation();
131
+ }
132
+ return runWithBackup({ cwd, command, operation, options });
133
+ }
134
+
135
+ /**
136
+ * @param {string} cwd
137
+ * @returns {Object[]}
138
+ */
139
+ function listBackupSessions(cwd) {
140
+ return Session.listAll(cwd);
141
+ }
142
+
143
+ /**
144
+ * @param {string} cwd
145
+ * @param {string} sessionId
146
+ * @returns {Session|null}
147
+ */
148
+ function getBackupSession(cwd, sessionId) {
149
+ return Session.load(cwd, sessionId);
150
+ }
151
+
152
+ /**
153
+ * @param {string} cwd
154
+ * @returns {Session|null}
155
+ */
156
+ function getLatestBackupSession(cwd) {
157
+ return Session.loadLatest(cwd);
158
+ }
159
+
160
+ module.exports = {
161
+ Session,
162
+ SessionStatus,
163
+ DEFAULT_CONFIG,
164
+ BACKUP_ROOT,
165
+ createBackupSession,
166
+ withBackup,
167
+ listBackupSessions,
168
+ getBackupSession,
169
+ getLatestBackupSession,
170
+ restoreSession,
171
+ restoreLatest,
172
+ autoRollback,
173
+ checkIncompleteSession,
174
+ getRecoveryInstructions,
175
+ formatRestorePreview,
176
+ cleanupOldSessions,
177
+ getCleanupPreview,
178
+ initializeBackupStructure,
179
+ ensureI18nkitGitignore,
180
+ suggestGitignoreUpdate,
181
+ addToProjectGitignore,
182
+ generateSessionId,
183
+ parseSessionId,
184
+ validateManifest,
185
+ formatManifestForDisplay,
186
+ getBackupRoot,
187
+ getBackupsDir,
188
+ getSessionDir,
189
+ };
@@ -0,0 +1,185 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview Session manifest creation and persistence.
5
+ * @module backup/manifest
6
+ */
7
+
8
+ const fs = require('../fs-adapter');
9
+ const path = require('path');
10
+ const { atomicWrite, atomicWriteSync } = require('./file-ops');
11
+ const { MANIFEST_FILE, getSessionDir } = require('./constants');
12
+
13
+ const MANIFEST_VERSION = '1.0';
14
+
15
+ /**
16
+ * Creates new session manifest
17
+ * @param {string} sessionId
18
+ * @param {string} command
19
+ * @param {string} cwd
20
+ * @returns {Object}
21
+ */
22
+ function createManifest(sessionId, command, cwd) {
23
+ return {
24
+ version: MANIFEST_VERSION,
25
+ id: sessionId,
26
+ timestamp: new Date().toISOString(),
27
+ command,
28
+ cwd,
29
+ status: 'pending',
30
+ files: [],
31
+ reportFile: null,
32
+ stats: { filesModified: 0, filesBackedUp: 0 },
33
+ error: null,
34
+ };
35
+ }
36
+
37
+ /**
38
+ * @param {Object} manifest
39
+ * @param {{original: string, backup: string, size: number}} fileInfo
40
+ */
41
+ function addFileToManifest(manifest, fileInfo) {
42
+ manifest.files.push({
43
+ original: fileInfo.original,
44
+ backup: fileInfo.backup,
45
+ size: fileInfo.size,
46
+ backedUpAt: new Date().toISOString(),
47
+ });
48
+ manifest.stats.filesBackedUp = manifest.files.length;
49
+ }
50
+
51
+ /**
52
+ * @param {Object} manifest
53
+ * @param {Object} ctx - Context with status, stats, error properties
54
+ */
55
+ function updateManifestStatus(manifest, ctx) {
56
+ const { status, stats = null, error = null } = ctx;
57
+ manifest.status = status;
58
+ manifest.updatedAt = new Date().toISOString();
59
+ if (stats) {
60
+ manifest.stats = { ...manifest.stats, ...stats };
61
+ }
62
+ if (error) {
63
+ manifest.error = {
64
+ message: error.message,
65
+ stack: error.stack,
66
+ occurredAt: new Date().toISOString(),
67
+ };
68
+ }
69
+ }
70
+
71
+ function getManifestPath(cwd, sessionId) {
72
+ return path.join(getSessionDir(cwd, sessionId), MANIFEST_FILE);
73
+ }
74
+
75
+ /**
76
+ * @param {string} cwd
77
+ * @param {string} sessionId
78
+ * @param {Object} manifest
79
+ * @returns {Promise<void>}
80
+ */
81
+ async function writeManifest(cwd, sessionId, manifest) {
82
+ const manifestPath = getManifestPath(cwd, sessionId);
83
+ const content = JSON.stringify(manifest, null, 2);
84
+ await atomicWrite(manifestPath, content);
85
+ }
86
+
87
+ /**
88
+ * @param {string} cwd
89
+ * @param {string} sessionId
90
+ * @param {Object} manifest
91
+ */
92
+ function writeManifestSync(cwd, sessionId, manifest) {
93
+ const manifestPath = getManifestPath(cwd, sessionId);
94
+ const content = JSON.stringify(manifest, null, 2);
95
+ atomicWriteSync(manifestPath, content);
96
+ }
97
+
98
+ /**
99
+ * @param {string} cwd
100
+ * @param {string} sessionId
101
+ * @returns {Promise<Object|null>}
102
+ */
103
+ async function readManifest(cwd, sessionId) {
104
+ const manifestPath = getManifestPath(cwd, sessionId);
105
+ try {
106
+ const content = await fs.readFile(manifestPath, 'utf-8');
107
+ return JSON.parse(content);
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * @param {string} cwd
115
+ * @param {string} sessionId
116
+ * @returns {Object|null}
117
+ */
118
+ function readManifestSync(cwd, sessionId) {
119
+ const manifestPath = getManifestPath(cwd, sessionId);
120
+ try {
121
+ const content = fs.readFileSync(manifestPath, 'utf-8');
122
+ return JSON.parse(content);
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+
128
+ function hasRequiredField(manifest, field) {
129
+ return Boolean(manifest[field]);
130
+ }
131
+
132
+ /**
133
+ * @param {Object} manifest
134
+ * @returns {Object} Result with valid and error properties
135
+ */
136
+ function validateManifest(manifest) {
137
+ if (!manifest) {
138
+ return { valid: false, error: 'Manifest is null' };
139
+ }
140
+ const requiredFields = [
141
+ { field: 'version', error: 'Missing version' },
142
+ { field: 'id', error: 'Missing session id' },
143
+ { field: 'timestamp', error: 'Missing timestamp' },
144
+ ];
145
+ for (const { field, error } of requiredFields) {
146
+ if (!hasRequiredField(manifest, field)) {
147
+ return { valid: false, error };
148
+ }
149
+ }
150
+ if (!Array.isArray(manifest.files)) {
151
+ return { valid: false, error: 'Invalid files array' };
152
+ }
153
+ return { valid: true };
154
+ }
155
+
156
+ function formatDate(timestamp) {
157
+ return timestamp ? new Date(timestamp).toLocaleString() : 'unknown';
158
+ }
159
+
160
+ /**
161
+ * @param {Object} manifest
162
+ * @returns {Object}
163
+ */
164
+ function formatManifestForDisplay(manifest) {
165
+ return {
166
+ id: manifest.id,
167
+ date: formatDate(manifest.timestamp),
168
+ status: manifest.status || 'unknown',
169
+ fileCount: manifest.files?.length || 0,
170
+ command: manifest.command || 'unknown',
171
+ hasReport: Boolean(manifest.reportFile),
172
+ };
173
+ }
174
+
175
+ module.exports = {
176
+ createManifest,
177
+ addFileToManifest,
178
+ updateManifestStatus,
179
+ writeManifest,
180
+ writeManifestSync,
181
+ readManifest,
182
+ readManifestSync,
183
+ validateManifest,
184
+ formatManifestForDisplay,
185
+ };