@lumy-pack/syncpoint 0.0.8 → 0.0.10

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,8 +1,124 @@
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
+ });
2
89
 
3
90
  // src/cli.ts
4
91
  import { Command } from "commander";
5
92
 
93
+ // ../shared/src/respond.ts
94
+ function respond(command, data, startTime, version) {
95
+ const response = {
96
+ ok: true,
97
+ command,
98
+ data,
99
+ meta: {
100
+ version,
101
+ durationMs: Date.now() - startTime,
102
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
103
+ }
104
+ };
105
+ process.stdout.write(JSON.stringify(response) + "\n");
106
+ }
107
+ function respondError(command, code, message, startTime, version, details) {
108
+ const response = {
109
+ ok: false,
110
+ command,
111
+ error: { code, message, ...details !== void 0 ? { details } : {} },
112
+ meta: {
113
+ version,
114
+ durationMs: Date.now() - startTime,
115
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
116
+ }
117
+ };
118
+ process.stdout.write(JSON.stringify(response) + "\n");
119
+ process.exitCode = 1;
120
+ }
121
+
6
122
  // src/commands/Backup.tsx
7
123
  import { Box, Static, Text as Text2, useApp } from "ink";
8
124
  import { render } from "ink";
@@ -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.10";
490
606
 
491
607
  // src/core/metadata.ts
492
608
  var METADATA_VERSION = "1.0.0";
