@lumy-pack/syncpoint 0.0.3 → 0.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/assets/config.default.yml +15 -7
- package/assets/schemas/config.schema.json +68 -0
- package/assets/schemas/metadata.schema.json +121 -0
- package/assets/schemas/template.schema.json +61 -0
- package/assets/template.example.yml +6 -5
- package/dist/cli.mjs +746 -338
- package/dist/commands/Migrate.d.ts +2 -0
- package/dist/constants.d.ts +0 -1
- package/dist/core/migrate.d.ts +11 -0
- package/dist/index.cjs +225 -80
- package/dist/index.mjs +226 -81
- package/dist/utils/types.d.ts +11 -1
- package/dist/version.d.ts +1 -1
- package/package.json +5 -6
package/dist/cli.mjs
CHANGED
|
@@ -193,7 +193,7 @@ function generateFilename(pattern, options) {
|
|
|
193
193
|
import { appendFile, mkdir as mkdir2 } from "fs/promises";
|
|
194
194
|
import { join as join3 } from "path";
|
|
195
195
|
import pc from "picocolors";
|
|
196
|
-
var ANSI_RE =
|
|
196
|
+
var ANSI_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
197
197
|
function stripAnsi(str) {
|
|
198
198
|
return str.replace(ANSI_RE, "");
|
|
199
199
|
}
|
|
@@ -334,25 +334,13 @@ function isValidPattern(pattern) {
|
|
|
334
334
|
|
|
335
335
|
// src/core/metadata.ts
|
|
336
336
|
import { createHash } from "crypto";
|
|
337
|
-
import { lstat, readFile } from "fs/promises";
|
|
337
|
+
import { lstat, readFile, readlink } from "fs/promises";
|
|
338
338
|
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
ajv.addKeyword({
|
|
345
|
-
keyword: "validPattern",
|
|
346
|
-
type: "string",
|
|
347
|
-
validate: function validate(schema, data) {
|
|
348
|
-
if (!schema) return true;
|
|
349
|
-
return isValidPattern(data);
|
|
350
|
-
},
|
|
351
|
-
errors: true
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
// src/schemas/metadata.schema.ts
|
|
355
|
-
var metadataSchema = {
|
|
339
|
+
// assets/schemas/metadata.schema.json
|
|
340
|
+
var metadata_schema_default = {
|
|
341
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
342
|
+
title: "Syncpoint Backup Metadata",
|
|
343
|
+
description: "Metadata stored inside backup archives as _metadata.json",
|
|
356
344
|
type: "object",
|
|
357
345
|
required: [
|
|
358
346
|
"version",
|
|
@@ -365,57 +353,129 @@ var metadataSchema = {
|
|
|
365
353
|
"summary"
|
|
366
354
|
],
|
|
367
355
|
properties: {
|
|
368
|
-
version: {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
356
|
+
version: {
|
|
357
|
+
type: "string",
|
|
358
|
+
description: "Metadata schema version."
|
|
359
|
+
},
|
|
360
|
+
toolVersion: {
|
|
361
|
+
type: "string",
|
|
362
|
+
description: "Syncpoint tool version used to create the backup."
|
|
363
|
+
},
|
|
364
|
+
createdAt: {
|
|
365
|
+
type: "string",
|
|
366
|
+
description: "ISO 8601 timestamp of backup creation."
|
|
367
|
+
},
|
|
368
|
+
hostname: {
|
|
369
|
+
type: "string",
|
|
370
|
+
description: "Hostname of the machine where the backup was created."
|
|
371
|
+
},
|
|
372
372
|
system: {
|
|
373
373
|
type: "object",
|
|
374
|
+
description: "System information at backup time.",
|
|
374
375
|
required: ["platform", "release", "arch"],
|
|
375
376
|
properties: {
|
|
376
|
-
platform: {
|
|
377
|
-
|
|
378
|
-
|
|
377
|
+
platform: {
|
|
378
|
+
type: "string",
|
|
379
|
+
description: "Operating system platform (e.g. darwin, linux)."
|
|
380
|
+
},
|
|
381
|
+
release: {
|
|
382
|
+
type: "string",
|
|
383
|
+
description: "OS kernel release version."
|
|
384
|
+
},
|
|
385
|
+
arch: {
|
|
386
|
+
type: "string",
|
|
387
|
+
description: "CPU architecture (e.g. arm64, x64)."
|
|
388
|
+
}
|
|
379
389
|
},
|
|
380
390
|
additionalProperties: false
|
|
381
391
|
},
|
|
382
392
|
config: {
|
|
383
393
|
type: "object",
|
|
394
|
+
description: "Backup configuration snapshot.",
|
|
384
395
|
required: ["filename"],
|
|
385
396
|
properties: {
|
|
386
|
-
filename: {
|
|
387
|
-
|
|
397
|
+
filename: {
|
|
398
|
+
type: "string",
|
|
399
|
+
description: "Filename pattern used for the backup."
|
|
400
|
+
},
|
|
401
|
+
destination: {
|
|
402
|
+
type: "string",
|
|
403
|
+
description: "Custom destination path, if configured."
|
|
404
|
+
}
|
|
388
405
|
},
|
|
389
406
|
additionalProperties: false
|
|
390
407
|
},
|
|
391
408
|
files: {
|
|
392
409
|
type: "array",
|
|
410
|
+
description: "List of files included in the backup.",
|
|
393
411
|
items: {
|
|
394
412
|
type: "object",
|
|
395
413
|
required: ["path", "absolutePath", "size", "hash"],
|
|
396
414
|
properties: {
|
|
397
|
-
path: {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
415
|
+
path: {
|
|
416
|
+
type: "string",
|
|
417
|
+
description: "Display path (e.g. ~/.zshrc)."
|
|
418
|
+
},
|
|
419
|
+
absolutePath: {
|
|
420
|
+
type: "string",
|
|
421
|
+
description: "Full filesystem path."
|
|
422
|
+
},
|
|
423
|
+
size: {
|
|
424
|
+
type: "number",
|
|
425
|
+
description: "File size in bytes.",
|
|
426
|
+
minimum: 0
|
|
427
|
+
},
|
|
428
|
+
hash: {
|
|
429
|
+
type: "string",
|
|
430
|
+
description: "SHA-256 hash of file contents."
|
|
431
|
+
},
|
|
432
|
+
type: {
|
|
433
|
+
type: "string",
|
|
434
|
+
description: "File type (e.g. symlink, directory)."
|
|
435
|
+
}
|
|
402
436
|
},
|
|
403
437
|
additionalProperties: false
|
|
404
438
|
}
|
|
405
439
|
},
|
|
406
440
|
summary: {
|
|
407
441
|
type: "object",
|
|
442
|
+
description: "Backup summary statistics.",
|
|
408
443
|
required: ["fileCount", "totalSize"],
|
|
409
444
|
properties: {
|
|
410
|
-
fileCount: {
|
|
411
|
-
|
|
445
|
+
fileCount: {
|
|
446
|
+
type: "integer",
|
|
447
|
+
description: "Total number of files in the backup.",
|
|
448
|
+
minimum: 0
|
|
449
|
+
},
|
|
450
|
+
totalSize: {
|
|
451
|
+
type: "number",
|
|
452
|
+
description: "Total size of all files in bytes.",
|
|
453
|
+
minimum: 0
|
|
454
|
+
}
|
|
412
455
|
},
|
|
413
456
|
additionalProperties: false
|
|
414
457
|
}
|
|
415
458
|
},
|
|
416
459
|
additionalProperties: false
|
|
417
460
|
};
|
|
418
|
-
|
|
461
|
+
|
|
462
|
+
// src/schemas/ajv.ts
|
|
463
|
+
import Ajv from "ajv";
|
|
464
|
+
import addFormats from "ajv-formats";
|
|
465
|
+
var ajv = new Ajv({ allErrors: true });
|
|
466
|
+
addFormats(ajv);
|
|
467
|
+
ajv.addKeyword({
|
|
468
|
+
keyword: "validPattern",
|
|
469
|
+
type: "string",
|
|
470
|
+
validate: function validate(schema, data) {
|
|
471
|
+
if (!schema) return true;
|
|
472
|
+
return isValidPattern(data);
|
|
473
|
+
},
|
|
474
|
+
errors: true
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// src/schemas/metadata.schema.ts
|
|
478
|
+
var validate2 = ajv.compile(metadata_schema_default);
|
|
419
479
|
function validateMetadata(data) {
|
|
420
480
|
const valid = validate2(data);
|
|
421
481
|
if (valid) return { valid: true };
|
|
@@ -426,7 +486,7 @@ function validateMetadata(data) {
|
|
|
426
486
|
}
|
|
427
487
|
|
|
428
488
|
// src/version.ts
|
|
429
|
-
var VERSION = "0.0.
|
|
489
|
+
var VERSION = "0.0.5";
|
|
430
490
|
|
|
431
491
|
// src/core/metadata.ts
|
|
432
492
|
var METADATA_VERSION = "1.0.0";
|
|
@@ -477,7 +537,8 @@ async function collectFileInfo(absolutePath, logicalPath) {
|
|
|
477
537
|
}
|
|
478
538
|
let hash;
|
|
479
539
|
if (lstats.isSymbolicLink()) {
|
|
480
|
-
|
|
540
|
+
const linkTarget = await readlink(absolutePath);
|
|
541
|
+
hash = `sha256:${createHash("sha256").update(linkTarget).digest("hex")}`;
|
|
481
542
|
} else {
|
|
482
543
|
hash = await computeFileHash(absolutePath);
|
|
483
544
|
}
|
|
@@ -538,7 +599,8 @@ async function extractArchive(archivePath, destDir) {
|
|
|
538
599
|
const normalizedPath = normalize2(path);
|
|
539
600
|
if (normalizedPath.includes("..")) return false;
|
|
540
601
|
if (normalizedPath.startsWith("/")) return false;
|
|
541
|
-
if (entry.type === "SymbolicLink" || entry.type === "Link")
|
|
602
|
+
if ("type" in entry && (entry.type === "SymbolicLink" || entry.type === "Link"))
|
|
603
|
+
return false;
|
|
542
604
|
return true;
|
|
543
605
|
}
|
|
544
606
|
});
|
|
@@ -609,6 +671,10 @@ async function scanTargets(config) {
|
|
|
609
671
|
});
|
|
610
672
|
for (const match of allFiles) {
|
|
611
673
|
if (regex.test(match) && !isExcluded(match)) {
|
|
674
|
+
if (!config.backup.includeSensitiveFiles && isSensitiveFile(match)) {
|
|
675
|
+
logger.warn(`Sensitive file excluded: ${match}`);
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
612
678
|
const entry = await collectFileInfo(match, match);
|
|
613
679
|
found.push(entry);
|
|
614
680
|
}
|
|
@@ -637,6 +703,10 @@ async function scanTargets(config) {
|
|
|
637
703
|
});
|
|
638
704
|
for (const match of matches) {
|
|
639
705
|
if (!isExcluded(match)) {
|
|
706
|
+
if (!config.backup.includeSensitiveFiles && isSensitiveFile(match)) {
|
|
707
|
+
logger.warn(`Sensitive file excluded: ${match}`);
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
640
710
|
const entry = await collectFileInfo(match, match);
|
|
641
711
|
found.push(entry);
|
|
642
712
|
}
|
|
@@ -670,6 +740,10 @@ async function scanTargets(config) {
|
|
|
670
740
|
});
|
|
671
741
|
for (const match of matches) {
|
|
672
742
|
if (!isExcluded(match)) {
|
|
743
|
+
if (!config.backup.includeSensitiveFiles && isSensitiveFile(match)) {
|
|
744
|
+
logger.warn(`Sensitive file excluded: ${match}`);
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
673
747
|
const entry = await collectFileInfo(match, match);
|
|
674
748
|
found.push(entry);
|
|
675
749
|
}
|
|
@@ -681,15 +755,16 @@ async function scanTargets(config) {
|
|
|
681
755
|
if (isExcluded(absPath)) {
|
|
682
756
|
continue;
|
|
683
757
|
}
|
|
758
|
+
if (!config.backup.includeSensitiveFiles && isSensitiveFile(absPath)) {
|
|
759
|
+
logger.warn(`Sensitive file excluded: ${target}`);
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
684
762
|
const entry = await collectFileInfo(absPath, absPath);
|
|
685
763
|
if (entry.size > LARGE_FILE_THRESHOLD) {
|
|
686
764
|
logger.warn(
|
|
687
765
|
`Large file (>${Math.round(LARGE_FILE_THRESHOLD / 1024 / 1024)}MB): ${target}`
|
|
688
766
|
);
|
|
689
767
|
}
|
|
690
|
-
if (isSensitiveFile(absPath)) {
|
|
691
|
-
logger.warn(`Sensitive file detected: ${target}`);
|
|
692
|
-
}
|
|
693
768
|
found.push(entry);
|
|
694
769
|
}
|
|
695
770
|
}
|
|
@@ -723,7 +798,7 @@ async function createBackup(config, options = {}) {
|
|
|
723
798
|
}
|
|
724
799
|
}
|
|
725
800
|
let allFiles = [...found];
|
|
726
|
-
if (config.scripts
|
|
801
|
+
if (config.scripts?.includeInBackup) {
|
|
727
802
|
const scripts = await collectScripts();
|
|
728
803
|
allFiles = [...allFiles, ...scripts];
|
|
729
804
|
}
|
|
@@ -762,46 +837,78 @@ import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
|
762
837
|
import { join as join7 } from "path";
|
|
763
838
|
import YAML from "yaml";
|
|
764
839
|
|
|
765
|
-
//
|
|
766
|
-
var
|
|
840
|
+
// assets/schemas/config.schema.json
|
|
841
|
+
var config_schema_default = {
|
|
842
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
843
|
+
title: "Syncpoint Config",
|
|
844
|
+
description: "Configuration for syncpoint backup tool",
|
|
767
845
|
type: "object",
|
|
768
|
-
required: [
|
|
846
|
+
required: [
|
|
847
|
+
"backup"
|
|
848
|
+
],
|
|
769
849
|
properties: {
|
|
770
850
|
backup: {
|
|
771
851
|
type: "object",
|
|
772
|
-
|
|
852
|
+
description: "Backup configuration",
|
|
853
|
+
required: [
|
|
854
|
+
"targets",
|
|
855
|
+
"exclude",
|
|
856
|
+
"filename"
|
|
857
|
+
],
|
|
773
858
|
properties: {
|
|
774
859
|
targets: {
|
|
775
860
|
type: "array",
|
|
776
|
-
|
|
861
|
+
description: "List of files/directories to backup. Supports literal paths (e.g. ~/.zshrc), glob patterns (e.g. ~/.config/*.conf), and regex patterns (e.g. /\\.conf$/).",
|
|
862
|
+
items: {
|
|
863
|
+
type: "string",
|
|
864
|
+
validPattern: true
|
|
865
|
+
}
|
|
777
866
|
},
|
|
778
867
|
exclude: {
|
|
779
868
|
type: "array",
|
|
780
|
-
|
|
869
|
+
description: "List of patterns to exclude from backup. Supports glob (e.g. **/*.swp) and regex (e.g. /\\.bak$/) patterns.",
|
|
870
|
+
items: {
|
|
871
|
+
type: "string",
|
|
872
|
+
validPattern: true
|
|
873
|
+
}
|
|
781
874
|
},
|
|
782
875
|
filename: {
|
|
783
876
|
type: "string",
|
|
877
|
+
description: "Backup archive filename pattern. Available variables: {hostname}, {datetime}.",
|
|
784
878
|
minLength: 1
|
|
785
879
|
},
|
|
786
880
|
destination: {
|
|
787
|
-
type: "string"
|
|
881
|
+
type: "string",
|
|
882
|
+
description: "Backup archive destination path. Default: ~/.syncpoint/backups/"
|
|
883
|
+
},
|
|
884
|
+
includeSensitiveFiles: {
|
|
885
|
+
type: "boolean",
|
|
886
|
+
description: "Include sensitive files (SSH keys, certificates, etc.) in backup. When false (default), files matching sensitive patterns (id_rsa, id_ed25519, *.pem, *.key) are automatically excluded."
|
|
788
887
|
}
|
|
789
888
|
},
|
|
790
889
|
additionalProperties: false
|
|
791
890
|
},
|
|
792
891
|
scripts: {
|
|
793
892
|
type: "object",
|
|
893
|
+
description: "Scripts configuration",
|
|
794
894
|
properties: {
|
|
795
895
|
includeInBackup: {
|
|
796
|
-
type: "boolean"
|
|
896
|
+
type: "boolean",
|
|
897
|
+
description: "Whether to include scripts/ directory in backup. Default: true."
|
|
797
898
|
}
|
|
798
899
|
},
|
|
799
900
|
additionalProperties: false
|
|
901
|
+
},
|
|
902
|
+
"yaml-language-server": {
|
|
903
|
+
type: "string",
|
|
904
|
+
description: "Editor directive for schema association; ignored at runtime."
|
|
800
905
|
}
|
|
801
906
|
},
|
|
802
907
|
additionalProperties: false
|
|
803
908
|
};
|
|
804
|
-
|
|
909
|
+
|
|
910
|
+
// src/schemas/config.schema.ts
|
|
911
|
+
var validate3 = ajv.compile(config_schema_default);
|
|
805
912
|
function validateConfig(data) {
|
|
806
913
|
const valid = validate3(data);
|
|
807
914
|
if (valid) return { valid: true };
|
|
@@ -1049,6 +1156,21 @@ var COMMANDS = {
|
|
|
1049
1156
|
"npx @lumy-pack/syncpoint status --cleanup"
|
|
1050
1157
|
]
|
|
1051
1158
|
},
|
|
1159
|
+
migrate: {
|
|
1160
|
+
name: "migrate",
|
|
1161
|
+
description: "Migrate config.yml to match the current schema",
|
|
1162
|
+
usage: "npx @lumy-pack/syncpoint migrate [options]",
|
|
1163
|
+
options: [
|
|
1164
|
+
{
|
|
1165
|
+
flag: "--dry-run",
|
|
1166
|
+
description: "Preview changes without writing"
|
|
1167
|
+
}
|
|
1168
|
+
],
|
|
1169
|
+
examples: [
|
|
1170
|
+
"npx @lumy-pack/syncpoint migrate",
|
|
1171
|
+
"npx @lumy-pack/syncpoint migrate --dry-run"
|
|
1172
|
+
]
|
|
1173
|
+
},
|
|
1052
1174
|
help: {
|
|
1053
1175
|
name: "help",
|
|
1054
1176
|
description: "Display help information",
|
|
@@ -1081,6 +1203,7 @@ var BackupView = ({ options }) => {
|
|
|
1081
1203
|
const [error, setError] = useState(null);
|
|
1082
1204
|
useEffect(() => {
|
|
1083
1205
|
(async () => {
|
|
1206
|
+
let progressInterval;
|
|
1084
1207
|
try {
|
|
1085
1208
|
const cfg = await loadConfig();
|
|
1086
1209
|
setConfig(cfg);
|
|
@@ -1093,7 +1216,7 @@ var BackupView = ({ options }) => {
|
|
|
1093
1216
|
return;
|
|
1094
1217
|
}
|
|
1095
1218
|
setPhase("compressing");
|
|
1096
|
-
|
|
1219
|
+
progressInterval = setInterval(() => {
|
|
1097
1220
|
setProgress((prev) => {
|
|
1098
1221
|
if (prev >= 90) return prev;
|
|
1099
1222
|
return prev + 10;
|
|
@@ -1106,9 +1229,10 @@ var BackupView = ({ options }) => {
|
|
|
1106
1229
|
setPhase("done");
|
|
1107
1230
|
setTimeout(() => exit(), 100);
|
|
1108
1231
|
} catch (err) {
|
|
1232
|
+
if (progressInterval) clearInterval(progressInterval);
|
|
1109
1233
|
setError(err instanceof Error ? err.message : String(err));
|
|
1110
1234
|
setPhase("error");
|
|
1111
|
-
exit();
|
|
1235
|
+
setTimeout(() => exit(), 100);
|
|
1112
1236
|
}
|
|
1113
1237
|
})();
|
|
1114
1238
|
}, []);
|
|
@@ -1162,7 +1286,7 @@ var BackupView = ({ options }) => {
|
|
|
1162
1286
|
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1163
1287
|
" ",
|
|
1164
1288
|
"File: ",
|
|
1165
|
-
result.
|
|
1289
|
+
result.archivePath.split("/").pop()
|
|
1166
1290
|
] }),
|
|
1167
1291
|
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1168
1292
|
" ",
|
|
@@ -1199,30 +1323,64 @@ import { useState as useState2, useEffect as useEffect2 } from "react";
|
|
|
1199
1323
|
import { Text as Text3, Box as Box2, useApp as useApp2 } from "ink";
|
|
1200
1324
|
import Spinner from "ink-spinner";
|
|
1201
1325
|
import { render as render2 } from "ink";
|
|
1202
|
-
import { join as
|
|
1203
|
-
import { writeFile as
|
|
1326
|
+
import { join as join8 } from "path";
|
|
1327
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
1204
1328
|
|
|
1205
|
-
//
|
|
1206
|
-
var
|
|
1329
|
+
// assets/schemas/template.schema.json
|
|
1330
|
+
var template_schema_default = {
|
|
1331
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1332
|
+
title: "Syncpoint Template",
|
|
1333
|
+
description: "Provisioning template for syncpoint",
|
|
1207
1334
|
type: "object",
|
|
1208
1335
|
required: ["name", "steps"],
|
|
1209
1336
|
properties: {
|
|
1210
|
-
name: {
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1337
|
+
name: {
|
|
1338
|
+
type: "string",
|
|
1339
|
+
description: "Template name.",
|
|
1340
|
+
minLength: 1
|
|
1341
|
+
},
|
|
1342
|
+
description: {
|
|
1343
|
+
type: "string",
|
|
1344
|
+
description: "Template description."
|
|
1345
|
+
},
|
|
1346
|
+
backup: {
|
|
1347
|
+
type: "string",
|
|
1348
|
+
description: "Backup name to restore automatically after provisioning."
|
|
1349
|
+
},
|
|
1350
|
+
sudo: {
|
|
1351
|
+
type: "boolean",
|
|
1352
|
+
description: "Whether sudo privilege is required. If true, requests sudo authentication before execution."
|
|
1353
|
+
},
|
|
1214
1354
|
steps: {
|
|
1215
1355
|
type: "array",
|
|
1356
|
+
description: "List of provisioning steps. At least 1 step required.",
|
|
1216
1357
|
minItems: 1,
|
|
1217
1358
|
items: {
|
|
1218
1359
|
type: "object",
|
|
1219
1360
|
required: ["name", "command"],
|
|
1220
1361
|
properties: {
|
|
1221
|
-
name: {
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1362
|
+
name: {
|
|
1363
|
+
type: "string",
|
|
1364
|
+
description: "Step name.",
|
|
1365
|
+
minLength: 1
|
|
1366
|
+
},
|
|
1367
|
+
description: {
|
|
1368
|
+
type: "string",
|
|
1369
|
+
description: "Step description."
|
|
1370
|
+
},
|
|
1371
|
+
command: {
|
|
1372
|
+
type: "string",
|
|
1373
|
+
description: "Shell command to execute.",
|
|
1374
|
+
minLength: 1
|
|
1375
|
+
},
|
|
1376
|
+
skip_if: {
|
|
1377
|
+
type: "string",
|
|
1378
|
+
description: "Skip this step if this command exits with code 0."
|
|
1379
|
+
},
|
|
1380
|
+
continue_on_error: {
|
|
1381
|
+
type: "boolean",
|
|
1382
|
+
description: "Continue to next step even if this fails. Default: false."
|
|
1383
|
+
}
|
|
1226
1384
|
},
|
|
1227
1385
|
additionalProperties: false
|
|
1228
1386
|
}
|
|
@@ -1230,7 +1388,9 @@ var templateSchema = {
|
|
|
1230
1388
|
},
|
|
1231
1389
|
additionalProperties: false
|
|
1232
1390
|
};
|
|
1233
|
-
|
|
1391
|
+
|
|
1392
|
+
// src/schemas/template.schema.ts
|
|
1393
|
+
var validate4 = ajv.compile(template_schema_default);
|
|
1234
1394
|
function validateTemplate(data) {
|
|
1235
1395
|
const valid = validate4(data);
|
|
1236
1396
|
if (valid) return { valid: true };
|
|
@@ -1282,9 +1442,6 @@ Begin by asking the user to describe their provisioning needs.`;
|
|
|
1282
1442
|
|
|
1283
1443
|
// src/utils/claude-code-runner.ts
|
|
1284
1444
|
import { spawn } from "child_process";
|
|
1285
|
-
import { unlink, writeFile as writeFile3 } from "fs/promises";
|
|
1286
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
1287
|
-
import { join as join8 } from "path";
|
|
1288
1445
|
async function isClaudeCodeAvailable() {
|
|
1289
1446
|
return new Promise((resolve2) => {
|
|
1290
1447
|
const child = spawn("which", ["claude"], { shell: true });
|
|
@@ -1298,58 +1455,49 @@ async function isClaudeCodeAvailable() {
|
|
|
1298
1455
|
}
|
|
1299
1456
|
async function invokeClaudeCode(prompt, options) {
|
|
1300
1457
|
const timeout = options?.timeout ?? 12e4;
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1458
|
+
return await new Promise((resolve2, reject) => {
|
|
1459
|
+
const args = ["--permission-mode", "acceptEdits", "--model", "sonnet"];
|
|
1460
|
+
if (options?.sessionId) {
|
|
1461
|
+
args.push("--session", options.sessionId);
|
|
1462
|
+
}
|
|
1463
|
+
const child = spawn("claude", args, {
|
|
1464
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1465
|
+
});
|
|
1466
|
+
let stdout = "";
|
|
1467
|
+
let stderr = "";
|
|
1468
|
+
child.stdout.on("data", (data) => {
|
|
1469
|
+
stdout += data.toString();
|
|
1470
|
+
});
|
|
1471
|
+
child.stderr.on("data", (data) => {
|
|
1472
|
+
stderr += data.toString();
|
|
1473
|
+
});
|
|
1474
|
+
child.stdin.write(prompt);
|
|
1475
|
+
child.stdin.end();
|
|
1476
|
+
const timer = setTimeout(() => {
|
|
1477
|
+
child.kill();
|
|
1478
|
+
reject(new Error(`Claude Code invocation timeout after ${timeout}ms`));
|
|
1479
|
+
}, timeout);
|
|
1480
|
+
child.on("close", (code) => {
|
|
1481
|
+
clearTimeout(timer);
|
|
1482
|
+
if (code === 0) {
|
|
1483
|
+
resolve2({
|
|
1484
|
+
success: true,
|
|
1485
|
+
output: stdout,
|
|
1486
|
+
sessionId: options?.sessionId
|
|
1487
|
+
});
|
|
1488
|
+
} else {
|
|
1489
|
+
resolve2({
|
|
1490
|
+
success: false,
|
|
1491
|
+
output: stdout,
|
|
1492
|
+
error: stderr || `Process exited with code ${code}`
|
|
1493
|
+
});
|
|
1308
1494
|
}
|
|
1309
|
-
const child = spawn("claude", args, {
|
|
1310
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1311
|
-
});
|
|
1312
|
-
let stdout = "";
|
|
1313
|
-
let stderr = "";
|
|
1314
|
-
child.stdout.on("data", (data) => {
|
|
1315
|
-
stdout += data.toString();
|
|
1316
|
-
});
|
|
1317
|
-
child.stderr.on("data", (data) => {
|
|
1318
|
-
stderr += data.toString();
|
|
1319
|
-
});
|
|
1320
|
-
child.stdin.write(prompt);
|
|
1321
|
-
child.stdin.end();
|
|
1322
|
-
const timer = setTimeout(() => {
|
|
1323
|
-
child.kill();
|
|
1324
|
-
reject(new Error(`Claude Code invocation timeout after ${timeout}ms`));
|
|
1325
|
-
}, timeout);
|
|
1326
|
-
child.on("close", (code) => {
|
|
1327
|
-
clearTimeout(timer);
|
|
1328
|
-
if (code === 0) {
|
|
1329
|
-
resolve2({
|
|
1330
|
-
success: true,
|
|
1331
|
-
output: stdout,
|
|
1332
|
-
sessionId: options?.sessionId
|
|
1333
|
-
});
|
|
1334
|
-
} else {
|
|
1335
|
-
resolve2({
|
|
1336
|
-
success: false,
|
|
1337
|
-
output: stdout,
|
|
1338
|
-
error: stderr || `Process exited with code ${code}`
|
|
1339
|
-
});
|
|
1340
|
-
}
|
|
1341
|
-
});
|
|
1342
|
-
child.on("error", (err) => {
|
|
1343
|
-
clearTimeout(timer);
|
|
1344
|
-
reject(err);
|
|
1345
|
-
});
|
|
1346
1495
|
});
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
}
|
|
1496
|
+
child.on("error", (err) => {
|
|
1497
|
+
clearTimeout(timer);
|
|
1498
|
+
reject(err);
|
|
1499
|
+
});
|
|
1500
|
+
});
|
|
1353
1501
|
}
|
|
1354
1502
|
async function resumeClaudeCodeSession(sessionId, prompt, options) {
|
|
1355
1503
|
return invokeClaudeCode(prompt, {
|
|
@@ -1533,14 +1681,14 @@ var CreateTemplateView = ({
|
|
|
1533
1681
|
setPhase("writing");
|
|
1534
1682
|
setMessage("Writing template...");
|
|
1535
1683
|
const filename = templateName ? `${templateName}.yml` : `${parsedTemplate.name.toLowerCase().replace(/\s+/g, "-")}.yml`;
|
|
1536
|
-
const templatePath =
|
|
1684
|
+
const templatePath = join8(templatesDir, filename);
|
|
1537
1685
|
if (await fileExists(templatePath)) {
|
|
1538
1686
|
throw new Error(
|
|
1539
1687
|
`Template already exists: ${filename}
|
|
1540
1688
|
Please choose a different name or delete the existing template.`
|
|
1541
1689
|
);
|
|
1542
1690
|
}
|
|
1543
|
-
await
|
|
1691
|
+
await writeFile3(templatePath, yamlContent, "utf-8");
|
|
1544
1692
|
setPhase("done");
|
|
1545
1693
|
setMessage(`\u2713 Template created: ${filename}`);
|
|
1546
1694
|
setTimeout(() => exit(), 100);
|
|
@@ -1766,7 +1914,7 @@ function registerHelpCommand(program2) {
|
|
|
1766
1914
|
import { useState as useState3, useEffect as useEffect3 } from "react";
|
|
1767
1915
|
import { Text as Text5, Box as Box4, useApp as useApp3 } from "ink";
|
|
1768
1916
|
import { render as render4 } from "ink";
|
|
1769
|
-
import { join as
|
|
1917
|
+
import { join as join9 } from "path";
|
|
1770
1918
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1771
1919
|
var InitView = () => {
|
|
1772
1920
|
const { exit } = useApp3();
|
|
@@ -1777,7 +1925,7 @@ var InitView = () => {
|
|
|
1777
1925
|
(async () => {
|
|
1778
1926
|
try {
|
|
1779
1927
|
const appDir = getAppDir();
|
|
1780
|
-
if (await fileExists(
|
|
1928
|
+
if (await fileExists(join9(appDir, CONFIG_FILENAME))) {
|
|
1781
1929
|
setError(`Already initialized: ${appDir}`);
|
|
1782
1930
|
exit();
|
|
1783
1931
|
return;
|
|
@@ -1810,7 +1958,7 @@ var InitView = () => {
|
|
|
1810
1958
|
await initDefaultConfig();
|
|
1811
1959
|
completed.push({ name: `Created ${CONFIG_FILENAME} (defaults)`, done: true });
|
|
1812
1960
|
setSteps([...completed]);
|
|
1813
|
-
const exampleTemplatePath =
|
|
1961
|
+
const exampleTemplatePath = join9(getSubDir(TEMPLATES_DIR), "example.yml");
|
|
1814
1962
|
if (!await fileExists(exampleTemplatePath)) {
|
|
1815
1963
|
const { writeFile: writeFile6 } = await import("fs/promises");
|
|
1816
1964
|
const exampleYaml = readAsset("template.example.yml");
|
|
@@ -1946,7 +2094,7 @@ var Table = ({
|
|
|
1946
2094
|
// src/core/provision.ts
|
|
1947
2095
|
import { exec } from "child_process";
|
|
1948
2096
|
import { readFile as readFile4, readdir as readdir2 } from "fs/promises";
|
|
1949
|
-
import { join as
|
|
2097
|
+
import { join as join10 } from "path";
|
|
1950
2098
|
import YAML3 from "yaml";
|
|
1951
2099
|
var REMOTE_SCRIPT_PATTERNS = [
|
|
1952
2100
|
/curl\s.*\|\s*(ba)?sh/,
|
|
@@ -1986,7 +2134,7 @@ async function listTemplates() {
|
|
|
1986
2134
|
if (!entry.isFile() || !entry.name.endsWith(".yml") && !entry.name.endsWith(".yaml")) {
|
|
1987
2135
|
continue;
|
|
1988
2136
|
}
|
|
1989
|
-
const fullPath =
|
|
2137
|
+
const fullPath = join10(templatesDir, entry.name);
|
|
1990
2138
|
try {
|
|
1991
2139
|
const config = await loadTemplate(fullPath);
|
|
1992
2140
|
templates.push({
|
|
@@ -2063,8 +2211,10 @@ async function executeStep(step) {
|
|
|
2063
2211
|
output: output || void 0
|
|
2064
2212
|
};
|
|
2065
2213
|
} catch (err) {
|
|
2066
|
-
const error = err;
|
|
2067
|
-
const
|
|
2214
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2215
|
+
const stdout = err?.stdout ?? "";
|
|
2216
|
+
const stderr = err?.stderr ?? "";
|
|
2217
|
+
const errorOutput = [stdout, stderr, error.message].filter(Boolean).join("\n").trim();
|
|
2068
2218
|
return {
|
|
2069
2219
|
name: step.name,
|
|
2070
2220
|
status: "failed",
|
|
@@ -2103,7 +2253,7 @@ async function* runProvision(templatePath, options = {}) {
|
|
|
2103
2253
|
|
|
2104
2254
|
// src/core/restore.ts
|
|
2105
2255
|
import { copyFile, lstat as lstat2, readdir as readdir3, stat as stat2 } from "fs/promises";
|
|
2106
|
-
import { dirname as dirname2, join as
|
|
2256
|
+
import { dirname as dirname2, join as join11 } from "path";
|
|
2107
2257
|
async function getBackupList(config) {
|
|
2108
2258
|
const backupDir = config?.backup.destination ? resolveTargetPath(config.backup.destination) : getSubDir(BACKUPS_DIR);
|
|
2109
2259
|
const exists = await fileExists(backupDir);
|
|
@@ -2112,7 +2262,7 @@ async function getBackupList(config) {
|
|
|
2112
2262
|
const backups = [];
|
|
2113
2263
|
for (const entry of entries) {
|
|
2114
2264
|
if (!entry.isFile() || !entry.name.endsWith(".tar.gz")) continue;
|
|
2115
|
-
const fullPath =
|
|
2265
|
+
const fullPath = join11(backupDir, entry.name);
|
|
2116
2266
|
const fileStat = await stat2(fullPath);
|
|
2117
2267
|
let hostname;
|
|
2118
2268
|
let fileCount;
|
|
@@ -2187,7 +2337,7 @@ async function createSafetyBackup(filePaths) {
|
|
|
2187
2337
|
const filename = `_pre-restore_${formatDatetime(now)}.tar.gz`;
|
|
2188
2338
|
const backupDir = getSubDir(BACKUPS_DIR);
|
|
2189
2339
|
await ensureDir(backupDir);
|
|
2190
|
-
const archivePath =
|
|
2340
|
+
const archivePath = join11(backupDir, filename);
|
|
2191
2341
|
const files = [];
|
|
2192
2342
|
for (const fp of filePaths) {
|
|
2193
2343
|
const absPath = resolveTargetPath(fp);
|
|
@@ -2221,8 +2371,8 @@ async function restoreBackup(archivePath, options = {}) {
|
|
|
2221
2371
|
};
|
|
2222
2372
|
}
|
|
2223
2373
|
const { mkdtemp: mkdtemp2, rm: rm2 } = await import("fs/promises");
|
|
2224
|
-
const { tmpdir:
|
|
2225
|
-
const tmpDir = await mkdtemp2(
|
|
2374
|
+
const { tmpdir: tmpdir2 } = await import("os");
|
|
2375
|
+
const tmpDir = await mkdtemp2(join11(tmpdir2(), "syncpoint-restore-"));
|
|
2226
2376
|
try {
|
|
2227
2377
|
await extractArchive(archivePath, tmpDir);
|
|
2228
2378
|
for (const action of plan.actions) {
|
|
@@ -2231,7 +2381,7 @@ async function restoreBackup(archivePath, options = {}) {
|
|
|
2231
2381
|
continue;
|
|
2232
2382
|
}
|
|
2233
2383
|
const archiveName = action.path.startsWith("~/") ? action.path.slice(2) : action.path;
|
|
2234
|
-
const extractedPath =
|
|
2384
|
+
const extractedPath = join11(tmpDir, archiveName);
|
|
2235
2385
|
const destPath = resolveTargetPath(action.path);
|
|
2236
2386
|
const extractedExists = await fileExists(extractedPath);
|
|
2237
2387
|
if (!extractedExists) {
|
|
@@ -2626,6 +2776,10 @@ var ListView = ({ type, deleteIndex }) => {
|
|
|
2626
2776
|
function registerListCommand(program2) {
|
|
2627
2777
|
program2.command("list [type]").description("List backups and templates").option("--delete <n>", "Delete item #n").action(async (type, opts) => {
|
|
2628
2778
|
const deleteIndex = opts.delete ? parseInt(opts.delete, 10) : void 0;
|
|
2779
|
+
if (deleteIndex !== void 0 && isNaN(deleteIndex)) {
|
|
2780
|
+
console.error(`Invalid delete index: ${opts.delete}`);
|
|
2781
|
+
process.exit(1);
|
|
2782
|
+
}
|
|
2629
2783
|
const { waitUntilExit } = render5(
|
|
2630
2784
|
/* @__PURE__ */ jsx8(ListView, { type, deleteIndex })
|
|
2631
2785
|
);
|
|
@@ -2633,11 +2787,246 @@ function registerListCommand(program2) {
|
|
|
2633
2787
|
});
|
|
2634
2788
|
}
|
|
2635
2789
|
|
|
2636
|
-
// src/commands/
|
|
2790
|
+
// src/commands/Migrate.tsx
|
|
2637
2791
|
import { useState as useState6, useEffect as useEffect5 } from "react";
|
|
2638
|
-
import { Text as
|
|
2792
|
+
import { Text as Text9, Box as Box7, useApp as useApp5 } from "ink";
|
|
2639
2793
|
import { render as render6 } from "ink";
|
|
2640
2794
|
|
|
2795
|
+
// src/core/migrate.ts
|
|
2796
|
+
import { copyFile as copyFile2, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
2797
|
+
import YAML4 from "yaml";
|
|
2798
|
+
function extractSchemaPaths(schema, prefix = []) {
|
|
2799
|
+
const paths = [];
|
|
2800
|
+
const properties = schema.properties;
|
|
2801
|
+
if (!properties) return paths;
|
|
2802
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
2803
|
+
const currentPath = [...prefix, key];
|
|
2804
|
+
if (value.type === "object" && value.properties) {
|
|
2805
|
+
paths.push(
|
|
2806
|
+
...extractSchemaPaths(value, currentPath)
|
|
2807
|
+
);
|
|
2808
|
+
} else {
|
|
2809
|
+
paths.push(currentPath);
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
return paths;
|
|
2813
|
+
}
|
|
2814
|
+
function extractDataPaths(data, prefix = []) {
|
|
2815
|
+
const paths = [];
|
|
2816
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return paths;
|
|
2817
|
+
for (const [key, value] of Object.entries(data)) {
|
|
2818
|
+
const currentPath = [...prefix, key];
|
|
2819
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
2820
|
+
paths.push(...extractDataPaths(value, currentPath));
|
|
2821
|
+
} else {
|
|
2822
|
+
paths.push(currentPath);
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
return paths;
|
|
2826
|
+
}
|
|
2827
|
+
function pathKey(path) {
|
|
2828
|
+
return path.join(".");
|
|
2829
|
+
}
|
|
2830
|
+
function getNestedValue(obj, path) {
|
|
2831
|
+
let current = obj;
|
|
2832
|
+
for (const key of path) {
|
|
2833
|
+
if (!current || typeof current !== "object") return void 0;
|
|
2834
|
+
current = current[key];
|
|
2835
|
+
}
|
|
2836
|
+
return current;
|
|
2837
|
+
}
|
|
2838
|
+
function diffConfigFields(userData) {
|
|
2839
|
+
const schemaPaths = extractSchemaPaths(
|
|
2840
|
+
config_schema_default
|
|
2841
|
+
);
|
|
2842
|
+
const templateData = YAML4.parse(readAsset("config.default.yml"));
|
|
2843
|
+
const templatePaths = extractDataPaths(templateData);
|
|
2844
|
+
const userPaths = extractDataPaths(userData);
|
|
2845
|
+
const schemaKeys = new Set(schemaPaths.map(pathKey));
|
|
2846
|
+
const userKeys = new Set(userPaths.map(pathKey));
|
|
2847
|
+
const isEditorDirective = (p) => p.length === 1 && p[0] === "yaml-language-server";
|
|
2848
|
+
return {
|
|
2849
|
+
// Fields present in template with defaults AND valid in schema, but missing from user
|
|
2850
|
+
added: templatePaths.filter((p) => {
|
|
2851
|
+
if (isEditorDirective(p)) return false;
|
|
2852
|
+
const key = pathKey(p);
|
|
2853
|
+
return schemaKeys.has(key) && !userKeys.has(key);
|
|
2854
|
+
}),
|
|
2855
|
+
// Fields in user but not in schema (truly deprecated)
|
|
2856
|
+
removed: userPaths.filter(
|
|
2857
|
+
(p) => !isEditorDirective(p) && !schemaKeys.has(pathKey(p))
|
|
2858
|
+
),
|
|
2859
|
+
// Fields in user AND in schema (preserve user values)
|
|
2860
|
+
existing: userPaths.filter((p) => schemaKeys.has(pathKey(p)))
|
|
2861
|
+
};
|
|
2862
|
+
}
|
|
2863
|
+
function buildMigratedDocument(templateText, userData, diff) {
|
|
2864
|
+
const doc = YAML4.parseDocument(templateText);
|
|
2865
|
+
for (const path of diff.existing) {
|
|
2866
|
+
const userValue = getNestedValue(userData, path);
|
|
2867
|
+
if (userValue === void 0) continue;
|
|
2868
|
+
const node = doc.getIn(path, true);
|
|
2869
|
+
if (YAML4.isScalar(node)) {
|
|
2870
|
+
node.value = userValue;
|
|
2871
|
+
} else {
|
|
2872
|
+
doc.setIn(path, userValue);
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
let output = doc.toString();
|
|
2876
|
+
if (diff.removed.length > 0) {
|
|
2877
|
+
const lines = [
|
|
2878
|
+
"",
|
|
2879
|
+
"# [deprecated] The following fields are no longer in the current schema.",
|
|
2880
|
+
"# They have been preserved as comments for reference."
|
|
2881
|
+
];
|
|
2882
|
+
for (const path of diff.removed) {
|
|
2883
|
+
const value = getNestedValue(userData, path);
|
|
2884
|
+
const valueStr = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
2885
|
+
lines.push(`# [deprecated] ${path.join(".")}: ${valueStr}`);
|
|
2886
|
+
}
|
|
2887
|
+
output += lines.join("\n") + "\n";
|
|
2888
|
+
}
|
|
2889
|
+
return output;
|
|
2890
|
+
}
|
|
2891
|
+
async function migrateConfig(options) {
|
|
2892
|
+
const configPath = getConfigPath();
|
|
2893
|
+
if (!await fileExists(configPath)) {
|
|
2894
|
+
throw new Error(
|
|
2895
|
+
`Config file not found: ${configPath}
|
|
2896
|
+
Run "syncpoint init" first.`
|
|
2897
|
+
);
|
|
2898
|
+
}
|
|
2899
|
+
const userText = await readFile5(configPath, "utf-8");
|
|
2900
|
+
const userData = YAML4.parse(userText);
|
|
2901
|
+
const templateText = readAsset("config.default.yml");
|
|
2902
|
+
const diff = diffConfigFields(userData);
|
|
2903
|
+
if (diff.added.length === 0 && diff.removed.length === 0) {
|
|
2904
|
+
return {
|
|
2905
|
+
added: [],
|
|
2906
|
+
deprecated: [],
|
|
2907
|
+
preserved: diff.existing.map(pathKey),
|
|
2908
|
+
backupPath: "",
|
|
2909
|
+
migrated: false
|
|
2910
|
+
};
|
|
2911
|
+
}
|
|
2912
|
+
const result = {
|
|
2913
|
+
added: diff.added.map(pathKey),
|
|
2914
|
+
deprecated: diff.removed.map(pathKey),
|
|
2915
|
+
preserved: diff.existing.map(pathKey),
|
|
2916
|
+
backupPath: "",
|
|
2917
|
+
migrated: false
|
|
2918
|
+
};
|
|
2919
|
+
if (!options?.dryRun) {
|
|
2920
|
+
const migratedText = buildMigratedDocument(templateText, userData, diff);
|
|
2921
|
+
const backupPath = configPath + ".bak";
|
|
2922
|
+
await copyFile2(configPath, backupPath);
|
|
2923
|
+
result.backupPath = backupPath;
|
|
2924
|
+
await writeFile4(configPath, migratedText, "utf-8");
|
|
2925
|
+
const migrated = YAML4.parse(migratedText);
|
|
2926
|
+
const validation = validateConfig(migrated);
|
|
2927
|
+
if (!validation.valid) {
|
|
2928
|
+
await copyFile2(backupPath, configPath);
|
|
2929
|
+
throw new Error(
|
|
2930
|
+
`Migration produced invalid config (restored from backup):
|
|
2931
|
+
${(validation.errors ?? []).join("\n")}`
|
|
2932
|
+
);
|
|
2933
|
+
}
|
|
2934
|
+
result.migrated = true;
|
|
2935
|
+
}
|
|
2936
|
+
return result;
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
// src/commands/Migrate.tsx
|
|
2940
|
+
import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2941
|
+
var MigrateView = ({ dryRun }) => {
|
|
2942
|
+
const { exit } = useApp5();
|
|
2943
|
+
const [result, setResult] = useState6(null);
|
|
2944
|
+
const [error, setError] = useState6(null);
|
|
2945
|
+
const [loading, setLoading] = useState6(true);
|
|
2946
|
+
useEffect5(() => {
|
|
2947
|
+
(async () => {
|
|
2948
|
+
try {
|
|
2949
|
+
const res = await migrateConfig({ dryRun });
|
|
2950
|
+
setResult(res);
|
|
2951
|
+
setLoading(false);
|
|
2952
|
+
setTimeout(() => exit(), 100);
|
|
2953
|
+
} catch (err) {
|
|
2954
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
2955
|
+
setLoading(false);
|
|
2956
|
+
exit();
|
|
2957
|
+
}
|
|
2958
|
+
})();
|
|
2959
|
+
}, []);
|
|
2960
|
+
if (error) {
|
|
2961
|
+
return /* @__PURE__ */ jsx9(Box7, { flexDirection: "column", children: /* @__PURE__ */ jsxs9(Text9, { color: "red", children: [
|
|
2962
|
+
"\u2717 ",
|
|
2963
|
+
error
|
|
2964
|
+
] }) });
|
|
2965
|
+
}
|
|
2966
|
+
if (loading) {
|
|
2967
|
+
return /* @__PURE__ */ jsx9(Text9, { children: "Analyzing config..." });
|
|
2968
|
+
}
|
|
2969
|
+
if (!result) return null;
|
|
2970
|
+
if (!result.migrated && result.added.length === 0 && result.deprecated.length === 0) {
|
|
2971
|
+
return /* @__PURE__ */ jsx9(Box7, { flexDirection: "column", children: /* @__PURE__ */ jsx9(Text9, { color: "green", children: "\u2713 Config is already up to date." }) });
|
|
2972
|
+
}
|
|
2973
|
+
return /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", children: [
|
|
2974
|
+
dryRun && /* @__PURE__ */ jsx9(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "yellow", bold: true, children: "[dry-run] Preview only \u2014 no changes written." }) }),
|
|
2975
|
+
result.added.length > 0 && /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", children: [
|
|
2976
|
+
/* @__PURE__ */ jsx9(Text9, { bold: true, children: "New fields (added with defaults):" }),
|
|
2977
|
+
result.added.map((field, i) => /* @__PURE__ */ jsxs9(Text9, { children: [
|
|
2978
|
+
" ",
|
|
2979
|
+
/* @__PURE__ */ jsx9(Text9, { color: "green", children: "+" }),
|
|
2980
|
+
" ",
|
|
2981
|
+
field
|
|
2982
|
+
] }, i))
|
|
2983
|
+
] }),
|
|
2984
|
+
result.deprecated.length > 0 && /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", marginTop: result.added.length > 0 ? 1 : 0, children: [
|
|
2985
|
+
/* @__PURE__ */ jsx9(Text9, { bold: true, children: "Deprecated fields (commented out):" }),
|
|
2986
|
+
result.deprecated.map((field, i) => /* @__PURE__ */ jsxs9(Text9, { children: [
|
|
2987
|
+
" ",
|
|
2988
|
+
/* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "~" }),
|
|
2989
|
+
" ",
|
|
2990
|
+
field
|
|
2991
|
+
] }, i))
|
|
2992
|
+
] }),
|
|
2993
|
+
result.preserved.length > 0 && /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", marginTop: 1, children: [
|
|
2994
|
+
/* @__PURE__ */ jsxs9(Text9, { bold: true, children: [
|
|
2995
|
+
"Preserved fields (",
|
|
2996
|
+
result.preserved.length,
|
|
2997
|
+
"):"
|
|
2998
|
+
] }),
|
|
2999
|
+
result.preserved.map((field, i) => /* @__PURE__ */ jsxs9(Text9, { children: [
|
|
3000
|
+
" ",
|
|
3001
|
+
/* @__PURE__ */ jsx9(Text9, { color: "blue", children: "\u2022" }),
|
|
3002
|
+
" ",
|
|
3003
|
+
field
|
|
3004
|
+
] }, i))
|
|
3005
|
+
] }),
|
|
3006
|
+
result.migrated && /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", marginTop: 1, children: [
|
|
3007
|
+
/* @__PURE__ */ jsx9(Text9, { color: "green", children: "\u2713 Migration complete." }),
|
|
3008
|
+
result.backupPath && /* @__PURE__ */ jsxs9(Text9, { children: [
|
|
3009
|
+
" ",
|
|
3010
|
+
"Backup saved to: ",
|
|
3011
|
+
result.backupPath
|
|
3012
|
+
] })
|
|
3013
|
+
] })
|
|
3014
|
+
] });
|
|
3015
|
+
};
|
|
3016
|
+
function registerMigrateCommand(program2) {
|
|
3017
|
+
program2.command("migrate").description("Migrate config.yml to match the current schema").option("--dry-run", "Preview changes without writing").action(async (opts) => {
|
|
3018
|
+
const { waitUntilExit } = render6(
|
|
3019
|
+
/* @__PURE__ */ jsx9(MigrateView, { dryRun: opts.dryRun ?? false })
|
|
3020
|
+
);
|
|
3021
|
+
await waitUntilExit();
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
// src/commands/Provision.tsx
|
|
3026
|
+
import { useState as useState7, useEffect as useEffect6 } from "react";
|
|
3027
|
+
import { Text as Text11, Box as Box9, useApp as useApp6 } from "ink";
|
|
3028
|
+
import { render as render7 } from "ink";
|
|
3029
|
+
|
|
2641
3030
|
// src/utils/sudo.ts
|
|
2642
3031
|
import { execSync } from "child_process";
|
|
2643
3032
|
import pc2 from "picocolors";
|
|
@@ -2671,37 +3060,37 @@ ${pc2.red("\u2717")} Sudo authentication failed or was cancelled. Aborting.`
|
|
|
2671
3060
|
}
|
|
2672
3061
|
|
|
2673
3062
|
// src/components/StepRunner.tsx
|
|
2674
|
-
import { Text as
|
|
3063
|
+
import { Text as Text10, Box as Box8 } from "ink";
|
|
2675
3064
|
import Spinner2 from "ink-spinner";
|
|
2676
|
-
import { jsx as
|
|
3065
|
+
import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2677
3066
|
var StepIcon = ({ status }) => {
|
|
2678
3067
|
switch (status) {
|
|
2679
3068
|
case "success":
|
|
2680
|
-
return /* @__PURE__ */
|
|
3069
|
+
return /* @__PURE__ */ jsx10(Text10, { color: "green", children: "\u2713" });
|
|
2681
3070
|
case "running":
|
|
2682
|
-
return /* @__PURE__ */
|
|
3071
|
+
return /* @__PURE__ */ jsx10(Text10, { color: "yellow", children: /* @__PURE__ */ jsx10(Spinner2, { type: "dots" }) });
|
|
2683
3072
|
case "skipped":
|
|
2684
|
-
return /* @__PURE__ */
|
|
3073
|
+
return /* @__PURE__ */ jsx10(Text10, { color: "blue", children: "\u23ED" });
|
|
2685
3074
|
case "failed":
|
|
2686
|
-
return /* @__PURE__ */
|
|
3075
|
+
return /* @__PURE__ */ jsx10(Text10, { color: "red", children: "\u2717" });
|
|
2687
3076
|
case "pending":
|
|
2688
3077
|
default:
|
|
2689
|
-
return /* @__PURE__ */
|
|
3078
|
+
return /* @__PURE__ */ jsx10(Text10, { color: "gray", children: "\u25CB" });
|
|
2690
3079
|
}
|
|
2691
3080
|
};
|
|
2692
3081
|
var StepStatusText = ({ step }) => {
|
|
2693
3082
|
switch (step.status) {
|
|
2694
3083
|
case "success":
|
|
2695
|
-
return /* @__PURE__ */
|
|
3084
|
+
return /* @__PURE__ */ jsxs10(Text10, { color: "green", children: [
|
|
2696
3085
|
"Done",
|
|
2697
3086
|
step.duration != null ? ` (${Math.round(step.duration / 1e3)}s)` : ""
|
|
2698
3087
|
] });
|
|
2699
3088
|
case "running":
|
|
2700
|
-
return /* @__PURE__ */
|
|
3089
|
+
return /* @__PURE__ */ jsx10(Text10, { color: "yellow", children: "Running..." });
|
|
2701
3090
|
case "skipped":
|
|
2702
|
-
return /* @__PURE__ */
|
|
3091
|
+
return /* @__PURE__ */ jsx10(Text10, { color: "blue", children: "Skipped (already installed)" });
|
|
2703
3092
|
case "failed":
|
|
2704
|
-
return /* @__PURE__ */
|
|
3093
|
+
return /* @__PURE__ */ jsxs10(Text10, { color: "red", children: [
|
|
2705
3094
|
"Failed",
|
|
2706
3095
|
step.error ? `: ${step.error}` : ""
|
|
2707
3096
|
] });
|
|
@@ -2714,13 +3103,13 @@ var StepRunner = ({
|
|
|
2714
3103
|
steps,
|
|
2715
3104
|
total
|
|
2716
3105
|
}) => {
|
|
2717
|
-
return /* @__PURE__ */
|
|
2718
|
-
/* @__PURE__ */
|
|
3106
|
+
return /* @__PURE__ */ jsx10(Box8, { flexDirection: "column", children: steps.map((step, idx) => /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", marginBottom: idx < steps.length - 1 ? 1 : 0, children: [
|
|
3107
|
+
/* @__PURE__ */ jsxs10(Text10, { children: [
|
|
2719
3108
|
" ",
|
|
2720
|
-
/* @__PURE__ */
|
|
2721
|
-
/* @__PURE__ */
|
|
3109
|
+
/* @__PURE__ */ jsx10(StepIcon, { status: step.status }),
|
|
3110
|
+
/* @__PURE__ */ jsxs10(Text10, { children: [
|
|
2722
3111
|
" ",
|
|
2723
|
-
/* @__PURE__ */
|
|
3112
|
+
/* @__PURE__ */ jsxs10(Text10, { bold: true, children: [
|
|
2724
3113
|
"Step ",
|
|
2725
3114
|
idx + 1,
|
|
2726
3115
|
"/",
|
|
@@ -2730,36 +3119,36 @@ var StepRunner = ({
|
|
|
2730
3119
|
step.name
|
|
2731
3120
|
] })
|
|
2732
3121
|
] }),
|
|
2733
|
-
step.output && step.status !== "pending" && /* @__PURE__ */
|
|
3122
|
+
step.output && step.status !== "pending" && /* @__PURE__ */ jsxs10(Text10, { color: "gray", children: [
|
|
2734
3123
|
" ",
|
|
2735
3124
|
step.output
|
|
2736
3125
|
] }),
|
|
2737
|
-
/* @__PURE__ */
|
|
3126
|
+
/* @__PURE__ */ jsxs10(Text10, { children: [
|
|
2738
3127
|
" ",
|
|
2739
|
-
/* @__PURE__ */
|
|
3128
|
+
/* @__PURE__ */ jsx10(StepStatusText, { step })
|
|
2740
3129
|
] })
|
|
2741
3130
|
] }, idx)) });
|
|
2742
3131
|
};
|
|
2743
3132
|
|
|
2744
3133
|
// src/commands/Provision.tsx
|
|
2745
|
-
import { jsx as
|
|
3134
|
+
import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
2746
3135
|
var ProvisionView = ({
|
|
2747
3136
|
template,
|
|
2748
3137
|
templatePath,
|
|
2749
3138
|
options
|
|
2750
3139
|
}) => {
|
|
2751
|
-
const { exit } =
|
|
2752
|
-
const [phase, setPhase] =
|
|
2753
|
-
const [steps, setSteps] =
|
|
3140
|
+
const { exit } = useApp6();
|
|
3141
|
+
const [phase, setPhase] = useState7(options.dryRun ? "done" : "running");
|
|
3142
|
+
const [steps, setSteps] = useState7(
|
|
2754
3143
|
template.steps.map((s) => ({
|
|
2755
3144
|
name: s.name,
|
|
2756
3145
|
status: "pending",
|
|
2757
3146
|
output: s.description
|
|
2758
3147
|
}))
|
|
2759
3148
|
);
|
|
2760
|
-
const [currentStep, setCurrentStep] =
|
|
2761
|
-
const [error, setError] =
|
|
2762
|
-
|
|
3149
|
+
const [currentStep, setCurrentStep] = useState7(0);
|
|
3150
|
+
const [error, setError] = useState7(null);
|
|
3151
|
+
useEffect6(() => {
|
|
2763
3152
|
if (options.dryRun) {
|
|
2764
3153
|
setTimeout(() => exit(), 100);
|
|
2765
3154
|
return;
|
|
@@ -2779,6 +3168,18 @@ var ProvisionView = ({
|
|
|
2779
3168
|
stepIdx++;
|
|
2780
3169
|
}
|
|
2781
3170
|
}
|
|
3171
|
+
if (template.backup && !options.skipRestore) {
|
|
3172
|
+
try {
|
|
3173
|
+
const backups = await getBackupList();
|
|
3174
|
+
const match = backups.find(
|
|
3175
|
+
(b) => b.filename.includes(template.backup)
|
|
3176
|
+
);
|
|
3177
|
+
if (match) {
|
|
3178
|
+
await restoreBackup(match.path);
|
|
3179
|
+
}
|
|
3180
|
+
} catch {
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
2782
3183
|
setPhase("done");
|
|
2783
3184
|
setTimeout(() => exit(), 100);
|
|
2784
3185
|
} catch (err) {
|
|
@@ -2789,7 +3190,7 @@ var ProvisionView = ({
|
|
|
2789
3190
|
})();
|
|
2790
3191
|
}, []);
|
|
2791
3192
|
if (phase === "error" || error) {
|
|
2792
|
-
return /* @__PURE__ */
|
|
3193
|
+
return /* @__PURE__ */ jsx11(Box9, { flexDirection: "column", children: /* @__PURE__ */ jsxs11(Text11, { color: "red", children: [
|
|
2793
3194
|
"\u2717 ",
|
|
2794
3195
|
error
|
|
2795
3196
|
] }) });
|
|
@@ -2797,27 +3198,27 @@ var ProvisionView = ({
|
|
|
2797
3198
|
const successCount = steps.filter((s) => s.status === "success").length;
|
|
2798
3199
|
const skippedCount = steps.filter((s) => s.status === "skipped").length;
|
|
2799
3200
|
const failedCount = steps.filter((s) => s.status === "failed").length;
|
|
2800
|
-
return /* @__PURE__ */
|
|
2801
|
-
/* @__PURE__ */
|
|
2802
|
-
/* @__PURE__ */
|
|
3201
|
+
return /* @__PURE__ */ jsxs11(Box9, { flexDirection: "column", children: [
|
|
3202
|
+
/* @__PURE__ */ jsxs11(Box9, { flexDirection: "column", marginBottom: 1, children: [
|
|
3203
|
+
/* @__PURE__ */ jsxs11(Text11, { bold: true, children: [
|
|
2803
3204
|
"\u25B8 ",
|
|
2804
3205
|
template.name
|
|
2805
3206
|
] }),
|
|
2806
|
-
template.description && /* @__PURE__ */
|
|
3207
|
+
template.description && /* @__PURE__ */ jsxs11(Text11, { color: "gray", children: [
|
|
2807
3208
|
" ",
|
|
2808
3209
|
template.description
|
|
2809
3210
|
] })
|
|
2810
3211
|
] }),
|
|
2811
|
-
options.dryRun && phase === "done" && /* @__PURE__ */
|
|
2812
|
-
/* @__PURE__ */
|
|
2813
|
-
template.sudo && /* @__PURE__ */
|
|
3212
|
+
options.dryRun && phase === "done" && /* @__PURE__ */ jsxs11(Box9, { flexDirection: "column", children: [
|
|
3213
|
+
/* @__PURE__ */ jsx11(Text11, { color: "yellow", children: "(dry-run) Showing execution plan only" }),
|
|
3214
|
+
template.sudo && /* @__PURE__ */ jsxs11(Text11, { color: "yellow", children: [
|
|
2814
3215
|
" ",
|
|
2815
3216
|
"\u26A0 This template requires sudo privileges (will prompt on actual run)"
|
|
2816
3217
|
] }),
|
|
2817
|
-
/* @__PURE__ */
|
|
2818
|
-
/* @__PURE__ */
|
|
3218
|
+
/* @__PURE__ */ jsx11(Box9, { flexDirection: "column", marginTop: 1, children: template.steps.map((step, idx) => /* @__PURE__ */ jsxs11(Box9, { flexDirection: "column", marginBottom: 1, children: [
|
|
3219
|
+
/* @__PURE__ */ jsxs11(Text11, { children: [
|
|
2819
3220
|
" ",
|
|
2820
|
-
/* @__PURE__ */
|
|
3221
|
+
/* @__PURE__ */ jsxs11(Text11, { bold: true, children: [
|
|
2821
3222
|
"Step ",
|
|
2822
3223
|
idx + 1,
|
|
2823
3224
|
"/",
|
|
@@ -2826,18 +3227,18 @@ var ProvisionView = ({
|
|
|
2826
3227
|
" ",
|
|
2827
3228
|
step.name
|
|
2828
3229
|
] }),
|
|
2829
|
-
step.description && /* @__PURE__ */
|
|
3230
|
+
step.description && /* @__PURE__ */ jsxs11(Text11, { color: "gray", children: [
|
|
2830
3231
|
" ",
|
|
2831
3232
|
step.description
|
|
2832
3233
|
] }),
|
|
2833
|
-
step.skip_if && /* @__PURE__ */
|
|
3234
|
+
step.skip_if && /* @__PURE__ */ jsxs11(Text11, { color: "blue", children: [
|
|
2834
3235
|
" ",
|
|
2835
3236
|
"Skip condition: ",
|
|
2836
3237
|
step.skip_if
|
|
2837
3238
|
] })
|
|
2838
3239
|
] }, idx)) })
|
|
2839
3240
|
] }),
|
|
2840
|
-
(phase === "running" || phase === "done" && !options.dryRun) && /* @__PURE__ */
|
|
3241
|
+
(phase === "running" || phase === "done" && !options.dryRun) && /* @__PURE__ */ jsx11(
|
|
2841
3242
|
StepRunner,
|
|
2842
3243
|
{
|
|
2843
3244
|
steps,
|
|
@@ -2845,34 +3246,34 @@ var ProvisionView = ({
|
|
|
2845
3246
|
total: template.steps.length
|
|
2846
3247
|
}
|
|
2847
3248
|
),
|
|
2848
|
-
phase === "done" && !options.dryRun && /* @__PURE__ */
|
|
2849
|
-
/* @__PURE__ */
|
|
3249
|
+
phase === "done" && !options.dryRun && /* @__PURE__ */ jsxs11(Box9, { flexDirection: "column", marginTop: 1, children: [
|
|
3250
|
+
/* @__PURE__ */ jsxs11(Text11, { color: "gray", children: [
|
|
2850
3251
|
" ",
|
|
2851
3252
|
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"
|
|
2852
3253
|
] }),
|
|
2853
|
-
/* @__PURE__ */
|
|
3254
|
+
/* @__PURE__ */ jsxs11(Text11, { children: [
|
|
2854
3255
|
" ",
|
|
2855
3256
|
"Result: ",
|
|
2856
|
-
/* @__PURE__ */
|
|
3257
|
+
/* @__PURE__ */ jsxs11(Text11, { color: "green", children: [
|
|
2857
3258
|
successCount,
|
|
2858
3259
|
" succeeded"
|
|
2859
3260
|
] }),
|
|
2860
3261
|
" \xB7",
|
|
2861
3262
|
" ",
|
|
2862
|
-
/* @__PURE__ */
|
|
3263
|
+
/* @__PURE__ */ jsxs11(Text11, { color: "blue", children: [
|
|
2863
3264
|
skippedCount,
|
|
2864
3265
|
" skipped"
|
|
2865
3266
|
] }),
|
|
2866
3267
|
" \xB7",
|
|
2867
3268
|
" ",
|
|
2868
|
-
/* @__PURE__ */
|
|
3269
|
+
/* @__PURE__ */ jsxs11(Text11, { color: "red", children: [
|
|
2869
3270
|
failedCount,
|
|
2870
3271
|
" failed"
|
|
2871
3272
|
] })
|
|
2872
3273
|
] }),
|
|
2873
|
-
template.backup && !options.skipRestore && /* @__PURE__ */
|
|
2874
|
-
/* @__PURE__ */
|
|
2875
|
-
/* @__PURE__ */
|
|
3274
|
+
template.backup && !options.skipRestore && /* @__PURE__ */ jsxs11(Box9, { flexDirection: "column", marginTop: 1, children: [
|
|
3275
|
+
/* @__PURE__ */ jsx11(Text11, { bold: true, children: "\u25B8 Proceeding with config file restore..." }),
|
|
3276
|
+
/* @__PURE__ */ jsxs11(Text11, { color: "gray", children: [
|
|
2876
3277
|
" ",
|
|
2877
3278
|
"Backup link: ",
|
|
2878
3279
|
template.backup
|
|
@@ -2915,8 +3316,8 @@ function registerProvisionCommand(program2) {
|
|
|
2915
3316
|
if (tmpl.sudo && !opts.dryRun) {
|
|
2916
3317
|
ensureSudo(tmpl.name);
|
|
2917
3318
|
}
|
|
2918
|
-
const { waitUntilExit } =
|
|
2919
|
-
/* @__PURE__ */
|
|
3319
|
+
const { waitUntilExit } = render7(
|
|
3320
|
+
/* @__PURE__ */ jsx11(
|
|
2920
3321
|
ProvisionView,
|
|
2921
3322
|
{
|
|
2922
3323
|
template: tmpl,
|
|
@@ -2934,24 +3335,25 @@ function registerProvisionCommand(program2) {
|
|
|
2934
3335
|
}
|
|
2935
3336
|
|
|
2936
3337
|
// src/commands/Restore.tsx
|
|
2937
|
-
import { useState as
|
|
2938
|
-
import { Text as
|
|
3338
|
+
import { useState as useState8, useEffect as useEffect7 } from "react";
|
|
3339
|
+
import { Text as Text12, Box as Box10, useApp as useApp7 } from "ink";
|
|
2939
3340
|
import SelectInput2 from "ink-select-input";
|
|
2940
|
-
import { render as
|
|
2941
|
-
import { jsx as
|
|
3341
|
+
import { render as render8 } from "ink";
|
|
3342
|
+
import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
2942
3343
|
var RestoreView = ({ filename, options }) => {
|
|
2943
|
-
const { exit } =
|
|
2944
|
-
const [phase, setPhase] =
|
|
2945
|
-
const [backups, setBackups] =
|
|
2946
|
-
const [selectedPath, setSelectedPath] =
|
|
2947
|
-
const [plan, setPlan] =
|
|
2948
|
-
const [result, setResult] =
|
|
2949
|
-
const [safetyDone, setSafetyDone] =
|
|
2950
|
-
const [error, setError] =
|
|
2951
|
-
|
|
3344
|
+
const { exit } = useApp7();
|
|
3345
|
+
const [phase, setPhase] = useState8("loading");
|
|
3346
|
+
const [backups, setBackups] = useState8([]);
|
|
3347
|
+
const [selectedPath, setSelectedPath] = useState8(null);
|
|
3348
|
+
const [plan, setPlan] = useState8(null);
|
|
3349
|
+
const [result, setResult] = useState8(null);
|
|
3350
|
+
const [safetyDone, setSafetyDone] = useState8(false);
|
|
3351
|
+
const [error, setError] = useState8(null);
|
|
3352
|
+
useEffect7(() => {
|
|
2952
3353
|
(async () => {
|
|
2953
3354
|
try {
|
|
2954
|
-
const
|
|
3355
|
+
const config = await loadConfig();
|
|
3356
|
+
const list2 = await getBackupList(config);
|
|
2955
3357
|
setBackups(list2);
|
|
2956
3358
|
if (list2.length === 0) {
|
|
2957
3359
|
setError("No backups available.");
|
|
@@ -2981,7 +3383,7 @@ var RestoreView = ({ filename, options }) => {
|
|
|
2981
3383
|
}
|
|
2982
3384
|
})();
|
|
2983
3385
|
}, []);
|
|
2984
|
-
|
|
3386
|
+
useEffect7(() => {
|
|
2985
3387
|
if (phase !== "planning" || !selectedPath) return;
|
|
2986
3388
|
(async () => {
|
|
2987
3389
|
try {
|
|
@@ -3029,7 +3431,7 @@ var RestoreView = ({ filename, options }) => {
|
|
|
3029
3431
|
}
|
|
3030
3432
|
};
|
|
3031
3433
|
if (phase === "error" || error) {
|
|
3032
|
-
return /* @__PURE__ */
|
|
3434
|
+
return /* @__PURE__ */ jsx12(Box10, { flexDirection: "column", children: /* @__PURE__ */ jsxs12(Text12, { color: "red", children: [
|
|
3033
3435
|
"\u2717 ",
|
|
3034
3436
|
error
|
|
3035
3437
|
] }) });
|
|
@@ -3040,30 +3442,30 @@ var RestoreView = ({ filename, options }) => {
|
|
|
3040
3442
|
}));
|
|
3041
3443
|
const currentHostname = getHostname();
|
|
3042
3444
|
const isRemoteBackup = plan?.metadata.hostname && plan.metadata.hostname !== currentHostname;
|
|
3043
|
-
return /* @__PURE__ */
|
|
3044
|
-
phase === "loading" && /* @__PURE__ */
|
|
3045
|
-
phase === "selecting" && /* @__PURE__ */
|
|
3046
|
-
/* @__PURE__ */
|
|
3047
|
-
/* @__PURE__ */
|
|
3445
|
+
return /* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", children: [
|
|
3446
|
+
phase === "loading" && /* @__PURE__ */ jsx12(Text12, { children: "\u25B8 Loading backup list..." }),
|
|
3447
|
+
phase === "selecting" && /* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", children: [
|
|
3448
|
+
/* @__PURE__ */ jsx12(Text12, { bold: true, children: "\u25B8 Select backup" }),
|
|
3449
|
+
/* @__PURE__ */ jsx12(SelectInput2, { items: selectItems, onSelect: handleSelect })
|
|
3048
3450
|
] }),
|
|
3049
|
-
(phase === "planning" || phase === "confirming" || phase === "restoring" || phase === "done" && plan) && plan && /* @__PURE__ */
|
|
3050
|
-
/* @__PURE__ */
|
|
3051
|
-
/* @__PURE__ */
|
|
3451
|
+
(phase === "planning" || phase === "confirming" || phase === "restoring" || phase === "done" && plan) && plan && /* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", children: [
|
|
3452
|
+
/* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", marginBottom: 1, children: [
|
|
3453
|
+
/* @__PURE__ */ jsxs12(Text12, { bold: true, children: [
|
|
3052
3454
|
"\u25B8 Metadata (",
|
|
3053
3455
|
plan.metadata.config.filename ?? "",
|
|
3054
3456
|
")"
|
|
3055
3457
|
] }),
|
|
3056
|
-
/* @__PURE__ */
|
|
3458
|
+
/* @__PURE__ */ jsxs12(Text12, { children: [
|
|
3057
3459
|
" ",
|
|
3058
3460
|
"Host: ",
|
|
3059
3461
|
plan.metadata.hostname
|
|
3060
3462
|
] }),
|
|
3061
|
-
/* @__PURE__ */
|
|
3463
|
+
/* @__PURE__ */ jsxs12(Text12, { children: [
|
|
3062
3464
|
" ",
|
|
3063
3465
|
"Created: ",
|
|
3064
3466
|
plan.metadata.createdAt
|
|
3065
3467
|
] }),
|
|
3066
|
-
/* @__PURE__ */
|
|
3468
|
+
/* @__PURE__ */ jsxs12(Text12, { children: [
|
|
3067
3469
|
" ",
|
|
3068
3470
|
"Files: ",
|
|
3069
3471
|
plan.metadata.summary.fileCount,
|
|
@@ -3071,15 +3473,15 @@ var RestoreView = ({ filename, options }) => {
|
|
|
3071
3473
|
formatBytes(plan.metadata.summary.totalSize),
|
|
3072
3474
|
")"
|
|
3073
3475
|
] }),
|
|
3074
|
-
isRemoteBackup && /* @__PURE__ */
|
|
3476
|
+
isRemoteBackup && /* @__PURE__ */ jsxs12(Text12, { color: "yellow", children: [
|
|
3075
3477
|
" ",
|
|
3076
3478
|
"\u26A0 This backup was created on a different machine (",
|
|
3077
3479
|
plan.metadata.hostname,
|
|
3078
3480
|
")"
|
|
3079
3481
|
] })
|
|
3080
3482
|
] }),
|
|
3081
|
-
/* @__PURE__ */
|
|
3082
|
-
/* @__PURE__ */
|
|
3483
|
+
/* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", marginBottom: 1, children: [
|
|
3484
|
+
/* @__PURE__ */ jsx12(Text12, { bold: true, children: "\u25B8 Restore plan:" }),
|
|
3083
3485
|
plan.actions.map((action, idx) => {
|
|
3084
3486
|
let icon;
|
|
3085
3487
|
let color;
|
|
@@ -3101,45 +3503,45 @@ var RestoreView = ({ filename, options }) => {
|
|
|
3101
3503
|
label = "(not present)";
|
|
3102
3504
|
break;
|
|
3103
3505
|
}
|
|
3104
|
-
return /* @__PURE__ */
|
|
3506
|
+
return /* @__PURE__ */ jsxs12(Text12, { children: [
|
|
3105
3507
|
" ",
|
|
3106
|
-
/* @__PURE__ */
|
|
3508
|
+
/* @__PURE__ */ jsx12(Text12, { color, children: icon.padEnd(8) }),
|
|
3107
3509
|
" ",
|
|
3108
3510
|
contractTilde(action.path),
|
|
3109
3511
|
" ",
|
|
3110
|
-
/* @__PURE__ */
|
|
3512
|
+
/* @__PURE__ */ jsx12(Text12, { color: "gray", children: label })
|
|
3111
3513
|
] }, idx);
|
|
3112
3514
|
})
|
|
3113
3515
|
] }),
|
|
3114
|
-
options.dryRun && phase === "done" && /* @__PURE__ */
|
|
3516
|
+
options.dryRun && phase === "done" && /* @__PURE__ */ jsx12(Text12, { color: "yellow", children: "(dry-run) No actual restore was performed" })
|
|
3115
3517
|
] }),
|
|
3116
|
-
phase === "confirming" && /* @__PURE__ */
|
|
3117
|
-
phase === "restoring" && /* @__PURE__ */
|
|
3118
|
-
safetyDone && /* @__PURE__ */
|
|
3119
|
-
/* @__PURE__ */
|
|
3518
|
+
phase === "confirming" && /* @__PURE__ */ jsx12(Box10, { flexDirection: "column", children: /* @__PURE__ */ jsx12(Confirm, { message: "Proceed with restore?", onConfirm: handleConfirm }) }),
|
|
3519
|
+
phase === "restoring" && /* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", children: [
|
|
3520
|
+
safetyDone && /* @__PURE__ */ jsxs12(Text12, { children: [
|
|
3521
|
+
/* @__PURE__ */ jsx12(Text12, { color: "green", children: "\u2713" }),
|
|
3120
3522
|
" Safety backup of current files complete"
|
|
3121
3523
|
] }),
|
|
3122
|
-
/* @__PURE__ */
|
|
3524
|
+
/* @__PURE__ */ jsx12(Text12, { children: "\u25B8 Restoring..." })
|
|
3123
3525
|
] }),
|
|
3124
|
-
phase === "done" && result && !options.dryRun && /* @__PURE__ */
|
|
3125
|
-
safetyDone && /* @__PURE__ */
|
|
3126
|
-
/* @__PURE__ */
|
|
3526
|
+
phase === "done" && result && !options.dryRun && /* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", marginTop: 1, children: [
|
|
3527
|
+
safetyDone && /* @__PURE__ */ jsxs12(Text12, { children: [
|
|
3528
|
+
/* @__PURE__ */ jsx12(Text12, { color: "green", children: "\u2713" }),
|
|
3127
3529
|
" Safety backup of current files complete"
|
|
3128
3530
|
] }),
|
|
3129
|
-
/* @__PURE__ */
|
|
3130
|
-
/* @__PURE__ */
|
|
3531
|
+
/* @__PURE__ */ jsx12(Text12, { color: "green", bold: true, children: "\u2713 Restore complete" }),
|
|
3532
|
+
/* @__PURE__ */ jsxs12(Text12, { children: [
|
|
3131
3533
|
" ",
|
|
3132
3534
|
"Restored: ",
|
|
3133
3535
|
result.restoredFiles.length,
|
|
3134
3536
|
" files"
|
|
3135
3537
|
] }),
|
|
3136
|
-
/* @__PURE__ */
|
|
3538
|
+
/* @__PURE__ */ jsxs12(Text12, { children: [
|
|
3137
3539
|
" ",
|
|
3138
3540
|
"Skipped: ",
|
|
3139
3541
|
result.skippedFiles.length,
|
|
3140
3542
|
" files"
|
|
3141
3543
|
] }),
|
|
3142
|
-
result.safetyBackupPath && /* @__PURE__ */
|
|
3544
|
+
result.safetyBackupPath && /* @__PURE__ */ jsxs12(Text12, { children: [
|
|
3143
3545
|
" ",
|
|
3144
3546
|
"Safety backup: ",
|
|
3145
3547
|
contractTilde(result.safetyBackupPath)
|
|
@@ -3149,8 +3551,8 @@ var RestoreView = ({ filename, options }) => {
|
|
|
3149
3551
|
};
|
|
3150
3552
|
function registerRestoreCommand(program2) {
|
|
3151
3553
|
program2.command("restore [filename]").description("Restore config files from a backup").option("--dry-run", "Show planned changes without actual restore", false).action(async (filename, opts) => {
|
|
3152
|
-
const { waitUntilExit } =
|
|
3153
|
-
/* @__PURE__ */
|
|
3554
|
+
const { waitUntilExit } = render8(
|
|
3555
|
+
/* @__PURE__ */ jsx12(
|
|
3154
3556
|
RestoreView,
|
|
3155
3557
|
{
|
|
3156
3558
|
filename,
|
|
@@ -3164,12 +3566,12 @@ function registerRestoreCommand(program2) {
|
|
|
3164
3566
|
|
|
3165
3567
|
// src/commands/Status.tsx
|
|
3166
3568
|
import { readdirSync, statSync, unlinkSync as unlinkSync2 } from "fs";
|
|
3167
|
-
import { join as
|
|
3168
|
-
import { Box as
|
|
3169
|
-
import { render as
|
|
3569
|
+
import { join as join12 } from "path";
|
|
3570
|
+
import { Box as Box11, Text as Text13, useApp as useApp8, useInput as useInput3 } from "ink";
|
|
3571
|
+
import { render as render9 } from "ink";
|
|
3170
3572
|
import SelectInput3 from "ink-select-input";
|
|
3171
|
-
import { useEffect as
|
|
3172
|
-
import { jsx as
|
|
3573
|
+
import { useEffect as useEffect8, useState as useState9 } from "react";
|
|
3574
|
+
import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
3173
3575
|
function getDirStats(dirPath) {
|
|
3174
3576
|
try {
|
|
3175
3577
|
const entries = readdirSync(dirPath);
|
|
@@ -3177,7 +3579,7 @@ function getDirStats(dirPath) {
|
|
|
3177
3579
|
let count = 0;
|
|
3178
3580
|
for (const entry of entries) {
|
|
3179
3581
|
try {
|
|
3180
|
-
const stat4 = statSync(
|
|
3582
|
+
const stat4 = statSync(join12(dirPath, entry));
|
|
3181
3583
|
if (stat4.isFile()) {
|
|
3182
3584
|
totalSize += stat4.size;
|
|
3183
3585
|
count++;
|
|
@@ -3191,34 +3593,34 @@ function getDirStats(dirPath) {
|
|
|
3191
3593
|
}
|
|
3192
3594
|
}
|
|
3193
3595
|
var DisplayActionItem = ({ isSelected = false, label }) => {
|
|
3194
|
-
return /* @__PURE__ */
|
|
3596
|
+
return /* @__PURE__ */ jsx13(Text13, { bold: isSelected, children: label });
|
|
3195
3597
|
};
|
|
3196
3598
|
var CleanupActionItem = ({ isSelected = false, label }) => {
|
|
3197
3599
|
if (label === "Cancel" || label === "Select specific backups to delete") {
|
|
3198
|
-
return /* @__PURE__ */
|
|
3600
|
+
return /* @__PURE__ */ jsx13(Text13, { bold: isSelected, children: label });
|
|
3199
3601
|
}
|
|
3200
3602
|
const parts = label.split(/\s{2,}/);
|
|
3201
3603
|
if (parts.length === 2) {
|
|
3202
|
-
return /* @__PURE__ */
|
|
3604
|
+
return /* @__PURE__ */ jsxs13(Text13, { bold: isSelected, children: [
|
|
3203
3605
|
parts[0],
|
|
3204
3606
|
" ",
|
|
3205
|
-
/* @__PURE__ */
|
|
3607
|
+
/* @__PURE__ */ jsx13(Text13, { dimColor: true, children: parts[1] })
|
|
3206
3608
|
] });
|
|
3207
3609
|
}
|
|
3208
|
-
return /* @__PURE__ */
|
|
3610
|
+
return /* @__PURE__ */ jsx13(Text13, { bold: isSelected, children: label });
|
|
3209
3611
|
};
|
|
3210
3612
|
var StatusView = ({ cleanup }) => {
|
|
3211
|
-
const { exit } =
|
|
3212
|
-
const [phase, setPhase] =
|
|
3213
|
-
const [status, setStatus] =
|
|
3214
|
-
const [backups, setBackups] =
|
|
3215
|
-
const [cleanupAction, setCleanupAction] =
|
|
3216
|
-
const [cleanupMessage, setCleanupMessage] =
|
|
3217
|
-
const [error, setError] =
|
|
3218
|
-
const [selectedForDeletion, setSelectedForDeletion] =
|
|
3613
|
+
const { exit } = useApp8();
|
|
3614
|
+
const [phase, setPhase] = useState9("loading");
|
|
3615
|
+
const [status, setStatus] = useState9(null);
|
|
3616
|
+
const [backups, setBackups] = useState9([]);
|
|
3617
|
+
const [cleanupAction, setCleanupAction] = useState9(null);
|
|
3618
|
+
const [cleanupMessage, setCleanupMessage] = useState9("");
|
|
3619
|
+
const [error, setError] = useState9(null);
|
|
3620
|
+
const [selectedForDeletion, setSelectedForDeletion] = useState9(
|
|
3219
3621
|
[]
|
|
3220
3622
|
);
|
|
3221
|
-
const [backupDir, setBackupDir] =
|
|
3623
|
+
const [backupDir, setBackupDir] = useState9(getSubDir("backups"));
|
|
3222
3624
|
useInput3((_input, key) => {
|
|
3223
3625
|
if (!key.escape) return;
|
|
3224
3626
|
if (phase === "display") {
|
|
@@ -3230,7 +3632,7 @@ var StatusView = ({ cleanup }) => {
|
|
|
3230
3632
|
setPhase("cleanup");
|
|
3231
3633
|
}
|
|
3232
3634
|
});
|
|
3233
|
-
|
|
3635
|
+
useEffect8(() => {
|
|
3234
3636
|
(async () => {
|
|
3235
3637
|
try {
|
|
3236
3638
|
const config = await loadConfig();
|
|
@@ -3323,9 +3725,14 @@ var StatusView = ({ cleanup }) => {
|
|
|
3323
3725
|
try {
|
|
3324
3726
|
const entries = readdirSync(logsDir);
|
|
3325
3727
|
for (const entry of entries) {
|
|
3326
|
-
const logPath =
|
|
3728
|
+
const logPath = join12(logsDir, entry);
|
|
3327
3729
|
if (!isInsideDir(logPath, logsDir)) throw new Error(`Refusing to delete file outside logs directory: ${logPath}`);
|
|
3328
|
-
|
|
3730
|
+
try {
|
|
3731
|
+
if (statSync(logPath).isFile()) {
|
|
3732
|
+
unlinkSync2(logPath);
|
|
3733
|
+
}
|
|
3734
|
+
} catch {
|
|
3735
|
+
}
|
|
3329
3736
|
}
|
|
3330
3737
|
} catch {
|
|
3331
3738
|
}
|
|
@@ -3362,26 +3769,26 @@ var StatusView = ({ cleanup }) => {
|
|
|
3362
3769
|
}
|
|
3363
3770
|
};
|
|
3364
3771
|
if (phase === "error" || error) {
|
|
3365
|
-
return /* @__PURE__ */
|
|
3772
|
+
return /* @__PURE__ */ jsx13(Box11, { flexDirection: "column", children: /* @__PURE__ */ jsxs13(Text13, { color: "red", children: [
|
|
3366
3773
|
"\u2717 ",
|
|
3367
3774
|
error
|
|
3368
3775
|
] }) });
|
|
3369
3776
|
}
|
|
3370
3777
|
if (phase === "loading") {
|
|
3371
|
-
return /* @__PURE__ */
|
|
3778
|
+
return /* @__PURE__ */ jsx13(Text13, { color: "cyan", children: "Loading..." });
|
|
3372
3779
|
}
|
|
3373
3780
|
if (!status) return null;
|
|
3374
3781
|
const totalCount = status.backups.count + status.templates.count + status.scripts.count + status.logs.count;
|
|
3375
3782
|
const totalSize = status.backups.totalSize + status.templates.totalSize + status.scripts.totalSize + status.logs.totalSize;
|
|
3376
|
-
const statusDisplay = /* @__PURE__ */
|
|
3377
|
-
/* @__PURE__ */
|
|
3783
|
+
const statusDisplay = /* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", children: [
|
|
3784
|
+
/* @__PURE__ */ jsxs13(Text13, { bold: true, children: [
|
|
3378
3785
|
"\u25B8 ",
|
|
3379
3786
|
APP_NAME,
|
|
3380
3787
|
" status \u2014 ~/.",
|
|
3381
3788
|
APP_NAME,
|
|
3382
3789
|
"/"
|
|
3383
3790
|
] }),
|
|
3384
|
-
/* @__PURE__ */
|
|
3791
|
+
/* @__PURE__ */ jsx13(Box11, { marginLeft: 2, marginTop: 1, children: /* @__PURE__ */ jsx13(
|
|
3385
3792
|
Table,
|
|
3386
3793
|
{
|
|
3387
3794
|
headers: ["Directory", "Count", "Size"],
|
|
@@ -3410,15 +3817,15 @@ var StatusView = ({ cleanup }) => {
|
|
|
3410
3817
|
]
|
|
3411
3818
|
}
|
|
3412
3819
|
) }),
|
|
3413
|
-
status.lastBackup && /* @__PURE__ */
|
|
3414
|
-
/* @__PURE__ */
|
|
3820
|
+
status.lastBackup && /* @__PURE__ */ jsxs13(Box11, { marginTop: 1, marginLeft: 2, flexDirection: "column", children: [
|
|
3821
|
+
/* @__PURE__ */ jsxs13(Text13, { children: [
|
|
3415
3822
|
"Latest backup: ",
|
|
3416
3823
|
formatDate(status.lastBackup),
|
|
3417
3824
|
" (",
|
|
3418
3825
|
formatRelativeTime(status.lastBackup),
|
|
3419
3826
|
")"
|
|
3420
3827
|
] }),
|
|
3421
|
-
status.oldestBackup && /* @__PURE__ */
|
|
3828
|
+
status.oldestBackup && /* @__PURE__ */ jsxs13(Text13, { children: [
|
|
3422
3829
|
"Oldest backup: ",
|
|
3423
3830
|
formatDate(status.oldestBackup),
|
|
3424
3831
|
" (",
|
|
@@ -3427,18 +3834,18 @@ var StatusView = ({ cleanup }) => {
|
|
|
3427
3834
|
] })
|
|
3428
3835
|
] })
|
|
3429
3836
|
] });
|
|
3430
|
-
const escHint = (action) => /* @__PURE__ */
|
|
3837
|
+
const escHint = (action) => /* @__PURE__ */ jsx13(Box11, { marginTop: 1, children: /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
|
|
3431
3838
|
"Press ",
|
|
3432
|
-
/* @__PURE__ */
|
|
3839
|
+
/* @__PURE__ */ jsx13(Text13, { bold: true, children: "ESC" }),
|
|
3433
3840
|
" to ",
|
|
3434
3841
|
action
|
|
3435
3842
|
] }) });
|
|
3436
3843
|
if (phase === "display") {
|
|
3437
|
-
return /* @__PURE__ */
|
|
3844
|
+
return /* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", children: [
|
|
3438
3845
|
statusDisplay,
|
|
3439
|
-
/* @__PURE__ */
|
|
3440
|
-
/* @__PURE__ */
|
|
3441
|
-
/* @__PURE__ */
|
|
3846
|
+
/* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", marginTop: 1, children: [
|
|
3847
|
+
/* @__PURE__ */ jsx13(Text13, { bold: true, children: "\u25B8 Actions" }),
|
|
3848
|
+
/* @__PURE__ */ jsx13(
|
|
3442
3849
|
SelectInput3,
|
|
3443
3850
|
{
|
|
3444
3851
|
items: [
|
|
@@ -3486,11 +3893,11 @@ var StatusView = ({ cleanup }) => {
|
|
|
3486
3893
|
value: "cancel"
|
|
3487
3894
|
}
|
|
3488
3895
|
];
|
|
3489
|
-
return /* @__PURE__ */
|
|
3896
|
+
return /* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", children: [
|
|
3490
3897
|
statusDisplay,
|
|
3491
|
-
/* @__PURE__ */
|
|
3492
|
-
/* @__PURE__ */
|
|
3493
|
-
/* @__PURE__ */
|
|
3898
|
+
/* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", marginTop: 1, children: [
|
|
3899
|
+
/* @__PURE__ */ jsx13(Text13, { bold: true, children: "\u25B8 Cleanup options" }),
|
|
3900
|
+
/* @__PURE__ */ jsx13(
|
|
3494
3901
|
SelectInput3,
|
|
3495
3902
|
{
|
|
3496
3903
|
items: cleanupItems,
|
|
@@ -3516,26 +3923,26 @@ var StatusView = ({ cleanup }) => {
|
|
|
3516
3923
|
value: "done"
|
|
3517
3924
|
}
|
|
3518
3925
|
];
|
|
3519
|
-
return /* @__PURE__ */
|
|
3926
|
+
return /* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", children: [
|
|
3520
3927
|
statusDisplay,
|
|
3521
|
-
/* @__PURE__ */
|
|
3522
|
-
/* @__PURE__ */
|
|
3523
|
-
selectedForDeletion.length > 0 && /* @__PURE__ */
|
|
3928
|
+
/* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", marginTop: 1, children: [
|
|
3929
|
+
/* @__PURE__ */ jsx13(Text13, { bold: true, children: "\u25B8 Select backups to delete" }),
|
|
3930
|
+
selectedForDeletion.length > 0 && /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
|
|
3524
3931
|
" ",
|
|
3525
3932
|
selectedForDeletion.length,
|
|
3526
3933
|
" backup(s) selected (",
|
|
3527
3934
|
formatBytes(selectedForDeletion.reduce((s, b) => s + b.size, 0)),
|
|
3528
3935
|
")"
|
|
3529
3936
|
] }),
|
|
3530
|
-
/* @__PURE__ */
|
|
3937
|
+
/* @__PURE__ */ jsx13(SelectInput3, { items: selectItems, onSelect: handleSelectBackup })
|
|
3531
3938
|
] }),
|
|
3532
3939
|
escHint("go back")
|
|
3533
3940
|
] });
|
|
3534
3941
|
}
|
|
3535
3942
|
if (phase === "confirming") {
|
|
3536
|
-
return /* @__PURE__ */
|
|
3537
|
-
/* @__PURE__ */
|
|
3538
|
-
/* @__PURE__ */
|
|
3943
|
+
return /* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", children: [
|
|
3944
|
+
/* @__PURE__ */ jsx13(Text13, { children: cleanupMessage }),
|
|
3945
|
+
/* @__PURE__ */ jsx13(
|
|
3539
3946
|
Confirm,
|
|
3540
3947
|
{
|
|
3541
3948
|
message: "Proceed?",
|
|
@@ -3546,24 +3953,24 @@ var StatusView = ({ cleanup }) => {
|
|
|
3546
3953
|
] });
|
|
3547
3954
|
}
|
|
3548
3955
|
if (phase === "done") {
|
|
3549
|
-
return /* @__PURE__ */
|
|
3956
|
+
return /* @__PURE__ */ jsx13(Box11, { flexDirection: "column", children: /* @__PURE__ */ jsx13(Text13, { color: "green", children: "\u2713 Cleanup complete" }) });
|
|
3550
3957
|
}
|
|
3551
3958
|
return null;
|
|
3552
3959
|
};
|
|
3553
3960
|
function registerStatusCommand(program2) {
|
|
3554
3961
|
program2.command("status").description(`Show ~/.${APP_NAME}/ status summary`).option("--cleanup", "Interactive cleanup mode", false).action(async (opts) => {
|
|
3555
|
-
const { waitUntilExit } =
|
|
3962
|
+
const { waitUntilExit } = render9(/* @__PURE__ */ jsx13(StatusView, { cleanup: opts.cleanup }));
|
|
3556
3963
|
await waitUntilExit();
|
|
3557
3964
|
});
|
|
3558
3965
|
}
|
|
3559
3966
|
|
|
3560
3967
|
// src/commands/Wizard.tsx
|
|
3561
|
-
import { copyFile as
|
|
3562
|
-
import { join as
|
|
3563
|
-
import { Box as
|
|
3564
|
-
import { render as
|
|
3968
|
+
import { copyFile as copyFile3, readFile as readFile6, rename, unlink, writeFile as writeFile5 } from "fs/promises";
|
|
3969
|
+
import { join as join14 } from "path";
|
|
3970
|
+
import { Box as Box12, Text as Text14, useApp as useApp9 } from "ink";
|
|
3971
|
+
import { render as render10 } from "ink";
|
|
3565
3972
|
import Spinner3 from "ink-spinner";
|
|
3566
|
-
import { useEffect as
|
|
3973
|
+
import { useEffect as useEffect9, useState as useState10 } from "react";
|
|
3567
3974
|
|
|
3568
3975
|
// src/prompts/wizard-config.ts
|
|
3569
3976
|
function generateConfigWizardPrompt(variables) {
|
|
@@ -3605,7 +4012,7 @@ ${variables.defaultConfig}
|
|
|
3605
4012
|
|
|
3606
4013
|
// src/utils/file-scanner.ts
|
|
3607
4014
|
import { stat as stat3 } from "fs/promises";
|
|
3608
|
-
import { join as
|
|
4015
|
+
import { join as join13 } from "path";
|
|
3609
4016
|
import glob from "fast-glob";
|
|
3610
4017
|
var FILE_CATEGORIES = {
|
|
3611
4018
|
shell: {
|
|
@@ -3684,7 +4091,7 @@ async function scanHomeDirectory(options) {
|
|
|
3684
4091
|
const validFiles = [];
|
|
3685
4092
|
for (const file of files) {
|
|
3686
4093
|
try {
|
|
3687
|
-
const fullPath =
|
|
4094
|
+
const fullPath = join13(homeDir, file);
|
|
3688
4095
|
await stat3(fullPath);
|
|
3689
4096
|
validFiles.push(file);
|
|
3690
4097
|
categorizedFiles.add(file);
|
|
@@ -3719,7 +4126,7 @@ async function scanHomeDirectory(options) {
|
|
|
3719
4126
|
if (categorizedFiles.has(file)) continue;
|
|
3720
4127
|
if (totalFiles >= maxFiles) break;
|
|
3721
4128
|
try {
|
|
3722
|
-
const fullPath =
|
|
4129
|
+
const fullPath = join13(homeDir, file);
|
|
3723
4130
|
await stat3(fullPath);
|
|
3724
4131
|
uncategorizedFiles.push(file);
|
|
3725
4132
|
totalFiles++;
|
|
@@ -3730,7 +4137,7 @@ async function scanHomeDirectory(options) {
|
|
|
3730
4137
|
if (uncategorizedFiles.length > 0) {
|
|
3731
4138
|
categories.push({
|
|
3732
4139
|
category: "Other Files",
|
|
3733
|
-
files: uncategorizedFiles.sort()
|
|
4140
|
+
files: uncategorizedFiles.sort()
|
|
3734
4141
|
});
|
|
3735
4142
|
}
|
|
3736
4143
|
} catch {
|
|
@@ -3744,12 +4151,12 @@ async function scanHomeDirectory(options) {
|
|
|
3744
4151
|
}
|
|
3745
4152
|
|
|
3746
4153
|
// src/commands/Wizard.tsx
|
|
3747
|
-
import { jsx as
|
|
4154
|
+
import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
3748
4155
|
var MAX_RETRIES2 = 3;
|
|
3749
4156
|
async function restoreBackup2(configPath) {
|
|
3750
4157
|
const bakPath = `${configPath}.bak`;
|
|
3751
4158
|
if (await fileExists(bakPath)) {
|
|
3752
|
-
await
|
|
4159
|
+
await copyFile3(bakPath, configPath);
|
|
3753
4160
|
}
|
|
3754
4161
|
}
|
|
3755
4162
|
async function runScanPhase() {
|
|
@@ -3775,7 +4182,7 @@ async function runValidationPhase(configPath) {
|
|
|
3775
4182
|
console.log("\u26A0\uFE0F Config file was not created. Restored backup.");
|
|
3776
4183
|
return;
|
|
3777
4184
|
}
|
|
3778
|
-
const content = await
|
|
4185
|
+
const content = await readFile6(configPath, "utf-8");
|
|
3779
4186
|
const parsed = parseYAML(content);
|
|
3780
4187
|
const validation = validateConfig(parsed);
|
|
3781
4188
|
if (!validation.valid) {
|
|
@@ -3794,24 +4201,22 @@ ${formatValidationErrors(validation.errors || [])}`
|
|
|
3794
4201
|
}
|
|
3795
4202
|
}
|
|
3796
4203
|
var WizardView = ({ printMode }) => {
|
|
3797
|
-
const { exit } =
|
|
3798
|
-
const [phase, setPhase] =
|
|
3799
|
-
const [message, setMessage] =
|
|
3800
|
-
const [error, setError] =
|
|
3801
|
-
const [prompt, setPrompt] =
|
|
3802
|
-
const [sessionId, setSessionId] =
|
|
3803
|
-
const [attemptNumber, setAttemptNumber] =
|
|
3804
|
-
|
|
4204
|
+
const { exit } = useApp9();
|
|
4205
|
+
const [phase, setPhase] = useState10("init");
|
|
4206
|
+
const [message, setMessage] = useState10("");
|
|
4207
|
+
const [error, setError] = useState10(null);
|
|
4208
|
+
const [prompt, setPrompt] = useState10("");
|
|
4209
|
+
const [sessionId, setSessionId] = useState10(void 0);
|
|
4210
|
+
const [attemptNumber, setAttemptNumber] = useState10(1);
|
|
4211
|
+
useEffect9(() => {
|
|
3805
4212
|
(async () => {
|
|
3806
4213
|
try {
|
|
3807
|
-
const configPath =
|
|
4214
|
+
const configPath = join14(getAppDir(), CONFIG_FILENAME);
|
|
3808
4215
|
if (await fileExists(configPath)) {
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
);
|
|
3813
|
-
await rename(configPath, `${configPath}.bak`);
|
|
3814
|
-
setMessage(`Backed up existing config to config.yml.bak`);
|
|
4216
|
+
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4217
|
+
const bakPath = `${configPath}.${timestamp2}.bak`;
|
|
4218
|
+
await rename(configPath, bakPath);
|
|
4219
|
+
setMessage(`Backed up existing config to ${bakPath}`);
|
|
3815
4220
|
}
|
|
3816
4221
|
setPhase("scanning");
|
|
3817
4222
|
setMessage("Scanning home directory for backup targets...");
|
|
@@ -3878,7 +4283,7 @@ Would you like to backup and overwrite? (Backup will be saved as config.yml.bak)
|
|
|
3878
4283
|
if (verification.valid) {
|
|
3879
4284
|
await rename(tmpPath, configPath);
|
|
3880
4285
|
} else {
|
|
3881
|
-
await
|
|
4286
|
+
await unlink(tmpPath);
|
|
3882
4287
|
throw new Error("Final validation failed");
|
|
3883
4288
|
}
|
|
3884
4289
|
setPhase("done");
|
|
@@ -3917,37 +4322,37 @@ ${formatValidationErrors(validation.errors || [])}`
|
|
|
3917
4322
|
}
|
|
3918
4323
|
}
|
|
3919
4324
|
if (error) {
|
|
3920
|
-
return /* @__PURE__ */
|
|
4325
|
+
return /* @__PURE__ */ jsx14(Box12, { flexDirection: "column", children: /* @__PURE__ */ jsxs14(Text14, { color: "red", children: [
|
|
3921
4326
|
"\u2717 ",
|
|
3922
4327
|
error
|
|
3923
4328
|
] }) });
|
|
3924
4329
|
}
|
|
3925
4330
|
if (printMode && phase === "done") {
|
|
3926
|
-
return /* @__PURE__ */
|
|
3927
|
-
/* @__PURE__ */
|
|
3928
|
-
/* @__PURE__ */
|
|
3929
|
-
/* @__PURE__ */
|
|
3930
|
-
/* @__PURE__ */
|
|
3931
|
-
/* @__PURE__ */
|
|
4331
|
+
return /* @__PURE__ */ jsxs14(Box12, { flexDirection: "column", children: [
|
|
4332
|
+
/* @__PURE__ */ jsx14(Text14, { bold: true, children: "Config Wizard Prompt (Copy and paste to your LLM):" }),
|
|
4333
|
+
/* @__PURE__ */ jsx14(Box12, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: "\u2500".repeat(60) }) }),
|
|
4334
|
+
/* @__PURE__ */ jsx14(Text14, { children: prompt }),
|
|
4335
|
+
/* @__PURE__ */ jsx14(Box12, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: "\u2500".repeat(60) }) }),
|
|
4336
|
+
/* @__PURE__ */ jsx14(Text14, { dimColor: true, children: "After getting the YAML response, save it to ~/.syncpoint/config.yml" })
|
|
3932
4337
|
] });
|
|
3933
4338
|
}
|
|
3934
4339
|
if (phase === "done") {
|
|
3935
|
-
return /* @__PURE__ */
|
|
3936
|
-
/* @__PURE__ */
|
|
3937
|
-
/* @__PURE__ */
|
|
3938
|
-
/* @__PURE__ */
|
|
3939
|
-
/* @__PURE__ */
|
|
3940
|
-
/* @__PURE__ */
|
|
4340
|
+
return /* @__PURE__ */ jsxs14(Box12, { flexDirection: "column", children: [
|
|
4341
|
+
/* @__PURE__ */ jsx14(Text14, { color: "green", children: message }),
|
|
4342
|
+
/* @__PURE__ */ jsxs14(Box12, { marginTop: 1, children: [
|
|
4343
|
+
/* @__PURE__ */ jsx14(Text14, { children: "Next steps:" }),
|
|
4344
|
+
/* @__PURE__ */ jsx14(Text14, { children: " 1. Review your config: ~/.syncpoint/config.yml" }),
|
|
4345
|
+
/* @__PURE__ */ jsx14(Text14, { children: " 2. Run: syncpoint backup" })
|
|
3941
4346
|
] })
|
|
3942
4347
|
] });
|
|
3943
4348
|
}
|
|
3944
|
-
return /* @__PURE__ */
|
|
3945
|
-
/* @__PURE__ */
|
|
3946
|
-
/* @__PURE__ */
|
|
4349
|
+
return /* @__PURE__ */ jsxs14(Box12, { flexDirection: "column", children: [
|
|
4350
|
+
/* @__PURE__ */ jsxs14(Text14, { children: [
|
|
4351
|
+
/* @__PURE__ */ jsx14(Text14, { color: "cyan", children: /* @__PURE__ */ jsx14(Spinner3, { type: "dots" }) }),
|
|
3947
4352
|
" ",
|
|
3948
4353
|
message
|
|
3949
4354
|
] }),
|
|
3950
|
-
attemptNumber > 1 && /* @__PURE__ */
|
|
4355
|
+
attemptNumber > 1 && /* @__PURE__ */ jsxs14(Text14, { dimColor: true, children: [
|
|
3951
4356
|
"Attempt ",
|
|
3952
4357
|
attemptNumber,
|
|
3953
4358
|
"/",
|
|
@@ -3963,15 +4368,17 @@ function registerWizardCommand(program2) {
|
|
|
3963
4368
|
});
|
|
3964
4369
|
cmd.action(async (opts) => {
|
|
3965
4370
|
if (opts.print) {
|
|
3966
|
-
const { waitUntilExit } =
|
|
4371
|
+
const { waitUntilExit } = render10(/* @__PURE__ */ jsx14(WizardView, { printMode: true }));
|
|
3967
4372
|
await waitUntilExit();
|
|
3968
4373
|
return;
|
|
3969
4374
|
}
|
|
3970
|
-
const configPath =
|
|
4375
|
+
const configPath = join14(getAppDir(), CONFIG_FILENAME);
|
|
3971
4376
|
try {
|
|
3972
4377
|
if (await fileExists(configPath)) {
|
|
3973
|
-
|
|
3974
|
-
|
|
4378
|
+
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4379
|
+
const bakPath = `${configPath}.${timestamp2}.bak`;
|
|
4380
|
+
console.log(`\u{1F4CB} Backing up existing config to ${bakPath}`);
|
|
4381
|
+
await rename(configPath, bakPath);
|
|
3975
4382
|
}
|
|
3976
4383
|
if (!await isClaudeCodeAvailable()) {
|
|
3977
4384
|
throw new Error(
|
|
@@ -4004,6 +4411,7 @@ registerRestoreCommand(program);
|
|
|
4004
4411
|
registerProvisionCommand(program);
|
|
4005
4412
|
registerCreateTemplateCommand(program);
|
|
4006
4413
|
registerListCommand(program);
|
|
4414
|
+
registerMigrateCommand(program);
|
|
4007
4415
|
registerStatusCommand(program);
|
|
4008
4416
|
registerHelpCommand(program);
|
|
4009
4417
|
program.parseAsync(process.argv).catch((error) => {
|