@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,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
|
+
};
|