@@ -843,18 +959,12 @@ var config_schema_default = {
843
959
  title: "Syncpoint Config",
844
960
  description: "Configuration for syncpoint backup tool",
845
961
  type: "object",
846
- required: [
847
- "backup"
848
- ],
962
+ required: ["backup"],
849
963
  properties: {
850
964
  backup: {
851
965
  type: "object",
852
966
  description: "Backup configuration",
853
- required: [
854
- "targets",
855
- "exclude",
856
- "filename"
857
- ],
967
+ required: ["targets", "exclude", "filename"],
858
968
  properties: {
859
969
  targets: {
860
970
  type: "array",
@@ -918,26 +1028,8 @@ function validateConfig(data) {
918
1028
  return { valid: false, errors };
919
1029
  }
920
1030
 
921
- // src/utils/assets.ts
922
- import { existsSync, readFileSync } from "fs";
923
- import { dirname, join as join6 } from "path";
924
- import { fileURLToPath } from "url";
925
- function getPackageRoot() {
926
- let dir = dirname(fileURLToPath(import.meta.url));
927
- while (dir !== dirname(dir)) {
928
- if (existsSync(join6(dir, "package.json"))) return dir;
929
- dir = dirname(dir);
930
- }
931
- throw new Error("Could not find package root");
932
- }
933
- function getAssetPath(filename) {
934
- return join6(getPackageRoot(), "assets", filename);
935
- }
936
- function readAsset(filename) {
937
- return readFileSync(getAssetPath(filename), "utf-8");
938
- }
939
-
940
1031
  // src/core/config.ts
1032
+ init_assets();
941
1033
  function stripDangerousKeys(obj) {
942
1034
  if (obj === null || typeof obj !== "object") return obj;
943
1035
  if (Array.isArray(obj)) return obj.map(stripDangerousKeys);
@@ -1000,6 +1092,35 @@ async function initDefaultConfig() {
1000
1092
  return { created, skipped };
1001
1093
  }
1002
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
+ UNKNOWN: "UNKNOWN"
1106
+ };
1107
+ function classifyError(err) {
1108
+ const msg = err instanceof Error ? err.message : String(err);
1109
+ if (msg.includes("Config file not found") || msg.includes('Run "syncpoint init"')) {
1110
+ return SyncpointErrorCode.CONFIG_NOT_FOUND;
1111
+ }
1112
+ if (msg.includes("Invalid config")) {
1113
+ return SyncpointErrorCode.CONFIG_INVALID;
1114
+ }
1115
+ if (msg.includes("Template not found") || msg.includes("template not found")) {
1116
+ return SyncpointErrorCode.TEMPLATE_NOT_FOUND;
1117
+ }
1118
+ if (msg.includes("Template file not found")) {
1119
+ return SyncpointErrorCode.TEMPLATE_NOT_FOUND;
1120
+ }
1121
+ return SyncpointErrorCode.UNKNOWN;
1122
+ }
1123
+
1003
1124
  // src/utils/command-registry.ts
1004
1125
  var COMMANDS = {
1005
1126
  init: {
@@ -1015,7 +1136,8 @@ var COMMANDS = {
1015
1136
  options: [
1016
1137
  {
1017
1138
  flag: "-p, --print",
1018
- description: "Print prompt instead of invoking Claude Code"
1139
+ description: "Print prompt instead of invoking Claude Code",
1140
+ type: "boolean"
1019
1141
  }
1020
1142
  ],
1021
1143
  examples: [
@@ -1030,15 +1152,18 @@ var COMMANDS = {
1030
1152
  options: [
1031
1153
  {
1032
1154
  flag: "--dry-run",
1033
- description: "Preview files to be backed up without creating archive"
1155
+ description: "Preview files to be backed up without creating archive",
1156
+ type: "boolean"
1034
1157
  },
1035
1158
  {
1036
1159
  flag: "--tag <name>",
1037
- description: "Add custom tag to backup filename"
1160
+ description: "Add custom tag to backup filename",
1161
+ type: "string"
1038
1162
  },
1039
1163
  {
1040
1164
  flag: "-v, --verbose",
1041
- description: "Show detailed output including missing files"
1165
+ description: "Show detailed output including missing files",
1166
+ type: "boolean"
1042
1167
  }
1043
1168
  ],
1044
1169
  examples: [
@@ -1061,7 +1186,8 @@ var COMMANDS = {
1061
1186
  options: [
1062
1187
  {
1063
1188
  flag: "--dry-run",
1064
- description: "Show restore plan without actually restoring"
1189
+ description: "Show restore plan without actually restoring",
1190
+ type: "boolean"
1065
1191
  }
1066
1192
  ],
1067
1193
  examples: [
@@ -1084,15 +1210,18 @@ var COMMANDS = {
1084
1210
  options: [
1085
1211
  {
1086
1212
  flag: "-f, --file <path>",
1087
- description: "Path to template file (alternative to template name)"
1213
+ description: "Path to template file (alternative to template name)",
1214
+ type: "string"
1088
1215
  },
1089
1216
  {
1090
1217
  flag: "--dry-run",
1091
- description: "Show execution plan without running commands"
1218
+ description: "Show execution plan without running commands",
1219
+ type: "boolean"
1092
1220
  },
1093
1221
  {
1094
1222
  flag: "--skip-restore",
1095
- description: "Skip automatic config restore after provisioning"
1223
+ description: "Skip automatic config restore after provisioning",
1224
+ type: "boolean"
1096
1225
  }
1097
1226
  ],
1098
1227
  examples: [
@@ -1117,7 +1246,8 @@ var COMMANDS = {
1117
1246
  options: [
1118
1247
  {
1119
1248
  flag: "-p, --print",
1120
- description: "Print prompt instead of invoking Claude Code"
1249
+ description: "Print prompt instead of invoking Claude Code",
1250
+ type: "boolean"
1121
1251
  }
1122
1252
  ],
1123
1253
  examples: [
@@ -1137,7 +1267,7 @@ var COMMANDS = {
1137
1267
  required: false
1138
1268
  }
1139
1269
  ],
1140
- options: [{ flag: "--delete <n>", description: "Delete item number n" }],
1270
+ options: [{ flag: "--delete <filename>", description: "Delete item by filename", type: "string" }],
1141
1271
  examples: [
1142
1272
  "npx @lumy-pack/syncpoint list",
1143
1273
  "npx @lumy-pack/syncpoint list backups",
@@ -1149,7 +1279,7 @@ var COMMANDS = {
1149
1279
  description: "Show ~/.syncpoint/ status summary and manage cleanup",
1150
1280
  usage: "npx @lumy-pack/syncpoint status [options]",
1151
1281
  options: [
1152
- { flag: "--cleanup", description: "Enter interactive cleanup mode" }
1282
+ { flag: "--cleanup", description: "Enter interactive cleanup mode", type: "boolean" }
1153
1283
  ],
1154
1284
  examples: [
1155
1285
  "npx @lumy-pack/syncpoint status",
@@ -1163,7 +1293,8 @@ var COMMANDS = {
1163
1293
  options: [
1164
1294
  {
1165
1295
  flag: "--dry-run",
1166
- description: "Preview changes without writing"
1296
+ description: "Preview changes without writing",
1297
+ type: "boolean"
1167
1298
  }
1168
1299
  ],
1169
1300
  examples: [
@@ -1210,7 +1341,11 @@ var BackupView = ({ options }) => {
1210
1341
  for (const f of foundFiles) {
1211
1342
  if (seen.has(f.absolutePath)) continue;
1212
1343
  seen.add(f.absolutePath);
1213
- deduped.push({ id: `found-${f.absolutePath}`, type: "found", file: f });
1344
+ deduped.push({
1345
+ id: `found-${f.absolutePath}`,
1346
+ type: "found",
1347
+ file: f
1348
+ });
1214
1349
  }
1215
1350
  for (const p of missingFiles) {
1216
1351
  if (seen.has(p)) continue;
@@ -1334,21 +1469,60 @@ function registerBackupCommand(program2) {
1334
1469
  cmdInfo.options?.forEach((opt) => {
1335
1470
  cmd.option(opt.flag, opt.description);
1336
1471
  });
1337
- cmd.action(async (opts) => {
1338
- const { waitUntilExit } = render(
1339
- /* @__PURE__ */ jsx2(BackupView, { options: { dryRun: opts.dryRun, tag: opts.tag, verbose: opts.verbose } })
1340
- );
1341
- await waitUntilExit();
1342
- });
1472
+ cmd.action(
1473
+ async (opts) => {
1474
+ const globalOpts = program2.opts();
1475
+ const startTime = Date.now();
1476
+ if (globalOpts.json) {
1477
+ try {
1478
+ const config = await loadConfig();
1479
+ const result = await createBackup(config, {
1480
+ dryRun: opts.dryRun,
1481
+ tag: opts.tag,
1482
+ verbose: opts.verbose
1483
+ });
1484
+ respond(
1485
+ "backup",
1486
+ {
1487
+ archivePath: result.archivePath,
1488
+ fileCount: result.metadata.summary.fileCount,
1489
+ totalSize: result.metadata.summary.totalSize,
1490
+ tag: opts.tag ?? null
1491
+ },
1492
+ startTime,
1493
+ VERSION
1494
+ );
1495
+ } catch (error) {
1496
+ const code = classifyError(error);
1497
+ respondError("backup", code, error.message, startTime, VERSION);
1498
+ }
1499
+ return;
1500
+ }
1501
+ const { waitUntilExit } = render(
1502
+ /* @__PURE__ */ jsx2(
1503
+ BackupView,
1504
+ {
1505
+ options: {
1506
+ dryRun: opts.dryRun,
1507
+ tag: opts.tag,
1508
+ verbose: opts.verbose
1509
+ }
1510
+ }
1511
+ )
1512
+ );
1513
+ await waitUntilExit();
1514
+ }
1515
+ );
1343
1516
  }
1344
1517
 
1345
1518
  // src/commands/CreateTemplate.tsx
1346
- import { useState as useState2, useEffect as useEffect2 } from "react";
1347
- import { Text as Text3, Box as Box2, useApp as useApp2 } from "ink";
1348
- import Spinner from "ink-spinner";
1349
- import { render as render2 } from "ink";
1350
- import { join as join8 } from "path";
1351
1519
  import { writeFile as writeFile3 } from "fs/promises";
1520
+ import { join as join8 } from "path";
1521
+ import { Box as Box2, Text as Text3, useApp as useApp2 } from "ink";
1522
+ import { render as render2 } from "ink";
1523
+ import Spinner from "ink-spinner";
1524
+ import { useEffect as useEffect2, useState as useState2 } from "react";
1525
+ init_wizard_template();
1352
1526
 
1353
1527
  // assets/schemas/template.schema.json
1354
1528
  var template_schema_default = {
@@ -1424,45 +1598,8 @@ function validateTemplate(data) {
1424
1598
  return { valid: false, errors };
1425
1599
  }
1426
1600
 
1427
- // src/prompts/wizard-template.ts
1428
- function generateTemplateWizardPrompt(variables) {
1429
- return `You are a Syncpoint provisioning template assistant. Your role is to help users create automated environment setup templates.
1430
-
1431
- **Input:**
1432
- 1. User's provisioning requirements (described in natural language)
1433
- 2. Example template structure (YAML)
1434
-
1435
- **Your Task:**
1436
- 1. Ask clarifying questions to understand the provisioning workflow:
1437
- - What software/tools need to be installed?
1438
- - What dependencies should be checked?
1439
- - Are there any configuration steps after installation?
1440
- - Should any steps require sudo privileges?
1441
- - Should any steps be conditional (skip_if)?
1442
- 2. Based on user responses, generate a complete provision template
1443
-
1444
- **Output Requirements:**
1445
- - Pure YAML format only (no markdown, no code blocks, no explanations)
1446
- - Must be valid according to Syncpoint template schema
1447
- - Required fields:
1448
- - \`name\`: Template name
1449
- - \`steps\`: Array of provisioning steps (minimum 1)
1450
- - Each step must include:
1451
- - \`name\`: Step name (required)
1452
- - \`command\`: Shell command to execute (required)
1453
- - \`description\`: Step description (optional)
1454
- - \`skip_if\`: Condition to skip step (optional)
1455
- - \`continue_on_error\`: Whether to continue on failure (optional, default: false)
1456
- - Optional template fields:
1457
- - \`description\`: Template description
1458
- - \`backup\`: Backup name to restore after provisioning
1459
- - \`sudo\`: Whether sudo is required (boolean)
1460
-
1461
- **Example Template:**
1462
- ${variables.exampleTemplate}
1463
-
1464
- Begin by asking the user to describe their provisioning needs.`;
1465
- }
1601
+ // src/commands/CreateTemplate.tsx
1602
+ init_assets();
1466
1603
 
1467
1604
  // src/utils/claude-code-runner.ts
1468
1605
  import { spawn } from "child_process";
@@ -1566,6 +1703,39 @@ Start by asking the user about their backup priorities for the home directory st
1566
1703
  });
1567
1704
  }
1568
1705
 
1706
+ // src/utils/error-formatter.ts
1707
+ function formatValidationErrors(errors) {
1708
+ if (errors.length === 0) {
1709
+ return "No validation errors.";
1710
+ }
1711
+ const formattedErrors = errors.map((error, index) => {
1712
+ return `${index + 1}. ${error}`;
1713
+ });
1714
+ return `Validation failed with ${errors.length} error(s):
1715
+
1716
+ ${formattedErrors.join("\n")}`;
1717
+ }
1718
+ function createRetryPrompt(originalPrompt, errors, attemptNumber) {
1719
+ const errorSummary = formatValidationErrors(errors);
1720
+ return `${originalPrompt}
1721
+
1722
+ ---
1723
+
1724
+ **VALIDATION FAILED (Attempt ${attemptNumber})**
1725
+
1726
+ The previously generated YAML configuration did not pass validation:
1727
+
1728
+ ${errorSummary}
1729
+
1730
+ Please analyze these errors and generate a corrected YAML configuration that addresses all validation issues.
1731
+
1732
+ Remember:
1733
+ - Output pure YAML only (no markdown, no code blocks, no explanations)
1734
+ - Ensure all required fields are present
1735
+ - Follow the correct schema structure
1736
+ - Validate pattern syntax for targets and exclude arrays`;
1737
+ }
1738
+
1569
1739
  // src/utils/yaml-parser.ts
1570
1740
  import YAML2 from "yaml";
1571
1741
  function isStructuredYAML(parsed) {
@@ -1603,39 +1773,6 @@ function parseYAML(yamlString) {
1603
1773
  return YAML2.parse(yamlString);
1604
1774
  }
1605
1775
 
1606
- // src/utils/error-formatter.ts
1607
- function formatValidationErrors(errors) {
1608
- if (errors.length === 0) {
1609
- return "No validation errors.";
1610
- }
1611
- const formattedErrors = errors.map((error, index) => {
1612
- return `${index + 1}. ${error}`;
1613
- });
1614
- return `Validation failed with ${errors.length} error(s):
1615
-
1616
- ${formattedErrors.join("\n")}`;
1617
- }
1618
- function createRetryPrompt(originalPrompt, errors, attemptNumber) {
1619
- const errorSummary = formatValidationErrors(errors);
1620
- return `${originalPrompt}
1621
-
1622
- ---
1623
-
1624
- **VALIDATION FAILED (Attempt ${attemptNumber})**
1625
-
1626
- The previously generated YAML configuration did not pass validation:
1627
-
1628
- ${errorSummary}
1629
-
1630
- Please analyze these errors and generate a corrected YAML configuration that addresses all validation issues.
1631
-
1632
- Remember:
1633
- - Output pure YAML only (no markdown, no code blocks, no explanations)
1634
- - Ensure all required fields are present
1635
- - Follow the correct schema structure
1636
- - Validate pattern syntax for targets and exclude arrays`;
1637
- }
1638
-
1639
1776
  // src/commands/CreateTemplate.tsx
1640
1777
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1641
1778
  var MAX_RETRIES = 3;
@@ -1685,7 +1822,9 @@ var CreateTemplateView = ({
1685
1822
  while (currentAttempt <= MAX_RETRIES) {
1686
1823
  try {
1687
1824
  setPhase("llm-invoke");
1688
- setMessage(`Generating template... (Attempt ${currentAttempt}/${MAX_RETRIES})`);
1825
+ setMessage(
1826
+ `Generating template... (Attempt ${currentAttempt}/${MAX_RETRIES})`
1827
+ );
1689
1828
  const result = currentSessionId ? await resumeClaudeCodeSession(currentSessionId, currentPrompt) : await invokeClaudeCode(currentPrompt);
1690
1829
  if (!result.success) {
1691
1830
  throw new Error(result.error || "Failed to invoke Claude Code");
@@ -1762,8 +1901,11 @@ ${formatValidationErrors(validation.errors || [])}`
1762
1901
  /* @__PURE__ */ jsx3(Text3, { color: "green", children: message }),
1763
1902
  /* @__PURE__ */ jsxs3(Box2, { marginTop: 1, children: [
1764
1903
  /* @__PURE__ */ jsx3(Text3, { children: "Next steps:" }),
1765
- /* @__PURE__ */ jsx3(Text3, { children: " 1. Review your template: syncpoint list templates" }),
1766
- /* @__PURE__ */ jsx3(Text3, { children: " 2. Run provisioning: syncpoint provision <template-name>" })
1904
+ /* @__PURE__ */ jsx3(Text3, { children: " 1. Review your template: syncpoint list templates" }),
1905
+ /* @__PURE__ */ jsxs3(Text3, { children: [
1906
+ " ",
1907
+ "2. Run provisioning: syncpoint provision <template-name>"
1908
+ ] })
1767
1909
  ] })
1768
1910
  ] });
1769
1911
  }
@@ -1783,8 +1925,38 @@ ${formatValidationErrors(validation.errors || [])}`
1783
1925
  };
1784
1926
  function registerCreateTemplateCommand(program2) {
1785
1927
  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) => {
1928
+ const globalOpts = program2.opts();
1929
+ const startTime = Date.now();
1930
+ if (globalOpts.json) {
1931
+ if (!opts.print) {
1932
+ respondError(
1933
+ "create-template",
1934
+ SyncpointErrorCode.MISSING_ARGUMENT,
1935
+ "--print is required in --json mode (interactive mode requires a terminal)",
1936
+ startTime,
1937
+ VERSION
1938
+ );
1939
+ return;
1940
+ }
1941
+ try {
1942
+ const { generateTemplateWizardPrompt: generateTemplateWizardPrompt2 } = await Promise.resolve().then(() => (init_wizard_template(), wizard_template_exports));
1943
+ const { readAsset: readAsset2 } = await Promise.resolve().then(() => (init_assets(), assets_exports));
1944
+ const exampleTemplate = readAsset2("template.example.yml");
1945
+ const prompt = generateTemplateWizardPrompt2({ exampleTemplate });
1946
+ respond("create-template", { prompt }, startTime, VERSION);
1947
+ } catch (err) {
1948
+ respondError("create-template", SyncpointErrorCode.UNKNOWN, err.message, startTime, VERSION);
1949
+ }
1950
+ return;
1951
+ }
1786
1952
  const { waitUntilExit } = render2(
1787
- /* @__PURE__ */ jsx3(CreateTemplateView, { printMode: opts.print || false, templateName: name })
1953
+ /* @__PURE__ */ jsx3(
1954
+ CreateTemplateView,
1955
+ {
1956
+ printMode: opts.print || false,
1957
+ templateName: name
1958
+ }
1959
+ )
1788
1960
  );
1789
1961
  await waitUntilExit();
1790
1962
  });
@@ -1929,16 +2101,32 @@ var HelpView = ({ commandName }) => {
1929
2101
  };
1930
2102
  function registerHelpCommand(program2) {
1931
2103
  program2.command("help [command]").description("Display help information").action(async (commandName) => {
2104
+ const globalOpts = program2.opts();
2105
+ const startTime = Date.now();
2106
+ if (globalOpts.json) {
2107
+ if (commandName) {
2108
+ const commandInfo = COMMANDS[commandName];
2109
+ if (!commandInfo) {
2110
+ respond("help", { error: `Unknown command: ${commandName}`, commands: Object.keys(COMMANDS) }, startTime, VERSION);
2111
+ } else {
2112
+ respond("help", { command: commandInfo }, startTime, VERSION);
2113
+ }
2114
+ } else {
2115
+ respond("help", { commands: COMMANDS }, startTime, VERSION);
2116
+ }
2117
+ return;
2118
+ }
1932
2119
  const { waitUntilExit } = render3(/* @__PURE__ */ jsx4(HelpView, { commandName }));
1933
2120
  await waitUntilExit();
1934
2121
  });
1935
2122
  }
1936
2123
 
1937
2124
  // src/commands/Init.tsx
1938
- import { useState as useState3, useEffect as useEffect3 } from "react";
1939
- import { Text as Text5, Box as Box4, useApp as useApp3 } from "ink";
1940
- import { render as render4 } from "ink";
1941
2125
  import { join as join9 } from "path";
2126
+ import { Box as Box4, Text as Text5, useApp as useApp3 } from "ink";
2127
+ import { render as render4 } from "ink";
2128
+ import { useEffect as useEffect3, useState as useState3 } from "react";
2129
+ init_assets();
1942
2130
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1943
2131
  var InitView = () => {
1944
2132
  const { exit } = useApp3();
@@ -1980,9 +2168,15 @@ var InitView = () => {
1980
2168
  setSteps([...completed]);
1981
2169
  }
1982
2170
  await initDefaultConfig();
1983
- completed.push({ name: `Created ${CONFIG_FILENAME} (defaults)`, done: true });
2171
+ completed.push({
2172
+ name: `Created ${CONFIG_FILENAME} (defaults)`,
2173
+ done: true
2174
+ });
1984
2175
  setSteps([...completed]);
1985
- const exampleTemplatePath = join9(getSubDir(TEMPLATES_DIR), "example.yml");
2176
+ const exampleTemplatePath = join9(
2177
+ getSubDir(TEMPLATES_DIR),
2178
+ "example.yml"
2179
+ );
1986
2180
  if (!await fileExists(exampleTemplatePath)) {
1987
2181
  const { writeFile: writeFile6 } = await import("fs/promises");
1988
2182
  const exampleYaml = readAsset("template.example.yml");
@@ -2033,7 +2227,21 @@ var InitView = () => {
2033
2227
  ] });
2034
2228
  };
2035
2229
  function registerInitCommand(program2) {
2036
- program2.command("init").description(`Initialize ~/.${APP_NAME}/ directory structure and default config`).action(async () => {
2230
+ program2.command("init").description(
2231
+ `Initialize ~/.${APP_NAME}/ directory structure and default config`
2232
+ ).action(async () => {
2233
+ const globalOpts = program2.opts();
2234
+ const startTime = Date.now();
2235
+ if (globalOpts.json) {
2236
+ try {
2237
+ const result = await initDefaultConfig();
2238
+ respond("init", { created: result.created, skipped: result.skipped }, startTime, VERSION);
2239
+ } catch (error) {
2240
+ const code = classifyError(error);
2241
+ respondError("init", code, error.message, startTime, VERSION);
2242
+ }
2243
+ return;
2244
+ }
2037
2245
  const { waitUntilExit } = render4(/* @__PURE__ */ jsx5(InitView, {}));
2038
2246
  await waitUntilExit();
2039
2247
  });
@@ -2084,7 +2292,7 @@ var Confirm = ({
2084
2292
  };
2085
2293
 
2086
2294
  // src/components/Table.tsx
2087
- import { Text as Text7, Box as Box5 } from "ink";
2295
+ import { Box as Box5, Text as Text7 } from "ink";
2088
2296
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2089
2297
  var Table = ({
2090
2298
  headers,
@@ -2555,7 +2763,9 @@ var ListView = ({ type, deleteIndex }) => {
2555
2763
  if (yes && deleteTarget) {
2556
2764
  try {
2557
2765
  if (!isInsideDir(deleteTarget.path, backupDir)) {
2558
- throw new Error(`Refusing to delete file outside backups directory: ${deleteTarget.path}`);
2766
+ throw new Error(
2767
+ `Refusing to delete file outside backups directory: ${deleteTarget.path}`
2768
+ );
2559
2769
  }
2560
2770
  unlinkSync(deleteTarget.path);
2561
2771
  const deletedName = deleteTarget.name;
@@ -2814,11 +3024,72 @@ var ListView = ({ type, deleteIndex }) => {
2814
3024
  return null;
2815
3025
  };
2816
3026
  function registerListCommand(program2) {
2817
- program2.command("list [type]").description("List backups and templates").option("--delete <n>", "Delete item #n").action(async (type, opts) => {
3027
+ program2.command("list [type]").description("List backups and templates").option("--delete <filename>", "Delete item by filename").action(async (type, opts) => {
3028
+ const globalOpts = program2.opts();
3029
+ const startTime = Date.now();
3030
+ if (globalOpts.json) {
3031
+ try {
3032
+ const config = await loadConfig();
3033
+ if (opts.delete) {
3034
+ const filename = opts.delete;
3035
+ const backupDirectory = config.backup.destination ? resolveTargetPath(config.backup.destination) : getSubDir("backups");
3036
+ const isTemplate = type === "templates";
3037
+ if (isTemplate) {
3038
+ const templates = await listTemplates();
3039
+ const match = templates.find(
3040
+ (t) => t.name === filename || t.name === filename.replace(/\.ya?ml$/, "")
3041
+ );
3042
+ if (!match) {
3043
+ respondError("list", SyncpointErrorCode.INVALID_ARGUMENT, `Template not found: ${filename}`, startTime, VERSION);
3044
+ return;
3045
+ }
3046
+ if (!isInsideDir(match.path, getSubDir("templates"))) {
3047
+ respondError("list", SyncpointErrorCode.INVALID_ARGUMENT, `Refusing to delete file outside templates directory: ${match.path}`, startTime, VERSION);
3048
+ return;
3049
+ }
3050
+ unlinkSync(match.path);
3051
+ respond("list", { deleted: match.name, path: match.path }, startTime, VERSION);
3052
+ } else {
3053
+ const list2 = await getBackupList(config);
3054
+ const match = list2.find(
3055
+ (b) => b.filename === filename || b.filename.startsWith(filename)
3056
+ );
3057
+ if (!match) {
3058
+ respondError("list", SyncpointErrorCode.INVALID_ARGUMENT, `Backup not found: ${filename}`, startTime, VERSION);
3059
+ return;
3060
+ }
3061
+ if (!isInsideDir(match.path, backupDirectory)) {
3062
+ respondError("list", SyncpointErrorCode.INVALID_ARGUMENT, `Refusing to delete file outside backups directory: ${match.path}`, startTime, VERSION);
3063
+ return;
3064
+ }
3065
+ unlinkSync(match.path);
3066
+ respond("list", { deleted: match.filename, path: match.path }, startTime, VERSION);
3067
+ }
3068
+ return;
3069
+ }
3070
+ const showBackups = !type || type === "backups";
3071
+ const showTemplates = !type || type === "templates";
3072
+ const result = {};
3073
+ if (showBackups) {
3074
+ result.backups = await getBackupList(config);
3075
+ }
3076
+ if (showTemplates) {
3077
+ result.templates = await listTemplates();
3078
+ }
3079
+ respond("list", result, startTime, VERSION);
3080
+ } catch (error) {
3081
+ const code = classifyError(error);
3082
+ respondError("list", code, error.message, startTime, VERSION);
3083
+ }
3084
+ return;
3085
+ }
2818
3086
  const deleteIndex = opts.delete ? parseInt(opts.delete, 10) : void 0;
2819
3087
  if (deleteIndex !== void 0 && isNaN(deleteIndex)) {
2820
- console.error(`Invalid delete index: ${opts.delete}`);
2821
- process.exit(1);
3088
+ const { waitUntilExit: waitUntilExit2 } = render5(
3089
+ /* @__PURE__ */ jsx8(ListView, { type, deleteIndex: void 0 })
3090
+ );
3091
+ await waitUntilExit2();
3092
+ return;
2822
3093
  }
2823
3094
  const { waitUntilExit } = render5(
2824
3095
  /* @__PURE__ */ jsx8(ListView, { type, deleteIndex })
@@ -2828,13 +3099,14 @@ function registerListCommand(program2) {
2828
3099
  }
2829
3100
 
2830
3101
  // src/commands/Migrate.tsx
2831
- import { useState as useState6, useEffect as useEffect5 } from "react";
2832
- import { Text as Text9, Box as Box7, useApp as useApp5 } from "ink";
3102
+ import { Box as Box7, Text as Text9, useApp as useApp5 } from "ink";
2833
3103
  import { render as render6 } from "ink";
3104
+ import { useEffect as useEffect5, useState as useState6 } from "react";
2834
3105
 
2835
3106
  // src/core/migrate.ts
2836
3107
  import { copyFile as copyFile2, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
2837
3108
  import YAML4 from "yaml";
3109
+ init_assets();
2838
3110
  function extractSchemaPaths(schema, prefix = []) {
2839
3111
  const paths = [];
2840
3112
  const properties = schema.properties;
@@ -3055,6 +3327,18 @@ var MigrateView = ({ dryRun }) => {
3055
3327
  };
3056
3328
  function registerMigrateCommand(program2) {
3057
3329
  program2.command("migrate").description("Migrate config.yml to match the current schema").option("--dry-run", "Preview changes without writing").action(async (opts) => {
3330
+ const globalOpts = program2.opts();
3331
+ const startTime = Date.now();
3332
+ if (globalOpts.json) {
3333
+ try {
3334
+ const result = await migrateConfig({ dryRun: opts.dryRun ?? false });
3335
+ respond("migrate", result, startTime, VERSION);
3336
+ } catch (error) {
3337
+ const code = classifyError(error);
3338
+ respondError("migrate", code, error.message, startTime, VERSION);
3339
+ }
3340
+ return;
3341
+ }
3058
3342
  const { waitUntilExit } = render6(
3059
3343
  /* @__PURE__ */ jsx9(MigrateView, { dryRun: opts.dryRun ?? false })
3060
3344
  );
@@ -3063,44 +3347,12 @@ function registerMigrateCommand(program2) {
3063
3347
  }
3064
3348
 
3065
3349
  // src/commands/Provision.tsx
3066
- import { useState as useState7, useEffect as useEffect6 } from "react";
3067
- import { Text as Text11, Box as Box9, useApp as useApp6 } from "ink";
3350
+ import { Box as Box9, Text as Text11, useApp as useApp6 } from "ink";
3068
3351
  import { render as render7 } from "ink";
3069
-
3070
- // src/utils/sudo.ts
3071
- import { execSync } from "child_process";
3072
- import pc2 from "picocolors";
3073
- function isSudoCached() {
3074
- try {
3075
- execSync("sudo -n true", { stdio: "ignore" });
3076
- return true;
3077
- } catch {
3078
- return false;
3079
- }
3080
- }
3081
- function ensureSudo(templateName) {
3082
- if (isSudoCached()) return;
3083
- console.log(
3084
- `
3085
- ${pc2.yellow("\u26A0")} Template ${pc2.bold(templateName)} requires ${pc2.bold("sudo")} privileges.`
3086
- );
3087
- console.log(
3088
- pc2.gray(" Some provisioning steps need elevated permissions to execute.")
3089
- );
3090
- console.log(pc2.gray(" You will be prompted for your password.\n"));
3091
- try {
3092
- execSync("sudo -v", { stdio: "inherit", timeout: 6e4 });
3093
- } catch {
3094
- console.error(
3095
- `
3096
- ${pc2.red("\u2717")} Sudo authentication failed or was cancelled. Aborting.`
3097
- );
3098
- process.exit(1);
3099
- }
3100
- }
3352
+ import { useEffect as useEffect6, useState as useState7 } from "react";
3101
3353
 
3102
3354
  // src/components/StepRunner.tsx
3103
- import { Text as Text10, Box as Box8, Static as Static2 } from "ink";
3355
+ import { Box as Box8, Static as Static2, Text as Text10 } from "ink";
3104
3356
  import Spinner2 from "ink-spinner";
3105
3357
  import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
3106
3358
  var StepIcon = ({ status }) => {
@@ -3169,10 +3421,7 @@ var StepItemView = ({
3169
3421
  /* @__PURE__ */ jsx10(StepStatusText, { step })
3170
3422
  ] })
3171
3423
  ] });
3172
- var StepRunner = ({
3173
- steps,
3174
- total
3175
- }) => {
3424
+ var StepRunner = ({ steps, total }) => {
3176
3425
  const completedSteps = [];
3177
3426
  const activeSteps = [];
3178
3427
  steps.forEach((step, idx) => {
@@ -3208,6 +3457,38 @@ var StepRunner = ({
3208
3457
  ] });
3209
3458
  };
3210
3459
 
3460
+ // src/utils/sudo.ts
3461
+ import { execSync } from "child_process";
3462
+ import pc2 from "picocolors";
3463
+ function isSudoCached() {
3464
+ try {
3465
+ execSync("sudo -n true", { stdio: "ignore" });
3466
+ return true;
3467
+ } catch {
3468
+ return false;
3469
+ }
3470
+ }
3471
+ function ensureSudo(templateName) {
3472
+ if (isSudoCached()) return;
3473
+ console.log(
3474
+ `
3475
+ ${pc2.yellow("\u26A0")} Template ${pc2.bold(templateName)} requires ${pc2.bold("sudo")} privileges.`
3476
+ );
3477
+ console.log(
3478
+ pc2.gray(" Some provisioning steps need elevated permissions to execute.")
3479
+ );
3480
+ console.log(pc2.gray(" You will be prompted for your password.\n"));
3481
+ try {
3482
+ execSync("sudo -v", { stdio: "inherit", timeout: 6e4 });
3483
+ } catch {
3484
+ console.error(
3485
+ `
3486
+ ${pc2.red("\u2717")} Sudo authentication failed or was cancelled. Aborting.`
3487
+ );
3488
+ process.exit(1);
3489
+ }
3490
+ }
3491
+
3211
3492
  // src/commands/Provision.tsx
3212
3493
  import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
3213
3494
  var ProvisionView = ({
@@ -3216,7 +3497,9 @@ var ProvisionView = ({
3216
3497
  options
3217
3498
  }) => {
3218
3499
  const { exit } = useApp6();
3219
- const [phase, setPhase] = useState7(options.dryRun ? "done" : "running");
3500
+ const [phase, setPhase] = useState7(
3501
+ options.dryRun ? "done" : "running"
3502
+ );
3220
3503
  const [steps, setSteps] = useState7(
3221
3504
  template.steps.map((s) => ({
3222
3505
  name: s.name,
@@ -3361,17 +3644,33 @@ var ProvisionView = ({
3361
3644
  ] });
3362
3645
  };
3363
3646
  function registerProvisionCommand(program2) {
3364
- program2.command("provision [template]").description("Run template-based machine provisioning").option("--dry-run", "Show plan without execution", false).option("--skip-restore", "Skip automatic restore after template completion", false).option("-f, --file <path>", "Path to template file").action(
3647
+ program2.command("provision [template]").description("Run template-based machine provisioning").option("--dry-run", "Show plan without execution", false).option(
3648
+ "--skip-restore",
3649
+ "Skip automatic restore after template completion",
3650
+ false
3651
+ ).option("-f, --file <path>", "Path to template file").action(
3365
3652
  async (templateName, opts) => {
3653
+ const globalOpts = program2.opts();
3654
+ const startTime = Date.now();
3366
3655
  let templatePath;
3367
3656
  if (opts.file) {
3368
3657
  templatePath = resolveTargetPath(opts.file);
3369
3658
  if (!await fileExists(templatePath)) {
3659
+ if (globalOpts.json) {
3660
+ respondError("provision", SyncpointErrorCode.TEMPLATE_NOT_FOUND, `Template file not found: ${opts.file}`, startTime, VERSION);
3661
+ return;
3662
+ }
3370
3663
  console.error(`Template file not found: ${opts.file}`);
3371
3664
  process.exit(1);
3372
3665
  }
3373
3666
  if (!templatePath.endsWith(".yml") && !templatePath.endsWith(".yaml")) {
3374
- console.error(`Template file must have .yml or .yaml extension: ${opts.file}`);
3667
+ if (globalOpts.json) {
3668
+ respondError("provision", SyncpointErrorCode.INVALID_ARGUMENT, `Template file must have .yml or .yaml extension: ${opts.file}`, startTime, VERSION);
3669
+ return;
3670
+ }
3671
+ console.error(
3672
+ `Template file must have .yml or .yaml extension: ${opts.file}`
3673
+ );
3375
3674
  process.exit(1);
3376
3675
  }
3377
3676
  } else if (templateName) {
@@ -3380,12 +3679,22 @@ function registerProvisionCommand(program2) {
3380
3679
  (t) => t.name === templateName || t.name === `${templateName}.yml` || t.config.name === templateName
3381
3680
  );
3382
3681
  if (!match) {
3682
+ if (globalOpts.json) {
3683
+ respondError("provision", SyncpointErrorCode.TEMPLATE_NOT_FOUND, `Template not found: ${templateName}`, startTime, VERSION);
3684
+ return;
3685
+ }
3383
3686
  console.error(`Template not found: ${templateName}`);
3384
3687
  process.exit(1);
3385
3688
  }
3386
3689
  templatePath = match.path;
3387
3690
  } else {
3388
- console.error("Error: Either <template> name or --file option must be provided");
3691
+ if (globalOpts.json) {
3692
+ respondError("provision", SyncpointErrorCode.MISSING_ARGUMENT, "Either <template> name or --file option must be provided", startTime, VERSION);
3693
+ return;
3694
+ }
3695
+ console.error(
3696
+ "Error: Either <template> name or --file option must be provided"
3697
+ );
3389
3698
  console.error("Usage: syncpoint provision <template> [options]");
3390
3699
  console.error(" syncpoint provision --file <path> [options]");
3391
3700
  process.exit(1);
@@ -3394,6 +3703,30 @@ function registerProvisionCommand(program2) {
3394
3703
  if (tmpl.sudo && !opts.dryRun) {
3395
3704
  ensureSudo(tmpl.name);
3396
3705
  }
3706
+ if (globalOpts.json) {
3707
+ try {
3708
+ const collectedSteps = [];
3709
+ const generator = runProvision(templatePath, {
3710
+ dryRun: opts.dryRun,
3711
+ skipRestore: opts.skipRestore
3712
+ });
3713
+ for await (const result of generator) {
3714
+ if (result.status !== "running") {
3715
+ collectedSteps.push(result);
3716
+ }
3717
+ }
3718
+ respond(
3719
+ "provision",
3720
+ { steps: collectedSteps, totalDuration: Date.now() - startTime },
3721
+ startTime,
3722
+ VERSION
3723
+ );
3724
+ } catch (error) {
3725
+ const code = classifyError(error);
3726
+ respondError("provision", code, error.message, startTime, VERSION);
3727
+ }
3728
+ return;
3729
+ }
3397
3730
  const { waitUntilExit } = render7(
3398
3731
  /* @__PURE__ */ jsx11(
3399
3732
  ProvisionView,
@@ -3413,10 +3746,10 @@ function registerProvisionCommand(program2) {
3413
3746
  }
3414
3747
 
3415
3748
  // src/commands/Restore.tsx
3416
- import { useState as useState8, useEffect as useEffect7 } from "react";
3417
- import { Text as Text12, Box as Box10, useApp as useApp7 } from "ink";
3418
- import SelectInput2 from "ink-select-input";
3749
+ import { Box as Box10, Text as Text12, useApp as useApp7 } from "ink";
3419
3750
  import { render as render8 } from "ink";
3751
+ import SelectInput2 from "ink-select-input";
3752
+ import { useEffect as useEffect7, useState as useState8 } from "react";
3420
3753
  import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
3421
3754
  var RestoreView = ({ filename, options }) => {
3422
3755
  const { exit } = useApp7();
@@ -3629,14 +3962,54 @@ var RestoreView = ({ filename, options }) => {
3629
3962
  };
3630
3963
  function registerRestoreCommand(program2) {
3631
3964
  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) => {
3632
- const { waitUntilExit } = render8(
3633
- /* @__PURE__ */ jsx12(
3634
- RestoreView,
3635
- {
3636
- filename,
3637
- options: { dryRun: opts.dryRun }
3965
+ const globalOpts = program2.opts();
3966
+ const startTime = Date.now();
3967
+ if (globalOpts.json) {
3968
+ if (!filename) {
3969
+ respondError(
3970
+ "restore",
3971
+ SyncpointErrorCode.MISSING_ARGUMENT,
3972
+ "filename argument is required in --json mode",
3973
+ startTime,
3974
+ VERSION
3975
+ );
3976
+ return;
3977
+ }
3978
+ try {
3979
+ const config = await loadConfig();
3980
+ const list2 = await getBackupList(config);
3981
+ const match = list2.find(
3982
+ (b) => b.filename === filename || b.filename.startsWith(filename)
3983
+ );
3984
+ if (!match) {
3985
+ respondError(
3986
+ "restore",
3987
+ SyncpointErrorCode.RESTORE_FAILED,
3988
+ `Backup not found: ${filename}`,
3989
+ startTime,
3990
+ VERSION
3991
+ );
3992
+ return;
3638
3993
  }
3639
- )
3994
+ const result = await restoreBackup(match.path, { dryRun: opts.dryRun });
3995
+ respond(
3996
+ "restore",
3997
+ {
3998
+ restoredFiles: result.restoredFiles,
3999
+ skippedFiles: result.skippedFiles,
4000
+ safetyBackupPath: result.safetyBackupPath ?? null
4001
+ },
4002
+ startTime,
4003
+ VERSION
4004
+ );
4005
+ } catch (error) {
4006
+ const code = classifyError(error);
4007
+ respondError("restore", code, error.message, startTime, VERSION);
4008
+ }
4009
+ return;
4010
+ }
4011
+ const { waitUntilExit } = render8(
4012
+ /* @__PURE__ */ jsx12(RestoreView, { filename, options: { dryRun: opts.dryRun } })
3640
4013
  );
3641
4014
  await waitUntilExit();
3642
4015
  });
@@ -3787,7 +4160,10 @@ var StatusView = ({ cleanup }) => {
3787
4160
  if (cleanupAction === "keep-recent-5") {
3788
4161
  const toDelete = backups.slice(5);
3789
4162
  for (const b of toDelete) {
3790
- if (!isInsideDir(b.path, backupDir)) throw new Error(`Refusing to delete file outside backups directory: ${b.path}`);
4163
+ if (!isInsideDir(b.path, backupDir))
4164
+ throw new Error(
4165
+ `Refusing to delete file outside backups directory: ${b.path}`
4166
+ );
3791
4167
  unlinkSync2(b.path);
3792
4168
  }
3793
4169
  } else if (cleanupAction === "older-than-30") {
@@ -3795,7 +4171,10 @@ var StatusView = ({ cleanup }) => {
3795
4171
  cutoff.setDate(cutoff.getDate() - 30);
3796
4172
  const toDelete = backups.filter((b) => b.createdAt < cutoff);
3797
4173
  for (const b of toDelete) {
3798
- if (!isInsideDir(b.path, backupDir)) throw new Error(`Refusing to delete file outside backups directory: ${b.path}`);
4174
+ if (!isInsideDir(b.path, backupDir))
4175
+ throw new Error(
4176
+ `Refusing to delete file outside backups directory: ${b.path}`
4177
+ );
3799
4178
  unlinkSync2(b.path);
3800
4179
  }
3801
4180
  } else if (cleanupAction === "delete-logs") {
@@ -3804,7 +4183,10 @@ var StatusView = ({ cleanup }) => {
3804
4183
  const entries = readdirSync(logsDir);
3805
4184
  for (const entry of entries) {
3806
4185
  const logPath = join12(logsDir, entry);
3807
- if (!isInsideDir(logPath, logsDir)) throw new Error(`Refusing to delete file outside logs directory: ${logPath}`);
4186
+ if (!isInsideDir(logPath, logsDir))
4187
+ throw new Error(
4188
+ `Refusing to delete file outside logs directory: ${logPath}`
4189
+ );
3808
4190
  try {
3809
4191
  if (statSync(logPath).isFile()) {
3810
4192
  unlinkSync2(logPath);
@@ -3816,7 +4198,10 @@ var StatusView = ({ cleanup }) => {
3816
4198
  }
3817
4199
  } else if (cleanupAction === "select-specific") {
3818
4200
  for (const b of selectedForDeletion) {
3819
- if (!isInsideDir(b.path, backupDir)) throw new Error(`Refusing to delete file outside backups directory: ${b.path}`);
4201
+ if (!isInsideDir(b.path, backupDir))
4202
+ throw new Error(
4203
+ `Refusing to delete file outside backups directory: ${b.path}`
4204
+ );
3820
4205
  unlinkSync2(b.path);
3821
4206
  }
3822
4207
  }
@@ -4037,13 +4422,47 @@ var StatusView = ({ cleanup }) => {
4037
4422
  };
4038
4423
  function registerStatusCommand(program2) {
4039
4424
  program2.command("status").description(`Show ~/.${APP_NAME}/ status summary`).option("--cleanup", "Interactive cleanup mode", false).action(async (opts) => {
4425
+ const globalOpts = program2.opts();
4426
+ const startTime = Date.now();
4427
+ if (globalOpts.json) {
4428
+ try {
4429
+ const config = await loadConfig();
4430
+ const backupDirectory = config.backup.destination ? resolveTargetPath(config.backup.destination) : getSubDir("backups");
4431
+ const backupStats = getDirStats(backupDirectory);
4432
+ const templateStats = getDirStats(getSubDir("templates"));
4433
+ const scriptStats = getDirStats(getSubDir("scripts"));
4434
+ const logStats = getDirStats(getSubDir("logs"));
4435
+ const backupList = await getBackupList(config);
4436
+ const lastBackup = backupList.length > 0 ? backupList[0].createdAt : null;
4437
+ const oldestBackup = backupList.length > 0 ? backupList[backupList.length - 1].createdAt : null;
4438
+ const statusInfo = {
4439
+ backups: backupStats,
4440
+ templates: templateStats,
4441
+ scripts: scriptStats,
4442
+ logs: logStats,
4443
+ lastBackup: lastBackup ?? void 0,
4444
+ oldestBackup: oldestBackup ?? void 0
4445
+ };
4446
+ respond("status", statusInfo, startTime, VERSION);
4447
+ } catch (error) {
4448
+ const code = classifyError(error);
4449
+ respondError("status", code, error.message, startTime, VERSION);
4450
+ }
4451
+ return;
4452
+ }
4040
4453
  const { waitUntilExit } = render9(/* @__PURE__ */ jsx13(StatusView, { cleanup: opts.cleanup }));
4041
4454
  await waitUntilExit();
4042
4455
  });
4043
4456
  }
4044
4457
 
4045
4458
  // src/commands/Wizard.tsx
4046
- import { copyFile as copyFile3, readFile as readFile6, rename, unlink, writeFile as writeFile5 } from "fs/promises";
4459
+ import {
4460
+ copyFile as copyFile3,
4461
+ readFile as readFile6,
4462
+ rename,
4463
+ unlink,
4464
+ writeFile as writeFile5
4465
+ } from "fs/promises";
4047
4466
  import { join as join14 } from "path";
4048
4467
  import { Box as Box12, Text as Text14, useApp as useApp9 } from "ink";
4049
4468
  import { render as render10 } from "ink";
@@ -4088,6 +4507,9 @@ ${variables.defaultConfig}
4088
4507
  **Start by greeting the user and asking about their backup priorities. After understanding their needs, write the config.yml file directly.**`;
4089
4508
  }
4090
4509
 
4510
+ // src/commands/Wizard.tsx
4511
+ init_assets();
4512
+
4091
4513
  // src/utils/file-scanner.ts
4092
4514
  import { stat as stat3 } from "fs/promises";
4093
4515
  import { join as join13 } from "path";
@@ -4445,6 +4867,27 @@ function registerWizardCommand(program2) {
4445
4867
  cmd.option(opt.flag, opt.description);
4446
4868
  });
4447
4869
  cmd.action(async (opts) => {
4870
+ const globalOpts = program2.opts();
4871
+ const startTime = Date.now();
4872
+ if (globalOpts.json) {
4873
+ if (!opts.print) {
4874
+ respondError(
4875
+ "wizard",
4876
+ SyncpointErrorCode.MISSING_ARGUMENT,
4877
+ "--print is required in --json mode (interactive mode requires a terminal)",
4878
+ startTime,
4879
+ VERSION
4880
+ );
4881
+ return;
4882
+ }
4883
+ try {
4884
+ const scanResult = await runScanPhase();
4885
+ respond("wizard", { prompt: scanResult.prompt }, startTime, VERSION);
4886
+ } catch (err) {
4887
+ respondError("wizard", SyncpointErrorCode.UNKNOWN, err.message, startTime, VERSION);
4888
+ }
4889
+ return;
4890
+ }
4448
4891
  if (opts.print) {
4449
4892
  const { waitUntilExit } = render10(/* @__PURE__ */ jsx14(WizardView, { printMode: true }));
4450
4893
  await waitUntilExit();
@@ -4481,7 +4924,7 @@ function registerWizardCommand(program2) {
4481
4924
  var program = new Command();
4482
4925
  program.name("syncpoint").description(
4483
4926
  "Personal Environment Manager \u2014 Config backup/restore and machine provisioning CLI"
4484
- ).version(VERSION);
4927
+ ).version(VERSION).option("--json", "Output structured JSON to stdout").option("--yes", "Skip confirmation prompts (non-interactive mode)");
4485
4928
  registerInitCommand(program);
4486
4929
  registerWizardCommand(program);
4487
4930
  registerBackupCommand(program);
@@ -4492,7 +4935,34 @@ registerListCommand(program);
4492
4935
  registerMigrateCommand(program);
4493
4936
  registerStatusCommand(program);
4494
4937
  registerHelpCommand(program);
4938
+ if (process.argv.includes("--describe")) {
4939
+ const startTime = Date.now();
4940
+ const globalOptions = [
4941
+ { flag: "--json", description: "Output structured JSON to stdout", type: "boolean" },
4942
+ { flag: "--yes", description: "Skip confirmation prompts (non-interactive mode)", type: "boolean" },
4943
+ { flag: "--describe", description: "Print CLI schema as JSON and exit", type: "boolean" },
4944
+ { flag: "-V, --version", description: "Output the version number", type: "boolean" },
4945
+ { flag: "-h, --help", description: "Display help for command", type: "boolean" }
4946
+ ];
4947
+ respond(
4948
+ "describe",
4949
+ {
4950
+ name: "syncpoint",
4951
+ version: VERSION,
4952
+ description: program.description(),
4953
+ globalOptions,
4954
+ commands: COMMANDS
4955
+ },
4956
+ startTime,
4957
+ VERSION
4958
+ );
4959
+ process.exit(0);
4960
+ }
4495
4961
  program.parseAsync(process.argv).catch((error) => {
4962
+ if (process.argv.includes("--json")) {
4963
+ respondError("unknown", SyncpointErrorCode.UNKNOWN, error.message, Date.now(), VERSION);
4964
+ process.exit(1);
4965
+ }
4496
4966
  console.error("Fatal error:", error.message);
4497
4967
  process.exit(1);
4498
4968
  });