@lumy-pack/syncpoint 0.0.9 → 0.0.11

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
@@ -1,4 +1,120 @@
1
1
  #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/utils/assets.ts
13
+ var assets_exports = {};
14
+ __export(assets_exports, {
15
+ getAssetPath: () => getAssetPath,
16
+ readAsset: () => readAsset
17
+ });
18
+ import { existsSync, readFileSync } from "fs";
19
+ import { dirname, join as join6 } from "path";
20
+ import { fileURLToPath } from "url";
21
+ function getPackageRoot() {
22
+ let dir = dirname(fileURLToPath(import.meta.url));
23
+ while (dir !== dirname(dir)) {
24
+ if (existsSync(join6(dir, "package.json"))) return dir;
25
+ dir = dirname(dir);
26
+ }
27
+ throw new Error("Could not find package root");
28
+ }
29
+ function getAssetPath(filename) {
30
+ return join6(getPackageRoot(), "assets", filename);
31
+ }
32
+ function readAsset(filename) {
33
+ return readFileSync(getAssetPath(filename), "utf-8");
34
+ }
35
+ var init_assets = __esm({
36
+ "src/utils/assets.ts"() {
37
+ "use strict";
38
+ }
39
+ });
40
+
41
+ // src/prompts/wizard-template.ts
42
+ var wizard_template_exports = {};
43
+ __export(wizard_template_exports, {
44
+ generateTemplateWizardPrompt: () => generateTemplateWizardPrompt
45
+ });
46
+ function generateTemplateWizardPrompt(variables) {
47
+ return `You are a Syncpoint provisioning template assistant. Your role is to help users create automated environment setup templates.
48
+
49
+ **Input:**
50
+ 1. User's provisioning requirements (described in natural language)
51
+ 2. Example template structure (YAML)
52
+
53
+ **Your Task:**
54
+ 1. Ask clarifying questions to understand the provisioning workflow:
55
+ - What software/tools need to be installed?
56
+ - What dependencies should be checked?
57
+ - Are there any configuration steps after installation?
58
+ - Should any steps require sudo privileges?
59
+ - Should any steps be conditional (skip_if)?
60
+ 2. Based on user responses, generate a complete provision template
61
+
62
+ **Output Requirements:**
63
+ - Pure YAML format only (no markdown, no code blocks, no explanations)
64
+ - Must be valid according to Syncpoint template schema
65
+ - Required fields:
66
+ - \`name\`: Template name
67
+ - \`steps\`: Array of provisioning steps (minimum 1)
68
+ - Each step must include:
69
+ - \`name\`: Step name (required)
70
+ - \`command\`: Shell command to execute (required)
71
+ - \`description\`: Step description (optional)
72
+ - \`skip_if\`: Condition to skip step (optional)
73
+ - \`continue_on_error\`: Whether to continue on failure (optional, default: false)
74
+ - Optional template fields:
75
+ - \`description\`: Template description
76
+ - \`backup\`: Backup name to restore after provisioning
77
+ - \`sudo\`: Whether sudo is required (boolean)
78
+
79
+ **Example Template:**
80
+ ${variables.exampleTemplate}
81
+
82
+ Begin by asking the user to describe their provisioning needs.`;
83
+ }
84
+ var init_wizard_template = __esm({
85
+ "src/prompts/wizard-template.ts"() {
86
+ "use strict";
87
+ }
88
+ });
89
+
90
+ // ../shared/src/respond.ts
91
+ function respond(command, data, startTime, version) {
92
+ const response = {
93
+ ok: true,
94
+ command,
95
+ data,
96
+ meta: {
97
+ version,
98
+ durationMs: Date.now() - startTime,
99
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
100
+ }
101
+ };
102
+ process.stdout.write(JSON.stringify(response) + "\n");
103
+ }
104
+ function respondError(command, code, message, startTime, version, details) {
105
+ const response = {
106
+ ok: false,
107
+ command,
108
+ error: { code, message, ...details !== void 0 ? { details } : {} },
109
+ meta: {
110
+ version,
111
+ durationMs: Date.now() - startTime,
112
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
113
+ }
114
+ };
115
+ process.stdout.write(JSON.stringify(response) + "\n");
116
+ process.exitCode = 1;
117
+ }
2
118
 
3
119
  // src/cli.ts
4
120
  import { Command } from "commander";
@@ -486,7 +602,7 @@ function validateMetadata(data) {
486
602
  }
487
603
 
488
604
  // src/version.ts
489
- var VERSION = "0.0.8";
605
+ var VERSION = "0.0.11";
490
606
 
491
607
  // src/core/metadata.ts
492
608
  var METADATA_VERSION = "1.0.0";
@@ -912,26 +1028,8 @@ function validateConfig(data) {
912
1028
  return { valid: false, errors };
913
1029
  }
914
1030
 
915
- // src/utils/assets.ts
916
- import { existsSync, readFileSync } from "fs";
917
- import { dirname, join as join6 } from "path";
918
- import { fileURLToPath } from "url";
919
- function getPackageRoot() {
920
- let dir = dirname(fileURLToPath(import.meta.url));
921
- while (dir !== dirname(dir)) {
922
- if (existsSync(join6(dir, "package.json"))) return dir;
923
- dir = dirname(dir);
924
- }
925
- throw new Error("Could not find package root");
926
- }
927
- function getAssetPath(filename) {
928
- return join6(getPackageRoot(), "assets", filename);
929
- }
930
- function readAsset(filename) {
931
- return readFileSync(getAssetPath(filename), "utf-8");
932
- }
933
-
934
1031
  // src/core/config.ts
