@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.
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -21
- package/README.md +174 -15
- package/bin/commands/backup.js +226 -0
- package/bin/commands/extract.js +2 -2
- package/bin/core/applier-utils.js +8 -9
- package/bin/core/applier.js +90 -14
- package/bin/core/args.js +17 -0
- package/bin/core/backup/cleanup.js +176 -0
- package/bin/core/backup/constants.js +114 -0
- package/bin/core/backup/file-ops.js +188 -0
- package/bin/core/backup/gitignore.js +173 -0
- package/bin/core/backup/index.js +189 -0
- package/bin/core/backup/manifest.js +185 -0
- package/bin/core/backup/restore.js +216 -0
- package/bin/core/backup/session.js +258 -0
- package/bin/core/backup.js +91 -41
- package/bin/core/command-interface.js +1 -0
- package/bin/core/file-walker.js +26 -12
- package/bin/core/fs-adapter.js +10 -0
- package/bin/core/help-generator.js +1 -1
- package/bin/core/index.js +0 -4
- package/bin/core/json-utils.js +10 -6
- package/bin/core/key-generator.js +12 -12
- package/bin/core/orphan-finder.js +12 -15
- package/bin/core/paths.js +4 -4
- package/bin/core/plugin-resolver-utils.js +1 -1
- package/bin/core/translator.js +17 -8
- package/bin/plugins/adapter-transloco.js +3 -2
- package/bin/plugins/parser-primeng.js +1 -1
- package/bin/plugins/provider-deepl.js +22 -8
- package/package.json +1 -1
- package/bin/core/types.js +0 -297
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Session restoration and rollback operations.
|
|
5
|
+
* @module backup/restore
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { Session } = require('./session');
|
|
9
|
+
const { restoreFileSync } = require('./file-ops');
|
|
10
|
+
const { SessionStatus, getSessionDir } = require('./constants');
|
|
11
|
+
const fs = require('../fs-adapter');
|
|
12
|
+
|
|
13
|
+
function logIfVerbose(verbose, log, message) {
|
|
14
|
+
if (verbose) {
|
|
15
|
+
log(message);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function handleMissingBackup(file, ctx) {
|
|
20
|
+
logIfVerbose(ctx.verbose, ctx.log, ` Skip (backup missing): ${file.original}`);
|
|
21
|
+
return { restored: false, skipped: true };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function handleDryRun(file, ctx) {
|
|
25
|
+
ctx.log(` Would restore: ${file.original}`);
|
|
26
|
+
return { restored: true, skipped: false };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function performRestore(file, ctx) {
|
|
30
|
+
restoreFileSync(file.backup, file.original);
|
|
31
|
+
logIfVerbose(ctx.verbose, ctx.log, ` Restored: ${file.original}`);
|
|
32
|
+
return { restored: true, skipped: false };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function restoreFileWithLogging(file, ctx) {
|
|
36
|
+
if (!fs.existsSync(file.backup)) {
|
|
37
|
+
return handleMissingBackup(file, ctx);
|
|
38
|
+
}
|
|
39
|
+
return ctx.dryRun ? handleDryRun(file, ctx) : performRestore(file, ctx);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function processRestoreFile(file, ctx) {
|
|
43
|
+
try {
|
|
44
|
+
return restoreFileWithLogging(file, ctx);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
logIfVerbose(ctx.verbose, ctx.log, ` Error restoring ${file.original}: ${err.message}`);
|
|
47
|
+
return { restored: false, skipped: true };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function loadSessionOrThrow(cwd, sessionId) {
|
|
52
|
+
const session = Session.load(cwd, sessionId);
|
|
53
|
+
if (!session) {
|
|
54
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
55
|
+
}
|
|
56
|
+
return session;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function countResults(results) {
|
|
60
|
+
return {
|
|
61
|
+
restored: results.filter(r => r.restored).length,
|
|
62
|
+
skipped: results.filter(r => r.skipped).length,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function executeRestore(files, ctx) {
|
|
67
|
+
const results = files.map(file => processRestoreFile(file, ctx));
|
|
68
|
+
return countResults(results);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function loadFilesOrEmpty(cwd, sessionId, log) {
|
|
72
|
+
const session = loadSessionOrThrow(cwd, sessionId);
|
|
73
|
+
const files = session.getBackedUpFiles();
|
|
74
|
+
if (files.length === 0) {
|
|
75
|
+
log('No files to restore');
|
|
76
|
+
}
|
|
77
|
+
return files;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildRestoreContext(options) {
|
|
81
|
+
const { dryRun = false, verbose = false, log = console.log } = options;
|
|
82
|
+
return { dryRun, verbose, log };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Restores files from a backup session
|
|
87
|
+
* @param {string} cwd
|
|
88
|
+
* @param {string} sessionId
|
|
89
|
+
* @param {Object} [options]
|
|
90
|
+
* @returns {{restored: number, skipped: number}}
|
|
91
|
+
*/
|
|
92
|
+
function restoreSession(cwd, sessionId, options = {}) {
|
|
93
|
+
const ctx = buildRestoreContext(options);
|
|
94
|
+
const files = loadFilesOrEmpty(cwd, sessionId, ctx.log);
|
|
95
|
+
if (files.length === 0) {
|
|
96
|
+
return { restored: 0, skipped: 0 };
|
|
97
|
+
}
|
|
98
|
+
return executeRestore(files, ctx);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Restores files from the latest backup session
|
|
103
|
+
* @param {string} cwd
|
|
104
|
+
* @param {Object} [options]
|
|
105
|
+
* @returns {{restored: number, skipped: number}}
|
|
106
|
+
*/
|
|
107
|
+
function restoreLatest(cwd, options = {}) {
|
|
108
|
+
const latestId = Session.getLatestId(cwd);
|
|
109
|
+
if (!latestId) {
|
|
110
|
+
throw new Error('No backup sessions found');
|
|
111
|
+
}
|
|
112
|
+
return restoreSession(cwd, latestId, options);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Auto-rollback on failure during in-progress session
|
|
117
|
+
* @param {string} cwd
|
|
118
|
+
* @param {Session} session
|
|
119
|
+
* @param {Object} [options]
|
|
120
|
+
* @returns {{restored: number, skipped: number}}
|
|
121
|
+
*/
|
|
122
|
+
function autoRollback(cwd, session, options = {}) {
|
|
123
|
+
const { log = console.log } = options;
|
|
124
|
+
log('Auto-rollback initiated...');
|
|
125
|
+
const result = restoreSession(cwd, session.id, options);
|
|
126
|
+
session.rollback();
|
|
127
|
+
log(`Rolled back ${result.restored} files`);
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {string} cwd
|
|
133
|
+
* @returns {Object|null}
|
|
134
|
+
*/
|
|
135
|
+
function checkIncompleteSession(cwd) {
|
|
136
|
+
const incomplete = Session.findIncomplete(cwd);
|
|
137
|
+
return incomplete.length > 0 ? incomplete[0] : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Returns preview of restore operation
|
|
142
|
+
* @param {string} cwd
|
|
143
|
+
* @param {string} sessionId
|
|
144
|
+
* @returns {Object|null}
|
|
145
|
+
*/
|
|
146
|
+
function formatRestorePreview(cwd, sessionId) {
|
|
147
|
+
const session = Session.load(cwd, sessionId);
|
|
148
|
+
if (!session) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const files = session.getBackedUpFiles();
|
|
152
|
+
const sessionDir = getSessionDir(cwd, sessionId);
|
|
153
|
+
const { command, timestamp } = session.manifest;
|
|
154
|
+
return {
|
|
155
|
+
sessionId,
|
|
156
|
+
command,
|
|
157
|
+
timestamp,
|
|
158
|
+
status: session.status,
|
|
159
|
+
fileCount: files.length,
|
|
160
|
+
files: files.map(f => ({ path: f.original, exists: fs.existsSync(f.backup) })),
|
|
161
|
+
sessionDir,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildRecoveryActions(id) {
|
|
166
|
+
return [
|
|
167
|
+
{ command: `i18nkit --restore ${id}`, description: 'Rollback to pre-modification state' },
|
|
168
|
+
{ command: 'i18nkit --force', description: 'Continue without rollback (risky)' },
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function getInProgressRecovery(id) {
|
|
173
|
+
return {
|
|
174
|
+
severity: 'warning',
|
|
175
|
+
message: `Incomplete session found: ${id}`,
|
|
176
|
+
suggestion: `Files may have been partially modified. Run 'i18nkit --restore ${id}' to rollback.`,
|
|
177
|
+
actions: buildRecoveryActions(id),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function getBackingUpRecovery() {
|
|
182
|
+
return {
|
|
183
|
+
severity: 'info',
|
|
184
|
+
message: 'Interrupted backup session',
|
|
185
|
+
suggestion: 'Backup was incomplete. No files were modified. Safe to continue.',
|
|
186
|
+
actions: [{ command: 'i18nkit [command]', description: 'Continue normally' }],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Returns recovery instructions for incomplete session
|
|
192
|
+
* @param {Object|null} incomplete
|
|
193
|
+
* @returns {Object|null}
|
|
194
|
+
*/
|
|
195
|
+
function getRecoveryInstructions(incomplete) {
|
|
196
|
+
if (!incomplete) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const { status, id } = incomplete;
|
|
200
|
+
if (status === SessionStatus.IN_PROGRESS) {
|
|
201
|
+
return getInProgressRecovery(id);
|
|
202
|
+
}
|
|
203
|
+
if (status === SessionStatus.BACKING_UP) {
|
|
204
|
+
return getBackingUpRecovery();
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = {
|
|
210
|
+
restoreSession,
|
|
211
|
+
restoreLatest,
|
|
212
|
+
autoRollback,
|
|
213
|
+
checkIncompleteSession,
|
|
214
|
+
formatRestorePreview,
|
|
215
|
+
getRecoveryInstructions,
|
|
216
|
+
};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Backup session lifecycle management.
|
|
5
|
+
* @module backup/session
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
const {
|
|
11
|
+
SessionStatus,
|
|
12
|
+
VALID_TRANSITIONS,
|
|
13
|
+
getSessionDir,
|
|
14
|
+
getBackupsDir,
|
|
15
|
+
getLatestFile,
|
|
16
|
+
} = require('./constants');
|
|
17
|
+
const { ensureDirSync, copyFilePreservingStructureSync, listDirsSync } = require('./file-ops');
|
|
18
|
+
const {
|
|
19
|
+
createManifest,
|
|
20
|
+
addFileToManifest,
|
|
21
|
+
updateManifestStatus,
|
|
22
|
+
writeManifestSync,
|
|
23
|
+
readManifestSync,
|
|
24
|
+
} = require('./manifest');
|
|
25
|
+
const fs = require('../fs-adapter');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generates unique session ID: YYYY-MM-DD_HH-MM-SS_command_rand
|
|
29
|
+
* @param {string} command
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
function generateSessionId(command) {
|
|
33
|
+
const now = new Date();
|
|
34
|
+
const date = now.toISOString().slice(0, 10);
|
|
35
|
+
const time = now.toTimeString().slice(0, 8).replace(/:/g, '-');
|
|
36
|
+
const cmd = command.replace(/[^a-zA-Z0-9]/g, '-').slice(0, 20);
|
|
37
|
+
const rand = crypto.randomBytes(2).toString('hex');
|
|
38
|
+
return `${date}_${time}_${cmd}_${rand}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} sessionId
|
|
43
|
+
* @returns {{id: string, timestamp: Date, date: string, time: string}|null}
|
|
44
|
+
*/
|
|
45
|
+
function parseSessionId(sessionId) {
|
|
46
|
+
const parts = sessionId.split('_');
|
|
47
|
+
if (parts.length < 4) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const [date, time] = parts;
|
|
51
|
+
const timestamp = new Date(`${date}T${time.replace(/-/g, ':')}`);
|
|
52
|
+
return { id: sessionId, timestamp, date, time };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getManifestValue(manifest, key, defaultValue) {
|
|
56
|
+
return manifest?.[key] || defaultValue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function extractSessionInfo(manifest, dir) {
|
|
60
|
+
return {
|
|
61
|
+
id: dir,
|
|
62
|
+
status: getManifestValue(manifest, 'status', 'unknown'),
|
|
63
|
+
fileCount: manifest?.files?.length || 0,
|
|
64
|
+
command: getManifestValue(manifest, 'command', 'unknown'),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseSessionForList(cwd, dir) {
|
|
69
|
+
const parsed = parseSessionId(dir);
|
|
70
|
+
if (!parsed) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const manifest = readManifestSync(cwd, dir);
|
|
74
|
+
return {
|
|
75
|
+
...extractSessionInfo(manifest, dir),
|
|
76
|
+
timestamp: parsed.timestamp,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Backup session with state machine lifecycle
|
|
82
|
+
*/
|
|
83
|
+
class Session {
|
|
84
|
+
/**
|
|
85
|
+
* @param {string} cwd
|
|
86
|
+
* @param {string} command
|
|
87
|
+
* @param {string} [sessionId]
|
|
88
|
+
*/
|
|
89
|
+
constructor(cwd, command, sessionId = null) {
|
|
90
|
+
this.cwd = cwd;
|
|
91
|
+
this.command = command;
|
|
92
|
+
this.id = sessionId || generateSessionId(command);
|
|
93
|
+
this.sessionDir = getSessionDir(cwd, this.id);
|
|
94
|
+
this.manifest = createManifest(this.id, command, cwd);
|
|
95
|
+
this.files = [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
get status() {
|
|
99
|
+
return this.manifest.status;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
validateTransition(newStatus) {
|
|
103
|
+
const allowed = VALID_TRANSITIONS[this.status] || [];
|
|
104
|
+
if (!allowed.includes(newStatus)) {
|
|
105
|
+
throw new Error(`Invalid transition: ${this.status} -> ${newStatus}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
setStatus(status, stats = null, error = null) {
|
|
110
|
+
this.validateTransition(status);
|
|
111
|
+
updateManifestStatus(this.manifest, { status, stats, error });
|
|
112
|
+
this.save();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
save() {
|
|
116
|
+
ensureDirSync(this.sessionDir);
|
|
117
|
+
writeManifestSync(this.cwd, this.id, this.manifest);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
start() {
|
|
121
|
+
this.setStatus(SessionStatus.BACKING_UP);
|
|
122
|
+
return this;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
backupFile(filePath) {
|
|
126
|
+
if (this.status !== SessionStatus.BACKING_UP) {
|
|
127
|
+
throw new Error(`Cannot backup file in status: ${this.status}`);
|
|
128
|
+
}
|
|
129
|
+
const fileInfo = copyFilePreservingStructureSync(filePath, this.sessionDir, this.cwd);
|
|
130
|
+
addFileToManifest(this.manifest, fileInfo);
|
|
131
|
+
this.files.push(fileInfo);
|
|
132
|
+
this.save();
|
|
133
|
+
return fileInfo;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
markReady() {
|
|
137
|
+
this.setStatus(SessionStatus.READY);
|
|
138
|
+
return this;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
beginModifications() {
|
|
142
|
+
this.setStatus(SessionStatus.IN_PROGRESS);
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
saveReport(reportPath) {
|
|
147
|
+
if (!fs.existsSync(reportPath)) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const reportDest = path.join(this.sessionDir, 'report.json');
|
|
151
|
+
fs.copyFileSync(reportPath, reportDest);
|
|
152
|
+
this.manifest.reportFile = 'report.json';
|
|
153
|
+
this.save();
|
|
154
|
+
return reportDest;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
complete(stats = {}) {
|
|
158
|
+
this.setStatus(SessionStatus.COMPLETED, stats);
|
|
159
|
+
this.updateLatest();
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
rollback() {
|
|
164
|
+
this.setStatus(SessionStatus.ROLLED_BACK);
|
|
165
|
+
return this;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
fail(error) {
|
|
169
|
+
updateManifestStatus(this.manifest, { status: SessionStatus.FAILED, error });
|
|
170
|
+
this.save();
|
|
171
|
+
return this;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
updateLatest() {
|
|
175
|
+
const latestFile = getLatestFile(this.cwd);
|
|
176
|
+
try {
|
|
177
|
+
fs.writeFileSync(latestFile, this.id, 'utf-8');
|
|
178
|
+
} catch {
|
|
179
|
+
// Non-critical: latest file update failure is acceptable
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
getBackedUpFiles() {
|
|
184
|
+
return this.manifest.files.map(f => ({
|
|
185
|
+
original: path.join(this.cwd, f.original),
|
|
186
|
+
backup: path.join(this.sessionDir, f.backup),
|
|
187
|
+
size: f.size,
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @param {string} cwd
|
|
193
|
+
* @param {string} sessionId
|
|
194
|
+
* @returns {Session|null}
|
|
195
|
+
*/
|
|
196
|
+
static load(cwd, sessionId) {
|
|
197
|
+
const manifest = readManifestSync(cwd, sessionId);
|
|
198
|
+
if (!manifest) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const session = new Session(cwd, manifest.command, sessionId);
|
|
202
|
+
session.manifest = manifest;
|
|
203
|
+
session.files = manifest.files || [];
|
|
204
|
+
return session;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* @param {string} cwd
|
|
209
|
+
* @returns {string|null}
|
|
210
|
+
*/
|
|
211
|
+
static getLatestId(cwd) {
|
|
212
|
+
const latestFile = getLatestFile(cwd);
|
|
213
|
+
try {
|
|
214
|
+
return fs.readFileSync(latestFile, 'utf-8').trim();
|
|
215
|
+
} catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* @param {string} cwd
|
|
222
|
+
* @returns {Session|null}
|
|
223
|
+
*/
|
|
224
|
+
static loadLatest(cwd) {
|
|
225
|
+
const latestId = Session.getLatestId(cwd);
|
|
226
|
+
return latestId ? Session.load(cwd, latestId) : null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @param {string} cwd
|
|
231
|
+
* @returns {Object[]}
|
|
232
|
+
*/
|
|
233
|
+
static listAll(cwd) {
|
|
234
|
+
const backupsDir = getBackupsDir(cwd);
|
|
235
|
+
const dirs = listDirsSync(backupsDir);
|
|
236
|
+
return dirs
|
|
237
|
+
.map(dir => parseSessionForList(cwd, dir))
|
|
238
|
+
.filter(Boolean)
|
|
239
|
+
.sort((a, b) => b.timestamp - a.timestamp);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* @param {string} cwd
|
|
244
|
+
* @returns {Object[]}
|
|
245
|
+
*/
|
|
246
|
+
static findIncomplete(cwd) {
|
|
247
|
+
const sessions = Session.listAll(cwd);
|
|
248
|
+
return sessions.filter(
|
|
249
|
+
s => s.status === SessionStatus.BACKING_UP || s.status === SessionStatus.IN_PROGRESS,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
module.exports = {
|
|
255
|
+
Session,
|
|
256
|
+
generateSessionId,
|
|
257
|
+
parseSessionId,
|
|
258
|
+
};
|
package/bin/core/backup.js
CHANGED
|
@@ -1,74 +1,124 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @fileoverview
|
|
5
|
-
* Creates timestamped backups before applying changes.
|
|
4
|
+
* @fileoverview Backward-compatible backup API wrapping new session-based system.
|
|
6
5
|
* @module backup
|
|
6
|
+
* @deprecated Use backup/index.js for new code
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
const path = require('path');
|
|
9
|
+
const backup = require('./backup/index');
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
let currentSession = null;
|
|
12
|
+
const legacyBackupFiles = new Map();
|
|
13
13
|
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
function initSessionIfNeeded(cwd) {
|
|
15
|
+
if (!currentSession) {
|
|
16
|
+
currentSession = backup.createBackupSession(cwd, 'legacy-apply');
|
|
17
|
+
}
|
|
18
|
+
return currentSession;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function tryBackupFile(session, filePath) {
|
|
22
|
+
const fileInfo = session.backupFile(filePath);
|
|
23
|
+
const backupPath = require('path').join(session.sessionDir, fileInfo.backup);
|
|
24
|
+
legacyBackupFiles.set(filePath, backupPath);
|
|
25
|
+
return backupPath;
|
|
18
26
|
}
|
|
19
27
|
|
|
20
|
-
function
|
|
28
|
+
function shouldSkipLegacyBackup(enabled, dryRun) {
|
|
29
|
+
return !enabled || dryRun;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function performLegacyBackup(filePath) {
|
|
33
|
+
const cwd = process.cwd();
|
|
34
|
+
const session = initSessionIfNeeded(cwd);
|
|
21
35
|
try {
|
|
22
|
-
|
|
23
|
-
fs.writeFileSync(backupPath, content, 'utf-8');
|
|
24
|
-
backupFiles.set(filePath, backupPath);
|
|
25
|
-
return backupPath;
|
|
36
|
+
return tryBackupFile(session, filePath);
|
|
26
37
|
} catch (err) {
|
|
27
38
|
console.warn(`Warning: Cannot backup ${filePath}: ${err.message}`);
|
|
28
39
|
return null;
|
|
29
40
|
}
|
|
30
41
|
}
|
|
31
42
|
|
|
32
|
-
function shouldSkipBackup(enabled, dryRun) {
|
|
33
|
-
return !enabled || dryRun;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
43
|
/**
|
|
37
|
-
* Creates a
|
|
38
|
-
* @param {string} filePath
|
|
39
|
-
* @param {string}
|
|
40
|
-
* @param {Object} [options]
|
|
44
|
+
* Creates a backup of a file (legacy API)
|
|
45
|
+
* @param {string} filePath - File to backup
|
|
46
|
+
* @param {string} _backupDir - Backup directory (ignored, uses .i18nkit/backups)
|
|
47
|
+
* @param {Object} [options] - Options
|
|
41
48
|
* @returns {string|null} Backup path or null if skipped
|
|
42
49
|
*/
|
|
43
|
-
function createBackup(filePath,
|
|
50
|
+
function createBackup(filePath, _backupDir, options = {}) {
|
|
44
51
|
const { enabled = true, dryRun = false } = options;
|
|
45
|
-
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
fs.mkdirSync(backupDir, { recursive: true });
|
|
49
|
-
return writeBackupFile(filePath, buildBackupPath(filePath, backupDir));
|
|
52
|
+
return shouldSkipLegacyBackup(enabled, dryRun) ? null : performLegacyBackup(filePath);
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
/**
|
|
53
|
-
* Restores all backed-up files
|
|
56
|
+
* Restores all backed-up files (legacy API)
|
|
54
57
|
* @returns {number} Count of restored files
|
|
55
58
|
*/
|
|
56
59
|
function restoreBackups() {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
const content = fs.readFileSync(backup, 'utf-8');
|
|
61
|
-
fs.writeFileSync(original, content, 'utf-8');
|
|
62
|
-
restored++;
|
|
63
|
-
} catch {
|
|
64
|
-
console.error(`Cannot restore ${original} from ${backup}`);
|
|
65
|
-
}
|
|
60
|
+
if (!currentSession) {
|
|
61
|
+
return 0;
|
|
66
62
|
}
|
|
67
|
-
|
|
63
|
+
const cwd = process.cwd();
|
|
64
|
+
const result = backup.restoreSession(cwd, currentSession.id, { verbose: false });
|
|
65
|
+
return result.restored;
|
|
68
66
|
}
|
|
69
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Gets the map of backed-up files (legacy API)
|
|
70
|
+
* @returns {Map<string, string>}
|
|
71
|
+
*/
|
|
70
72
|
function getBackupFiles() {
|
|
71
|
-
return
|
|
73
|
+
return legacyBackupFiles;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Marks session as ready for modifications (new API bridge)
|
|
78
|
+
*/
|
|
79
|
+
function markReady() {
|
|
80
|
+
if (currentSession && currentSession.status === 'backing-up') {
|
|
81
|
+
currentSession.markReady();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Begins modification phase (new API bridge)
|
|
87
|
+
*/
|
|
88
|
+
function beginModifications() {
|
|
89
|
+
if (currentSession && currentSession.status === 'ready') {
|
|
90
|
+
currentSession.beginModifications();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Completes the backup session (new API bridge)
|
|
96
|
+
* @param {Object} [stats] - Operation stats
|
|
97
|
+
*/
|
|
98
|
+
function completeSession(stats = {}) {
|
|
99
|
+
if (currentSession) {
|
|
100
|
+
currentSession.complete(stats);
|
|
101
|
+
currentSession = null;
|
|
102
|
+
legacyBackupFiles.clear();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resets session state (for testing)
|
|
108
|
+
*/
|
|
109
|
+
function resetSession() {
|
|
110
|
+
currentSession = null;
|
|
111
|
+
legacyBackupFiles.clear();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
createBackup,
|
|
116
|
+
restoreBackups,
|
|
117
|
+
getBackupFiles,
|
|
118
|
+
markReady,
|
|
119
|
+
beginModifications,
|
|
120
|
+
completeSession,
|
|
121
|
+
resetSession,
|
|
122
|
+
|
|
123
|
+
...backup,
|
|
124
|
+
};
|
|
@@ -11,6 +11,7 @@ const COMMAND_CATEGORIES = Object.freeze([
|
|
|
11
11
|
{ id: 'validation', label: 'VALIDATION' },
|
|
12
12
|
{ id: 'translation', label: 'TRANSLATION' },
|
|
13
13
|
{ id: 'development', label: 'DEVELOPMENT' },
|
|
14
|
+
{ id: 'maintenance', label: 'MAINTENANCE' },
|
|
14
15
|
]);
|
|
15
16
|
|
|
16
17
|
const getDescription = cmd => cmd.description || cmd.meta?.description || 'No description';
|