@lumy-pack/syncpoint 0.0.1
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/assets/config.default.yml +29 -0
- package/assets/template.example.yml +28 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +2651 -0
- package/dist/commands/Backup.d.ts +2 -0
- package/dist/commands/Init.d.ts +2 -0
- package/dist/commands/List.d.ts +2 -0
- package/dist/commands/Provision.d.ts +2 -0
- package/dist/commands/Restore.d.ts +2 -0
- package/dist/commands/Status.d.ts +2 -0
- package/dist/components/Confirm.d.ts +8 -0
- package/dist/components/ProgressBar.d.ts +7 -0
- package/dist/components/StepRunner.d.ts +9 -0
- package/dist/components/Table.d.ts +8 -0
- package/dist/components/Viewer.d.ts +16 -0
- package/dist/constants.d.ts +16 -0
- package/dist/core/backup.d.ts +21 -0
- package/dist/core/config.d.ts +22 -0
- package/dist/core/metadata.d.ts +20 -0
- package/dist/core/provision.d.ts +28 -0
- package/dist/core/restore.d.ts +24 -0
- package/dist/core/storage.d.ts +22 -0
- package/dist/index.cjs +1069 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.mjs +1018 -0
- package/dist/schemas/ajv.d.ts +3 -0
- package/dist/schemas/config.schema.d.ts +4 -0
- package/dist/schemas/metadata.schema.d.ts +4 -0
- package/dist/schemas/template.schema.d.ts +4 -0
- package/dist/utils/assets.d.ts +2 -0
- package/dist/utils/format.d.ts +27 -0
- package/dist/utils/logger.d.ts +6 -0
- package/dist/utils/paths.d.ts +22 -0
- package/dist/utils/sudo.d.ts +16 -0
- package/dist/utils/system.d.ts +17 -0
- package/dist/utils/types.d.ts +136 -0
- package/package.json +77 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1069 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
createBackup: () => createBackup,
|
|
34
|
+
getBackupList: () => getBackupList,
|
|
35
|
+
getRestorePlan: () => getRestorePlan,
|
|
36
|
+
initDefaultConfig: () => initDefaultConfig,
|
|
37
|
+
listTemplates: () => listTemplates,
|
|
38
|
+
loadConfig: () => loadConfig,
|
|
39
|
+
loadTemplate: () => loadTemplate,
|
|
40
|
+
restoreBackup: () => restoreBackup,
|
|
41
|
+
runProvision: () => runProvision,
|
|
42
|
+
saveConfig: () => saveConfig,
|
|
43
|
+
scanTargets: () => scanTargets
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(src_exports);
|
|
46
|
+
|
|
47
|
+
// ../../node_modules/.pnpm/tsup@8.5.1_postcss@8.5.6_tsx@4.21.0_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/cjs_shims.js
|
|
48
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
49
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
50
|
+
|
|
51
|
+
// src/core/config.ts
|
|
52
|
+
var import_promises2 = require("fs/promises");
|
|
53
|
+
var import_node_path4 = require("path");
|
|
54
|
+
var import_yaml = __toESM(require("yaml"), 1);
|
|
55
|
+
|
|
56
|
+
// src/constants.ts
|
|
57
|
+
var import_node_path2 = require("path");
|
|
58
|
+
|
|
59
|
+
// src/utils/paths.ts
|
|
60
|
+
var import_promises = require("fs/promises");
|
|
61
|
+
var import_node_os = require("os");
|
|
62
|
+
var import_node_path = require("path");
|
|
63
|
+
function getHomeDir() {
|
|
64
|
+
const envHome = process.env.SYNCPOINT_HOME;
|
|
65
|
+
if (envHome) {
|
|
66
|
+
const normalized = (0, import_node_path.normalize)(envHome);
|
|
67
|
+
if (normalized.includes("..")) {
|
|
68
|
+
throw new Error(`SYNCPOINT_HOME contains path traversal: ${envHome}`);
|
|
69
|
+
}
|
|
70
|
+
if (!(0, import_node_path.resolve)(normalized).startsWith("/")) {
|
|
71
|
+
throw new Error(`SYNCPOINT_HOME must be an absolute path: ${envHome}`);
|
|
72
|
+
}
|
|
73
|
+
return normalized;
|
|
74
|
+
}
|
|
75
|
+
return (0, import_node_os.homedir)();
|
|
76
|
+
}
|
|
77
|
+
function expandTilde(p) {
|
|
78
|
+
if (p === "~") return getHomeDir();
|
|
79
|
+
if (p.startsWith("~/")) return (0, import_node_path.join)(getHomeDir(), p.slice(2));
|
|
80
|
+
return p;
|
|
81
|
+
}
|
|
82
|
+
function contractTilde(p) {
|
|
83
|
+
const home = getHomeDir();
|
|
84
|
+
if (p === home) return "~";
|
|
85
|
+
if (p.startsWith(home + "/")) return "~" + p.slice(home.length);
|
|
86
|
+
return p;
|
|
87
|
+
}
|
|
88
|
+
function resolveTargetPath(p) {
|
|
89
|
+
return (0, import_node_path.resolve)(expandTilde(p));
|
|
90
|
+
}
|
|
91
|
+
async function ensureDir(dirPath) {
|
|
92
|
+
await (0, import_promises.mkdir)(dirPath, { recursive: true });
|
|
93
|
+
}
|
|
94
|
+
async function fileExists(filePath) {
|
|
95
|
+
try {
|
|
96
|
+
await (0, import_promises.stat)(filePath);
|
|
97
|
+
return true;
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/constants.ts
|
|
104
|
+
var APP_NAME = "syncpoint";
|
|
105
|
+
var APP_DIR = `.${APP_NAME}`;
|
|
106
|
+
var CONFIG_FILENAME = "config.yml";
|
|
107
|
+
var METADATA_FILENAME = "_metadata.json";
|
|
108
|
+
var LARGE_FILE_THRESHOLD = 10 * 1024 * 1024;
|
|
109
|
+
var SENSITIVE_PATTERNS = ["id_rsa", "id_ed25519", "*.pem", "*.key"];
|
|
110
|
+
var BACKUPS_DIR = "backups";
|
|
111
|
+
var TEMPLATES_DIR = "templates";
|
|
112
|
+
var SCRIPTS_DIR = "scripts";
|
|
113
|
+
var LOGS_DIR = "logs";
|
|
114
|
+
function getAppDir() {
|
|
115
|
+
return (0, import_node_path2.join)(getHomeDir(), APP_DIR);
|
|
116
|
+
}
|
|
117
|
+
var APP_VERSION = "0.0.1";
|
|
118
|
+
function getSubDir(sub) {
|
|
119
|
+
return (0, import_node_path2.join)(getAppDir(), sub);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/schemas/ajv.ts
|
|
123
|
+
var import_ajv = __toESM(require("ajv"), 1);
|
|
124
|
+
var import_ajv_formats = __toESM(require("ajv-formats"), 1);
|
|
125
|
+
var ajv = new import_ajv.default({ allErrors: true });
|
|
126
|
+
(0, import_ajv_formats.default)(ajv);
|
|
127
|
+
|
|
128
|
+
// src/schemas/config.schema.ts
|
|
129
|
+
var configSchema = {
|
|
130
|
+
type: "object",
|
|
131
|
+
required: ["backup"],
|
|
132
|
+
properties: {
|
|
133
|
+
backup: {
|
|
134
|
+
type: "object",
|
|
135
|
+
required: ["targets", "exclude", "filename"],
|
|
136
|
+
properties: {
|
|
137
|
+
targets: {
|
|
138
|
+
type: "array",
|
|
139
|
+
items: { type: "string" }
|
|
140
|
+
},
|
|
141
|
+
exclude: {
|
|
142
|
+
type: "array",
|
|
143
|
+
items: { type: "string" }
|
|
144
|
+
},
|
|
145
|
+
filename: {
|
|
146
|
+
type: "string",
|
|
147
|
+
minLength: 1
|
|
148
|
+
},
|
|
149
|
+
destination: {
|
|
150
|
+
type: "string"
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
additionalProperties: false
|
|
154
|
+
},
|
|
155
|
+
scripts: {
|
|
156
|
+
type: "object",
|
|
157
|
+
properties: {
|
|
158
|
+
includeInBackup: {
|
|
159
|
+
type: "boolean"
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
additionalProperties: false
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
additionalProperties: false
|
|
166
|
+
};
|
|
167
|
+
var validate = ajv.compile(configSchema);
|
|
168
|
+
function validateConfig(data) {
|
|
169
|
+
const valid = validate(data);
|
|
170
|
+
if (valid) return { valid: true };
|
|
171
|
+
const errors = validate.errors?.map(
|
|
172
|
+
(e) => `${e.instancePath || "/"} ${e.message ?? "unknown error"}`
|
|
173
|
+
);
|
|
174
|
+
return { valid: false, errors };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/utils/assets.ts
|
|
178
|
+
var import_node_fs = require("fs");
|
|
179
|
+
var import_node_path3 = require("path");
|
|
180
|
+
var import_node_url = require("url");
|
|
181
|
+
function getPackageRoot() {
|
|
182
|
+
let dir = (0, import_node_path3.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
183
|
+
while (dir !== (0, import_node_path3.dirname)(dir)) {
|
|
184
|
+
if ((0, import_node_fs.existsSync)((0, import_node_path3.join)(dir, "package.json"))) return dir;
|
|
185
|
+
dir = (0, import_node_path3.dirname)(dir);
|
|
186
|
+
}
|
|
187
|
+
throw new Error("Could not find package root");
|
|
188
|
+
}
|
|
189
|
+
function getAssetPath(filename) {
|
|
190
|
+
return (0, import_node_path3.join)(getPackageRoot(), "assets", filename);
|
|
191
|
+
}
|
|
192
|
+
function readAsset(filename) {
|
|
193
|
+
return (0, import_node_fs.readFileSync)(getAssetPath(filename), "utf-8");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/core/config.ts
|
|
197
|
+
function stripDangerousKeys(obj) {
|
|
198
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
199
|
+
if (Array.isArray(obj)) return obj.map(stripDangerousKeys);
|
|
200
|
+
const cleaned = {};
|
|
201
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
202
|
+
if (["__proto__", "constructor", "prototype"].includes(key)) continue;
|
|
203
|
+
cleaned[key] = stripDangerousKeys(value);
|
|
204
|
+
}
|
|
205
|
+
return cleaned;
|
|
206
|
+
}
|
|
207
|
+
function getConfigPath() {
|
|
208
|
+
return (0, import_node_path4.join)(getAppDir(), CONFIG_FILENAME);
|
|
209
|
+
}
|
|
210
|
+
async function loadConfig() {
|
|
211
|
+
const configPath = getConfigPath();
|
|
212
|
+
const exists = await fileExists(configPath);
|
|
213
|
+
if (!exists) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Config file not found: ${configPath}
|
|
216
|
+
Run "syncpoint init" first.`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
const raw = await (0, import_promises2.readFile)(configPath, "utf-8");
|
|
220
|
+
const data = stripDangerousKeys(import_yaml.default.parse(raw));
|
|
221
|
+
const result = validateConfig(data);
|
|
222
|
+
if (!result.valid) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`Invalid config:
|
|
225
|
+
${(result.errors ?? []).join("\n")}`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return data;
|
|
229
|
+
}
|
|
230
|
+
async function saveConfig(config) {
|
|
231
|
+
const result = validateConfig(config);
|
|
232
|
+
if (!result.valid) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`Invalid config:
|
|
235
|
+
${(result.errors ?? []).join("\n")}`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const configPath = getConfigPath();
|
|
239
|
+
await ensureDir(getAppDir());
|
|
240
|
+
const yamlStr = import_yaml.default.stringify(config, { indent: 2 });
|
|
241
|
+
await (0, import_promises2.writeFile)(configPath, yamlStr, "utf-8");
|
|
242
|
+
}
|
|
243
|
+
async function initDefaultConfig() {
|
|
244
|
+
const created = [];
|
|
245
|
+
const skipped = [];
|
|
246
|
+
const dirs = [
|
|
247
|
+
getAppDir(),
|
|
248
|
+
getSubDir(BACKUPS_DIR),
|
|
249
|
+
getSubDir(TEMPLATES_DIR),
|
|
250
|
+
getSubDir(SCRIPTS_DIR),
|
|
251
|
+
getSubDir(LOGS_DIR)
|
|
252
|
+
];
|
|
253
|
+
for (const dir of dirs) {
|
|
254
|
+
const exists = await fileExists(dir);
|
|
255
|
+
if (!exists) {
|
|
256
|
+
await ensureDir(dir);
|
|
257
|
+
created.push(dir);
|
|
258
|
+
} else {
|
|
259
|
+
skipped.push(dir);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const configPath = getConfigPath();
|
|
263
|
+
const configExists = await fileExists(configPath);
|
|
264
|
+
if (!configExists) {
|
|
265
|
+
const yamlContent = readAsset("config.default.yml");
|
|
266
|
+
await (0, import_promises2.writeFile)(configPath, yamlContent, "utf-8");
|
|
267
|
+
created.push(configPath);
|
|
268
|
+
} else {
|
|
269
|
+
skipped.push(configPath);
|
|
270
|
+
}
|
|
271
|
+
return { created, skipped };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/core/backup.ts
|
|
275
|
+
var import_promises6 = require("fs/promises");
|
|
276
|
+
var import_node_path7 = require("path");
|
|
277
|
+
var import_fast_glob = __toESM(require("fast-glob"), 1);
|
|
278
|
+
|
|
279
|
+
// src/utils/system.ts
|
|
280
|
+
var import_node_os2 = require("os");
|
|
281
|
+
function getHostname() {
|
|
282
|
+
return (0, import_node_os2.hostname)();
|
|
283
|
+
}
|
|
284
|
+
function getSystemInfo() {
|
|
285
|
+
return {
|
|
286
|
+
platform: (0, import_node_os2.platform)(),
|
|
287
|
+
release: (0, import_node_os2.release)(),
|
|
288
|
+
arch: (0, import_node_os2.arch)()
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function formatHostname(name) {
|
|
292
|
+
const raw = name ?? getHostname();
|
|
293
|
+
return raw.replace(/\s+/g, "-").replace(/\./g, "-").replace(/[^a-zA-Z0-9\-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/utils/format.ts
|
|
297
|
+
function formatDatetime(date) {
|
|
298
|
+
const y = date.getFullYear();
|
|
299
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
300
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
301
|
+
const h = String(date.getHours()).padStart(2, "0");
|
|
302
|
+
const min = String(date.getMinutes()).padStart(2, "0");
|
|
303
|
+
const s = String(date.getSeconds()).padStart(2, "0");
|
|
304
|
+
return `${y}-${m}-${d}_${h}${min}${s}`;
|
|
305
|
+
}
|
|
306
|
+
function generateFilename(pattern, options) {
|
|
307
|
+
const now = options?.date ?? /* @__PURE__ */ new Date();
|
|
308
|
+
const host = formatHostname(options?.hostname);
|
|
309
|
+
const y = now.getFullYear();
|
|
310
|
+
const m = String(now.getMonth() + 1).padStart(2, "0");
|
|
311
|
+
const d = String(now.getDate()).padStart(2, "0");
|
|
312
|
+
const h = String(now.getHours()).padStart(2, "0");
|
|
313
|
+
const min = String(now.getMinutes()).padStart(2, "0");
|
|
314
|
+
const s = String(now.getSeconds()).padStart(2, "0");
|
|
315
|
+
let result = pattern.replace(/\{hostname\}/g, host).replace(/\{date\}/g, `${y}-${m}-${d}`).replace(/\{time\}/g, `${h}${min}${s}`).replace(/\{datetime\}/g, `${y}-${m}-${d}_${h}${min}${s}`);
|
|
316
|
+
if (options?.tag) {
|
|
317
|
+
result = result.replace(/\{tag\}/g, options.tag);
|
|
318
|
+
if (!pattern.includes("{tag}")) {
|
|
319
|
+
result += `_${options.tag}`;
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
result = result.replace(/\{tag\}/g, "").replace(/_+/g, "_").replace(/_$/, "");
|
|
323
|
+
}
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/utils/logger.ts
|
|
328
|
+
var import_promises3 = require("fs/promises");
|
|
329
|
+
var import_node_path5 = require("path");
|
|
330
|
+
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
331
|
+
var ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
332
|
+
function stripAnsi(str) {
|
|
333
|
+
return str.replace(ANSI_RE, "");
|
|
334
|
+
}
|
|
335
|
+
function timestamp() {
|
|
336
|
+
const now = /* @__PURE__ */ new Date();
|
|
337
|
+
const h = String(now.getHours()).padStart(2, "0");
|
|
338
|
+
const m = String(now.getMinutes()).padStart(2, "0");
|
|
339
|
+
const s = String(now.getSeconds()).padStart(2, "0");
|
|
340
|
+
return `${h}:${m}:${s}`;
|
|
341
|
+
}
|
|
342
|
+
function dateStamp() {
|
|
343
|
+
const now = /* @__PURE__ */ new Date();
|
|
344
|
+
const y = now.getFullYear();
|
|
345
|
+
const m = String(now.getMonth() + 1).padStart(2, "0");
|
|
346
|
+
const d = String(now.getDate()).padStart(2, "0");
|
|
347
|
+
return `${y}-${m}-${d}`;
|
|
348
|
+
}
|
|
349
|
+
var logDirCreated = false;
|
|
350
|
+
async function writeToFile(level, message) {
|
|
351
|
+
try {
|
|
352
|
+
const logsDir = (0, import_node_path5.join)(getAppDir(), LOGS_DIR);
|
|
353
|
+
if (!logDirCreated) {
|
|
354
|
+
await (0, import_promises3.mkdir)(logsDir, { recursive: true });
|
|
355
|
+
logDirCreated = true;
|
|
356
|
+
}
|
|
357
|
+
const logFile = (0, import_node_path5.join)(logsDir, `${dateStamp()}.log`);
|
|
358
|
+
const line = `[${timestamp()}] [${level.toUpperCase()}] ${stripAnsi(message)}
|
|
359
|
+
`;
|
|
360
|
+
await (0, import_promises3.appendFile)(logFile, line, "utf-8");
|
|
361
|
+
} catch {
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
var logger = {
|
|
365
|
+
info(message) {
|
|
366
|
+
console.log(`${import_picocolors.default.blue("info")} ${message}`);
|
|
367
|
+
void writeToFile("info", message);
|
|
368
|
+
},
|
|
369
|
+
success(message) {
|
|
370
|
+
console.log(`${import_picocolors.default.green("success")} ${message}`);
|
|
371
|
+
void writeToFile("success", message);
|
|
372
|
+
},
|
|
373
|
+
warn(message) {
|
|
374
|
+
console.warn(`${import_picocolors.default.yellow("warn")} ${message}`);
|
|
375
|
+
void writeToFile("warn", message);
|
|
376
|
+
},
|
|
377
|
+
error(message) {
|
|
378
|
+
console.error(`${import_picocolors.default.red("error")} ${message}`);
|
|
379
|
+
void writeToFile("error", message);
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// src/core/metadata.ts
|
|
384
|
+
var import_node_crypto = require("crypto");
|
|
385
|
+
var import_promises4 = require("fs/promises");
|
|
386
|
+
|
|
387
|
+
// src/schemas/metadata.schema.ts
|
|
388
|
+
var metadataSchema = {
|
|
389
|
+
type: "object",
|
|
390
|
+
required: [
|
|
391
|
+
"version",
|
|
392
|
+
"toolVersion",
|
|
393
|
+
"createdAt",
|
|
394
|
+
"hostname",
|
|
395
|
+
"system",
|
|
396
|
+
"config",
|
|
397
|
+
"files",
|
|
398
|
+
"summary"
|
|
399
|
+
],
|
|
400
|
+
properties: {
|
|
401
|
+
version: { type: "string" },
|
|
402
|
+
toolVersion: { type: "string" },
|
|
403
|
+
createdAt: { type: "string" },
|
|
404
|
+
hostname: { type: "string" },
|
|
405
|
+
system: {
|
|
406
|
+
type: "object",
|
|
407
|
+
required: ["platform", "release", "arch"],
|
|
408
|
+
properties: {
|
|
409
|
+
platform: { type: "string" },
|
|
410
|
+
release: { type: "string" },
|
|
411
|
+
arch: { type: "string" }
|
|
412
|
+
},
|
|
413
|
+
additionalProperties: false
|
|
414
|
+
},
|
|
415
|
+
config: {
|
|
416
|
+
type: "object",
|
|
417
|
+
required: ["filename"],
|
|
418
|
+
properties: {
|
|
419
|
+
filename: { type: "string" },
|
|
420
|
+
destination: { type: "string" }
|
|
421
|
+
},
|
|
422
|
+
additionalProperties: false
|
|
423
|
+
},
|
|
424
|
+
files: {
|
|
425
|
+
type: "array",
|
|
426
|
+
items: {
|
|
427
|
+
type: "object",
|
|
428
|
+
required: ["path", "absolutePath", "size", "hash"],
|
|
429
|
+
properties: {
|
|
430
|
+
path: { type: "string" },
|
|
431
|
+
absolutePath: { type: "string" },
|
|
432
|
+
size: { type: "number", minimum: 0 },
|
|
433
|
+
hash: { type: "string" },
|
|
434
|
+
type: { type: "string" }
|
|
435
|
+
},
|
|
436
|
+
additionalProperties: false
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
summary: {
|
|
440
|
+
type: "object",
|
|
441
|
+
required: ["fileCount", "totalSize"],
|
|
442
|
+
properties: {
|
|
443
|
+
fileCount: { type: "integer", minimum: 0 },
|
|
444
|
+
totalSize: { type: "number", minimum: 0 }
|
|
445
|
+
},
|
|
446
|
+
additionalProperties: false
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
additionalProperties: false
|
|
450
|
+
};
|
|
451
|
+
var validate2 = ajv.compile(metadataSchema);
|
|
452
|
+
function validateMetadata(data) {
|
|
453
|
+
const valid = validate2(data);
|
|
454
|
+
if (valid) return { valid: true };
|
|
455
|
+
const errors = validate2.errors?.map(
|
|
456
|
+
(e) => `${e.instancePath || "/"} ${e.message ?? "unknown error"}`
|
|
457
|
+
);
|
|
458
|
+
return { valid: false, errors };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/core/metadata.ts
|
|
462
|
+
var METADATA_VERSION = "1.0.0";
|
|
463
|
+
function createMetadata(files, config) {
|
|
464
|
+
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
465
|
+
return {
|
|
466
|
+
version: METADATA_VERSION,
|
|
467
|
+
toolVersion: APP_VERSION,
|
|
468
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
469
|
+
hostname: getHostname(),
|
|
470
|
+
system: getSystemInfo(),
|
|
471
|
+
config: {
|
|
472
|
+
filename: config.backup.filename,
|
|
473
|
+
destination: config.backup.destination
|
|
474
|
+
},
|
|
475
|
+
files,
|
|
476
|
+
summary: {
|
|
477
|
+
fileCount: files.length,
|
|
478
|
+
totalSize
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
function parseMetadata(data) {
|
|
483
|
+
const str = typeof data === "string" ? data : data.toString("utf-8");
|
|
484
|
+
const parsed = JSON.parse(str);
|
|
485
|
+
const result = validateMetadata(parsed);
|
|
486
|
+
if (!result.valid) {
|
|
487
|
+
throw new Error(
|
|
488
|
+
`Invalid metadata:
|
|
489
|
+
${(result.errors ?? []).join("\n")}`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
return parsed;
|
|
493
|
+
}
|
|
494
|
+
async function computeFileHash(filePath) {
|
|
495
|
+
const content = await (0, import_promises4.readFile)(filePath);
|
|
496
|
+
const hash = (0, import_node_crypto.createHash)("sha256").update(content).digest("hex");
|
|
497
|
+
return `sha256:${hash}`;
|
|
498
|
+
}
|
|
499
|
+
async function collectFileInfo(absolutePath, logicalPath) {
|
|
500
|
+
const lstats = await (0, import_promises4.lstat)(absolutePath);
|
|
501
|
+
let type;
|
|
502
|
+
if (lstats.isSymbolicLink()) {
|
|
503
|
+
type = "symlink";
|
|
504
|
+
} else if (lstats.isDirectory()) {
|
|
505
|
+
type = "directory";
|
|
506
|
+
}
|
|
507
|
+
let hash;
|
|
508
|
+
if (lstats.isSymbolicLink()) {
|
|
509
|
+
hash = `sha256:${(0, import_node_crypto.createHash)("sha256").update(absolutePath).digest("hex")}`;
|
|
510
|
+
} else {
|
|
511
|
+
hash = await computeFileHash(absolutePath);
|
|
512
|
+
}
|
|
513
|
+
return {
|
|
514
|
+
path: contractTilde(logicalPath),
|
|
515
|
+
absolutePath,
|
|
516
|
+
size: lstats.size,
|
|
517
|
+
hash,
|
|
518
|
+
type
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/core/storage.ts
|
|
523
|
+
var import_promises5 = require("fs/promises");
|
|
524
|
+
var import_node_os3 = require("os");
|
|
525
|
+
var import_node_path6 = require("path");
|
|
526
|
+
var tar = __toESM(require("tar"), 1);
|
|
527
|
+
async function createArchive(files, outputPath) {
|
|
528
|
+
const tmpDir = await (0, import_promises5.mkdtemp)((0, import_node_path6.join)((0, import_node_os3.tmpdir)(), "syncpoint-"));
|
|
529
|
+
try {
|
|
530
|
+
const fileNames = [];
|
|
531
|
+
for (const file of files) {
|
|
532
|
+
const targetPath = (0, import_node_path6.join)(tmpDir, file.name);
|
|
533
|
+
const parentDir = (0, import_node_path6.join)(tmpDir, file.name.split("/").slice(0, -1).join("/"));
|
|
534
|
+
if (parentDir !== tmpDir) {
|
|
535
|
+
await (0, import_promises5.mkdir)(parentDir, { recursive: true });
|
|
536
|
+
}
|
|
537
|
+
if (file.content !== void 0) {
|
|
538
|
+
await (0, import_promises5.writeFile)(targetPath, file.content);
|
|
539
|
+
} else if (file.sourcePath) {
|
|
540
|
+
const data = await (0, import_promises5.readFile)(file.sourcePath);
|
|
541
|
+
await (0, import_promises5.writeFile)(targetPath, data);
|
|
542
|
+
}
|
|
543
|
+
fileNames.push(file.name);
|
|
544
|
+
}
|
|
545
|
+
await tar.create(
|
|
546
|
+
{
|
|
547
|
+
gzip: true,
|
|
548
|
+
file: outputPath,
|
|
549
|
+
cwd: tmpDir
|
|
550
|
+
},
|
|
551
|
+
fileNames
|
|
552
|
+
);
|
|
553
|
+
} finally {
|
|
554
|
+
await (0, import_promises5.rm)(tmpDir, { recursive: true, force: true });
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
async function extractArchive(archivePath, destDir) {
|
|
558
|
+
await (0, import_promises5.mkdir)(destDir, { recursive: true });
|
|
559
|
+
await tar.extract({
|
|
560
|
+
file: archivePath,
|
|
561
|
+
cwd: destDir,
|
|
562
|
+
preservePaths: false,
|
|
563
|
+
filter: (path, entry) => {
|
|
564
|
+
const normalizedPath = (0, import_node_path6.normalize)(path);
|
|
565
|
+
if (normalizedPath.includes("..")) return false;
|
|
566
|
+
if (normalizedPath.startsWith("/")) return false;
|
|
567
|
+
if (entry.type === "SymbolicLink" || entry.type === "Link") return false;
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
async function readFileFromArchive(archivePath, filename) {
|
|
573
|
+
if (filename.includes("..") || filename.startsWith("/")) {
|
|
574
|
+
throw new Error(`Invalid filename: ${filename}`);
|
|
575
|
+
}
|
|
576
|
+
const tmpDir = await (0, import_promises5.mkdtemp)((0, import_node_path6.join)((0, import_node_os3.tmpdir)(), "syncpoint-read-"));
|
|
577
|
+
try {
|
|
578
|
+
await tar.extract({
|
|
579
|
+
file: archivePath,
|
|
580
|
+
cwd: tmpDir,
|
|
581
|
+
filter: (path) => {
|
|
582
|
+
const normalized = path.replace(/^\.\//, "");
|
|
583
|
+
return normalized === filename;
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
const extractedPath = (0, import_node_path6.join)(tmpDir, filename);
|
|
587
|
+
try {
|
|
588
|
+
return await (0, import_promises5.readFile)(extractedPath);
|
|
589
|
+
} catch {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
} finally {
|
|
593
|
+
await (0, import_promises5.rm)(tmpDir, { recursive: true, force: true });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/core/backup.ts
|
|
598
|
+
function isSensitiveFile(filePath) {
|
|
599
|
+
const name = (0, import_node_path7.basename)(filePath);
|
|
600
|
+
return SENSITIVE_PATTERNS.some((pattern) => {
|
|
601
|
+
if (pattern.startsWith("*")) {
|
|
602
|
+
return name.endsWith(pattern.slice(1));
|
|
603
|
+
}
|
|
604
|
+
return name === pattern || filePath.includes(pattern);
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
async function scanTargets(config) {
|
|
608
|
+
const found = [];
|
|
609
|
+
const missing = [];
|
|
610
|
+
for (const target of config.backup.targets) {
|
|
611
|
+
const expanded = expandTilde(target);
|
|
612
|
+
if (expanded.includes("*") || expanded.includes("?") || expanded.includes("{")) {
|
|
613
|
+
const matches = await (0, import_fast_glob.default)(expanded, {
|
|
614
|
+
dot: true,
|
|
615
|
+
absolute: true,
|
|
616
|
+
ignore: config.backup.exclude,
|
|
617
|
+
onlyFiles: true
|
|
618
|
+
});
|
|
619
|
+
for (const match of matches) {
|
|
620
|
+
const entry = await collectFileInfo(match, match);
|
|
621
|
+
found.push(entry);
|
|
622
|
+
}
|
|
623
|
+
} else {
|
|
624
|
+
const absPath = resolveTargetPath(target);
|
|
625
|
+
const exists = await fileExists(absPath);
|
|
626
|
+
if (!exists) {
|
|
627
|
+
missing.push(target);
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
const entry = await collectFileInfo(absPath, absPath);
|
|
631
|
+
if (entry.size > LARGE_FILE_THRESHOLD) {
|
|
632
|
+
logger.warn(
|
|
633
|
+
`Large file (>${Math.round(LARGE_FILE_THRESHOLD / 1024 / 1024)}MB): ${target}`
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
if (isSensitiveFile(absPath)) {
|
|
637
|
+
logger.warn(`Sensitive file detected: ${target}`);
|
|
638
|
+
}
|
|
639
|
+
found.push(entry);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return { found, missing };
|
|
643
|
+
}
|
|
644
|
+
async function collectScripts() {
|
|
645
|
+
const scriptsDir = getSubDir(SCRIPTS_DIR);
|
|
646
|
+
const exists = await fileExists(scriptsDir);
|
|
647
|
+
if (!exists) return [];
|
|
648
|
+
const entries = [];
|
|
649
|
+
try {
|
|
650
|
+
const files = await (0, import_promises6.readdir)(scriptsDir, { withFileTypes: true });
|
|
651
|
+
for (const file of files) {
|
|
652
|
+
if (file.isFile() && file.name.endsWith(".sh")) {
|
|
653
|
+
const absPath = (0, import_node_path7.join)(scriptsDir, file.name);
|
|
654
|
+
const entry = await collectFileInfo(absPath, absPath);
|
|
655
|
+
entries.push(entry);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
} catch {
|
|
659
|
+
logger.info("Skipping unreadable scripts directory");
|
|
660
|
+
}
|
|
661
|
+
return entries;
|
|
662
|
+
}
|
|
663
|
+
async function createBackup(config, options = {}) {
|
|
664
|
+
const { found, missing } = await scanTargets(config);
|
|
665
|
+
for (const m of missing) {
|
|
666
|
+
logger.warn(`File not found, skipping: ${m}`);
|
|
667
|
+
}
|
|
668
|
+
let allFiles = [...found];
|
|
669
|
+
if (config.scripts.includeInBackup) {
|
|
670
|
+
const scripts = await collectScripts();
|
|
671
|
+
allFiles = [...allFiles, ...scripts];
|
|
672
|
+
}
|
|
673
|
+
if (allFiles.length === 0) {
|
|
674
|
+
throw new Error("No files found to backup.");
|
|
675
|
+
}
|
|
676
|
+
const metadata = createMetadata(allFiles, config);
|
|
677
|
+
const filename = generateFilename(config.backup.filename, {
|
|
678
|
+
tag: options.tag
|
|
679
|
+
});
|
|
680
|
+
const archiveFilename = `${filename}.tar.gz`;
|
|
681
|
+
const destDir = config.backup.destination ? resolveTargetPath(config.backup.destination) : getSubDir(BACKUPS_DIR);
|
|
682
|
+
await ensureDir(destDir);
|
|
683
|
+
const archivePath = (0, import_node_path7.join)(destDir, archiveFilename);
|
|
684
|
+
if (options.dryRun) {
|
|
685
|
+
return { archivePath, metadata };
|
|
686
|
+
}
|
|
687
|
+
const archiveFiles = [];
|
|
688
|
+
archiveFiles.push({
|
|
689
|
+
name: METADATA_FILENAME,
|
|
690
|
+
content: JSON.stringify(metadata, null, 2)
|
|
691
|
+
});
|
|
692
|
+
for (const file of allFiles) {
|
|
693
|
+
archiveFiles.push({
|
|
694
|
+
name: file.path.startsWith("~/") ? file.path.slice(2) : file.path,
|
|
695
|
+
sourcePath: file.absolutePath
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
await createArchive(archiveFiles, archivePath);
|
|
699
|
+
logger.success(`Backup created: ${archivePath}`);
|
|
700
|
+
return { archivePath, metadata };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/core/restore.ts
|
|
704
|
+
var import_promises7 = require("fs/promises");
|
|
705
|
+
var import_node_path8 = require("path");
|
|
706
|
+
async function getBackupList(config) {
|
|
707
|
+
const backupDir = config?.backup.destination ? resolveTargetPath(config.backup.destination) : getSubDir(BACKUPS_DIR);
|
|
708
|
+
const exists = await fileExists(backupDir);
|
|
709
|
+
if (!exists) return [];
|
|
710
|
+
const entries = await (0, import_promises7.readdir)(backupDir, { withFileTypes: true });
|
|
711
|
+
const backups = [];
|
|
712
|
+
for (const entry of entries) {
|
|
713
|
+
if (!entry.isFile() || !entry.name.endsWith(".tar.gz")) continue;
|
|
714
|
+
const fullPath = (0, import_node_path8.join)(backupDir, entry.name);
|
|
715
|
+
const fileStat = await (0, import_promises7.stat)(fullPath);
|
|
716
|
+
let hostname;
|
|
717
|
+
let fileCount;
|
|
718
|
+
try {
|
|
719
|
+
const metaBuf = await readFileFromArchive(fullPath, METADATA_FILENAME);
|
|
720
|
+
if (metaBuf) {
|
|
721
|
+
const meta = parseMetadata(metaBuf);
|
|
722
|
+
hostname = meta.hostname;
|
|
723
|
+
fileCount = meta.summary.fileCount;
|
|
724
|
+
}
|
|
725
|
+
} catch {
|
|
726
|
+
logger.info(`Could not read metadata from: ${entry.name}`);
|
|
727
|
+
}
|
|
728
|
+
backups.push({
|
|
729
|
+
filename: entry.name,
|
|
730
|
+
path: fullPath,
|
|
731
|
+
size: fileStat.size,
|
|
732
|
+
createdAt: fileStat.mtime,
|
|
733
|
+
hostname,
|
|
734
|
+
fileCount
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
backups.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
738
|
+
return backups;
|
|
739
|
+
}
|
|
740
|
+
async function getRestorePlan(archivePath) {
|
|
741
|
+
const metaBuf = await readFileFromArchive(archivePath, METADATA_FILENAME);
|
|
742
|
+
if (!metaBuf) {
|
|
743
|
+
throw new Error(
|
|
744
|
+
`No metadata found in archive: ${archivePath}
|
|
745
|
+
This may not be a valid syncpoint backup.`
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
const metadata = parseMetadata(metaBuf);
|
|
749
|
+
const actions = [];
|
|
750
|
+
for (const file of metadata.files) {
|
|
751
|
+
const absPath = resolveTargetPath(file.path);
|
|
752
|
+
const exists = await fileExists(absPath);
|
|
753
|
+
if (!exists) {
|
|
754
|
+
actions.push({
|
|
755
|
+
path: file.path,
|
|
756
|
+
action: "create",
|
|
757
|
+
backupSize: file.size,
|
|
758
|
+
reason: "File does not exist on this machine"
|
|
759
|
+
});
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
const currentHash = await computeFileHash(absPath);
|
|
763
|
+
const currentStat = await (0, import_promises7.stat)(absPath);
|
|
764
|
+
if (currentHash === file.hash) {
|
|
765
|
+
actions.push({
|
|
766
|
+
path: file.path,
|
|
767
|
+
action: "skip",
|
|
768
|
+
currentSize: currentStat.size,
|
|
769
|
+
backupSize: file.size,
|
|
770
|
+
reason: "File is identical (same hash)"
|
|
771
|
+
});
|
|
772
|
+
} else {
|
|
773
|
+
actions.push({
|
|
774
|
+
path: file.path,
|
|
775
|
+
action: "overwrite",
|
|
776
|
+
currentSize: currentStat.size,
|
|
777
|
+
backupSize: file.size,
|
|
778
|
+
reason: "File has been modified"
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return { metadata, actions };
|
|
783
|
+
}
|
|
784
|
+
async function createSafetyBackup(filePaths) {
|
|
785
|
+
const now = /* @__PURE__ */ new Date();
|
|
786
|
+
const filename = `_pre-restore_${formatDatetime(now)}.tar.gz`;
|
|
787
|
+
const backupDir = getSubDir(BACKUPS_DIR);
|
|
788
|
+
await ensureDir(backupDir);
|
|
789
|
+
const archivePath = (0, import_node_path8.join)(backupDir, filename);
|
|
790
|
+
const files = [];
|
|
791
|
+
for (const fp of filePaths) {
|
|
792
|
+
const absPath = resolveTargetPath(fp);
|
|
793
|
+
const exists = await fileExists(absPath);
|
|
794
|
+
if (!exists) continue;
|
|
795
|
+
const archiveName = fp.startsWith("~/") ? fp.slice(2) : fp;
|
|
796
|
+
files.push({ name: archiveName, sourcePath: absPath });
|
|
797
|
+
}
|
|
798
|
+
if (files.length === 0) {
|
|
799
|
+
logger.info("No existing files to safety-backup.");
|
|
800
|
+
return archivePath;
|
|
801
|
+
}
|
|
802
|
+
await createArchive(files, archivePath);
|
|
803
|
+
logger.info(`Safety backup created: ${archivePath}`);
|
|
804
|
+
return archivePath;
|
|
805
|
+
}
|
|
806
|
+
async function restoreBackup(archivePath, options = {}) {
|
|
807
|
+
const plan = await getRestorePlan(archivePath);
|
|
808
|
+
const restoredFiles = [];
|
|
809
|
+
const skippedFiles = [];
|
|
810
|
+
const overwritePaths = plan.actions.filter((a) => a.action === "overwrite").map((a) => a.path);
|
|
811
|
+
let safetyBackupPath;
|
|
812
|
+
if (overwritePaths.length > 0 && !options.dryRun) {
|
|
813
|
+
safetyBackupPath = await createSafetyBackup(overwritePaths);
|
|
814
|
+
}
|
|
815
|
+
if (options.dryRun) {
|
|
816
|
+
return {
|
|
817
|
+
restoredFiles: plan.actions.filter((a) => a.action !== "skip").map((a) => a.path),
|
|
818
|
+
skippedFiles: plan.actions.filter((a) => a.action === "skip").map((a) => a.path),
|
|
819
|
+
safetyBackupPath
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
const { mkdtemp: mkdtemp2, rm: rm2 } = await import("fs/promises");
|
|
823
|
+
const { tmpdir: tmpdir2 } = await import("os");
|
|
824
|
+
const tmpDir = await mkdtemp2((0, import_node_path8.join)(tmpdir2(), "syncpoint-restore-"));
|
|
825
|
+
try {
|
|
826
|
+
await extractArchive(archivePath, tmpDir);
|
|
827
|
+
for (const action of plan.actions) {
|
|
828
|
+
if (action.action === "skip") {
|
|
829
|
+
skippedFiles.push(action.path);
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
const archiveName = action.path.startsWith("~/") ? action.path.slice(2) : action.path;
|
|
833
|
+
const extractedPath = (0, import_node_path8.join)(tmpDir, archiveName);
|
|
834
|
+
const destPath = resolveTargetPath(action.path);
|
|
835
|
+
const extractedExists = await fileExists(extractedPath);
|
|
836
|
+
if (!extractedExists) {
|
|
837
|
+
logger.warn(`File not found in archive: ${archiveName}`);
|
|
838
|
+
skippedFiles.push(action.path);
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
await ensureDir((0, import_node_path8.dirname)(destPath));
|
|
842
|
+
try {
|
|
843
|
+
const destStat = await (0, import_promises7.lstat)(destPath);
|
|
844
|
+
if (destStat.isSymbolicLink()) {
|
|
845
|
+
logger.warn(`Skipping symlink target: ${action.path}`);
|
|
846
|
+
skippedFiles.push(action.path);
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
} catch (err) {
|
|
850
|
+
if (err.code !== "ENOENT") throw err;
|
|
851
|
+
}
|
|
852
|
+
await (0, import_promises7.copyFile)(extractedPath, destPath);
|
|
853
|
+
restoredFiles.push(action.path);
|
|
854
|
+
}
|
|
855
|
+
} finally {
|
|
856
|
+
await rm2(tmpDir, { recursive: true, force: true });
|
|
857
|
+
}
|
|
858
|
+
return { restoredFiles, skippedFiles, safetyBackupPath };
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// src/core/provision.ts
|
|
862
|
+
var import_node_child_process = require("child_process");
|
|
863
|
+
var import_promises8 = require("fs/promises");
|
|
864
|
+
var import_node_path9 = require("path");
|
|
865
|
+
var import_yaml2 = __toESM(require("yaml"), 1);
|
|
866
|
+
|
|
867
|
+
// src/schemas/template.schema.ts
|
|
868
|
+
var templateSchema = {
|
|
869
|
+
type: "object",
|
|
870
|
+
required: ["name", "steps"],
|
|
871
|
+
properties: {
|
|
872
|
+
name: { type: "string", minLength: 1 },
|
|
873
|
+
description: { type: "string" },
|
|
874
|
+
backup: { type: "string" },
|
|
875
|
+
sudo: { type: "boolean" },
|
|
876
|
+
steps: {
|
|
877
|
+
type: "array",
|
|
878
|
+
minItems: 1,
|
|
879
|
+
items: {
|
|
880
|
+
type: "object",
|
|
881
|
+
required: ["name", "command"],
|
|
882
|
+
properties: {
|
|
883
|
+
name: { type: "string", minLength: 1 },
|
|
884
|
+
description: { type: "string" },
|
|
885
|
+
command: { type: "string", minLength: 1 },
|
|
886
|
+
skip_if: { type: "string" },
|
|
887
|
+
continue_on_error: { type: "boolean" }
|
|
888
|
+
},
|
|
889
|
+
additionalProperties: false
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
},
|
|
893
|
+
additionalProperties: false
|
|
894
|
+
};
|
|
895
|
+
var validate3 = ajv.compile(templateSchema);
|
|
896
|
+
function validateTemplate(data) {
|
|
897
|
+
const valid = validate3(data);
|
|
898
|
+
if (valid) return { valid: true };
|
|
899
|
+
const errors = validate3.errors?.map(
|
|
900
|
+
(e) => `${e.instancePath || "/"} ${e.message ?? "unknown error"}`
|
|
901
|
+
);
|
|
902
|
+
return { valid: false, errors };
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// src/core/provision.ts
|
|
906
|
+
var REMOTE_SCRIPT_PATTERNS = [
|
|
907
|
+
/curl\s.*\|\s*(ba)?sh/,
|
|
908
|
+
/wget\s.*\|\s*(ba)?sh/,
|
|
909
|
+
/curl\s.*\|\s*python/,
|
|
910
|
+
/wget\s.*\|\s*python/
|
|
911
|
+
];
|
|
912
|
+
function containsRemoteScriptPattern(command) {
|
|
913
|
+
return REMOTE_SCRIPT_PATTERNS.some((p) => p.test(command));
|
|
914
|
+
}
|
|
915
|
+
function sanitizeErrorOutput(output) {
|
|
916
|
+
return output.replace(/\/Users\/[^\s/]+/g, "/Users/***").replace(/\/home\/[^\s/]+/g, "/home/***").replace(/(password|token|key|secret)[=:]\s*\S+/gi, "$1=***").slice(0, 500);
|
|
917
|
+
}
|
|
918
|
+
async function loadTemplate(templatePath) {
|
|
919
|
+
const exists = await fileExists(templatePath);
|
|
920
|
+
if (!exists) {
|
|
921
|
+
throw new Error(`Template not found: ${templatePath}`);
|
|
922
|
+
}
|
|
923
|
+
const raw = await (0, import_promises8.readFile)(templatePath, "utf-8");
|
|
924
|
+
const data = import_yaml2.default.parse(raw);
|
|
925
|
+
const result = validateTemplate(data);
|
|
926
|
+
if (!result.valid) {
|
|
927
|
+
throw new Error(
|
|
928
|
+
`Invalid template ${templatePath}:
|
|
929
|
+
${(result.errors ?? []).join("\n")}`
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
return data;
|
|
933
|
+
}
|
|
934
|
+
async function listTemplates() {
|
|
935
|
+
const templatesDir = getSubDir(TEMPLATES_DIR);
|
|
936
|
+
const exists = await fileExists(templatesDir);
|
|
937
|
+
if (!exists) return [];
|
|
938
|
+
const entries = await (0, import_promises8.readdir)(templatesDir, { withFileTypes: true });
|
|
939
|
+
const templates = [];
|
|
940
|
+
for (const entry of entries) {
|
|
941
|
+
if (!entry.isFile() || !entry.name.endsWith(".yml") && !entry.name.endsWith(".yaml")) {
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
const fullPath = (0, import_node_path9.join)(templatesDir, entry.name);
|
|
945
|
+
try {
|
|
946
|
+
const config = await loadTemplate(fullPath);
|
|
947
|
+
templates.push({
|
|
948
|
+
name: entry.name.replace(/\.ya?ml$/, ""),
|
|
949
|
+
path: fullPath,
|
|
950
|
+
config
|
|
951
|
+
});
|
|
952
|
+
} catch {
|
|
953
|
+
logger.warn(`Skipping invalid template: ${entry.name}`);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return templates;
|
|
957
|
+
}
|
|
958
|
+
function execAsync(command) {
|
|
959
|
+
return new Promise((resolve2, reject) => {
|
|
960
|
+
(0, import_node_child_process.exec)(
|
|
961
|
+
command,
|
|
962
|
+
{ shell: "/bin/bash", timeout: 3e5 },
|
|
963
|
+
(error, stdout, stderr) => {
|
|
964
|
+
if (error) {
|
|
965
|
+
reject(
|
|
966
|
+
Object.assign(error, {
|
|
967
|
+
stdout: stdout?.toString() ?? "",
|
|
968
|
+
stderr: stderr?.toString() ?? ""
|
|
969
|
+
})
|
|
970
|
+
);
|
|
971
|
+
} else {
|
|
972
|
+
resolve2({
|
|
973
|
+
stdout: stdout?.toString() ?? "",
|
|
974
|
+
stderr: stderr?.toString() ?? ""
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
);
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
async function evaluateSkipIf(command, stepName) {
|
|
982
|
+
if (containsRemoteScriptPattern(command)) {
|
|
983
|
+
throw new Error(`Blocked dangerous remote script pattern in skip_if: ${stepName}`);
|
|
984
|
+
}
|
|
985
|
+
try {
|
|
986
|
+
await execAsync(command);
|
|
987
|
+
return true;
|
|
988
|
+
} catch {
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
async function executeStep(step) {
|
|
993
|
+
const startTime = Date.now();
|
|
994
|
+
if (containsRemoteScriptPattern(step.command)) {
|
|
995
|
+
throw new Error(`Blocked dangerous remote script pattern in command: ${step.name}`);
|
|
996
|
+
}
|
|
997
|
+
if (step.skip_if) {
|
|
998
|
+
const shouldSkip = await evaluateSkipIf(step.skip_if, step.name);
|
|
999
|
+
if (shouldSkip) {
|
|
1000
|
+
return {
|
|
1001
|
+
name: step.name,
|
|
1002
|
+
status: "skipped",
|
|
1003
|
+
duration: Date.now() - startTime
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
try {
|
|
1008
|
+
const { stdout, stderr } = await execAsync(step.command);
|
|
1009
|
+
const output = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
1010
|
+
return {
|
|
1011
|
+
name: step.name,
|
|
1012
|
+
status: "success",
|
|
1013
|
+
duration: Date.now() - startTime,
|
|
1014
|
+
output: output || void 0
|
|
1015
|
+
};
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
const error = err;
|
|
1018
|
+
const errorOutput = [error.stdout, error.stderr, error.message].filter(Boolean).join("\n").trim();
|
|
1019
|
+
return {
|
|
1020
|
+
name: step.name,
|
|
1021
|
+
status: "failed",
|
|
1022
|
+
duration: Date.now() - startTime,
|
|
1023
|
+
error: sanitizeErrorOutput(errorOutput)
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
async function* runProvision(templatePath, options = {}) {
|
|
1028
|
+
const template = await loadTemplate(templatePath);
|
|
1029
|
+
for (const step of template.steps) {
|
|
1030
|
+
if (options.dryRun) {
|
|
1031
|
+
let status = "pending";
|
|
1032
|
+
if (step.skip_if) {
|
|
1033
|
+
const shouldSkip = await evaluateSkipIf(step.skip_if, step.name);
|
|
1034
|
+
if (shouldSkip) status = "skipped";
|
|
1035
|
+
}
|
|
1036
|
+
yield {
|
|
1037
|
+
name: step.name,
|
|
1038
|
+
status
|
|
1039
|
+
};
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
yield {
|
|
1043
|
+
name: step.name,
|
|
1044
|
+
status: "running"
|
|
1045
|
+
};
|
|
1046
|
+
const result = await executeStep(step);
|
|
1047
|
+
yield result;
|
|
1048
|
+
if (result.status === "failed" && !step.continue_on_error) {
|
|
1049
|
+
logger.error(
|
|
1050
|
+
`Step "${step.name}" failed. Stopping provisioning.`
|
|
1051
|
+
);
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1057
|
+
0 && (module.exports = {
|
|
1058
|
+
createBackup,
|
|
1059
|
+
getBackupList,
|
|
1060
|
+
getRestorePlan,
|
|
1061
|
+
initDefaultConfig,
|
|
1062
|
+
listTemplates,
|
|
1063
|
+
loadConfig,
|
|
1064
|
+
loadTemplate,
|
|
1065
|
+
restoreBackup,
|
|
1066
|
+
runProvision,
|
|
1067
|
+
saveConfig,
|
|
1068
|
+
scanTargets
|
|
1069
|
+
});
|