@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/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 = /\x1b\[[0-9;]*m/g;
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
- // src/schemas/ajv.ts
340
- import Ajv from "ajv";
341
- import addFormats from "ajv-formats";
342
- var ajv = new Ajv({ allErrors: true });
343
- addFormats(ajv);
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: { type: "string" },
369
- toolVersion: { type: "string" },
370
- createdAt: { type: "string" },
371
- hostname: { type: "string" },
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: { type: "string" },
377
- release: { type: "string" },
378
- arch: { type: "string" }
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: { type: "string" },
387
- destination: { type: "string" }
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: { type: "string" },
398
- absolutePath: { type: "string" },
399
- size: { type: "number", minimum: 0 },
400
- hash: { type: "string" },
401
- type: { type: "string" }
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: { type: "integer", minimum: 0 },
411
- totalSize: { type: "number", minimum: 0 }
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
- var validate2 = ajv.compile(metadataSchema);
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.3";
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
- hash = `sha256:${createHash("sha256").update(absolutePath).digest("hex")}`;
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") return false;
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.includeInBackup) {
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
- // src/schemas/config.schema.ts
766
- var configSchema = {
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: ["backup"],
846
+ required: [
847
+ "backup"
848
+ ],
769
849
  properties: {
770
850
  backup: {
771
851
  type: "object",
772
- required: ["targets", "exclude", "filename"],
852
+ description: "Backup configuration",
853
+ required: [
854
+ "targets",
855
+ "exclude",
856
+ "filename"
857
+ ],
773
858
  properties: {
774
859
  targets: {
775
860
  type: "array",
776
- items: { type: "string", validPattern: true }
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
- items: { type: "string", validPattern: true }
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
- var validate3 = ajv.compile(configSchema);
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
- const progressInterval = setInterval(() => {
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.metadata.config.filename
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 join9 } from "path";
1203
- import { writeFile as writeFile4 } from "fs/promises";
1326
+ import { join as join8 } from "path";
1327
+ import { writeFile as writeFile3 } from "fs/promises";
1204
1328
 
1205
- // src/schemas/template.schema.ts
1206
- var templateSchema = {
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: { type: "string", minLength: 1 },
1211
- description: { type: "string" },
1212
- backup: { type: "string" },
1213
- sudo: { type: "boolean" },
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: { type: "string", minLength: 1 },
1222
- description: { type: "string" },
1223
- command: { type: "string", minLength: 1 },
1224
- skip_if: { type: "string" },
1225
- continue_on_error: { type: "boolean" }
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
- var validate4 = ajv.compile(templateSchema);
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
- const promptFile = join8(tmpdir2(), `syncpoint-prompt-${Date.now()}.txt`);
1302
- await writeFile3(promptFile, prompt, "utf-8");
1303
- try {
1304
- return await new Promise((resolve2, reject) => {
1305
- const args = ["--permission-mode", "acceptEdits", "--model", "sonnet"];
1306
- if (options?.sessionId) {
1307
- args.push("--session", options.sessionId);
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
- } finally {
1348
- try {
1349
- await unlink(promptFile);
1350
- } catch {
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 = join9(templatesDir, filename);
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 writeFile4(templatePath, yamlContent, "utf-8");
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 join10 } from "path";
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(join10(appDir, CONFIG_FILENAME))) {
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 = join10(getSubDir(TEMPLATES_DIR), "example.yml");
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 join11 } from "path";
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 = join11(templatesDir, entry.name);
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 errorOutput = [error.stdout, error.stderr, error.message].filter(Boolean).join("\n").trim();
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 join12 } from "path";
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 = join12(backupDir, entry.name);
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 = join12(backupDir, filename);
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: tmpdir3 } = await import("os");
2225
- const tmpDir = await mkdtemp2(join12(tmpdir3(), "syncpoint-restore-"));
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 = join12(tmpDir, archiveName);
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/Provision.tsx
2790
+ // src/commands/Migrate.tsx
2637
2791
  import { useState as useState6, useEffect as useEffect5 } from "react";
2638
- import { Text as Text10, Box as Box8, useApp as useApp5 } from "ink";
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 Text9, Box as Box7 } from "ink";
3063
+ import { Text as Text10, Box as Box8 } from "ink";
2675
3064
  import Spinner2 from "ink-spinner";
2676
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
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__ */ jsx9(Text9, { color: "green", children: "\u2713" });
3069
+ return /* @__PURE__ */ jsx10(Text10, { color: "green", children: "\u2713" });
2681
3070
  case "running":
2682
- return /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: /* @__PURE__ */ jsx9(Spinner2, { type: "dots" }) });
3071
+ return /* @__PURE__ */ jsx10(Text10, { color: "yellow", children: /* @__PURE__ */ jsx10(Spinner2, { type: "dots" }) });
2683
3072
  case "skipped":
2684
- return /* @__PURE__ */ jsx9(Text9, { color: "blue", children: "\u23ED" });
3073
+ return /* @__PURE__ */ jsx10(Text10, { color: "blue", children: "\u23ED" });
2685
3074
  case "failed":
2686
- return /* @__PURE__ */ jsx9(Text9, { color: "red", children: "\u2717" });
3075
+ return /* @__PURE__ */ jsx10(Text10, { color: "red", children: "\u2717" });
2687
3076
  case "pending":
2688
3077
  default:
2689
- return /* @__PURE__ */ jsx9(Text9, { color: "gray", children: "\u25CB" });
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__ */ jsxs9(Text9, { color: "green", children: [
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__ */ jsx9(Text9, { color: "yellow", children: "Running..." });
3089
+ return /* @__PURE__ */ jsx10(Text10, { color: "yellow", children: "Running..." });
2701
3090
  case "skipped":
2702
- return /* @__PURE__ */ jsx9(Text9, { color: "blue", children: "Skipped (already installed)" });
3091
+ return /* @__PURE__ */ jsx10(Text10, { color: "blue", children: "Skipped (already installed)" });
2703
3092
  case "failed":
2704
- return /* @__PURE__ */ jsxs9(Text9, { color: "red", children: [
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__ */ jsx9(Box7, { flexDirection: "column", children: steps.map((step, idx) => /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", marginBottom: idx < steps.length - 1 ? 1 : 0, children: [
2718
- /* @__PURE__ */ jsxs9(Text9, { children: [
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__ */ jsx9(StepIcon, { status: step.status }),
2721
- /* @__PURE__ */ jsxs9(Text9, { children: [
3109
+ /* @__PURE__ */ jsx10(StepIcon, { status: step.status }),
3110
+ /* @__PURE__ */ jsxs10(Text10, { children: [
2722
3111
  " ",
2723
- /* @__PURE__ */ jsxs9(Text9, { bold: true, children: [
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__ */ jsxs9(Text9, { color: "gray", children: [
3122
+ step.output && step.status !== "pending" && /* @__PURE__ */ jsxs10(Text10, { color: "gray", children: [
2734
3123
  " ",
2735
3124
  step.output
2736
3125
  ] }),
2737
- /* @__PURE__ */ jsxs9(Text9, { children: [
3126
+ /* @__PURE__ */ jsxs10(Text10, { children: [
2738
3127
  " ",
2739
- /* @__PURE__ */ jsx9(StepStatusText, { step })
3128
+ /* @__PURE__ */ jsx10(StepStatusText, { step })
2740
3129
  ] })
2741
3130
  ] }, idx)) });
2742
3131
  };
2743
3132
 
2744
3133
  // src/commands/Provision.tsx
2745
- import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
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 } = useApp5();
2752
- const [phase, setPhase] = useState6(options.dryRun ? "done" : "running");
2753
- const [steps, setSteps] = useState6(
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] = useState6(0);
2761
- const [error, setError] = useState6(null);
2762
- useEffect5(() => {
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__ */ jsx10(Box8, { flexDirection: "column", children: /* @__PURE__ */ jsxs10(Text10, { color: "red", children: [
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__ */ jsxs10(Box8, { flexDirection: "column", children: [
2801
- /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", marginBottom: 1, children: [
2802
- /* @__PURE__ */ jsxs10(Text10, { bold: true, children: [
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__ */ jsxs10(Text10, { color: "gray", children: [
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__ */ jsxs10(Box8, { flexDirection: "column", children: [
2812
- /* @__PURE__ */ jsx10(Text10, { color: "yellow", children: "(dry-run) Showing execution plan only" }),
2813
- template.sudo && /* @__PURE__ */ jsxs10(Text10, { color: "yellow", children: [
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__ */ jsx10(Box8, { flexDirection: "column", marginTop: 1, children: template.steps.map((step, idx) => /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", marginBottom: 1, children: [
2818
- /* @__PURE__ */ jsxs10(Text10, { children: [
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__ */ jsxs10(Text10, { bold: true, children: [
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__ */ jsxs10(Text10, { color: "gray", children: [
3230
+ step.description && /* @__PURE__ */ jsxs11(Text11, { color: "gray", children: [
2830
3231
  " ",
2831
3232
  step.description
2832
3233
  ] }),
2833
- step.skip_if && /* @__PURE__ */ jsxs10(Text10, { color: "blue", children: [
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__ */ jsx10(
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__ */ jsxs10(Box8, { flexDirection: "column", marginTop: 1, children: [
2849
- /* @__PURE__ */ jsxs10(Text10, { color: "gray", children: [
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__ */ jsxs10(Text10, { children: [
3254
+ /* @__PURE__ */ jsxs11(Text11, { children: [
2854
3255
  " ",
2855
3256
  "Result: ",
2856
- /* @__PURE__ */ jsxs10(Text10, { color: "green", children: [
3257
+ /* @__PURE__ */ jsxs11(Text11, { color: "green", children: [
2857
3258
  successCount,
2858
3259
  " succeeded"
2859
3260
  ] }),
2860
3261
  " \xB7",
2861
3262
  " ",
2862
- /* @__PURE__ */ jsxs10(Text10, { color: "blue", children: [
3263
+ /* @__PURE__ */ jsxs11(Text11, { color: "blue", children: [
2863
3264
  skippedCount,
2864
3265
  " skipped"
2865
3266
  ] }),
2866
3267
  " \xB7",
2867
3268
  " ",
2868
- /* @__PURE__ */ jsxs10(Text10, { color: "red", children: [
3269
+ /* @__PURE__ */ jsxs11(Text11, { color: "red", children: [
2869
3270
  failedCount,
2870
3271
  " failed"
2871
3272
  ] })
2872
3273
  ] }),
2873
- template.backup && !options.skipRestore && /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", marginTop: 1, children: [
2874
- /* @__PURE__ */ jsx10(Text10, { bold: true, children: "\u25B8 Proceeding with config file restore..." }),
2875
- /* @__PURE__ */ jsxs10(Text10, { color: "gray", children: [
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 } = render6(
2919
- /* @__PURE__ */ jsx10(
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 useState7, useEffect as useEffect6 } from "react";
2938
- import { Text as Text11, Box as Box9, useApp as useApp6 } from "ink";
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 render7 } from "ink";
2941
- import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
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 } = useApp6();
2944
- const [phase, setPhase] = useState7("loading");
2945
- const [backups, setBackups] = useState7([]);
2946
- const [selectedPath, setSelectedPath] = useState7(null);
2947
- const [plan, setPlan] = useState7(null);
2948
- const [result, setResult] = useState7(null);
2949
- const [safetyDone, setSafetyDone] = useState7(false);
2950
- const [error, setError] = useState7(null);
2951
- useEffect6(() => {
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 list2 = await getBackupList();
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
- useEffect6(() => {
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__ */ jsx11(Box9, { flexDirection: "column", children: /* @__PURE__ */ jsxs11(Text11, { color: "red", children: [
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__ */ jsxs11(Box9, { flexDirection: "column", children: [
3044
- phase === "loading" && /* @__PURE__ */ jsx11(Text11, { children: "\u25B8 Loading backup list..." }),
3045
- phase === "selecting" && /* @__PURE__ */ jsxs11(Box9, { flexDirection: "column", children: [
3046
- /* @__PURE__ */ jsx11(Text11, { bold: true, children: "\u25B8 Select backup" }),
3047
- /* @__PURE__ */ jsx11(SelectInput2, { items: selectItems, onSelect: handleSelect })
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__ */ jsxs11(Box9, { flexDirection: "column", children: [
3050
- /* @__PURE__ */ jsxs11(Box9, { flexDirection: "column", marginBottom: 1, children: [
3051
- /* @__PURE__ */ jsxs11(Text11, { bold: true, children: [
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__ */ jsxs11(Text11, { children: [
3458
+ /* @__PURE__ */ jsxs12(Text12, { children: [
3057
3459
  " ",
3058
3460
  "Host: ",
3059
3461
  plan.metadata.hostname
3060
3462
  ] }),
3061
- /* @__PURE__ */ jsxs11(Text11, { children: [
3463
+ /* @__PURE__ */ jsxs12(Text12, { children: [
3062
3464
  " ",
3063
3465
  "Created: ",
3064
3466
  plan.metadata.createdAt
3065
3467
  ] }),
3066
- /* @__PURE__ */ jsxs11(Text11, { children: [
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__ */ jsxs11(Text11, { color: "yellow", children: [
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__ */ jsxs11(Box9, { flexDirection: "column", marginBottom: 1, children: [
3082
- /* @__PURE__ */ jsx11(Text11, { bold: true, children: "\u25B8 Restore plan:" }),
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__ */ jsxs11(Text11, { children: [
3506
+ return /* @__PURE__ */ jsxs12(Text12, { children: [
3105
3507
  " ",
3106
- /* @__PURE__ */ jsx11(Text11, { color, children: icon.padEnd(8) }),
3508
+ /* @__PURE__ */ jsx12(Text12, { color, children: icon.padEnd(8) }),
3107
3509
  " ",
3108
3510
  contractTilde(action.path),
3109
3511
  " ",
3110
- /* @__PURE__ */ jsx11(Text11, { color: "gray", children: label })
3512
+ /* @__PURE__ */ jsx12(Text12, { color: "gray", children: label })
3111
3513
  ] }, idx);
3112
3514
  })
3113
3515
  ] }),
3114
- options.dryRun && phase === "done" && /* @__PURE__ */ jsx11(Text11, { color: "yellow", children: "(dry-run) No actual restore was performed" })
3516
+ options.dryRun && phase === "done" && /* @__PURE__ */ jsx12(Text12, { color: "yellow", children: "(dry-run) No actual restore was performed" })
3115
3517
  ] }),
3116
- phase === "confirming" && /* @__PURE__ */ jsx11(Box9, { flexDirection: "column", children: /* @__PURE__ */ jsx11(Confirm, { message: "Proceed with restore?", onConfirm: handleConfirm }) }),
3117
- phase === "restoring" && /* @__PURE__ */ jsxs11(Box9, { flexDirection: "column", children: [
3118
- safetyDone && /* @__PURE__ */ jsxs11(Text11, { children: [
3119
- /* @__PURE__ */ jsx11(Text11, { color: "green", children: "\u2713" }),
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__ */ jsx11(Text11, { children: "\u25B8 Restoring..." })
3524
+ /* @__PURE__ */ jsx12(Text12, { children: "\u25B8 Restoring..." })
3123
3525
  ] }),
3124
- phase === "done" && result && !options.dryRun && /* @__PURE__ */ jsxs11(Box9, { flexDirection: "column", marginTop: 1, children: [
3125
- safetyDone && /* @__PURE__ */ jsxs11(Text11, { children: [
3126
- /* @__PURE__ */ jsx11(Text11, { color: "green", children: "\u2713" }),
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__ */ jsx11(Text11, { color: "green", bold: true, children: "\u2713 Restore complete" }),
3130
- /* @__PURE__ */ jsxs11(Text11, { children: [
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__ */ jsxs11(Text11, { children: [
3538
+ /* @__PURE__ */ jsxs12(Text12, { children: [
3137
3539
  " ",
3138
3540
  "Skipped: ",
3139
3541
  result.skippedFiles.length,
3140
3542
  " files"
3141
3543
  ] }),
3142
- result.safetyBackupPath && /* @__PURE__ */ jsxs11(Text11, { children: [
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 } = render7(
3153
- /* @__PURE__ */ jsx11(
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 join13 } from "path";
3168
- import { Box as Box10, Text as Text12, useApp as useApp7, useInput as useInput3 } from "ink";
3169
- import { render as render8 } from "ink";
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 useEffect7, useState as useState8 } from "react";
3172
- import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
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(join13(dirPath, entry));
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__ */ jsx12(Text12, { bold: isSelected, children: label });
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__ */ jsx12(Text12, { bold: isSelected, children: label });
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__ */ jsxs12(Text12, { bold: isSelected, children: [
3604
+ return /* @__PURE__ */ jsxs13(Text13, { bold: isSelected, children: [
3203
3605
  parts[0],
3204
3606
  " ",
3205
- /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: parts[1] })
3607
+ /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: parts[1] })
3206
3608
  ] });
3207
3609
  }
3208
- return /* @__PURE__ */ jsx12(Text12, { bold: isSelected, children: label });
3610
+ return /* @__PURE__ */ jsx13(Text13, { bold: isSelected, children: label });
3209
3611
  };
3210
3612
  var StatusView = ({ cleanup }) => {
3211
- const { exit } = useApp7();
3212
- const [phase, setPhase] = useState8("loading");
3213
- const [status, setStatus] = useState8(null);
3214
- const [backups, setBackups] = useState8([]);
3215
- const [cleanupAction, setCleanupAction] = useState8(null);
3216
- const [cleanupMessage, setCleanupMessage] = useState8("");
3217
- const [error, setError] = useState8(null);
3218
- const [selectedForDeletion, setSelectedForDeletion] = useState8(
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] = useState8(getSubDir("backups"));
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
- useEffect7(() => {
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 = join13(logsDir, entry);
3728
+ const logPath = join12(logsDir, entry);
3327
3729
  if (!isInsideDir(logPath, logsDir)) throw new Error(`Refusing to delete file outside logs directory: ${logPath}`);
3328
- unlinkSync2(logPath);
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__ */ jsx12(Box10, { flexDirection: "column", children: /* @__PURE__ */ jsxs12(Text12, { color: "red", children: [
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__ */ jsx12(Text12, { color: "cyan", children: "Loading..." });
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__ */ jsxs12(Box10, { flexDirection: "column", children: [
3377
- /* @__PURE__ */ jsxs12(Text12, { bold: true, children: [
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__ */ jsx12(Box10, { marginLeft: 2, marginTop: 1, children: /* @__PURE__ */ jsx12(
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__ */ jsxs12(Box10, { marginTop: 1, marginLeft: 2, flexDirection: "column", children: [
3414
- /* @__PURE__ */ jsxs12(Text12, { children: [
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__ */ jsxs12(Text12, { children: [
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__ */ jsx12(Box10, { marginTop: 1, children: /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
3837
+ const escHint = (action) => /* @__PURE__ */ jsx13(Box11, { marginTop: 1, children: /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
3431
3838
  "Press ",
3432
- /* @__PURE__ */ jsx12(Text12, { bold: true, children: "ESC" }),
3839
+ /* @__PURE__ */ jsx13(Text13, { bold: true, children: "ESC" }),
3433
3840
  " to ",
3434
3841
  action
3435
3842
  ] }) });
3436
3843
  if (phase === "display") {
3437
- return /* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", children: [
3844
+ return /* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", children: [
3438
3845
  statusDisplay,
3439
- /* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", marginTop: 1, children: [
3440
- /* @__PURE__ */ jsx12(Text12, { bold: true, children: "\u25B8 Actions" }),
3441
- /* @__PURE__ */ jsx12(
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__ */ jsxs12(Box10, { flexDirection: "column", children: [
3896
+ return /* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", children: [
3490
3897
  statusDisplay,
3491
- /* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", marginTop: 1, children: [
3492
- /* @__PURE__ */ jsx12(Text12, { bold: true, children: "\u25B8 Cleanup options" }),
3493
- /* @__PURE__ */ jsx12(
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__ */ jsxs12(Box10, { flexDirection: "column", children: [
3926
+ return /* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", children: [
3520
3927
  statusDisplay,
3521
- /* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", marginTop: 1, children: [
3522
- /* @__PURE__ */ jsx12(Text12, { bold: true, children: "\u25B8 Select backups to delete" }),
3523
- selectedForDeletion.length > 0 && /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
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__ */ jsx12(SelectInput3, { items: selectItems, onSelect: handleSelectBackup })
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__ */ jsxs12(Box10, { flexDirection: "column", children: [
3537
- /* @__PURE__ */ jsx12(Text12, { children: cleanupMessage }),
3538
- /* @__PURE__ */ jsx12(
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__ */ jsx12(Box10, { flexDirection: "column", children: /* @__PURE__ */ jsx12(Text12, { color: "green", children: "\u2713 Cleanup complete" }) });
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 } = render8(/* @__PURE__ */ jsx12(StatusView, { cleanup: opts.cleanup }));
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 copyFile2, readFile as readFile5, rename, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
3562
- import { join as join15 } from "path";
3563
- import { Box as Box11, Text as Text13, useApp as useApp8 } from "ink";
3564
- import { render as render9 } from "ink";
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 useEffect8, useState as useState9 } from "react";
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 join14 } from "path";
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 = join14(homeDir, file);
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 = join14(homeDir, file);
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().slice(0, maxFiles - totalFiles)
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 jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
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 copyFile2(bakPath, configPath);
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 readFile5(configPath, "utf-8");
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 } = useApp8();
3798
- const [phase, setPhase] = useState9("init");
3799
- const [message, setMessage] = useState9("");
3800
- const [error, setError] = useState9(null);
3801
- const [prompt, setPrompt] = useState9("");
3802
- const [sessionId, setSessionId] = useState9(void 0);
3803
- const [attemptNumber, setAttemptNumber] = useState9(1);
3804
- useEffect8(() => {
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 = join15(getAppDir(), CONFIG_FILENAME);
4214
+ const configPath = join14(getAppDir(), CONFIG_FILENAME);
3808
4215
  if (await fileExists(configPath)) {
3809
- setMessage(
3810
- `Config already exists: ${configPath}
3811
- Would you like to backup and overwrite? (Backup will be saved as config.yml.bak)`
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 unlink2(tmpPath);
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__ */ jsx13(Box11, { flexDirection: "column", children: /* @__PURE__ */ jsxs13(Text13, { color: "red", children: [
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__ */ jsxs13(Box11, { flexDirection: "column", children: [
3927
- /* @__PURE__ */ jsx13(Text13, { bold: true, children: "Config Wizard Prompt (Copy and paste to your LLM):" }),
3928
- /* @__PURE__ */ jsx13(Box11, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "\u2500".repeat(60) }) }),
3929
- /* @__PURE__ */ jsx13(Text13, { children: prompt }),
3930
- /* @__PURE__ */ jsx13(Box11, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "\u2500".repeat(60) }) }),
3931
- /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "After getting the YAML response, save it to ~/.syncpoint/config.yml" })
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__ */ jsxs13(Box11, { flexDirection: "column", children: [
3936
- /* @__PURE__ */ jsx13(Text13, { color: "green", children: message }),
3937
- /* @__PURE__ */ jsxs13(Box11, { marginTop: 1, children: [
3938
- /* @__PURE__ */ jsx13(Text13, { children: "Next steps:" }),
3939
- /* @__PURE__ */ jsx13(Text13, { children: " 1. Review your config: ~/.syncpoint/config.yml" }),
3940
- /* @__PURE__ */ jsx13(Text13, { children: " 2. Run: syncpoint backup" })
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__ */ jsxs13(Box11, { flexDirection: "column", children: [
3945
- /* @__PURE__ */ jsxs13(Text13, { children: [
3946
- /* @__PURE__ */ jsx13(Text13, { color: "cyan", children: /* @__PURE__ */ jsx13(Spinner3, { type: "dots" }) }),
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__ */ jsxs13(Text13, { dimColor: true, children: [
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 } = render9(/* @__PURE__ */ jsx13(WizardView, { printMode: true }));
4371
+ const { waitUntilExit } = render10(/* @__PURE__ */ jsx14(WizardView, { printMode: true }));
3967
4372
  await waitUntilExit();
3968
4373
  return;
3969
4374
  }
3970
- const configPath = join15(getAppDir(), CONFIG_FILENAME);
4375
+ const configPath = join14(getAppDir(), CONFIG_FILENAME);
3971
4376
  try {
3972
4377
  if (await fileExists(configPath)) {
3973
- console.log(`\u{1F4CB} Backing up existing config to ${configPath}.bak`);
3974
- await rename(configPath, `${configPath}.bak`);
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) => {