1032
+ init_assets();
935
1033
  function stripDangerousKeys(obj) {
936
1034
  if (obj === null || typeof obj !== "object") return obj;
937
1035
  if (Array.isArray(obj)) return obj.map(stripDangerousKeys);
@@ -994,6 +1092,43 @@ async function initDefaultConfig() {
994
1092
  return { created, skipped };
995
1093
  }
996
1094
 
1095
+ // src/errors.ts
1096
+ var SyncpointErrorCode = {
1097
+ CONFIG_NOT_FOUND: "CONFIG_NOT_FOUND",
1098
+ CONFIG_INVALID: "CONFIG_INVALID",
1099
+ BACKUP_FAILED: "BACKUP_FAILED",
1100
+ RESTORE_FAILED: "RESTORE_FAILED",
1101
+ TEMPLATE_NOT_FOUND: "TEMPLATE_NOT_FOUND",
1102
+ PROVISION_FAILED: "PROVISION_FAILED",
1103
+ MISSING_ARGUMENT: "MISSING_ARGUMENT",
1104
+ INVALID_ARGUMENT: "INVALID_ARGUMENT",
1105
+ LINK_FAILED: "LINK_FAILED",
1106
+ UNLINK_FAILED: "UNLINK_FAILED",
1107
+ UNKNOWN: "UNKNOWN"
1108
+ };
1109
+ function classifyError(err) {
1110
+ const msg = err instanceof Error ? err.message : String(err);
1111
+ if (msg.includes("Config file not found") || msg.includes('Run "syncpoint init"')) {
1112
+ return SyncpointErrorCode.CONFIG_NOT_FOUND;
1113
+ }
1114
+ if (msg.includes("Invalid config")) {
1115
+ return SyncpointErrorCode.CONFIG_INVALID;
1116
+ }
1117
+ if (msg.includes("Template not found") || msg.includes("template not found")) {
1118
+ return SyncpointErrorCode.TEMPLATE_NOT_FOUND;
1119
+ }
1120
+ if (msg.includes("Template file not found")) {
1121
+ return SyncpointErrorCode.TEMPLATE_NOT_FOUND;
1122
+ }
1123
+ if (msg.includes("not a symlink") || msg.includes('"syncpoint link"')) {
1124
+ return SyncpointErrorCode.UNLINK_FAILED;
1125
+ }
1126
+ if (msg.includes("destination is not set") || msg.includes("cross-device") || msg.includes("EXDEV")) {
1127
+ return SyncpointErrorCode.LINK_FAILED;
1128
+ }
1129
+ return SyncpointErrorCode.UNKNOWN;
1130
+ }
1131
+
997
1132
  // src/utils/command-registry.ts
998
1133
  var COMMANDS = {
999
1134
  init: {
@@ -1009,7 +1144,8 @@ var COMMANDS = {
1009
1144
  options: [
1010
1145
  {
1011
1146
  flag: "-p, --print",
1012
- description: "Print prompt instead of invoking Claude Code"
1147
+ description: "Print prompt instead of invoking Claude Code",
1148
+ type: "boolean"
1013
1149
  }
1014
1150
  ],
1015
1151
  examples: [
@@ -1024,15 +1160,18 @@ var COMMANDS = {
1024
1160
  options: [
1025
1161
  {
1026
1162
  flag: "--dry-run",
1027
- description: "Preview files to be backed up without creating archive"
1163
+ description: "Preview files to be backed up without creating archive",
1164
+ type: "boolean"
1028
1165
  },
1029
1166
  {
1030
1167
  flag: "--tag <name>",
1031
- description: "Add custom tag to backup filename"
1168
+ description: "Add custom tag to backup filename",
1169
+ type: "string"
1032
1170
  },
1033
1171
  {
1034
1172
  flag: "-v, --verbose",
1035
- description: "Show detailed output including missing files"
1173
+ description: "Show detailed output including missing files",
1174
+ type: "boolean"
1036
1175
  }
1037
1176
  ],
1038
1177
  examples: [
@@ -1055,7 +1194,8 @@ var COMMANDS = {
1055
1194
  options: [
1056
1195
  {
1057
1196
  flag: "--dry-run",
1058
- description: "Show restore plan without actually restoring"
1197
+ description: "Show restore plan without actually restoring",
1198
+ type: "boolean"
1059
1199
  }
1060
1200
  ],
1061
1201
  examples: [
@@ -1078,15 +1218,18 @@ var COMMANDS = {
1078
1218
  options: [
1079
1219
  {
1080
1220
  flag: "-f, --file <path>",
1081
- description: "Path to template file (alternative to template name)"
1221
+ description: "Path to template file (alternative to template name)",
1222
+ type: "string"
1082
1223
  },
1083
1224
  {
1084
1225
  flag: "--dry-run",
1085
- description: "Show execution plan without running commands"
1226
+ description: "Show execution plan without running commands",
1227
+ type: "boolean"
1086
1228
  },
1087
1229
  {
1088
1230
  flag: "--skip-restore",
1089
- description: "Skip automatic config restore after provisioning"
1231
+ description: "Skip automatic config restore after provisioning",
1232
+ type: "boolean"
1090
1233
  }
1091
1234
  ],
1092
1235
  examples: [
@@ -1111,7 +1254,8 @@ var COMMANDS = {
1111
1254
  options: [
1112
1255
  {
1113
1256
  flag: "-p, --print",
1114
- description: "Print prompt instead of invoking Claude Code"
1257
+ description: "Print prompt instead of invoking Claude Code",
1258
+ type: "boolean"
1115
1259
  }
1116
1260
  ],
1117
1261
  examples: [
@@ -1131,7 +1275,13 @@ var COMMANDS = {
1131
1275
  required: false
1132
1276
  }
1133
1277
  ],
1134
- options: [{ flag: "--delete <n>", description: "Delete item number n" }],
1278
+ options: [
1279
+ {
1280
+ flag: "--delete <filename>",
1281
+ description: "Delete item by filename",
1282
+ type: "string"
1283
+ }
1284
+ ],
1135
1285
  examples: [
1136
1286
  "npx @lumy-pack/syncpoint list",
1137
1287
  "npx @lumy-pack/syncpoint list backups",
@@ -1143,7 +1293,11 @@ var COMMANDS = {
1143
1293
  description: "Show ~/.syncpoint/ status summary and manage cleanup",
1144
1294
  usage: "npx @lumy-pack/syncpoint status [options]",
1145
1295
  options: [
1146
- { flag: "--cleanup", description: "Enter interactive cleanup mode" }
1296
+ {
1297
+ flag: "--cleanup",
1298
+ description: "Enter interactive cleanup mode",
1299
+ type: "boolean"
1300
+ }
1147
1301
  ],
1148
1302
  examples: [
1149
1303
  "npx @lumy-pack/syncpoint status",
@@ -1157,7 +1311,8 @@ var COMMANDS = {
1157
1311
  options: [
1158
1312
  {
1159
1313
  flag: "--dry-run",
1160
- description: "Preview changes without writing"
1314
+ description: "Preview changes without writing",
1315
+ type: "boolean"
1161
1316
  }
1162
1317
  ],
1163
1318
  examples: [
@@ -1165,6 +1320,38 @@ var COMMANDS = {
1165
1320
  "npx @lumy-pack/syncpoint migrate --dry-run"
1166
1321
  ]
1167
1322
  },
1323
+ link: {
1324
+ name: "link",
1325
+ description: "Move ~/.syncpoint to backup destination and create a symlink",
1326
+ usage: "npx @lumy-pack/syncpoint link [options]",
1327
+ options: [
1328
+ {
1329
+ flag: "-r, --ref <path>",
1330
+ description: "Adopt <path>/.syncpoint as ~/.syncpoint via symlink",
1331
+ type: "string"
1332
+ }
1333
+ ],
1334
+ examples: [
1335
+ "npx @lumy-pack/syncpoint link",
1336
+ "npx @lumy-pack/syncpoint link --ref ~/Dropbox"
1337
+ ]
1338
+ },
1339
+ unlink: {
1340
+ name: "unlink",
1341
+ description: "Remove the symlink and restore ~/.syncpoint from the destination copy",
1342
+ usage: "npx @lumy-pack/syncpoint unlink [options]",
1343
+ options: [
1344
+ {
1345
+ flag: "--clean",
1346
+ description: "Also remove the destination copy after restoring",
1347
+ type: "boolean"
1348
+ }
1349
+ ],
1350
+ examples: [
1351
+ "npx @lumy-pack/syncpoint unlink",
1352
+ "npx @lumy-pack/syncpoint unlink --clean"
1353
+ ]
1354
+ },
1168
1355
  help: {
1169
1356
  name: "help",
1170
1357
  description: "Display help information",
@@ -1334,6 +1521,33 @@ function registerBackupCommand(program2) {
1334
1521
  });
1335
1522
  cmd.action(
1336
1523
  async (opts) => {
1524
+ const globalOpts = program2.opts();
1525
+ const startTime = Date.now();
1526
+ if (globalOpts.json) {
1527
+ try {
1528
+ const config = await loadConfig();
1529
+ const result = await createBackup(config, {
1530
+ dryRun: opts.dryRun,
1531
+ tag: opts.tag,
1532
+ verbose: opts.verbose
1533
+ });
1534
+ respond(
1535
+ "backup",
1536
+ {
1537
+ archivePath: result.archivePath,
1538
+ fileCount: result.metadata.summary.fileCount,
1539
+ totalSize: result.metadata.summary.totalSize,
1540
+ tag: opts.tag ?? null
1541
+ },
1542
+ startTime,
1543
+ VERSION
1544
+ );
1545
+ } catch (error) {
1546
+ const code = classifyError(error);
1547
+ respondError("backup", code, error.message, startTime, VERSION);
1548
+ }
1549
+ return;
1550
+ }
1337
1551
  const { waitUntilExit } = render(
1338
1552
  /* @__PURE__ */ jsx2(
1339
1553
  BackupView,
@@ -1358,46 +1572,7 @@ import { Box as Box2, Text as Text3, useApp as useApp2 } from "ink";
1358
1572
  import { render as render2 } from "ink";
1359
1573
  import Spinner from "ink-spinner";
1360
1574
  import { useEffect as useEffect2, useState as useState2 } from "react";
1361
-
1362
- // src/prompts/wizard-template.ts
1363
- function generateTemplateWizardPrompt(variables) {
1364
- return `You are a Syncpoint provisioning template assistant. Your role is to help users create automated environment setup templates.
1365
-
1366
- **Input:**
1367
- 1. User's provisioning requirements (described in natural language)
1368
- 2. Example template structure (YAML)
1369
-
1370
- **Your Task:**
1371
- 1. Ask clarifying questions to understand the provisioning workflow:
1372
- - What software/tools need to be installed?
1373
- - What dependencies should be checked?
1374
- - Are there any configuration steps after installation?
1375
- - Should any steps require sudo privileges?
1376
- - Should any steps be conditional (skip_if)?
1377
- 2. Based on user responses, generate a complete provision template
1378
-
1379
- **Output Requirements:**
1380
- - Pure YAML format only (no markdown, no code blocks, no explanations)
1381
- - Must be valid according to Syncpoint template schema
1382
- - Required fields:
1383
- - \`name\`: Template name
1384
- - \`steps\`: Array of provisioning steps (minimum 1)
1385
- - Each step must include:
1386
- - \`name\`: Step name (required)
1387
- - \`command\`: Shell command to execute (required)
1388
- - \`description\`: Step description (optional)
1389
- - \`skip_if\`: Condition to skip step (optional)
1390
- - \`continue_on_error\`: Whether to continue on failure (optional, default: false)
1391
- - Optional template fields:
1392
- - \`description\`: Template description
1393
- - \`backup\`: Backup name to restore after provisioning
1394
- - \`sudo\`: Whether sudo is required (boolean)
1395
-
1396
- **Example Template:**
1397
- ${variables.exampleTemplate}
1398
-
1399
- Begin by asking the user to describe their provisioning needs.`;
1400
- }
1575
+ init_wizard_template();
1401
1576
 
1402
1577
  // assets/schemas/template.schema.json
1403
1578
  var template_schema_default = {
@@ -1473,22 +1648,25 @@ function validateTemplate(data) {
1473
1648
  return { valid: false, errors };
1474
1649
  }
1475
1650
 
1651
+ // src/commands/CreateTemplate.tsx
1652
+ init_assets();
1653
+
1476
1654
  // src/utils/claude-code-runner.ts
1477
1655
  import { spawn } from "child_process";
1478
1656
  async function isClaudeCodeAvailable() {
1479
- return new Promise((resolve2) => {
1657
+ return new Promise((resolve3) => {
1480
1658
  const child = spawn("which", ["claude"], { shell: true });
1481
1659
  child.on("close", (code) => {
1482
- resolve2(code === 0);
1660
+ resolve3(code === 0);
1483
1661
  });
1484
1662
  child.on("error", () => {
1485
- resolve2(false);
1663
+ resolve3(false);
1486
1664
  });
1487
1665
  });
1488
1666
  }
1489
1667
  async function invokeClaudeCode(prompt, options) {
1490
1668
  const timeout = options?.timeout ?? 12e4;
1491
- return await new Promise((resolve2, reject) => {
1669
+ return await new Promise((resolve3, reject) => {
1492
1670
  const args = ["--permission-mode", "acceptEdits", "--model", "sonnet"];
1493
1671
  if (options?.sessionId) {
1494
1672
  args.push("--session", options.sessionId);
@@ -1513,13 +1691,13 @@ async function invokeClaudeCode(prompt, options) {
1513
1691
  child.on("close", (code) => {
1514
1692
  clearTimeout(timer);
1515
1693
  if (code === 0) {
1516
- resolve2({
1694
+ resolve3({
1517
1695
  success: true,
1518
1696
  output: stdout,
1519
1697
  sessionId: options?.sessionId
1520
1698
  });
1521
1699
  } else {
1522
- resolve2({
1700
+ resolve3({
1523
1701
  success: false,
1524
1702
  output: stdout,
1525
1703
  error: stderr || `Process exited with code ${code}`
@@ -1539,7 +1717,7 @@ async function resumeClaudeCodeSession(sessionId, prompt, options) {
1539
1717
  });
1540
1718
  }
1541
1719
  async function invokeClaudeCodeInteractive(prompt) {
1542
- return await new Promise((resolve2, reject) => {
1720
+ return await new Promise((resolve3, reject) => {
1543
1721
  const initialMessage = `${prompt}
1544
1722
 
1545
1723
  IMPORTANT INSTRUCTIONS:
@@ -1563,7 +1741,7 @@ Start by asking the user about their backup priorities for the home directory st
1563
1741
  // Share stdin/stdout/stderr with parent process
1564
1742
  });
1565
1743
  child.on("close", (code) => {
1566
- resolve2({
1744
+ resolve3({
1567
1745
  success: code === 0,
1568
1746
  output: ""
1569
1747
  // No captured output in interactive mode
@@ -1797,6 +1975,30 @@ ${formatValidationErrors(validation.errors || [])}`
1797
1975
  };
1798
1976
  function registerCreateTemplateCommand(program2) {
1799
1977
  program2.command("create-template [name]").description("Interactive wizard to create a provisioning template").option("-p, --print", "Print prompt instead of invoking Claude Code").action(async (name, opts) => {
1978
+ const globalOpts = program2.opts();
1979
+ const startTime = Date.now();
1980
+ if (globalOpts.json) {
1981
+ if (!opts.print) {
1982
+ respondError(
1983
+ "create-template",
1984
+ SyncpointErrorCode.MISSING_ARGUMENT,
1985
+ "--print is required in --json mode (interactive mode requires a terminal)",
1986
+ startTime,
1987
+ VERSION
1988
+ );
1989
+ return;
1990
+ }
1991
+ try {
1992
+ const { generateTemplateWizardPrompt: generateTemplateWizardPrompt2 } = await Promise.resolve().then(() => (init_wizard_template(), wizard_template_exports));
1993
+ const { readAsset: readAsset2 } = await Promise.resolve().then(() => (init_assets(), assets_exports));
1994
+ const exampleTemplate = readAsset2("template.example.yml");
1995
+ const prompt = generateTemplateWizardPrompt2({ exampleTemplate });
1996
+ respond("create-template", { prompt }, startTime, VERSION);
1997
+ } catch (err) {
1998
+ respondError("create-template", SyncpointErrorCode.UNKNOWN, err.message, startTime, VERSION);
1999
+ }
2000
+ return;
2001
+ }
1800
2002
  const { waitUntilExit } = render2(
1801
2003
  /* @__PURE__ */ jsx3(
1802
2004
  CreateTemplateView,
@@ -1949,6 +2151,21 @@ var HelpView = ({ commandName }) => {
1949
2151
  };
1950
2152
  function registerHelpCommand(program2) {
1951
2153
  program2.command("help [command]").description("Display help information").action(async (commandName) => {
2154
+ const globalOpts = program2.opts();
2155
+ const startTime = Date.now();
2156
+ if (globalOpts.json) {
2157
+ if (commandName) {
2158
+ const commandInfo = COMMANDS[commandName];
2159
+ if (!commandInfo) {
2160
+ respond("help", { error: `Unknown command: ${commandName}`, commands: Object.keys(COMMANDS) }, startTime, VERSION);
2161
+ } else {
2162
+ respond("help", { command: commandInfo }, startTime, VERSION);
2163
+ }
2164
+ } else {
2165
+ respond("help", { commands: COMMANDS }, startTime, VERSION);
2166
+ }
2167
+ return;
2168
+ }
1952
2169
  const { waitUntilExit } = render3(/* @__PURE__ */ jsx4(HelpView, { commandName }));
1953
2170
  await waitUntilExit();
1954
2171
  });
@@ -1959,6 +2176,7 @@ import { join as join9 } from "path";
1959
2176
  import { Box as Box4, Text as Text5, useApp as useApp3 } from "ink";
1960
2177
  import { render as render4 } from "ink";
1961
2178
  import { useEffect as useEffect3, useState as useState3 } from "react";
2179
+ init_assets();
1962
2180
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1963
2181
  var InitView = () => {
1964
2182
  const { exit } = useApp3();
@@ -2062,16 +2280,27 @@ function registerInitCommand(program2) {
2062
2280
  program2.command("init").description(
2063
2281
  `Initialize ~/.${APP_NAME}/ directory structure and default config`
2064
2282
  ).action(async () => {
2283
+ const globalOpts = program2.opts();
2284
+ const startTime = Date.now();
2285
+ if (globalOpts.json) {
2286
+ try {
2287
+ const result = await initDefaultConfig();
2288
+ respond("init", { created: result.created, skipped: result.skipped }, startTime, VERSION);
2289
+ } catch (error) {
2290
+ const code = classifyError(error);
2291
+ respondError("init", code, error.message, startTime, VERSION);
2292
+ }
2293
+ return;
2294
+ }
2065
2295
  const { waitUntilExit } = render4(/* @__PURE__ */ jsx5(InitView, {}));
2066
2296
  await waitUntilExit();
2067
2297
  });
2068
2298
  }
2069
2299
 
2070
- // src/commands/List.tsx
2071
- import { unlinkSync } from "fs";
2072
- import { Box as Box6, Text as Text8, useApp as useApp4, useInput as useInput2 } from "ink";
2300
+ // src/commands/Link.tsx
2301
+ import { lstat as lstat3 } from "fs/promises";
2302
+ import { Box as Box5, Text as Text7, useApp as useApp4 } from "ink";
2073
2303
  import { render as render5 } from "ink";
2074
- import SelectInput from "ink-select-input";
2075
2304
  import { useEffect as useEffect4, useState as useState5 } from "react";
2076
2305
 
2077
2306
  // src/components/Confirm.tsx
@@ -2111,70 +2340,359 @@ var Confirm = ({
2111
2340
  ] });
2112
2341
  };
2113
2342
 
2114
- // src/components/Table.tsx
2115
- import { Box as Box5, Text as Text7 } from "ink";
2116
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2117
- var Table = ({
2118
- headers,
2119
- rows,
2120
- columnWidths
2121
- }) => {
2122
- const widths = columnWidths ?? headers.map((header, colIdx) => {
2123
- const dataMax = rows.reduce(
2124
- (max, row) => Math.max(max, (row[colIdx] ?? "").length),
2125
- 0
2126
- );
2127
- return Math.max(header.length, dataMax) + 2;
2128
- });
2129
- const padCell = (text, width) => {
2130
- return text.padEnd(width);
2131
- };
2132
- const separator = widths.map((w) => "\u2500".repeat(w)).join(" ");
2133
- return /* @__PURE__ */ jsxs7(Box5, { flexDirection: "column", children: [
2134
- /* @__PURE__ */ jsx7(Text7, { children: headers.map((h, i) => /* @__PURE__ */ jsxs7(Text7, { bold: true, children: [
2135
- padCell(h, widths[i]),
2136
- i < headers.length - 1 ? " " : ""
2137
- ] }, i)) }),
2138
- /* @__PURE__ */ jsx7(Text7, { color: "gray", children: separator }),
2139
- rows.map((row, rowIdx) => /* @__PURE__ */ jsx7(Text7, { children: row.map((cell, colIdx) => /* @__PURE__ */ jsxs7(Text7, { children: [
2140
- padCell(cell, widths[colIdx]),
2141
- colIdx < row.length - 1 ? " " : ""
2142
- ] }, colIdx)) }, rowIdx))
2143
- ] });
2144
- };
2145
-
2146
- // src/core/provision.ts
2147
- import { exec } from "child_process";
2148
- import { readFile as readFile4, readdir as readdir2 } from "fs/promises";
2149
- import { join as join10 } from "path";
2150
- import YAML3 from "yaml";
2151
- var REMOTE_SCRIPT_PATTERNS = [
2152
- /curl\s.*\|\s*(ba)?sh/,
2153
- /wget\s.*\|\s*(ba)?sh/,
2154
- /curl\s.*\|\s*python/,
2155
- /wget\s.*\|\s*python/
2156
- ];
2157
- function containsRemoteScriptPattern(command) {
2158
- return REMOTE_SCRIPT_PATTERNS.some((p) => p.test(command));
2159
- }
2160
- function sanitizeErrorOutput(output) {
2161
- return output.replace(/\/Users\/[^\s/]+/g, "/Users/***").replace(/\/home\/[^\s/]+/g, "/home/***").replace(/(password|token|key|secret)[=:]\s*\S+/gi, "$1=***").slice(0, 500);
2162
- }
2163
- async function loadTemplate(templatePath) {
2164
- const exists = await fileExists(templatePath);
2165
- if (!exists) {
2166
- throw new Error(`Template not found: ${templatePath}`);
2343
+ // src/core/link.ts
2344
+ import {
2345
+ cp,
2346
+ lstat as lstat2,
2347
+ readlink as readlink2,
2348
+ rename,
2349
+ rm as rm2,
2350
+ stat as stat2,
2351
+ symlink,
2352
+ unlink
2353
+ } from "fs/promises";
2354
+ import { basename as basename2, join as join10, resolve as resolve2 } from "path";
2355
+ async function linkSyncpoint(destination, _options = {}) {
2356
+ const appDir = getAppDir();
2357
+ const expandedDest = expandTilde(destination);
2358
+ const targetDir = join10(expandedDest, ".syncpoint");
2359
+ let wasAlreadyLinked = false;
2360
+ let lstats;
2361
+ try {
2362
+ lstats = await lstat2(appDir);
2363
+ } catch {
2364
+ throw new Error(`${appDir} does not exist. Run "syncpoint init" first.`);
2167
2365
  }
2168
- const raw = await readFile4(templatePath, "utf-8");
2169
- const data = YAML3.parse(raw);
2170
- const result = validateTemplate(data);
2171
- if (!result.valid) {
2172
- throw new Error(
2173
- `Invalid template ${templatePath}:
2174
- ${(result.errors ?? []).join("\n")}`
2175
- );
2366
+ if (lstats.isSymbolicLink()) {
2367
+ wasAlreadyLinked = true;
2368
+ const existingTarget = await readlink2(appDir);
2369
+ await unlink(appDir);
2370
+ if (existingTarget !== targetDir) {
2371
+ await rename(existingTarget, appDir);
2372
+ } else {
2373
+ await symlink(targetDir, appDir);
2374
+ return { appDir, targetDir, wasAlreadyLinked };
2375
+ }
2176
2376
  }
2177
- return data;
2377
+ try {
2378
+ await rename(appDir, targetDir);
2379
+ } catch (err) {
2380
+ const isExdev = err instanceof Error && (err.message.includes("EXDEV") || err.message.includes("cross-device"));
2381
+ if (isExdev) {
2382
+ throw new Error(
2383
+ `Cannot move ${appDir} to ${targetDir}: source and destination are on different filesystems. Use a destination on the same filesystem, or manually copy the directory.`
2384
+ );
2385
+ }
2386
+ throw err;
2387
+ }
2388
+ await symlink(targetDir, appDir);
2389
+ return { appDir, targetDir, wasAlreadyLinked };
2390
+ }
2391
+ async function linkSyncpointByRef(refPath) {
2392
+ const appDir = getAppDir();
2393
+ const absoluteRef = resolve2(expandTilde(refPath));
2394
+ const targetDir = basename2(absoluteRef) === ".syncpoint" ? absoluteRef : join10(absoluteRef, ".syncpoint");
2395
+ let refStats;
2396
+ try {
2397
+ refStats = await stat2(targetDir);
2398
+ } catch {
2399
+ throw new Error(`Reference syncpoint path does not exist: ${targetDir}`);
2400
+ }
2401
+ if (!refStats.isDirectory()) {
2402
+ throw new Error(
2403
+ `Reference syncpoint path is not a directory: ${targetDir}`
2404
+ );
2405
+ }
2406
+ try {
2407
+ const existing = await lstat2(appDir);
2408
+ if (existing.isSymbolicLink()) {
2409
+ await unlink(appDir);
2410
+ } else {
2411
+ await rm2(appDir, { recursive: true, force: true });
2412
+ }
2413
+ } catch {
2414
+ }
2415
+ await symlink(targetDir, appDir);
2416
+ return { appDir, targetDir, wasAlreadyLinked: false };
2417
+ }
2418
+ async function unlinkSyncpoint(options = {}) {
2419
+ const appDir = getAppDir();
2420
+ let lstats;
2421
+ try {
2422
+ lstats = await lstat2(appDir);
2423
+ } catch {
2424
+ throw new Error(`${appDir} does not exist.`);
2425
+ }
2426
+ if (!lstats.isSymbolicLink()) {
2427
+ throw new Error(`${appDir} is not a symlink. Run "syncpoint link" first.`);
2428
+ }
2429
+ const targetDir = await readlink2(appDir);
2430
+ await unlink(appDir);
2431
+ await cp(targetDir, appDir, { recursive: true });
2432
+ if (options.clean) {
2433
+ await rm2(targetDir, { recursive: true, force: true });
2434
+ }
2435
+ return { appDir, targetDir, cleaned: options.clean ?? false };
2436
+ }
2437
+
2438
+ // src/commands/Link.tsx
2439
+ import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2440
+ var LinkView = ({ refPath, yes }) => {
2441
+ const { exit } = useApp4();
2442
+ const [phase, setPhase] = useState5("checking");
2443
+ const [result, setResult] = useState5(null);
2444
+ const [error, setError] = useState5(null);
2445
+ const [existingType, setExistingType] = useState5("directory");
2446
+ useEffect4(() => {
2447
+ (async () => {
2448
+ try {
2449
+ if (!refPath) {
2450
+ setPhase("linking");
2451
+ return;
2452
+ }
2453
+ const appDir = getAppDir();
2454
+ try {
2455
+ const stats = await lstat3(appDir);
2456
+ setExistingType(stats.isSymbolicLink() ? "symlink" : "directory");
2457
+ if (yes) {
2458
+ setPhase("linking");
2459
+ } else {
2460
+ setPhase("confirming");
2461
+ }
2462
+ } catch {
2463
+ setPhase("linking");
2464
+ }
2465
+ } catch (err) {
2466
+ setError(err instanceof Error ? err.message : String(err));
2467
+ setPhase("error");
2468
+ setTimeout(() => exit(), 100);
2469
+ }
2470
+ })();
2471
+ }, []);
2472
+ useEffect4(() => {
2473
+ if (phase !== "linking") return;
2474
+ (async () => {
2475
+ try {
2476
+ let linkResult;
2477
+ if (refPath) {
2478
+ linkResult = await linkSyncpointByRef(refPath);
2479
+ } else {
2480
+ const config = await loadConfig();
2481
+ const destination = config.backup.destination;
2482
+ if (!destination) {
2483
+ throw new Error(
2484
+ 'backup.destination is not set in config.yml. Set it before running "syncpoint link".'
2485
+ );
2486
+ }
2487
+ linkResult = await linkSyncpoint(destination);
2488
+ }
2489
+ setResult(linkResult);
2490
+ setPhase("done");
2491
+ setTimeout(() => exit(), 100);
2492
+ } catch (err) {
2493
+ setError(err instanceof Error ? err.message : String(err));
2494
+ setPhase("error");
2495
+ setTimeout(() => exit(), 100);
2496
+ }
2497
+ })();
2498
+ }, [phase]);
2499
+ if (phase === "error" || error) {
2500
+ return /* @__PURE__ */ jsx7(Box5, { flexDirection: "column", children: /* @__PURE__ */ jsxs7(Text7, { color: "red", children: [
2501
+ "\u2717 Link failed: ",
2502
+ error
2503
+ ] }) });
2504
+ }
2505
+ if (phase === "confirming") {
2506
+ return /* @__PURE__ */ jsxs7(Box5, { flexDirection: "column", children: [
2507
+ /* @__PURE__ */ jsxs7(Text7, { color: "yellow", children: [
2508
+ "\u26A0 ~/.syncpoint already exists as a ",
2509
+ existingType,
2510
+ "."
2511
+ ] }),
2512
+ /* @__PURE__ */ jsx7(
2513
+ Confirm,
2514
+ {
2515
+ message: "Overwrite with symlink?",
2516
+ defaultYes: false,
2517
+ onConfirm: (confirmed) => {
2518
+ if (confirmed) {
2519
+ setPhase("linking");
2520
+ } else {
2521
+ setError("Aborted.");
2522
+ setPhase("error");
2523
+ setTimeout(() => exit(), 100);
2524
+ }
2525
+ }
2526
+ }
2527
+ )
2528
+ ] });
2529
+ }
2530
+ if (phase === "checking" || phase === "linking") {
2531
+ return /* @__PURE__ */ jsx7(Box5, { children: /* @__PURE__ */ jsx7(Text7, { children: "\u25B8 Linking ~/.syncpoint to destination..." }) });
2532
+ }
2533
+ return /* @__PURE__ */ jsxs7(Box5, { flexDirection: "column", children: [
2534
+ /* @__PURE__ */ jsx7(Text7, { color: "green", bold: true, children: "\u2713 Link complete" }),
2535
+ result && /* @__PURE__ */ jsxs7(Fragment2, { children: [
2536
+ /* @__PURE__ */ jsxs7(Text7, { children: [
2537
+ " ",
2538
+ "From: ",
2539
+ contractTilde(result.appDir)
2540
+ ] }),
2541
+ /* @__PURE__ */ jsxs7(Text7, { children: [
2542
+ " ",
2543
+ "To: ",
2544
+ contractTilde(result.targetDir)
2545
+ ] }),
2546
+ result.wasAlreadyLinked && /* @__PURE__ */ jsxs7(Text7, { color: "yellow", children: [
2547
+ " ",
2548
+ "(previous link was removed and re-linked)"
2549
+ ] })
2550
+ ] })
2551
+ ] });
2552
+ };
2553
+ function registerLinkCommand(program2) {
2554
+ const cmdInfo = COMMANDS.link;
2555
+ const cmd = program2.command("link").description(cmdInfo.description);
2556
+ cmd.option(
2557
+ "-r, --ref <path>",
2558
+ "Adopt <path>/.syncpoint as ~/.syncpoint via symlink"
2559
+ );
2560
+ cmd.action(async (opts) => {
2561
+ const globalOpts = program2.opts();
2562
+ const startTime = Date.now();
2563
+ if (globalOpts.json) {
2564
+ try {
2565
+ if (opts.ref) {
2566
+ const appDir = getAppDir();
2567
+ let appDirExists = false;
2568
+ try {
2569
+ await lstat3(appDir);
2570
+ appDirExists = true;
2571
+ } catch {
2572
+ }
2573
+ if (appDirExists && !globalOpts.yes) {
2574
+ respondError(
2575
+ "link",
2576
+ SyncpointErrorCode.LINK_FAILED,
2577
+ "~/.syncpoint already exists. Use --yes to overwrite.",
2578
+ startTime,
2579
+ VERSION
2580
+ );
2581
+ return;
2582
+ }
2583
+ const linkResult = await linkSyncpointByRef(opts.ref);
2584
+ respond(
2585
+ "link",
2586
+ {
2587
+ appDir: linkResult.appDir,
2588
+ targetDir: linkResult.targetDir,
2589
+ wasAlreadyLinked: linkResult.wasAlreadyLinked
2590
+ },
2591
+ startTime,
2592
+ VERSION
2593
+ );
2594
+ } else {
2595
+ const config = await loadConfig();
2596
+ const destination = config.backup.destination;
2597
+ if (!destination) {
2598
+ throw new Error("backup.destination is not set in config.yml.");
2599
+ }
2600
+ const linkResult = await linkSyncpoint(destination);
2601
+ respond(
2602
+ "link",
2603
+ {
2604
+ appDir: linkResult.appDir,
2605
+ targetDir: linkResult.targetDir,
2606
+ wasAlreadyLinked: linkResult.wasAlreadyLinked
2607
+ },
2608
+ startTime,
2609
+ VERSION
2610
+ );
2611
+ }
2612
+ } catch (error) {
2613
+ const code = classifyError(error);
2614
+ respondError("link", code, error.message, startTime, VERSION);
2615
+ }
2616
+ return;
2617
+ }
2618
+ const { waitUntilExit } = render5(
2619
+ /* @__PURE__ */ jsx7(LinkView, { refPath: opts.ref, yes: globalOpts.yes ?? false })
2620
+ );
2621
+ await waitUntilExit();
2622
+ });
2623
+ }
2624
+
2625
+ // src/commands/List.tsx
2626
+ import { unlinkSync } from "fs";
2627
+ import { Box as Box7, Text as Text9, useApp as useApp5, useInput as useInput2 } from "ink";
2628
+ import { render as render6 } from "ink";
2629
+ import SelectInput from "ink-select-input";
2630
+ import { useEffect as useEffect5, useState as useState6 } from "react";
2631
+
2632
+ // src/components/Table.tsx
2633
+ import { Box as Box6, Text as Text8 } from "ink";
2634
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
2635
+ var Table = ({
2636
+ headers,
2637
+ rows,
2638
+ columnWidths
2639
+ }) => {
2640
+ const widths = columnWidths ?? headers.map((header, colIdx) => {
2641
+ const dataMax = rows.reduce(
2642
+ (max, row) => Math.max(max, (row[colIdx] ?? "").length),
2643
+ 0
2644
+ );
2645
+ return Math.max(header.length, dataMax) + 2;
2646
+ });
2647
+ const padCell = (text, width) => {
2648
+ return text.padEnd(width);
2649
+ };
2650
+ const separator = widths.map((w) => "\u2500".repeat(w)).join(" ");
2651
+ return /* @__PURE__ */ jsxs8(Box6, { flexDirection: "column", children: [
2652
+ /* @__PURE__ */ jsx8(Text8, { children: headers.map((h, i) => /* @__PURE__ */ jsxs8(Text8, { bold: true, children: [
2653
+ padCell(h, widths[i]),
2654
+ i < headers.length - 1 ? " " : ""
2655
+ ] }, i)) }),
2656
+ /* @__PURE__ */ jsx8(Text8, { color: "gray", children: separator }),
2657
+ rows.map((row, rowIdx) => /* @__PURE__ */ jsx8(Text8, { children: row.map((cell, colIdx) => /* @__PURE__ */ jsxs8(Text8, { children: [
2658
+ padCell(cell, widths[colIdx]),
2659
+ colIdx < row.length - 1 ? " " : ""
2660
+ ] }, colIdx)) }, rowIdx))
2661
+ ] });
2662
+ };
2663
+
2664
+ // src/core/provision.ts
2665
+ import { exec } from "child_process";
2666
+ import { readFile as readFile4, readdir as readdir2 } from "fs/promises";
2667
+ import { join as join11 } from "path";
2668
+ import YAML3 from "yaml";
2669
+ var REMOTE_SCRIPT_PATTERNS = [
2670
+ /curl\s.*\|\s*(ba)?sh/,
2671
+ /wget\s.*\|\s*(ba)?sh/,
2672
+ /curl\s.*\|\s*python/,
2673
+ /wget\s.*\|\s*python/
2674
+ ];
2675
+ function containsRemoteScriptPattern(command) {
2676
+ return REMOTE_SCRIPT_PATTERNS.some((p) => p.test(command));
2677
+ }
2678
+ function sanitizeErrorOutput(output) {
2679
+ return output.replace(/\/Users\/[^\s/]+/g, "/Users/***").replace(/\/home\/[^\s/]+/g, "/home/***").replace(/(password|token|key|secret)[=:]\s*\S+/gi, "$1=***").slice(0, 500);
2680
+ }
2681
+ async function loadTemplate(templatePath) {
2682
+ const exists = await fileExists(templatePath);
2683
+ if (!exists) {
2684
+ throw new Error(`Template not found: ${templatePath}`);
2685
+ }
2686
+ const raw = await readFile4(templatePath, "utf-8");
2687
+ const data = YAML3.parse(raw);
2688
+ const result = validateTemplate(data);
2689
+ if (!result.valid) {
2690
+ throw new Error(
2691
+ `Invalid template ${templatePath}:
2692
+ ${(result.errors ?? []).join("\n")}`
2693
+ );
2694
+ }
2695
+ return data;
2178
2696
  }
2179
2697
  async function listTemplates() {
2180
2698
  const templatesDir = getSubDir(TEMPLATES_DIR);
@@ -2186,7 +2704,7 @@ async function listTemplates() {
2186
2704
  if (!entry.isFile() || !entry.name.endsWith(".yml") && !entry.name.endsWith(".yaml")) {
2187
2705
  continue;
2188
2706
  }
2189
- const fullPath = join10(templatesDir, entry.name);
2707
+ const fullPath = join11(templatesDir, entry.name);
2190
2708
  try {
2191
2709
  const config = await loadTemplate(fullPath);
2192
2710
  templates.push({
@@ -2201,7 +2719,7 @@ async function listTemplates() {
2201
2719
  return templates;
2202
2720
  }
2203
2721
  function execAsync(command) {
2204
- return new Promise((resolve2, reject) => {
2722
+ return new Promise((resolve3, reject) => {
2205
2723
  exec(
2206
2724
  command,
2207
2725
  { shell: "/bin/bash", timeout: 3e5 },
@@ -2214,7 +2732,7 @@ function execAsync(command) {
2214
2732
  })
2215
2733
  );
2216
2734
  } else {
2217
- resolve2({
2735
+ resolve3({
2218
2736
  stdout: stdout?.toString() ?? "",
2219
2737
  stderr: stderr?.toString() ?? ""
2220
2738
  });
@@ -2304,8 +2822,8 @@ async function* runProvision(templatePath, options = {}) {
2304
2822
  }
2305
2823
 
2306
2824
  // src/core/restore.ts
2307
- import { copyFile, lstat as lstat2, readdir as readdir3, stat as stat2 } from "fs/promises";
2308
- import { dirname as dirname2, join as join11 } from "path";
2825
+ import { copyFile, lstat as lstat4, readdir as readdir3, stat as stat3 } from "fs/promises";
2826
+ import { dirname as dirname2, join as join12 } from "path";
2309
2827
  async function getBackupList(config) {
2310
2828
  const backupDir = config?.backup.destination ? resolveTargetPath(config.backup.destination) : getSubDir(BACKUPS_DIR);
2311
2829
  const exists = await fileExists(backupDir);
@@ -2314,8 +2832,8 @@ async function getBackupList(config) {
2314
2832
  const backups = [];
2315
2833
  for (const entry of entries) {
2316
2834
  if (!entry.isFile() || !entry.name.endsWith(".tar.gz")) continue;
2317
- const fullPath = join11(backupDir, entry.name);
2318
- const fileStat = await stat2(fullPath);
2835
+ const fullPath = join12(backupDir, entry.name);
2836
+ const fileStat = await stat3(fullPath);
2319
2837
  let hostname;
2320
2838
  let fileCount;
2321
2839
  try {
@@ -2363,7 +2881,7 @@ This may not be a valid syncpoint backup.`
2363
2881
  continue;
2364
2882
  }
2365
2883
  const currentHash = await computeFileHash(absPath);
2366
- const currentStat = await stat2(absPath);
2884
+ const currentStat = await stat3(absPath);
2367
2885
  if (currentHash === file.hash) {
2368
2886
  actions.push({
2369
2887
  path: file.path,
@@ -2389,7 +2907,7 @@ async function createSafetyBackup(filePaths) {
2389
2907
  const filename = `_pre-restore_${formatDatetime(now)}.tar.gz`;
2390
2908
  const backupDir = getSubDir(BACKUPS_DIR);
2391
2909
  await ensureDir(backupDir);
2392
- const archivePath = join11(backupDir, filename);
2910
+ const archivePath = join12(backupDir, filename);
2393
2911
  const files = [];
2394
2912
  for (const fp of filePaths) {
2395
2913
  const absPath = resolveTargetPath(fp);
@@ -2422,9 +2940,9 @@ async function restoreBackup(archivePath, options = {}) {
2422
2940
  safetyBackupPath
2423
2941
  };
2424
2942
  }
2425
- const { mkdtemp: mkdtemp2, rm: rm2 } = await import("fs/promises");
2943
+ const { mkdtemp: mkdtemp2, rm: rm3 } = await import("fs/promises");
2426
2944
  const { tmpdir: tmpdir2 } = await import("os");
2427
- const tmpDir = await mkdtemp2(join11(tmpdir2(), "syncpoint-restore-"));
2945
+ const tmpDir = await mkdtemp2(join12(tmpdir2(), "syncpoint-restore-"));
2428
2946
  try {
2429
2947
  await extractArchive(archivePath, tmpDir);
2430
2948
  for (const action of plan.actions) {
@@ -2433,7 +2951,7 @@ async function restoreBackup(archivePath, options = {}) {
2433
2951
  continue;
2434
2952
  }
2435
2953
  const archiveName = action.path.startsWith("~/") ? action.path.slice(2) : action.path;
2436
- const extractedPath = join11(tmpDir, archiveName);
2954
+ const extractedPath = join12(tmpDir, archiveName);
2437
2955
  const destPath = resolveTargetPath(action.path);
2438
2956
  const extractedExists = await fileExists(extractedPath);
2439
2957
  if (!extractedExists) {
@@ -2443,7 +2961,7 @@ async function restoreBackup(archivePath, options = {}) {
2443
2961
  }
2444
2962
  await ensureDir(dirname2(destPath));
2445
2963
  try {
2446
- const destStat = await lstat2(destPath);
2964
+ const destStat = await lstat4(destPath);
2447
2965
  if (destStat.isSymbolicLink()) {
2448
2966
  logger.warn(`Skipping symlink target: ${action.path}`);
2449
2967
  skippedFiles.push(action.path);
@@ -2456,45 +2974,45 @@ async function restoreBackup(archivePath, options = {}) {
2456
2974
  restoredFiles.push(action.path);
2457
2975
  }
2458
2976
  } finally {
2459
- await rm2(tmpDir, { recursive: true, force: true });
2977
+ await rm3(tmpDir, { recursive: true, force: true });
2460
2978
  }
2461
2979
  return { restoredFiles, skippedFiles, safetyBackupPath };
2462
2980
  }
2463
2981
 
2464
2982
  // src/commands/List.tsx
2465
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
2983
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
2466
2984
  var MenuItem = ({ isSelected = false, label }) => {
2467
2985
  if (label === "Delete") {
2468
- return /* @__PURE__ */ jsx8(Text8, { bold: isSelected, color: "red", children: label });
2986
+ return /* @__PURE__ */ jsx9(Text9, { bold: isSelected, color: "red", children: label });
2469
2987
  }
2470
2988
  const match = label.match(/^(.+?)\s+\((\d+)\)$/);
2471
2989
  if (match) {
2472
2990
  const [, name, count] = match;
2473
- return /* @__PURE__ */ jsxs8(Text8, { children: [
2474
- /* @__PURE__ */ jsx8(Text8, { bold: isSelected, children: name }),
2991
+ return /* @__PURE__ */ jsxs9(Text9, { children: [
2992
+ /* @__PURE__ */ jsx9(Text9, { bold: isSelected, children: name }),
2475
2993
  " ",
2476
- /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
2994
+ /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
2477
2995
  "(",
2478
2996
  count,
2479
2997
  ")"
2480
2998
  ] })
2481
2999
  ] });
2482
3000
  }
2483
- return /* @__PURE__ */ jsx8(Text8, { bold: isSelected, children: label });
3001
+ return /* @__PURE__ */ jsx9(Text9, { bold: isSelected, children: label });
2484
3002
  };
2485
3003
  var ListView = ({ type, deleteIndex }) => {
2486
- const { exit } = useApp4();
2487
- const [phase, setPhase] = useState5("loading");
2488
- const [backups, setBackups] = useState5([]);
2489
- const [templates, setTemplates] = useState5([]);
2490
- const [selectedTemplate, setSelectedTemplate] = useState5(
3004
+ const { exit } = useApp5();
3005
+ const [phase, setPhase] = useState6("loading");
3006
+ const [backups, setBackups] = useState6([]);
3007
+ const [templates, setTemplates] = useState6([]);
3008
+ const [selectedTemplate, setSelectedTemplate] = useState6(
2491
3009
  null
2492
3010
  );
2493
- const [selectedBackup, setSelectedBackup] = useState5(null);
2494
- const [deleteTarget, setDeleteTarget] = useState5(null);
2495
- const [error, setError] = useState5(null);
2496
- const [backupDir, setBackupDir] = useState5(getSubDir("backups"));
2497
- const [successMessage, setSuccessMessage] = useState5(null);
3011
+ const [selectedBackup, setSelectedBackup] = useState6(null);
3012
+ const [deleteTarget, setDeleteTarget] = useState6(null);
3013
+ const [error, setError] = useState6(null);
3014
+ const [backupDir, setBackupDir] = useState6(getSubDir("backups"));
3015
+ const [successMessage, setSuccessMessage] = useState6(null);
2498
3016
  useInput2((_input, key) => {
2499
3017
  if (!key.escape) return;
2500
3018
  switch (phase) {
@@ -2518,7 +3036,7 @@ var ListView = ({ type, deleteIndex }) => {
2518
3036
  break;
2519
3037
  }
2520
3038
  });
2521
- useEffect4(() => {
3039
+ useEffect5(() => {
2522
3040
  (async () => {
2523
3041
  try {
2524
3042
  const config = await loadConfig();
@@ -2610,16 +3128,16 @@ var ListView = ({ type, deleteIndex }) => {
2610
3128
  }
2611
3129
  };
2612
3130
  if (phase === "error" || error) {
2613
- return /* @__PURE__ */ jsx8(Box6, { flexDirection: "column", children: /* @__PURE__ */ jsxs8(Text8, { color: "red", children: [
3131
+ return /* @__PURE__ */ jsx9(Box7, { flexDirection: "column", children: /* @__PURE__ */ jsxs9(Text9, { color: "red", children: [
2614
3132
  "\u2717 ",
2615
3133
  error
2616
3134
  ] }) });
2617
3135
  }
2618
3136
  if (phase === "loading") {
2619
- return /* @__PURE__ */ jsx8(Text8, { color: "cyan", children: "Loading..." });
3137
+ return /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "Loading..." });
2620
3138
  }
2621
3139
  if (phase === "deleting" && deleteTarget) {
2622
- return /* @__PURE__ */ jsx8(Box6, { flexDirection: "column", children: /* @__PURE__ */ jsx8(
3140
+ return /* @__PURE__ */ jsx9(Box7, { flexDirection: "column", children: /* @__PURE__ */ jsx9(
2623
3141
  Confirm,
2624
3142
  {
2625
3143
  message: `Delete ${deleteTarget.name}?`,
@@ -2629,7 +3147,7 @@ var ListView = ({ type, deleteIndex }) => {
2629
3147
  ) });
2630
3148
  }
2631
3149
  if (phase === "done" && deleteTarget) {
2632
- return /* @__PURE__ */ jsxs8(Text8, { color: "green", children: [
3150
+ return /* @__PURE__ */ jsxs9(Text9, { color: "green", children: [
2633
3151
  "\u2713 ",
2634
3152
  deleteTarget.name,
2635
3153
  " deleted"
@@ -2661,8 +3179,8 @@ var ListView = ({ type, deleteIndex }) => {
2661
3179
  setPhase("template-list");
2662
3180
  }
2663
3181
  };
2664
- return /* @__PURE__ */ jsxs8(Box6, { flexDirection: "column", children: [
2665
- /* @__PURE__ */ jsx8(
3182
+ return /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", children: [
3183
+ /* @__PURE__ */ jsx9(
2666
3184
  SelectInput,
2667
3185
  {
2668
3186
  items: menuItems,
@@ -2670,9 +3188,9 @@ var ListView = ({ type, deleteIndex }) => {
2670
3188
  itemComponent: MenuItem
2671
3189
  }
2672
3190
  ),
2673
- /* @__PURE__ */ jsx8(Box6, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
3191
+ /* @__PURE__ */ jsx9(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
2674
3192
  "Press ",
2675
- /* @__PURE__ */ jsx8(Text8, { bold: true, children: "ESC" }),
3193
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: "ESC" }),
2676
3194
  " to exit"
2677
3195
  ] }) })
2678
3196
  ] });
@@ -2689,10 +3207,10 @@ var ListView = ({ type, deleteIndex }) => {
2689
3207
  setPhase("backup-detail");
2690
3208
  }
2691
3209
  };
2692
- return /* @__PURE__ */ jsxs8(Box6, { flexDirection: "column", children: [
2693
- successMessage && /* @__PURE__ */ jsx8(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsx8(Text8, { color: "green", children: successMessage }) }),
2694
- /* @__PURE__ */ jsx8(Text8, { bold: true, children: "\u25B8 Backups" }),
2695
- /* @__PURE__ */ jsx8(Box6, { marginTop: 1, children: backups.length === 0 ? /* @__PURE__ */ jsx8(Text8, { color: "gray", children: " No backups found." }) : /* @__PURE__ */ jsx8(
3210
+ return /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", children: [
3211
+ successMessage && /* @__PURE__ */ jsx9(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "green", children: successMessage }) }),
3212
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: "\u25B8 Backups" }),
3213
+ /* @__PURE__ */ jsx9(Box7, { marginTop: 1, children: backups.length === 0 ? /* @__PURE__ */ jsx9(Text9, { color: "gray", children: " No backups found." }) : /* @__PURE__ */ jsx9(
2696
3214
  SelectInput,
2697
3215
  {
2698
3216
  items,
@@ -2700,9 +3218,9 @@ var ListView = ({ type, deleteIndex }) => {
2700
3218
  itemComponent: MenuItem
2701
3219
  }
2702
3220
  ) }),
2703
- /* @__PURE__ */ jsx8(Box6, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
3221
+ /* @__PURE__ */ jsx9(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
2704
3222
  "Press ",
2705
- /* @__PURE__ */ jsx8(Text8, { bold: true, children: "ESC" }),
3223
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: "ESC" }),
2706
3224
  " to go back"
2707
3225
  ] }) })
2708
3226
  ] });
@@ -2719,9 +3237,9 @@ var ListView = ({ type, deleteIndex }) => {
2719
3237
  setPhase("template-detail");
2720
3238
  }
2721
3239
  };
2722
- return /* @__PURE__ */ jsxs8(Box6, { flexDirection: "column", children: [
2723
- /* @__PURE__ */ jsx8(Text8, { bold: true, children: "\u25B8 Templates" }),
2724
- /* @__PURE__ */ jsx8(Box6, { marginTop: 1, children: templates.length === 0 ? /* @__PURE__ */ jsx8(Text8, { color: "gray", children: " No templates found." }) : /* @__PURE__ */ jsx8(
3240
+ return /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", children: [
3241
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: "\u25B8 Templates" }),
3242
+ /* @__PURE__ */ jsx9(Box7, { marginTop: 1, children: templates.length === 0 ? /* @__PURE__ */ jsx9(Text9, { color: "gray", children: " No templates found." }) : /* @__PURE__ */ jsx9(
2725
3243
  SelectInput,
2726
3244
  {
2727
3245
  items,
@@ -2729,9 +3247,9 @@ var ListView = ({ type, deleteIndex }) => {
2729
3247
  itemComponent: MenuItem
2730
3248
  }
2731
3249
  ) }),
2732
- /* @__PURE__ */ jsx8(Box6, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
3250
+ /* @__PURE__ */ jsx9(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
2733
3251
  "Press ",
2734
- /* @__PURE__ */ jsx8(Text8, { bold: true, children: "ESC" }),
3252
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: "ESC" }),
2735
3253
  " to go back"
2736
3254
  ] }) })
2737
3255
  ] });
@@ -2759,20 +3277,20 @@ var ListView = ({ type, deleteIndex }) => {
2759
3277
  goBackToBackupList();
2760
3278
  }
2761
3279
  };
2762
- return /* @__PURE__ */ jsxs8(Box6, { flexDirection: "column", children: [
2763
- /* @__PURE__ */ jsxs8(Text8, { bold: true, children: [
3280
+ return /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", children: [
3281
+ /* @__PURE__ */ jsxs9(Text9, { bold: true, children: [
2764
3282
  "\u25B8 ",
2765
3283
  selectedBackup.filename.replace(".tar.gz", "")
2766
3284
  ] }),
2767
- /* @__PURE__ */ jsx8(Box6, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: sections.map((section, idx) => {
3285
+ /* @__PURE__ */ jsx9(Box7, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: sections.map((section, idx) => {
2768
3286
  const labelWidth = Math.max(...sections.map((s) => s.label.length)) + 1;
2769
- return /* @__PURE__ */ jsxs8(Box6, { children: [
2770
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: section.label.padEnd(labelWidth) }),
2771
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
2772
- /* @__PURE__ */ jsx8(Text8, { children: section.value })
3287
+ return /* @__PURE__ */ jsxs9(Box7, { children: [
3288
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: section.label.padEnd(labelWidth) }),
3289
+ /* @__PURE__ */ jsx9(Text9, { children: " " }),
3290
+ /* @__PURE__ */ jsx9(Text9, { children: section.value })
2773
3291
  ] }, idx);
2774
3292
  }) }),
2775
- /* @__PURE__ */ jsx8(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx8(
3293
+ /* @__PURE__ */ jsx9(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx9(
2776
3294
  SelectInput,
2777
3295
  {
2778
3296
  items: actionItems,
@@ -2780,9 +3298,9 @@ var ListView = ({ type, deleteIndex }) => {
2780
3298
  itemComponent: MenuItem
2781
3299
  }
2782
3300
  ) }),
2783
- /* @__PURE__ */ jsx8(Box6, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
3301
+ /* @__PURE__ */ jsx9(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
2784
3302
  "Press ",
2785
- /* @__PURE__ */ jsx8(Text8, { bold: true, children: "ESC" }),
3303
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: "ESC" }),
2786
3304
  " to go back"
2787
3305
  ] }) })
2788
3306
  ] });
@@ -2802,19 +3320,19 @@ var ListView = ({ type, deleteIndex }) => {
2802
3320
  }
2803
3321
  };
2804
3322
  const labelWidth = Math.max(...sections.map((s) => s.label.length)) + 1;
2805
- return /* @__PURE__ */ jsxs8(Box6, { flexDirection: "column", children: [
2806
- /* @__PURE__ */ jsxs8(Text8, { bold: true, children: [
3323
+ return /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", children: [
3324
+ /* @__PURE__ */ jsxs9(Text9, { bold: true, children: [
2807
3325
  "\u25B8 ",
2808
3326
  selectedTemplate.name
2809
3327
  ] }),
2810
- /* @__PURE__ */ jsx8(Box6, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: sections.map((section, idx) => /* @__PURE__ */ jsxs8(Box6, { children: [
2811
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: section.label.padEnd(labelWidth) }),
2812
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
2813
- /* @__PURE__ */ jsx8(Text8, { children: section.value })
3328
+ /* @__PURE__ */ jsx9(Box7, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: sections.map((section, idx) => /* @__PURE__ */ jsxs9(Box7, { children: [
3329
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: section.label.padEnd(labelWidth) }),
3330
+ /* @__PURE__ */ jsx9(Text9, { children: " " }),
3331
+ /* @__PURE__ */ jsx9(Text9, { children: section.value })
2814
3332
  ] }, idx)) }),
2815
- t.steps.length > 0 && /* @__PURE__ */ jsxs8(Box6, { flexDirection: "column", marginTop: 1, children: [
2816
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " Provisioning Steps" }),
2817
- /* @__PURE__ */ jsx8(Box6, { marginLeft: 2, children: /* @__PURE__ */ jsx8(
3333
+ t.steps.length > 0 && /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", marginTop: 1, children: [
3334
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " Provisioning Steps" }),
3335
+ /* @__PURE__ */ jsx9(Box7, { marginLeft: 2, children: /* @__PURE__ */ jsx9(
2818
3336
  Table,
2819
3337
  {
2820
3338
  headers: ["#", "Step", "Description"],
@@ -2826,7 +3344,7 @@ var ListView = ({ type, deleteIndex }) => {
2826
3344
  }
2827
3345
  ) })
2828
3346
  ] }),
2829
- /* @__PURE__ */ jsx8(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx8(
3347
+ /* @__PURE__ */ jsx9(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx9(
2830
3348
  SelectInput,
2831
3349
  {
2832
3350
  items: actionItems,
@@ -2834,9 +3352,9 @@ var ListView = ({ type, deleteIndex }) => {
2834
3352
  itemComponent: MenuItem
2835
3353
  }
2836
3354
  ) }),
2837
- /* @__PURE__ */ jsx8(Box6, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
3355
+ /* @__PURE__ */ jsx9(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
2838
3356
  "Press ",
2839
- /* @__PURE__ */ jsx8(Text8, { bold: true, children: "ESC" }),
3357
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: "ESC" }),
2840
3358
  " to go back"
2841
3359
  ] }) })
2842
3360
  ] });
@@ -2844,27 +3362,89 @@ var ListView = ({ type, deleteIndex }) => {
2844
3362
  return null;
2845
3363
  };
2846
3364
  function registerListCommand(program2) {
2847
- program2.command("list [type]").description("List backups and templates").option("--delete <n>", "Delete item #n").action(async (type, opts) => {
3365
+ program2.command("list [type]").description("List backups and templates").option("--delete <filename>", "Delete item by filename").action(async (type, opts) => {
3366
+ const globalOpts = program2.opts();
3367
+ const startTime = Date.now();
3368
+ if (globalOpts.json) {
3369
+ try {
3370
+ const config = await loadConfig();
3371
+ if (opts.delete) {
3372
+ const filename = opts.delete;
3373
+ const backupDirectory = config.backup.destination ? resolveTargetPath(config.backup.destination) : getSubDir("backups");
3374
+ const isTemplate = type === "templates";
3375
+ if (isTemplate) {
3376
+ const templates = await listTemplates();
3377
+ const match = templates.find(
3378
+ (t) => t.name === filename || t.name === filename.replace(/\.ya?ml$/, "")
3379
+ );
3380
+ if (!match) {
3381
+ respondError("list", SyncpointErrorCode.INVALID_ARGUMENT, `Template not found: ${filename}`, startTime, VERSION);
3382
+ return;
3383
+ }
3384
+ if (!isInsideDir(match.path, getSubDir("templates"))) {
3385
+ respondError("list", SyncpointErrorCode.INVALID_ARGUMENT, `Refusing to delete file outside templates directory: ${match.path}`, startTime, VERSION);
3386
+ return;
3387
+ }
3388
+ unlinkSync(match.path);
3389
+ respond("list", { deleted: match.name, path: match.path }, startTime, VERSION);
3390
+ } else {
3391
+ const list2 = await getBackupList(config);
3392
+ const match = list2.find(
3393
+ (b) => b.filename === filename || b.filename.startsWith(filename)
3394
+ );
3395
+ if (!match) {
3396
+ respondError("list", SyncpointErrorCode.INVALID_ARGUMENT, `Backup not found: ${filename}`, startTime, VERSION);
3397
+ return;
3398
+ }
3399
+ if (!isInsideDir(match.path, backupDirectory)) {
3400
+ respondError("list", SyncpointErrorCode.INVALID_ARGUMENT, `Refusing to delete file outside backups directory: ${match.path}`, startTime, VERSION);
3401
+ return;
3402
+ }
3403
+ unlinkSync(match.path);
3404
+ respond("list", { deleted: match.filename, path: match.path }, startTime, VERSION);
3405
+ }
3406
+ return;
3407
+ }
3408
+ const showBackups = !type || type === "backups";
3409
+ const showTemplates = !type || type === "templates";
3410
+ const result = {};
3411
+ if (showBackups) {
3412
+ result.backups = await getBackupList(config);
3413
+ }
3414
+ if (showTemplates) {
3415
+ result.templates = await listTemplates();
3416
+ }
3417
+ respond("list", result, startTime, VERSION);
3418
+ } catch (error) {
3419
+ const code = classifyError(error);
3420
+ respondError("list", code, error.message, startTime, VERSION);
3421
+ }
3422
+ return;
3423
+ }
2848
3424
  const deleteIndex = opts.delete ? parseInt(opts.delete, 10) : void 0;
2849
3425
  if (deleteIndex !== void 0 && isNaN(deleteIndex)) {
2850
- console.error(`Invalid delete index: ${opts.delete}`);
2851
- process.exit(1);
3426
+ const { waitUntilExit: waitUntilExit2 } = render6(
3427
+ /* @__PURE__ */ jsx9(ListView, { type, deleteIndex: void 0 })
3428
+ );
3429
+ await waitUntilExit2();
3430
+ return;
2852
3431
  }
2853
- const { waitUntilExit } = render5(
2854
- /* @__PURE__ */ jsx8(ListView, { type, deleteIndex })
3432
+ const { waitUntilExit } = render6(
3433
+ /* @__PURE__ */ jsx9(ListView, { type, deleteIndex })
2855
3434
  );
2856
3435
  await waitUntilExit();
2857
3436
  });
2858
3437
  }
2859
3438
 
2860
3439
  // src/commands/Migrate.tsx
2861
- import { Box as Box7, Text as Text9, useApp as useApp5 } from "ink";
2862
- import { render as render6 } from "ink";
2863
- import { useEffect as useEffect5, useState as useState6 } from "react";
3440
+ import { Box as Box8, Text as Text10, useApp as useApp6 } from "ink";
3441
+ import { render as render7 } from "ink";
3442
+ import { useEffect as useEffect6, useState as useState7 } from "react";
2864
3443
 
2865
3444
  // src/core/migrate.ts
2866
3445
  import { copyFile as copyFile2, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
2867
3446
  import YAML4 from "yaml";
3447
+ init_assets();
2868
3448
  function extractSchemaPaths(schema, prefix = []) {
2869
3449
  const paths = [];
2870
3450
  const properties = schema.properties;
@@ -3007,13 +3587,13 @@ ${(validation.errors ?? []).join("\n")}`
3007
3587
  }
3008
3588
 
3009
3589
  // src/commands/Migrate.tsx
3010
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
3590
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
3011
3591
  var MigrateView = ({ dryRun }) => {
3012
- const { exit } = useApp5();
3013
- const [result, setResult] = useState6(null);
3014
- const [error, setError] = useState6(null);
3015
- const [loading, setLoading] = useState6(true);
3016
- useEffect5(() => {
3592
+ const { exit } = useApp6();
3593
+ const [result, setResult] = useState7(null);
3594
+ const [error, setError] = useState7(null);
3595
+ const [loading, setLoading] = useState7(true);
3596
+ useEffect6(() => {
3017
3597
  (async () => {
3018
3598
  try {
3019
3599
  const res = await migrateConfig({ dryRun });
@@ -3028,54 +3608,54 @@ var MigrateView = ({ dryRun }) => {
3028
3608
  })();
3029
3609
  }, []);
3030
3610
  if (error) {
3031
- return /* @__PURE__ */ jsx9(Box7, { flexDirection: "column", children: /* @__PURE__ */ jsxs9(Text9, { color: "red", children: [
3611
+ return /* @__PURE__ */ jsx10(Box8, { flexDirection: "column", children: /* @__PURE__ */ jsxs10(Text10, { color: "red", children: [
3032
3612
  "\u2717 ",
3033
3613
  error
3034
3614
  ] }) });
3035
3615
  }
3036
3616
  if (loading) {
3037
- return /* @__PURE__ */ jsx9(Text9, { children: "Analyzing config..." });
3617
+ return /* @__PURE__ */ jsx10(Text10, { children: "Analyzing config..." });
3038
3618
  }
3039
3619
  if (!result) return null;
3040
3620
  if (!result.migrated && result.added.length === 0 && result.deprecated.length === 0) {
3041
- return /* @__PURE__ */ jsx9(Box7, { flexDirection: "column", children: /* @__PURE__ */ jsx9(Text9, { color: "green", children: "\u2713 Config is already up to date." }) });
3621
+ return /* @__PURE__ */ jsx10(Box8, { flexDirection: "column", children: /* @__PURE__ */ jsx10(Text10, { color: "green", children: "\u2713 Config is already up to date." }) });
3042
3622
  }
3043
- return /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", children: [
3044
- dryRun && /* @__PURE__ */ jsx9(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "yellow", bold: true, children: "[dry-run] Preview only \u2014 no changes written." }) }),
3045
- result.added.length > 0 && /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", children: [
3046
- /* @__PURE__ */ jsx9(Text9, { bold: true, children: "New fields (added with defaults):" }),
3047
- result.added.map((field, i) => /* @__PURE__ */ jsxs9(Text9, { children: [
3623
+ return /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", children: [
3624
+ dryRun && /* @__PURE__ */ jsx10(Box8, { marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { color: "yellow", bold: true, children: "[dry-run] Preview only \u2014 no changes written." }) }),
3625
+ result.added.length > 0 && /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", children: [
3626
+ /* @__PURE__ */ jsx10(Text10, { bold: true, children: "New fields (added with defaults):" }),
3627
+ result.added.map((field, i) => /* @__PURE__ */ jsxs10(Text10, { children: [
3048
3628
  " ",
3049
- /* @__PURE__ */ jsx9(Text9, { color: "green", children: "+" }),
3629
+ /* @__PURE__ */ jsx10(Text10, { color: "green", children: "+" }),
3050
3630
  " ",
3051
3631
  field
3052
3632
  ] }, i))
3053
3633
  ] }),
3054
- result.deprecated.length > 0 && /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", marginTop: result.added.length > 0 ? 1 : 0, children: [
3055
- /* @__PURE__ */ jsx9(Text9, { bold: true, children: "Deprecated fields (commented out):" }),
3056
- result.deprecated.map((field, i) => /* @__PURE__ */ jsxs9(Text9, { children: [
3634
+ result.deprecated.length > 0 && /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", marginTop: result.added.length > 0 ? 1 : 0, children: [
3635
+ /* @__PURE__ */ jsx10(Text10, { bold: true, children: "Deprecated fields (commented out):" }),
3636
+ result.deprecated.map((field, i) => /* @__PURE__ */ jsxs10(Text10, { children: [
3057
3637
  " ",
3058
- /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "~" }),
3638
+ /* @__PURE__ */ jsx10(Text10, { color: "yellow", children: "~" }),
3059
3639
  " ",
3060
3640
  field
3061
3641
  ] }, i))
3062
3642
  ] }),
3063
- result.preserved.length > 0 && /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", marginTop: 1, children: [
3064
- /* @__PURE__ */ jsxs9(Text9, { bold: true, children: [
3643
+ result.preserved.length > 0 && /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", marginTop: 1, children: [
3644
+ /* @__PURE__ */ jsxs10(Text10, { bold: true, children: [
3065
3645
  "Preserved fields (",
3066
3646
  result.preserved.length,
3067
3647
  "):"
3068
3648
  ] }),
3069
- result.preserved.map((field, i) => /* @__PURE__ */ jsxs9(Text9, { children: [
3649
+ result.preserved.map((field, i) => /* @__PURE__ */ jsxs10(Text10, { children: [
3070
3650
  " ",
3071
- /* @__PURE__ */ jsx9(Text9, { color: "blue", children: "\u2022" }),
3651
+ /* @__PURE__ */ jsx10(Text10, { color: "blue", children: "\u2022" }),
3072
3652
  " ",
3073
3653
  field
3074
3654
  ] }, i))
3075
3655
  ] }),
3076
- result.migrated && /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", marginTop: 1, children: [
3077
- /* @__PURE__ */ jsx9(Text9, { color: "green", children: "\u2713 Migration complete." }),
3078
- result.backupPath && /* @__PURE__ */ jsxs9(Text9, { children: [
3656
+ result.migrated && /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", marginTop: 1, children: [
3657
+ /* @__PURE__ */ jsx10(Text10, { color: "green", children: "\u2713 Migration complete." }),
3658
+ result.backupPath && /* @__PURE__ */ jsxs10(Text10, { children: [
3079
3659
  " ",
3080
3660
  "Backup saved to: ",
3081
3661
  result.backupPath
@@ -3085,50 +3665,62 @@ var MigrateView = ({ dryRun }) => {
3085
3665
  };
3086
3666
  function registerMigrateCommand(program2) {
3087
3667
  program2.command("migrate").description("Migrate config.yml to match the current schema").option("--dry-run", "Preview changes without writing").action(async (opts) => {
3088
- const { waitUntilExit } = render6(
3089
- /* @__PURE__ */ jsx9(MigrateView, { dryRun: opts.dryRun ?? false })
3668
+ const globalOpts = program2.opts();
3669
+ const startTime = Date.now();
3670
+ if (globalOpts.json) {
3671
+ try {
3672
+ const result = await migrateConfig({ dryRun: opts.dryRun ?? false });
3673
+ respond("migrate", result, startTime, VERSION);
3674
+ } catch (error) {
3675
+ const code = classifyError(error);
3676
+ respondError("migrate", code, error.message, startTime, VERSION);
3677
+ }
3678
+ return;
3679
+ }
3680
+ const { waitUntilExit } = render7(
3681
+ /* @__PURE__ */ jsx10(MigrateView, { dryRun: opts.dryRun ?? false })
3090
3682
  );
3091
3683
  await waitUntilExit();
3092
3684
  });
3093
3685
  }
3094
3686
 
3095
3687
  // src/commands/Provision.tsx
3096
- import { Box as Box9, Text as Text11, useApp as useApp6 } from "ink";
3097
- import { render as render7 } from "ink";
3098
- import { useEffect as useEffect6, useState as useState7 } from "react";
3688
+ import { Box as Box10, Text as Text12, useApp as useApp7 } from "ink";
3689
+ import { render as render8 } from "ink";
3690
+ import { useEffect as useEffect7, useState as useState8 } from "react";
3099
3691
 
3100
3692
  // src/components/StepRunner.tsx
3101
- import { Box as Box8, Static as Static2, Text as Text10 } from "ink";
3693
+ import { Box as Box9, Static as Static2, Text as Text11 } from "ink";
3102
3694
  import Spinner2 from "ink-spinner";
3103
- import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
3695
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
3104
3696
  var StepIcon = ({ status }) => {
3105
3697
  switch (status) {
3106
3698
  case "success":
3107
- return /* @__PURE__ */ jsx10(Text10, { color: "green", children: "\u2713" });
3699
+ return /* @__PURE__ */ jsx11(Text11, { color: "green", children: "\u2713" });
3108
3700
  case "running":
3109
- return /* @__PURE__ */ jsx10(Text10, { color: "yellow", children: /* @__PURE__ */ jsx10(Spinner2, { type: "dots" }) });
3701
+ return /* @__PURE__ */ jsx11(Text11, { color: "yellow", children: /* @__PURE__ */ jsx11(Spinner2, { type: "dots" }) });
3110
3702
  case "skipped":
3111
- return /* @__PURE__ */ jsx10(Text10, { color: "blue", children: "\u23ED" });
3703
+ return /* @__PURE__ */ jsx11(Text11, { color: "blue", children: "\u23ED" });
3112
3704
  case "failed":
3113
- return /* @__PURE__ */ jsx10(Text10, { color: "red", children: "\u2717" });
3705
+ return /* @__PURE__ */ jsx11(Text11, { color: "red", children: "\u2717" });
3114
3706
  case "pending":
3115
3707
  default:
3116
- return /* @__PURE__ */ jsx10(Text10, { color: "gray", children: "\u25CB" });
3708
+ return /* @__PURE__ */ jsx11(Text11, { color: "gray", children: "\u25CB" });
3117
3709
  }
3118
3710
  };
3119
3711
  var StepStatusText = ({ step }) => {
3120
3712
  switch (step.status) {
3121
3713
  case "success":
3122
- return /* @__PURE__ */ jsxs10(Text10, { color: "green", children: [
3714
+ return /* @__PURE__ */ jsxs11(Text11, { color: "green", children: [
3123
3715
  "Done",
3124
3716
  step.duration != null ? ` (${Math.round(step.duration / 1e3)}s)` : ""
3125
3717
  ] });
3126
3718
  case "running":
3127
- return /* @__PURE__ */ jsx10(Text10, { color: "yellow", children: "Running..." });
3719
+ return /* @__PURE__ */ jsx11(Text11, { color: "yellow", children: "Running..." });
3128
3720
  case "skipped":
3129
- return /* @__PURE__ */ jsx10(Text10, { color: "blue", children: "Skipped (already installed)" });
3721
+ return /* @__PURE__ */ jsx11(Text11, { color: "blue", children: "Skipped (already installed)" });
3130
3722
  case "failed":
3131
- return /* @__PURE__ */ jsxs10(Text10, { color: "red", children: [
3723
+ return /* @__PURE__ */ jsxs11(Text11, { color: "red", children: [
3132
3724
  "Failed",
3133
3725
  step.error ? `: ${step.error}` : ""
3134
3726
  ] });
@@ -3142,13 +3734,13 @@ var StepItemView = ({
3142
3734
  index,
3143
3735
  total,
3144
3736
  isLast
3145
- }) => /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", marginBottom: isLast ? 0 : 1, children: [
3146
- /* @__PURE__ */ jsxs10(Text10, { children: [
3737
+ }) => /* @__PURE__ */ jsxs11(Box9, { flexDirection: "column", marginBottom: isLast ? 0 : 1, children: [
3738
+ /* @__PURE__ */ jsxs11(Text11, { children: [
3147
3739
  " ",
3148
- /* @__PURE__ */ jsx10(StepIcon, { status: step.status }),
3149
- /* @__PURE__ */ jsxs10(Text10, { children: [
3740
+ /* @__PURE__ */ jsx11(StepIcon, { status: step.status }),
3741
+ /* @__PURE__ */ jsxs11(Text11, { children: [
3150
3742
  " ",
3151
- /* @__PURE__ */ jsxs10(Text10, { bold: true, children: [
3743
+ /* @__PURE__ */ jsxs11(Text11, { bold: true, children: [
3152
3744
  "Step ",
3153
3745
  index + 1,
3154
3746
  "/",
@@ -3158,13 +3750,13 @@ var StepItemView = ({
3158
3750
  step.name
3159
3751
  ] })
3160
3752
  ] }),
3161
- step.output && step.status !== "pending" && /* @__PURE__ */ jsxs10(Text10, { color: "gray", children: [
3753
+ step.output && step.status !== "pending" && /* @__PURE__ */ jsxs11(Text11, { color: "gray", children: [
3162
3754
  " ",
3163
3755
  step.output
3164
3756
  ] }),
3165
- /* @__PURE__ */ jsxs10(Text10, { children: [
3757
+ /* @__PURE__ */ jsxs11(Text11, { children: [
3166
3758
  " ",
3167
- /* @__PURE__ */ jsx10(StepStatusText, { step })
3759
+ /* @__PURE__ */ jsx11(StepStatusText, { step })
3168
3760
  ] })
3169
3761
  ] });
3170
3762
  var StepRunner = ({ steps, total }) => {
@@ -3179,8 +3771,8 @@ var StepRunner = ({ steps, total }) => {
3179
3771
  }
3180
3772
  });
3181
3773
  const lastIdx = steps.length - 1;
3182
- return /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", children: [
3183
- /* @__PURE__ */ jsx10(Static2, { items: completedSteps, children: (item) => /* @__PURE__ */ jsx10(
3774
+ return /* @__PURE__ */ jsxs11(Box9, { flexDirection: "column", children: [
3775
+ /* @__PURE__ */ jsx11(Static2, { items: completedSteps, children: (item) => /* @__PURE__ */ jsx11(
3184
3776
  StepItemView,
3185
3777
  {
3186
3778
  step: item,
@@ -3190,7 +3782,7 @@ var StepRunner = ({ steps, total }) => {
3190
3782
  },
3191
3783
  item.idx
3192
3784
  ) }),
3193
- activeSteps.map((item) => /* @__PURE__ */ jsx10(
3785
+ activeSteps.map((item) => /* @__PURE__ */ jsx11(
3194
3786
  StepItemView,
3195
3787
  {
3196
3788
  step: item,
@@ -3236,26 +3828,26 @@ ${pc2.red("\u2717")} Sudo authentication failed or was cancelled. Aborting.`
3236
3828
  }
3237
3829
 
3238
3830
  // src/commands/Provision.tsx
3239
- import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
3831
+ import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
3240
3832
  var ProvisionView = ({
3241
3833
  template,
3242
3834
  templatePath,
3243
3835
  options
3244
3836
  }) => {
3245
- const { exit } = useApp6();
3246
- const [phase, setPhase] = useState7(
3837
+ const { exit } = useApp7();
3838
+ const [phase, setPhase] = useState8(
3247
3839
  options.dryRun ? "done" : "running"
3248
3840
  );
3249
- const [steps, setSteps] = useState7(
3841
+ const [steps, setSteps] = useState8(
3250
3842
  template.steps.map((s) => ({
3251
3843
  name: s.name,
3252
3844
  status: "pending",
3253
3845
  output: s.description
3254
3846
  }))
3255
3847
  );
3256
- const [currentStep, setCurrentStep] = useState7(0);
3257
- const [error, setError] = useState7(null);
3258
- useEffect6(() => {
3848
+ const [currentStep, setCurrentStep] = useState8(0);
3849
+ const [error, setError] = useState8(null);
3850
+ useEffect7(() => {
3259
3851
  if (options.dryRun) {
3260
3852
  setTimeout(() => exit(), 100);
3261
3853
  return;
@@ -3297,7 +3889,7 @@ var ProvisionView = ({
3297
3889
  })();
3298
3890
  }, []);
3299
3891
  if (phase === "error" || error) {
3300
- return /* @__PURE__ */ jsx11(Box9, { flexDirection: "column", children: /* @__PURE__ */ jsxs11(Text11, { color: "red", children: [
3892
+ return /* @__PURE__ */ jsx12(Box10, { flexDirection: "column", children: /* @__PURE__ */ jsxs12(Text12, { color: "red", children: [
3301
3893
  "\u2717 ",
3302
3894
  error
3303
3895
  ] }) });
@@ -3305,27 +3897,27 @@ var ProvisionView = ({
3305
3897
  const successCount = steps.filter((s) => s.status === "success").length;
3306
3898
  const skippedCount = steps.filter((s) => s.status === "skipped").length;
3307
3899
  const failedCount = steps.filter((s) => s.status === "failed").length;
3308
- return /* @__PURE__ */ jsxs11(Box9, { flexDirection: "column", children: [
3309
- /* @__PURE__ */ jsxs11(Box9, { flexDirection: "column", marginBottom: 1, children: [
3310
- /* @__PURE__ */ jsxs11(Text11, { bold: true, children: [
3900
+ return /* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", children: [
3901
+ /* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", marginBottom: 1, children: [
3902
+ /* @__PURE__ */ jsxs12(Text12, { bold: true, children: [
3311
3903
  "\u25B8 ",
3312
3904
  template.name
3313
3905
  ] }),
3314
- template.description && /* @__PURE__ */ jsxs11(Text11, { color: "gray", children: [
3906
+ template.description && /* @__PURE__ */ jsxs12(Text12, { color: "gray", children: [
3315
3907
  " ",
3316
3908
  template.description
3317
3909
  ] })
3318
3910
  ] }),
3319
- options.dryRun && phase === "done" && /* @__PURE__ */ jsxs11(Box9, { flexDirection: "column", children: [
3320
- /* @__PURE__ */ jsx11(Text11, { color: "yellow", children: "(dry-run) Showing execution plan only" }),
3321
- template.sudo && /* @__PURE__ */ jsxs11(Text11, { color: "yellow", children: [
3911
+ options.dryRun && phase === "done" && /* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", children: [
3912
+ /* @__PURE__ */ jsx12(Text12, { color: "yellow", children: "(dry-run) Showing execution plan only" }),
3913
+ template.sudo && /* @__PURE__ */ jsxs12(Text12, { color: "yellow", children: [
3322
3914
  " ",
3323
3915
  "\u26A0 This template requires sudo privileges (will prompt on actual run)"
3324
3916
  ] }),
3325
- /* @__PURE__ */ jsx11(Box9, { flexDirection: "column", marginTop: 1, children: template.steps.map((step, idx) => /* @__PURE__ */ jsxs11(Box9, { flexDirection: "column", marginBottom: 1, children: [
3326
- /* @__PURE__ */ jsxs11(Text11, { children: [
3917
+ /* @__PURE__ */ jsx12(Box10, { flexDirection: "column", marginTop: 1, children: template.steps.map((step, idx) => /* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", marginBottom: 1, children: [
3918
+ /* @__PURE__ */ jsxs12(Text12, { children: [
3327
3919
  " ",
3328
- /* @__PURE__ */ jsxs11(Text11, { bold: true, children: [
3920
+ /* @__PURE__ */ jsxs12(Text12, { bold: true, children: [
3329
3921
  "Step ",
3330
3922
  idx + 1,
3331
3923
  "/",
@@ -3334,18 +3926,18 @@ var ProvisionView = ({
3334
3926
  " ",
3335
3927
  step.name
3336
3928
  ] }),
3337
- step.description && /* @__PURE__ */ jsxs11(Text11, { color: "gray", children: [
3929
+ step.description && /* @__PURE__ */ jsxs12(Text12, { color: "gray", children: [
3338
3930
  " ",
3339
3931
  step.description
3340
3932
  ] }),
3341
- step.skip_if && /* @__PURE__ */ jsxs11(Text11, { color: "blue", children: [
3933
+ step.skip_if && /* @__PURE__ */ jsxs12(Text12, { color: "blue", children: [
3342
3934
  " ",
3343
3935
  "Skip condition: ",
3344
3936
  step.skip_if
3345
3937
  ] })
3346
3938
  ] }, idx)) })
3347
3939
  ] }),
3348
- (phase === "running" || phase === "done" && !options.dryRun) && /* @__PURE__ */ jsx11(
3940
+ (phase === "running" || phase === "done" && !options.dryRun) && /* @__PURE__ */ jsx12(
3349
3941
  StepRunner,
3350
3942
  {
3351
3943
  steps,
@@ -3353,34 +3945,34 @@ var ProvisionView = ({
3353
3945
  total: template.steps.length
3354
3946
  }
3355
3947
  ),
3356
- phase === "done" && !options.dryRun && /* @__PURE__ */ jsxs11(Box9, { flexDirection: "column", marginTop: 1, children: [
3357
- /* @__PURE__ */ jsxs11(Text11, { color: "gray", children: [
3948
+ phase === "done" && !options.dryRun && /* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", marginTop: 1, children: [
3949
+ /* @__PURE__ */ jsxs12(Text12, { color: "gray", children: [
3358
3950
  " ",
3359
3951
  "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"
3360
3952
  ] }),
3361
- /* @__PURE__ */ jsxs11(Text11, { children: [
3953
+ /* @__PURE__ */ jsxs12(Text12, { children: [
3362
3954
  " ",
3363
3955
  "Result: ",
3364
- /* @__PURE__ */ jsxs11(Text11, { color: "green", children: [
3956
+ /* @__PURE__ */ jsxs12(Text12, { color: "green", children: [
3365
3957
  successCount,
3366
3958
  " succeeded"
3367
3959
  ] }),
3368
3960
  " \xB7",
3369
3961
  " ",
3370
- /* @__PURE__ */ jsxs11(Text11, { color: "blue", children: [
3962
+ /* @__PURE__ */ jsxs12(Text12, { color: "blue", children: [
3371
3963
  skippedCount,
3372
3964
  " skipped"
3373
3965
  ] }),
3374
3966
  " \xB7",
3375
3967
  " ",
3376
- /* @__PURE__ */ jsxs11(Text11, { color: "red", children: [
3968
+ /* @__PURE__ */ jsxs12(Text12, { color: "red", children: [
3377
3969
  failedCount,
3378
3970
  " failed"
3379
3971
  ] })
3380
3972
  ] }),
3381
- template.backup && !options.skipRestore && /* @__PURE__ */ jsxs11(Box9, { flexDirection: "column", marginTop: 1, children: [
3382
- /* @__PURE__ */ jsx11(Text11, { bold: true, children: "\u25B8 Proceeding with config file restore..." }),
3383
- /* @__PURE__ */ jsxs11(Text11, { color: "gray", children: [
3973
+ template.backup && !options.skipRestore && /* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", marginTop: 1, children: [
3974
+ /* @__PURE__ */ jsx12(Text12, { bold: true, children: "\u25B8 Proceeding with config file restore..." }),
3975
+ /* @__PURE__ */ jsxs12(Text12, { color: "gray", children: [
3384
3976
  " ",
3385
3977
  "Backup link: ",
3386
3978
  template.backup
@@ -3396,14 +3988,24 @@ function registerProvisionCommand(program2) {
3396
3988
  false
3397
3989
  ).option("-f, --file <path>", "Path to template file").action(
3398
3990
  async (templateName, opts) => {
3991
+ const globalOpts = program2.opts();
3992
+ const startTime = Date.now();
3399
3993
  let templatePath;
3400
3994
  if (opts.file) {
3401
3995
  templatePath = resolveTargetPath(opts.file);
3402
3996
  if (!await fileExists(templatePath)) {
3997
+ if (globalOpts.json) {
3998
+ respondError("provision", SyncpointErrorCode.TEMPLATE_NOT_FOUND, `Template file not found: ${opts.file}`, startTime, VERSION);
3999
+ return;
4000
+ }
3403
4001
  console.error(`Template file not found: ${opts.file}`);
3404
4002
  process.exit(1);
3405
4003
  }
3406
4004
  if (!templatePath.endsWith(".yml") && !templatePath.endsWith(".yaml")) {
4005
+ if (globalOpts.json) {
4006
+ respondError("provision", SyncpointErrorCode.INVALID_ARGUMENT, `Template file must have .yml or .yaml extension: ${opts.file}`, startTime, VERSION);
4007
+ return;
4008
+ }
3407
4009
  console.error(
3408
4010
  `Template file must have .yml or .yaml extension: ${opts.file}`
3409
4011
  );
@@ -3415,11 +4017,19 @@ function registerProvisionCommand(program2) {
3415
4017
  (t) => t.name === templateName || t.name === `${templateName}.yml` || t.config.name === templateName
3416
4018
  );
3417
4019
  if (!match) {
4020
+ if (globalOpts.json) {
4021
+ respondError("provision", SyncpointErrorCode.TEMPLATE_NOT_FOUND, `Template not found: ${templateName}`, startTime, VERSION);
4022
+ return;
4023
+ }
3418
4024
  console.error(`Template not found: ${templateName}`);
3419
4025
  process.exit(1);
3420
4026
  }
3421
4027
  templatePath = match.path;
3422
4028
  } else {
4029
+ if (globalOpts.json) {
4030
+ respondError("provision", SyncpointErrorCode.MISSING_ARGUMENT, "Either <template> name or --file option must be provided", startTime, VERSION);
4031
+ return;
4032
+ }
3423
4033
  console.error(
3424
4034
  "Error: Either <template> name or --file option must be provided"
3425
4035
  );
@@ -3431,8 +4041,32 @@ function registerProvisionCommand(program2) {
3431
4041
  if (tmpl.sudo && !opts.dryRun) {
3432
4042
  ensureSudo(tmpl.name);
3433
4043
  }
3434
- const { waitUntilExit } = render7(
3435
- /* @__PURE__ */ jsx11(
4044
+ if (globalOpts.json) {
4045
+ try {
4046
+ const collectedSteps = [];
4047
+ const generator = runProvision(templatePath, {
4048
+ dryRun: opts.dryRun,
4049
+ skipRestore: opts.skipRestore
4050
+ });
4051
+ for await (const result of generator) {
4052
+ if (result.status !== "running") {
4053
+ collectedSteps.push(result);
4054
+ }
4055
+ }
4056
+ respond(
4057
+ "provision",
4058
+ { steps: collectedSteps, totalDuration: Date.now() - startTime },
4059
+ startTime,
4060
+ VERSION
4061
+ );
4062
+ } catch (error) {
4063
+ const code = classifyError(error);
4064
+ respondError("provision", code, error.message, startTime, VERSION);
4065
+ }
4066
+ return;
4067
+ }
4068
+ const { waitUntilExit } = render8(
4069
+ /* @__PURE__ */ jsx12(
3436
4070
  ProvisionView,
3437
4071
  {
3438
4072
  template: tmpl,
@@ -3450,21 +4084,21 @@ function registerProvisionCommand(program2) {
3450
4084
  }
3451
4085
 
3452
4086
  // src/commands/Restore.tsx
3453
- import { Box as Box10, Text as Text12, useApp as useApp7 } from "ink";
3454
- import { render as render8 } from "ink";
4087
+ import { Box as Box11, Text as Text13, useApp as useApp8 } from "ink";
4088
+ import { render as render9 } from "ink";
3455
4089
  import SelectInput2 from "ink-select-input";
3456
- import { useEffect as useEffect7, useState as useState8 } from "react";
3457
- import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
4090
+ import { useEffect as useEffect8, useState as useState9 } from "react";
4091
+ import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
3458
4092
  var RestoreView = ({ filename, options }) => {
3459
- const { exit } = useApp7();
3460
- const [phase, setPhase] = useState8("loading");
3461
- const [backups, setBackups] = useState8([]);
3462
- const [selectedPath, setSelectedPath] = useState8(null);
3463
- const [plan, setPlan] = useState8(null);
3464
- const [result, setResult] = useState8(null);
3465
- const [safetyDone, setSafetyDone] = useState8(false);
3466
- const [error, setError] = useState8(null);
3467
- useEffect7(() => {
4093
+ const { exit } = useApp8();
4094
+ const [phase, setPhase] = useState9("loading");
4095
+ const [backups, setBackups] = useState9([]);
4096
+ const [selectedPath, setSelectedPath] = useState9(null);
4097
+ const [plan, setPlan] = useState9(null);
4098
+ const [result, setResult] = useState9(null);
4099
+ const [safetyDone, setSafetyDone] = useState9(false);
4100
+ const [error, setError] = useState9(null);
4101
+ useEffect8(() => {
3468
4102
  (async () => {
3469
4103
  try {
3470
4104
  const config = await loadConfig();
@@ -3498,7 +4132,7 @@ var RestoreView = ({ filename, options }) => {
3498
4132
  }
3499
4133
  })();
3500
4134
  }, []);
3501
- useEffect7(() => {
4135
+ useEffect8(() => {
3502
4136
  if (phase !== "planning" || !selectedPath) return;
3503
4137
  (async () => {
3504
4138
  try {
@@ -3546,7 +4180,7 @@ var RestoreView = ({ filename, options }) => {
3546
4180
  }
3547
4181
  };
3548
4182
  if (phase === "error" || error) {
3549
- return /* @__PURE__ */ jsx12(Box10, { flexDirection: "column", children: /* @__PURE__ */ jsxs12(Text12, { color: "red", children: [
4183
+ return /* @__PURE__ */ jsx13(Box11, { flexDirection: "column", children: /* @__PURE__ */ jsxs13(Text13, { color: "red", children: [
3550
4184
  "\u2717 ",
3551
4185
  error
3552
4186
  ] }) });
@@ -3557,30 +4191,30 @@ var RestoreView = ({ filename, options }) => {
3557
4191
  }));
3558
4192
  const currentHostname = getHostname();
3559
4193
  const isRemoteBackup = plan?.metadata.hostname && plan.metadata.hostname !== currentHostname;
3560
- return /* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", children: [
3561
- phase === "loading" && /* @__PURE__ */ jsx12(Text12, { children: "\u25B8 Loading backup list..." }),
3562
- phase === "selecting" && /* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", children: [
3563
- /* @__PURE__ */ jsx12(Text12, { bold: true, children: "\u25B8 Select backup" }),
3564
- /* @__PURE__ */ jsx12(SelectInput2, { items: selectItems, onSelect: handleSelect })
4194
+ return /* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", children: [
4195
+ phase === "loading" && /* @__PURE__ */ jsx13(Text13, { children: "\u25B8 Loading backup list..." }),
4196
+ phase === "selecting" && /* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", children: [
4197
+ /* @__PURE__ */ jsx13(Text13, { bold: true, children: "\u25B8 Select backup" }),
4198
+ /* @__PURE__ */ jsx13(SelectInput2, { items: selectItems, onSelect: handleSelect })
3565
4199
  ] }),
3566
- (phase === "planning" || phase === "confirming" || phase === "restoring" || phase === "done" && plan) && plan && /* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", children: [
3567
- /* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", marginBottom: 1, children: [
3568
- /* @__PURE__ */ jsxs12(Text12, { bold: true, children: [
4200
+ (phase === "planning" || phase === "confirming" || phase === "restoring" || phase === "done" && plan) && plan && /* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", children: [
4201
+ /* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", marginBottom: 1, children: [
4202
+ /* @__PURE__ */ jsxs13(Text13, { bold: true, children: [
3569
4203
  "\u25B8 Metadata (",
3570
4204
  plan.metadata.config.filename ?? "",
3571
4205
  ")"
3572
4206
  ] }),
3573
- /* @__PURE__ */ jsxs12(Text12, { children: [
4207
+ /* @__PURE__ */ jsxs13(Text13, { children: [
3574
4208
  " ",
3575
4209
  "Host: ",
3576
4210
  plan.metadata.hostname
3577
4211
  ] }),
3578
- /* @__PURE__ */ jsxs12(Text12, { children: [
4212
+ /* @__PURE__ */ jsxs13(Text13, { children: [
3579
4213
  " ",
3580
4214
  "Created: ",
3581
4215
  plan.metadata.createdAt
3582
4216
  ] }),
3583
- /* @__PURE__ */ jsxs12(Text12, { children: [
4217
+ /* @__PURE__ */ jsxs13(Text13, { children: [
3584
4218
  " ",
3585
4219
  "Files: ",
3586
4220
  plan.metadata.summary.fileCount,
@@ -3588,15 +4222,15 @@ var RestoreView = ({ filename, options }) => {
3588
4222
  formatBytes(plan.metadata.summary.totalSize),
3589
4223
  ")"
3590
4224
  ] }),
3591
- isRemoteBackup && /* @__PURE__ */ jsxs12(Text12, { color: "yellow", children: [
4225
+ isRemoteBackup && /* @__PURE__ */ jsxs13(Text13, { color: "yellow", children: [
3592
4226
  " ",
3593
4227
  "\u26A0 This backup was created on a different machine (",
3594
4228
  plan.metadata.hostname,
3595
4229
  ")"
3596
4230
  ] })
3597
4231
  ] }),
3598
- /* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", marginBottom: 1, children: [
3599
- /* @__PURE__ */ jsx12(Text12, { bold: true, children: "\u25B8 Restore plan:" }),
4232
+ /* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", marginBottom: 1, children: [
4233
+ /* @__PURE__ */ jsx13(Text13, { bold: true, children: "\u25B8 Restore plan:" }),
3600
4234
  plan.actions.map((action, idx) => {
3601
4235
  let icon;
3602
4236
  let color;
@@ -3618,45 +4252,45 @@ var RestoreView = ({ filename, options }) => {
3618
4252
  label = "(not present)";
3619
4253
  break;
3620
4254
  }
3621
- return /* @__PURE__ */ jsxs12(Text12, { children: [
4255
+ return /* @__PURE__ */ jsxs13(Text13, { children: [
3622
4256
  " ",
3623
- /* @__PURE__ */ jsx12(Text12, { color, children: icon.padEnd(8) }),
4257
+ /* @__PURE__ */ jsx13(Text13, { color, children: icon.padEnd(8) }),
3624
4258
  " ",
3625
4259
  contractTilde(action.path),
3626
4260
  " ",
3627
- /* @__PURE__ */ jsx12(Text12, { color: "gray", children: label })
4261
+ /* @__PURE__ */ jsx13(Text13, { color: "gray", children: label })
3628
4262
  ] }, idx);
3629
4263
  })
3630
4264
  ] }),
3631
- options.dryRun && phase === "done" && /* @__PURE__ */ jsx12(Text12, { color: "yellow", children: "(dry-run) No actual restore was performed" })
4265
+ options.dryRun && phase === "done" && /* @__PURE__ */ jsx13(Text13, { color: "yellow", children: "(dry-run) No actual restore was performed" })
3632
4266
  ] }),
3633
- phase === "confirming" && /* @__PURE__ */ jsx12(Box10, { flexDirection: "column", children: /* @__PURE__ */ jsx12(Confirm, { message: "Proceed with restore?", onConfirm: handleConfirm }) }),
3634
- phase === "restoring" && /* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", children: [
3635
- safetyDone && /* @__PURE__ */ jsxs12(Text12, { children: [
3636
- /* @__PURE__ */ jsx12(Text12, { color: "green", children: "\u2713" }),
4267
+ phase === "confirming" && /* @__PURE__ */ jsx13(Box11, { flexDirection: "column", children: /* @__PURE__ */ jsx13(Confirm, { message: "Proceed with restore?", onConfirm: handleConfirm }) }),
4268
+ phase === "restoring" && /* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", children: [
4269
+ safetyDone && /* @__PURE__ */ jsxs13(Text13, { children: [
4270
+ /* @__PURE__ */ jsx13(Text13, { color: "green", children: "\u2713" }),
3637
4271
  " Safety backup of current files complete"
3638
4272
  ] }),
3639
- /* @__PURE__ */ jsx12(Text12, { children: "\u25B8 Restoring..." })
4273
+ /* @__PURE__ */ jsx13(Text13, { children: "\u25B8 Restoring..." })
3640
4274
  ] }),
3641
- phase === "done" && result && !options.dryRun && /* @__PURE__ */ jsxs12(Box10, { flexDirection: "column", marginTop: 1, children: [
3642
- safetyDone && /* @__PURE__ */ jsxs12(Text12, { children: [
3643
- /* @__PURE__ */ jsx12(Text12, { color: "green", children: "\u2713" }),
4275
+ phase === "done" && result && !options.dryRun && /* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", marginTop: 1, children: [
4276
+ safetyDone && /* @__PURE__ */ jsxs13(Text13, { children: [
4277
+ /* @__PURE__ */ jsx13(Text13, { color: "green", children: "\u2713" }),
3644
4278
  " Safety backup of current files complete"
3645
4279
  ] }),
3646
- /* @__PURE__ */ jsx12(Text12, { color: "green", bold: true, children: "\u2713 Restore complete" }),
3647
- /* @__PURE__ */ jsxs12(Text12, { children: [
4280
+ /* @__PURE__ */ jsx13(Text13, { color: "green", bold: true, children: "\u2713 Restore complete" }),
4281
+ /* @__PURE__ */ jsxs13(Text13, { children: [
3648
4282
  " ",
3649
4283
  "Restored: ",
3650
4284
  result.restoredFiles.length,
3651
4285
  " files"
3652
4286
  ] }),
3653
- /* @__PURE__ */ jsxs12(Text12, { children: [
4287
+ /* @__PURE__ */ jsxs13(Text13, { children: [
3654
4288
  " ",
3655
4289
  "Skipped: ",
3656
4290
  result.skippedFiles.length,
3657
4291
  " files"
3658
4292
  ] }),
3659
- result.safetyBackupPath && /* @__PURE__ */ jsxs12(Text12, { children: [
4293
+ result.safetyBackupPath && /* @__PURE__ */ jsxs13(Text13, { children: [
3660
4294
  " ",
3661
4295
  "Safety backup: ",
3662
4296
  contractTilde(result.safetyBackupPath)
@@ -3666,8 +4300,54 @@ var RestoreView = ({ filename, options }) => {
3666
4300
  };
3667
4301
  function registerRestoreCommand(program2) {
3668
4302
  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) => {
3669
- const { waitUntilExit } = render8(
3670
- /* @__PURE__ */ jsx12(RestoreView, { filename, options: { dryRun: opts.dryRun } })
4303
+ const globalOpts = program2.opts();
4304
+ const startTime = Date.now();
4305
+ if (globalOpts.json) {
4306
+ if (!filename) {
4307
+ respondError(
4308
+ "restore",
4309
+ SyncpointErrorCode.MISSING_ARGUMENT,
4310
+ "filename argument is required in --json mode",
4311
+ startTime,
4312
+ VERSION
4313
+ );
4314
+ return;
4315
+ }
4316
+ try {
4317
+ const config = await loadConfig();
4318
+ const list2 = await getBackupList(config);
4319
+ const match = list2.find(
4320
+ (b) => b.filename === filename || b.filename.startsWith(filename)
4321
+ );
4322
+ if (!match) {
4323
+ respondError(
4324
+ "restore",
4325
+ SyncpointErrorCode.RESTORE_FAILED,
4326
+ `Backup not found: ${filename}`,
4327
+ startTime,
4328
+ VERSION
4329
+ );
4330
+ return;
4331
+ }
4332
+ const result = await restoreBackup(match.path, { dryRun: opts.dryRun });
4333
+ respond(
4334
+ "restore",
4335
+ {
4336
+ restoredFiles: result.restoredFiles,
4337
+ skippedFiles: result.skippedFiles,
4338
+ safetyBackupPath: result.safetyBackupPath ?? null
4339
+ },
4340
+ startTime,
4341
+ VERSION
4342
+ );
4343
+ } catch (error) {
4344
+ const code = classifyError(error);
4345
+ respondError("restore", code, error.message, startTime, VERSION);
4346
+ }
4347
+ return;
4348
+ }
4349
+ const { waitUntilExit } = render9(
4350
+ /* @__PURE__ */ jsx13(RestoreView, { filename, options: { dryRun: opts.dryRun } })
3671
4351
  );
3672
4352
  await waitUntilExit();
3673
4353
  });
@@ -3675,12 +4355,12 @@ function registerRestoreCommand(program2) {
3675
4355
 
3676
4356
  // src/commands/Status.tsx
3677
4357
  import { readdirSync, statSync, unlinkSync as unlinkSync2 } from "fs";
3678
- import { join as join12 } from "path";
3679
- import { Box as Box11, Text as Text13, useApp as useApp8, useInput as useInput3 } from "ink";
3680
- import { render as render9 } from "ink";
4358
+ import { join as join13 } from "path";
4359
+ import { Box as Box12, Text as Text14, useApp as useApp9, useInput as useInput3 } from "ink";
4360
+ import { render as render10 } from "ink";
3681
4361
  import SelectInput3 from "ink-select-input";
3682
- import { useEffect as useEffect8, useState as useState9 } from "react";
3683
- import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
4362
+ import { useEffect as useEffect9, useState as useState10 } from "react";
4363
+ import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
3684
4364
  function getDirStats(dirPath) {
3685
4365
  try {
3686
4366
  const entries = readdirSync(dirPath);
@@ -3688,9 +4368,9 @@ function getDirStats(dirPath) {
3688
4368
  let count = 0;
3689
4369
  for (const entry of entries) {
3690
4370
  try {
3691
- const stat4 = statSync(join12(dirPath, entry));
3692
- if (stat4.isFile()) {
3693
- totalSize += stat4.size;
4371
+ const stat5 = statSync(join13(dirPath, entry));
4372
+ if (stat5.isFile()) {
4373
+ totalSize += stat5.size;
3694
4374
  count++;
3695
4375
  }
3696
4376
  } catch {
@@ -3702,34 +4382,34 @@ function getDirStats(dirPath) {
3702
4382
  }
3703
4383
  }
3704
4384
  var DisplayActionItem = ({ isSelected = false, label }) => {
3705
- return /* @__PURE__ */ jsx13(Text13, { bold: isSelected, children: label });
4385
+ return /* @__PURE__ */ jsx14(Text14, { bold: isSelected, children: label });
3706
4386
  };
3707
4387
  var CleanupActionItem = ({ isSelected = false, label }) => {
3708
4388
  if (label === "Cancel" || label === "Select specific backups to delete") {
3709
- return /* @__PURE__ */ jsx13(Text13, { bold: isSelected, children: label });
4389
+ return /* @__PURE__ */ jsx14(Text14, { bold: isSelected, children: label });
3710
4390
  }
3711
4391
  const parts = label.split(/\s{2,}/);
3712
4392
  if (parts.length === 2) {
3713
- return /* @__PURE__ */ jsxs13(Text13, { bold: isSelected, children: [
4393
+ return /* @__PURE__ */ jsxs14(Text14, { bold: isSelected, children: [
3714
4394
  parts[0],
3715
4395
  " ",
3716
- /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: parts[1] })
4396
+ /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: parts[1] })
3717
4397
  ] });
3718
4398
  }
3719
- return /* @__PURE__ */ jsx13(Text13, { bold: isSelected, children: label });
4399
+ return /* @__PURE__ */ jsx14(Text14, { bold: isSelected, children: label });
3720
4400
  };
3721
4401
  var StatusView = ({ cleanup }) => {
3722
- const { exit } = useApp8();
3723
- const [phase, setPhase] = useState9("loading");
3724
- const [status, setStatus] = useState9(null);
3725
- const [backups, setBackups] = useState9([]);
3726
- const [cleanupAction, setCleanupAction] = useState9(null);
3727
- const [cleanupMessage, setCleanupMessage] = useState9("");
3728
- const [error, setError] = useState9(null);
3729
- const [selectedForDeletion, setSelectedForDeletion] = useState9(
4402
+ const { exit } = useApp9();
4403
+ const [phase, setPhase] = useState10("loading");
4404
+ const [status, setStatus] = useState10(null);
4405
+ const [backups, setBackups] = useState10([]);
4406
+ const [cleanupAction, setCleanupAction] = useState10(null);
4407
+ const [cleanupMessage, setCleanupMessage] = useState10("");
4408
+ const [error, setError] = useState10(null);
4409
+ const [selectedForDeletion, setSelectedForDeletion] = useState10(
3730
4410
  []
3731
4411
  );
3732
- const [backupDir, setBackupDir] = useState9(getSubDir("backups"));
4412
+ const [backupDir, setBackupDir] = useState10(getSubDir("backups"));
3733
4413
  useInput3((_input, key) => {
3734
4414
  if (!key.escape) return;
3735
4415
  if (phase === "display") {
@@ -3741,7 +4421,7 @@ var StatusView = ({ cleanup }) => {
3741
4421
  setPhase("cleanup");
3742
4422
  }
3743
4423
  });
3744
- useEffect8(() => {
4424
+ useEffect9(() => {
3745
4425
  (async () => {
3746
4426
  try {
3747
4427
  const config = await loadConfig();
@@ -3840,7 +4520,7 @@ var StatusView = ({ cleanup }) => {
3840
4520
  try {
3841
4521
  const entries = readdirSync(logsDir);
3842
4522
  for (const entry of entries) {
3843
- const logPath = join12(logsDir, entry);
4523
+ const logPath = join13(logsDir, entry);
3844
4524
  if (!isInsideDir(logPath, logsDir))
3845
4525
  throw new Error(
3846
4526
  `Refusing to delete file outside logs directory: ${logPath}`
@@ -3890,26 +4570,26 @@ var StatusView = ({ cleanup }) => {
3890
4570
  }
3891
4571
  };
3892
4572
  if (phase === "error" || error) {
3893
- return /* @__PURE__ */ jsx13(Box11, { flexDirection: "column", children: /* @__PURE__ */ jsxs13(Text13, { color: "red", children: [
4573
+ return /* @__PURE__ */ jsx14(Box12, { flexDirection: "column", children: /* @__PURE__ */ jsxs14(Text14, { color: "red", children: [
3894
4574
  "\u2717 ",
3895
4575
  error
3896
4576
  ] }) });
3897
4577
  }
3898
4578
  if (phase === "loading") {
3899
- return /* @__PURE__ */ jsx13(Text13, { color: "cyan", children: "Loading..." });
4579
+ return /* @__PURE__ */ jsx14(Text14, { color: "cyan", children: "Loading..." });
3900
4580
  }
3901
4581
  if (!status) return null;
3902
4582
  const totalCount = status.backups.count + status.templates.count + status.scripts.count + status.logs.count;
3903
4583
  const totalSize = status.backups.totalSize + status.templates.totalSize + status.scripts.totalSize + status.logs.totalSize;
3904
- const statusDisplay = /* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", children: [
3905
- /* @__PURE__ */ jsxs13(Text13, { bold: true, children: [
4584
+ const statusDisplay = /* @__PURE__ */ jsxs14(Box12, { flexDirection: "column", children: [
4585
+ /* @__PURE__ */ jsxs14(Text14, { bold: true, children: [
3906
4586
  "\u25B8 ",
3907
4587
  APP_NAME,
3908
4588
  " status \u2014 ~/.",
3909
4589
  APP_NAME,
3910
4590
  "/"
3911
4591
  ] }),
3912
- /* @__PURE__ */ jsx13(Box11, { marginLeft: 2, marginTop: 1, children: /* @__PURE__ */ jsx13(
4592
+ /* @__PURE__ */ jsx14(Box12, { marginLeft: 2, marginTop: 1, children: /* @__PURE__ */ jsx14(
3913
4593
  Table,
3914
4594
  {
3915
4595
  headers: ["Directory", "Count", "Size"],
@@ -3938,15 +4618,15 @@ var StatusView = ({ cleanup }) => {
3938
4618
  ]
3939
4619
  }
3940
4620
  ) }),
3941
- status.lastBackup && /* @__PURE__ */ jsxs13(Box11, { marginTop: 1, marginLeft: 2, flexDirection: "column", children: [
3942
- /* @__PURE__ */ jsxs13(Text13, { children: [
4621
+ status.lastBackup && /* @__PURE__ */ jsxs14(Box12, { marginTop: 1, marginLeft: 2, flexDirection: "column", children: [
4622
+ /* @__PURE__ */ jsxs14(Text14, { children: [
3943
4623
  "Latest backup: ",
3944
4624
  formatDate(status.lastBackup),
3945
4625
  " (",
3946
4626
  formatRelativeTime(status.lastBackup),
3947
4627
  ")"
3948
4628
  ] }),
3949
- status.oldestBackup && /* @__PURE__ */ jsxs13(Text13, { children: [
4629
+ status.oldestBackup && /* @__PURE__ */ jsxs14(Text14, { children: [
3950
4630
  "Oldest backup: ",
3951
4631
  formatDate(status.oldestBackup),
3952
4632
  " (",
@@ -3955,18 +4635,18 @@ var StatusView = ({ cleanup }) => {
3955
4635
  ] })
3956
4636
  ] })
3957
4637
  ] });
3958
- const escHint = (action) => /* @__PURE__ */ jsx13(Box11, { marginTop: 1, children: /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
4638
+ const escHint = (action) => /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsxs14(Text14, { dimColor: true, children: [
3959
4639
  "Press ",
3960
- /* @__PURE__ */ jsx13(Text13, { bold: true, children: "ESC" }),
4640
+ /* @__PURE__ */ jsx14(Text14, { bold: true, children: "ESC" }),
3961
4641
  " to ",
3962
4642
  action
3963
4643
  ] }) });
3964
4644
  if (phase === "display") {
3965
- return /* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", children: [
4645
+ return /* @__PURE__ */ jsxs14(Box12, { flexDirection: "column", children: [
3966
4646
  statusDisplay,
3967
- /* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", marginTop: 1, children: [
3968
- /* @__PURE__ */ jsx13(Text13, { bold: true, children: "\u25B8 Actions" }),
3969
- /* @__PURE__ */ jsx13(
4647
+ /* @__PURE__ */ jsxs14(Box12, { flexDirection: "column", marginTop: 1, children: [
4648
+ /* @__PURE__ */ jsx14(Text14, { bold: true, children: "\u25B8 Actions" }),
4649
+ /* @__PURE__ */ jsx14(
3970
4650
  SelectInput3,
3971
4651
  {
3972
4652
  items: [
@@ -4014,11 +4694,11 @@ var StatusView = ({ cleanup }) => {
4014
4694
  value: "cancel"
4015
4695
  }
4016
4696
  ];
4017
- return /* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", children: [
4697
+ return /* @__PURE__ */ jsxs14(Box12, { flexDirection: "column", children: [
4018
4698
  statusDisplay,
4019
- /* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", marginTop: 1, children: [
4020
- /* @__PURE__ */ jsx13(Text13, { bold: true, children: "\u25B8 Cleanup options" }),
4021
- /* @__PURE__ */ jsx13(
4699
+ /* @__PURE__ */ jsxs14(Box12, { flexDirection: "column", marginTop: 1, children: [
4700
+ /* @__PURE__ */ jsx14(Text14, { bold: true, children: "\u25B8 Cleanup options" }),
4701
+ /* @__PURE__ */ jsx14(
4022
4702
  SelectInput3,
4023
4703
  {
4024
4704
  items: cleanupItems,
@@ -4044,26 +4724,26 @@ var StatusView = ({ cleanup }) => {
4044
4724
  value: "done"
4045
4725
  }
4046
4726
  ];
4047
- return /* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", children: [
4727
+ return /* @__PURE__ */ jsxs14(Box12, { flexDirection: "column", children: [
4048
4728
  statusDisplay,
4049
- /* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", marginTop: 1, children: [
4050
- /* @__PURE__ */ jsx13(Text13, { bold: true, children: "\u25B8 Select backups to delete" }),
4051
- selectedForDeletion.length > 0 && /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
4729
+ /* @__PURE__ */ jsxs14(Box12, { flexDirection: "column", marginTop: 1, children: [
4730
+ /* @__PURE__ */ jsx14(Text14, { bold: true, children: "\u25B8 Select backups to delete" }),
4731
+ selectedForDeletion.length > 0 && /* @__PURE__ */ jsxs14(Text14, { dimColor: true, children: [
4052
4732
  " ",
4053
4733
  selectedForDeletion.length,
4054
4734
  " backup(s) selected (",
4055
4735
  formatBytes(selectedForDeletion.reduce((s, b) => s + b.size, 0)),
4056
4736
  ")"
4057
4737
  ] }),
4058
- /* @__PURE__ */ jsx13(SelectInput3, { items: selectItems, onSelect: handleSelectBackup })
4738
+ /* @__PURE__ */ jsx14(SelectInput3, { items: selectItems, onSelect: handleSelectBackup })
4059
4739
  ] }),
4060
4740
  escHint("go back")
4061
4741
  ] });
4062
4742
  }
4063
4743
  if (phase === "confirming") {
4064
- return /* @__PURE__ */ jsxs13(Box11, { flexDirection: "column", children: [
4065
- /* @__PURE__ */ jsx13(Text13, { children: cleanupMessage }),
4066
- /* @__PURE__ */ jsx13(
4744
+ return /* @__PURE__ */ jsxs14(Box12, { flexDirection: "column", children: [
4745
+ /* @__PURE__ */ jsx14(Text14, { children: cleanupMessage }),
4746
+ /* @__PURE__ */ jsx14(
4067
4747
  Confirm,
4068
4748
  {
4069
4749
  message: "Proceed?",
@@ -4074,13 +4754,127 @@ var StatusView = ({ cleanup }) => {
4074
4754
  ] });
4075
4755
  }
4076
4756
  if (phase === "done") {
4077
- return /* @__PURE__ */ jsx13(Box11, { flexDirection: "column", children: /* @__PURE__ */ jsx13(Text13, { color: "green", children: "\u2713 Cleanup complete" }) });
4757
+ return /* @__PURE__ */ jsx14(Box12, { flexDirection: "column", children: /* @__PURE__ */ jsx14(Text14, { color: "green", children: "\u2713 Cleanup complete" }) });
4078
4758
  }
4079
4759
  return null;
4080
4760
  };
4081
4761
  function registerStatusCommand(program2) {
4082
4762
  program2.command("status").description(`Show ~/.${APP_NAME}/ status summary`).option("--cleanup", "Interactive cleanup mode", false).action(async (opts) => {
4083
- const { waitUntilExit } = render9(/* @__PURE__ */ jsx13(StatusView, { cleanup: opts.cleanup }));
4763
+ const globalOpts = program2.opts();
4764
+ const startTime = Date.now();
4765
+ if (globalOpts.json) {
4766
+ try {
4767
+ const config = await loadConfig();
4768
+ const backupDirectory = config.backup.destination ? resolveTargetPath(config.backup.destination) : getSubDir("backups");
4769
+ const backupStats = getDirStats(backupDirectory);
4770
+ const templateStats = getDirStats(getSubDir("templates"));
4771
+ const scriptStats = getDirStats(getSubDir("scripts"));
4772
+ const logStats = getDirStats(getSubDir("logs"));
4773
+ const backupList = await getBackupList(config);
4774
+ const lastBackup = backupList.length > 0 ? backupList[0].createdAt : null;
4775
+ const oldestBackup = backupList.length > 0 ? backupList[backupList.length - 1].createdAt : null;
4776
+ const statusInfo = {
4777
+ backups: backupStats,
4778
+ templates: templateStats,
4779
+ scripts: scriptStats,
4780
+ logs: logStats,
4781
+ lastBackup: lastBackup ?? void 0,
4782
+ oldestBackup: oldestBackup ?? void 0
4783
+ };
4784
+ respond("status", statusInfo, startTime, VERSION);
4785
+ } catch (error) {
4786
+ const code = classifyError(error);
4787
+ respondError("status", code, error.message, startTime, VERSION);
4788
+ }
4789
+ return;
4790
+ }
4791
+ const { waitUntilExit } = render10(/* @__PURE__ */ jsx14(StatusView, { cleanup: opts.cleanup }));
4792
+ await waitUntilExit();
4793
+ });
4794
+ }
4795
+
4796
+ // src/commands/Unlink.tsx
4797
+ import { Box as Box13, Text as Text15, useApp as useApp10 } from "ink";
4798
+ import { render as render11 } from "ink";
4799
+ import { useEffect as useEffect10, useState as useState11 } from "react";
4800
+ import { Fragment as Fragment3, jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
4801
+ var UnlinkView = ({ clean }) => {
4802
+ const { exit } = useApp10();
4803
+ const [phase, setPhase] = useState11("unlinking");
4804
+ const [result, setResult] = useState11(null);
4805
+ const [error, setError] = useState11(null);
4806
+ useEffect10(() => {
4807
+ (async () => {
4808
+ try {
4809
+ const unlinkResult = await unlinkSyncpoint({ clean });
4810
+ setResult(unlinkResult);
4811
+ setPhase("done");
4812
+ setTimeout(() => exit(), 100);
4813
+ } catch (err) {
4814
+ setError(err instanceof Error ? err.message : String(err));
4815
+ setPhase("error");
4816
+ setTimeout(() => exit(), 100);
4817
+ }
4818
+ })();
4819
+ }, []);
4820
+ if (phase === "error" || error) {
4821
+ return /* @__PURE__ */ jsx15(Box13, { flexDirection: "column", children: /* @__PURE__ */ jsxs15(Text15, { color: "red", children: [
4822
+ "\u2717 Unlink failed: ",
4823
+ error
4824
+ ] }) });
4825
+ }
4826
+ if (phase === "unlinking") {
4827
+ return /* @__PURE__ */ jsx15(Box13, { children: /* @__PURE__ */ jsx15(Text15, { children: "\u25B8 Restoring ~/.syncpoint from destination..." }) });
4828
+ }
4829
+ return /* @__PURE__ */ jsxs15(Box13, { flexDirection: "column", children: [
4830
+ /* @__PURE__ */ jsx15(Text15, { color: "green", bold: true, children: "\u2713 Unlink complete" }),
4831
+ result && /* @__PURE__ */ jsxs15(Fragment3, { children: [
4832
+ /* @__PURE__ */ jsxs15(Text15, { children: [
4833
+ " ",
4834
+ "Restored: ",
4835
+ contractTilde(result.appDir)
4836
+ ] }),
4837
+ /* @__PURE__ */ jsxs15(Text15, { children: [
4838
+ " ",
4839
+ "Source: ",
4840
+ contractTilde(result.targetDir)
4841
+ ] }),
4842
+ result.cleaned && /* @__PURE__ */ jsxs15(Text15, { color: "yellow", children: [
4843
+ " ",
4844
+ "Destination copy removed (--clean)"
4845
+ ] })
4846
+ ] })
4847
+ ] });
4848
+ };
4849
+ function registerUnlinkCommand(program2) {
4850
+ const cmdInfo = COMMANDS.unlink;
4851
+ const cmd = program2.command("unlink").description(cmdInfo.description);
4852
+ cmdInfo.options?.forEach((opt) => {
4853
+ cmd.option(opt.flag, opt.description);
4854
+ });
4855
+ cmd.action(async (opts) => {
4856
+ const globalOpts = program2.opts();
4857
+ const startTime = Date.now();
4858
+ if (globalOpts.json) {
4859
+ try {
4860
+ const unlinkResult = await unlinkSyncpoint({ clean: opts.clean });
4861
+ respond(
4862
+ "unlink",
4863
+ {
4864
+ appDir: unlinkResult.appDir,
4865
+ targetDir: unlinkResult.targetDir,
4866
+ cleaned: unlinkResult.cleaned
4867
+ },
4868
+ startTime,
4869
+ VERSION
4870
+ );
4871
+ } catch (error) {
4872
+ const code = classifyError(error);
4873
+ respondError("unlink", code, error.message, startTime, VERSION);
4874
+ }
4875
+ return;
4876
+ }
4877
+ const { waitUntilExit } = render11(/* @__PURE__ */ jsx15(UnlinkView, { clean: opts.clean ?? false }));
4084
4878
  await waitUntilExit();
4085
4879
  });
4086
4880
  }
@@ -4089,15 +4883,15 @@ function registerStatusCommand(program2) {
4089
4883
  import {
4090
4884
  copyFile as copyFile3,
4091
4885
  readFile as readFile6,
4092
- rename,
4093
- unlink,
4886
+ rename as rename2,
4887
+ unlink as unlink2,
4094
4888
  writeFile as writeFile5
4095
4889
  } from "fs/promises";
4096
- import { join as join14 } from "path";
4097
- import { Box as Box12, Text as Text14, useApp as useApp9 } from "ink";
4098
- import { render as render10 } from "ink";
4890
+ import { join as join15 } from "path";
4891
+ import { Box as Box14, Text as Text16, useApp as useApp11 } from "ink";
4892
+ import { render as render12 } from "ink";
4099
4893
  import Spinner3 from "ink-spinner";
4100
- import { useEffect as useEffect9, useState as useState10 } from "react";
4894
+ import { useEffect as useEffect11, useState as useState12 } from "react";
4101
4895
 
4102
4896
  // src/prompts/wizard-config.ts
4103
4897
  function generateConfigWizardPrompt(variables) {
@@ -4137,9 +4931,12 @@ ${variables.defaultConfig}
4137
4931
  **Start by greeting the user and asking about their backup priorities. After understanding their needs, write the config.yml file directly.**`;
4138
4932
  }
4139
4933
 
4934
+ // src/commands/Wizard.tsx
4935
+ init_assets();
4936
+
4140
4937
  // src/utils/file-scanner.ts
4141
- import { stat as stat3 } from "fs/promises";
4142
- import { join as join13 } from "path";
4938
+ import { stat as stat4 } from "fs/promises";
4939
+ import { join as join14 } from "path";
4143
4940
  import glob from "fast-glob";
4144
4941
  var FILE_CATEGORIES = {
4145
4942
  shell: {
@@ -4218,8 +5015,8 @@ async function scanHomeDirectory(options) {
4218
5015
  const validFiles = [];
4219
5016
  for (const file of files) {
4220
5017
  try {
4221
- const fullPath = join13(homeDir, file);
4222
- await stat3(fullPath);
5018
+ const fullPath = join14(homeDir, file);
5019
+ await stat4(fullPath);
4223
5020
  validFiles.push(file);
4224
5021
  categorizedFiles.add(file);
4225
5022
  } catch {
@@ -4253,8 +5050,8 @@ async function scanHomeDirectory(options) {
4253
5050
  if (categorizedFiles.has(file)) continue;
4254
5051
  if (totalFiles >= maxFiles) break;
4255
5052
  try {
4256
- const fullPath = join13(homeDir, file);
4257
- await stat3(fullPath);
5053
+ const fullPath = join14(homeDir, file);
5054
+ await stat4(fullPath);
4258
5055
  uncategorizedFiles.push(file);
4259
5056
  totalFiles++;
4260
5057
  } catch {
@@ -4278,7 +5075,7 @@ async function scanHomeDirectory(options) {
4278
5075
  }
4279
5076
 
4280
5077
  // src/commands/Wizard.tsx
4281
- import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
5078
+ import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
4282
5079
  var MAX_RETRIES2 = 3;
4283
5080
  async function restoreBackup2(configPath) {
4284
5081
  const bakPath = `${configPath}.bak`;
@@ -4328,21 +5125,21 @@ ${formatValidationErrors(validation.errors || [])}`
4328
5125
  }
4329
5126
  }
4330
5127
  var WizardView = ({ printMode }) => {
4331
- const { exit } = useApp9();
4332
- const [phase, setPhase] = useState10("init");
4333
- const [message, setMessage] = useState10("");
4334
- const [error, setError] = useState10(null);
4335
- const [prompt, setPrompt] = useState10("");
4336
- const [sessionId, setSessionId] = useState10(void 0);
4337
- const [attemptNumber, setAttemptNumber] = useState10(1);
4338
- useEffect9(() => {
5128
+ const { exit } = useApp11();
5129
+ const [phase, setPhase] = useState12("init");
5130
+ const [message, setMessage] = useState12("");
5131
+ const [error, setError] = useState12(null);
5132
+ const [prompt, setPrompt] = useState12("");
5133
+ const [sessionId, setSessionId] = useState12(void 0);
5134
+ const [attemptNumber, setAttemptNumber] = useState12(1);
5135
+ useEffect11(() => {
4339
5136
  (async () => {
4340
5137
  try {
4341
- const configPath = join14(getAppDir(), CONFIG_FILENAME);
5138
+ const configPath = join15(getAppDir(), CONFIG_FILENAME);
4342
5139
  if (await fileExists(configPath)) {
4343
5140
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4344
5141
  const bakPath = `${configPath}.${timestamp2}.bak`;
4345
- await rename(configPath, bakPath);
5142
+ await rename2(configPath, bakPath);
4346
5143
  setMessage(`Backed up existing config to ${bakPath}`);
4347
5144
  }
4348
5145
  setPhase("scanning");
@@ -4408,9 +5205,9 @@ var WizardView = ({ printMode }) => {
4408
5205
  await writeFile5(tmpPath, yamlContent, "utf-8");
4409
5206
  const verification = validateConfig(parseYAML(yamlContent));
4410
5207
  if (verification.valid) {
4411
- await rename(tmpPath, configPath);
5208
+ await rename2(tmpPath, configPath);
4412
5209
  } else {
4413
- await unlink(tmpPath);
5210
+ await unlink2(tmpPath);
4414
5211
  throw new Error("Final validation failed");
4415
5212
  }
4416
5213
  setPhase("done");
@@ -4449,37 +5246,37 @@ ${formatValidationErrors(validation.errors || [])}`
4449
5246
  }
4450
5247
  }
4451
5248
  if (error) {
4452
- return /* @__PURE__ */ jsx14(Box12, { flexDirection: "column", children: /* @__PURE__ */ jsxs14(Text14, { color: "red", children: [
5249
+ return /* @__PURE__ */ jsx16(Box14, { flexDirection: "column", children: /* @__PURE__ */ jsxs16(Text16, { color: "red", children: [
4453
5250
  "\u2717 ",
4454
5251
  error
4455
5252
  ] }) });
4456
5253
  }
4457
5254
  if (printMode && phase === "done") {
4458
- return /* @__PURE__ */ jsxs14(Box12, { flexDirection: "column", children: [
4459
- /* @__PURE__ */ jsx14(Text14, { bold: true, children: "Config Wizard Prompt (Copy and paste to your LLM):" }),
4460
- /* @__PURE__ */ jsx14(Box12, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: "\u2500".repeat(60) }) }),
4461
- /* @__PURE__ */ jsx14(Text14, { children: prompt }),
4462
- /* @__PURE__ */ jsx14(Box12, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: "\u2500".repeat(60) }) }),
4463
- /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: "After getting the YAML response, save it to ~/.syncpoint/config.yml" })
5255
+ return /* @__PURE__ */ jsxs16(Box14, { flexDirection: "column", children: [
5256
+ /* @__PURE__ */ jsx16(Text16, { bold: true, children: "Config Wizard Prompt (Copy and paste to your LLM):" }),
5257
+ /* @__PURE__ */ jsx16(Box14, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx16(Text16, { dimColor: true, children: "\u2500".repeat(60) }) }),
5258
+ /* @__PURE__ */ jsx16(Text16, { children: prompt }),
5259
+ /* @__PURE__ */ jsx16(Box14, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx16(Text16, { dimColor: true, children: "\u2500".repeat(60) }) }),
5260
+ /* @__PURE__ */ jsx16(Text16, { dimColor: true, children: "After getting the YAML response, save it to ~/.syncpoint/config.yml" })
4464
5261
  ] });
4465
5262
  }
4466
5263
  if (phase === "done") {
4467
- return /* @__PURE__ */ jsxs14(Box12, { flexDirection: "column", children: [
4468
- /* @__PURE__ */ jsx14(Text14, { color: "green", children: message }),
4469
- /* @__PURE__ */ jsxs14(Box12, { marginTop: 1, children: [
4470
- /* @__PURE__ */ jsx14(Text14, { children: "Next steps:" }),
4471
- /* @__PURE__ */ jsx14(Text14, { children: " 1. Review your config: ~/.syncpoint/config.yml" }),
4472
- /* @__PURE__ */ jsx14(Text14, { children: " 2. Run: syncpoint backup" })
5264
+ return /* @__PURE__ */ jsxs16(Box14, { flexDirection: "column", children: [
5265
+ /* @__PURE__ */ jsx16(Text16, { color: "green", children: message }),
5266
+ /* @__PURE__ */ jsxs16(Box14, { marginTop: 1, children: [
5267
+ /* @__PURE__ */ jsx16(Text16, { children: "Next steps:" }),
5268
+ /* @__PURE__ */ jsx16(Text16, { children: " 1. Review your config: ~/.syncpoint/config.yml" }),
5269
+ /* @__PURE__ */ jsx16(Text16, { children: " 2. Run: syncpoint backup" })
4473
5270
  ] })
4474
5271
  ] });
4475
5272
  }
4476
- return /* @__PURE__ */ jsxs14(Box12, { flexDirection: "column", children: [
4477
- /* @__PURE__ */ jsxs14(Text14, { children: [
4478
- /* @__PURE__ */ jsx14(Text14, { color: "cyan", children: /* @__PURE__ */ jsx14(Spinner3, { type: "dots" }) }),
5273
+ return /* @__PURE__ */ jsxs16(Box14, { flexDirection: "column", children: [
5274
+ /* @__PURE__ */ jsxs16(Text16, { children: [
5275
+ /* @__PURE__ */ jsx16(Text16, { color: "cyan", children: /* @__PURE__ */ jsx16(Spinner3, { type: "dots" }) }),
4479
5276
  " ",
4480
5277
  message
4481
5278
  ] }),
4482
- attemptNumber > 1 && /* @__PURE__ */ jsxs14(Text14, { dimColor: true, children: [
5279
+ attemptNumber > 1 && /* @__PURE__ */ jsxs16(Text16, { dimColor: true, children: [
4483
5280
  "Attempt ",
4484
5281
  attemptNumber,
4485
5282
  "/",
@@ -4494,18 +5291,39 @@ function registerWizardCommand(program2) {
4494
5291
  cmd.option(opt.flag, opt.description);
4495
5292
  });
4496
5293
  cmd.action(async (opts) => {
5294
+ const globalOpts = program2.opts();
5295
+ const startTime = Date.now();
5296
+ if (globalOpts.json) {
5297
+ if (!opts.print) {
5298
+ respondError(
5299
+ "wizard",
5300
+ SyncpointErrorCode.MISSING_ARGUMENT,
5301
+ "--print is required in --json mode (interactive mode requires a terminal)",
5302
+ startTime,
5303
+ VERSION
5304
+ );
5305
+ return;
5306
+ }
5307
+ try {
5308
+ const scanResult = await runScanPhase();
5309
+ respond("wizard", { prompt: scanResult.prompt }, startTime, VERSION);
5310
+ } catch (err) {
5311
+ respondError("wizard", SyncpointErrorCode.UNKNOWN, err.message, startTime, VERSION);
5312
+ }
5313
+ return;
5314
+ }
4497
5315
  if (opts.print) {
4498
- const { waitUntilExit } = render10(/* @__PURE__ */ jsx14(WizardView, { printMode: true }));
5316
+ const { waitUntilExit } = render12(/* @__PURE__ */ jsx16(WizardView, { printMode: true }));
4499
5317
  await waitUntilExit();
4500
5318
  return;
4501
5319
  }
4502
- const configPath = join14(getAppDir(), CONFIG_FILENAME);
5320
+ const configPath = join15(getAppDir(), CONFIG_FILENAME);
4503
5321
  try {
4504
5322
  if (await fileExists(configPath)) {
4505
5323
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4506
5324
  const bakPath = `${configPath}.${timestamp2}.bak`;
4507
5325
  console.log(`\u{1F4CB} Backing up existing config to ${bakPath}`);
4508
- await rename(configPath, bakPath);
5326
+ await rename2(configPath, bakPath);
4509
5327
  }
4510
5328
  if (!await isClaudeCodeAvailable()) {
4511
5329
  throw new Error(
@@ -4530,7 +5348,7 @@ function registerWizardCommand(program2) {
4530
5348
  var program = new Command();
4531
5349
  program.name("syncpoint").description(
4532
5350
  "Personal Environment Manager \u2014 Config backup/restore and machine provisioning CLI"
4533
- ).version(VERSION);
5351
+ ).version(VERSION).option("--json", "Output structured JSON to stdout").option("--yes", "Skip confirmation prompts (non-interactive mode)");
4534
5352
  registerInitCommand(program);
4535
5353
  registerWizardCommand(program);
4536
5354
  registerBackupCommand(program);
@@ -4540,8 +5358,63 @@ registerCreateTemplateCommand(program);
4540
5358
  registerListCommand(program);
4541
5359
  registerMigrateCommand(program);
4542
5360
  registerStatusCommand(program);
5361
+ registerLinkCommand(program);
5362
+ registerUnlinkCommand(program);
4543
5363
  registerHelpCommand(program);
5364
+ if (process.argv.includes("--describe")) {
5365
+ const startTime = Date.now();
5366
+ const globalOptions = [
5367
+ {
5368
+ flag: "--json",
5369
+ description: "Output structured JSON to stdout",
5370
+ type: "boolean"
5371
+ },
5372
+ {
5373
+ flag: "--yes",
5374
+ description: "Skip confirmation prompts (non-interactive mode)",
5375
+ type: "boolean"
5376
+ },
5377
+ {
5378
+ flag: "--describe",
5379
+ description: "Print CLI schema as JSON and exit",
5380
+ type: "boolean"
5381
+ },
5382
+ {
5383
+ flag: "-V, --version",
5384
+ description: "Output the version number",
5385
+ type: "boolean"
5386
+ },
5387
+ {
5388
+ flag: "-h, --help",
5389
+ description: "Display help for command",
5390
+ type: "boolean"
5391
+ }
5392
+ ];
5393
+ respond(
5394
+ "describe",
5395
+ {
5396
+ name: "syncpoint",
5397
+ version: VERSION,
5398
+ description: program.description(),
5399
+ globalOptions,
5400
+ commands: COMMANDS
5401
+ },
5402
+ startTime,
5403
+ VERSION
5404
+ );
5405
+ process.exit(0);
5406
+ }
4544
5407
  program.parseAsync(process.argv).catch((error) => {
5408
+ if (process.argv.includes("--json")) {
5409
+ respondError(
5410
+ "unknown",
5411
+ SyncpointErrorCode.UNKNOWN,
5412
+ error.message,
5413
+ Date.now(),
5414
+ VERSION
5415
+ );
5416
+ process.exit(1);
5417
+ }
4545
5418
  console.error("Fatal error:", error.message);
4546
5419
  process.exit(1);
4547
5420
  });