@lumy-pack/syncpoint 0.0.9 → 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";
@@ -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,35 @@ 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
+ 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
+
997
1124
  // src/utils/command-registry.ts
998
1125
  var COMMANDS = {
999
1126
  init: {
@@ -1009,7 +1136,8 @@ var COMMANDS = {
1009
1136
  options: [
1010
1137
  {
1011
1138
  flag: "-p, --print",
1012
- description: "Print prompt instead of invoking Claude Code"
1139
+ description: "Print prompt instead of invoking Claude Code",
1140
+ type: "boolean"
1013
1141
  }
1014
1142
  ],
1015
1143
  examples: [
@@ -1024,15 +1152,18 @@ var COMMANDS = {
1024
1152
  options: [
1025
1153
  {
1026
1154
  flag: "--dry-run",
1027
- description: "Preview files to be backed up without creating archive"
1155
+ description: "Preview files to be backed up without creating archive",
1156
+ type: "boolean"
1028
1157
  },
1029
1158
  {
1030
1159
  flag: "--tag <name>",
1031
- description: "Add custom tag to backup filename"
1160
+ description: "Add custom tag to backup filename",
1161
+ type: "string"
1032
1162
  },
1033
1163
  {
1034
1164
  flag: "-v, --verbose",
1035
- description: "Show detailed output including missing files"
1165
+ description: "Show detailed output including missing files",
1166
+ type: "boolean"
1036
1167
  }
1037
1168
  ],
1038
1169
  examples: [
@@ -1055,7 +1186,8 @@ var COMMANDS = {
1055
1186
  options: [
1056
1187
  {
1057
1188
  flag: "--dry-run",
1058
- description: "Show restore plan without actually restoring"
1189
+ description: "Show restore plan without actually restoring",
1190
+ type: "boolean"
1059
1191
  }
1060
1192
  ],
1061
1193
  examples: [
@@ -1078,15 +1210,18 @@ var COMMANDS = {
1078
1210
  options: [
1079
1211
  {
1080
1212
  flag: "-f, --file <path>",
1081
- description: "Path to template file (alternative to template name)"
1213
+ description: "Path to template file (alternative to template name)",
1214
+ type: "string"
1082
1215
  },
1083
1216
  {
1084
1217
  flag: "--dry-run",
1085
- description: "Show execution plan without running commands"
1218
+ description: "Show execution plan without running commands",
1219
+ type: "boolean"
1086
1220
  },
1087
1221
  {
1088
1222
  flag: "--skip-restore",
1089
- description: "Skip automatic config restore after provisioning"
1223
+ description: "Skip automatic config restore after provisioning",
1224
+ type: "boolean"
1090
1225
  }
1091
1226
  ],
1092
1227
  examples: [
@@ -1111,7 +1246,8 @@ var COMMANDS = {
1111
1246
  options: [
1112
1247
  {
1113
1248
  flag: "-p, --print",
1114
- description: "Print prompt instead of invoking Claude Code"
1249
+ description: "Print prompt instead of invoking Claude Code",
1250
+ type: "boolean"
1115
1251
  }
1116
1252
  ],
1117
1253
  examples: [
@@ -1131,7 +1267,7 @@ var COMMANDS = {
1131
1267
  required: false
1132
1268
  }
1133
1269
  ],
1134
- options: [{ flag: "--delete <n>", description: "Delete item number n" }],
1270
+ options: [{ flag: "--delete <filename>", description: "Delete item by filename", type: "string" }],
1135
1271
  examples: [
1136
1272
  "npx @lumy-pack/syncpoint list",
1137
1273
  "npx @lumy-pack/syncpoint list backups",
@@ -1143,7 +1279,7 @@ var COMMANDS = {
1143
1279
  description: "Show ~/.syncpoint/ status summary and manage cleanup",
1144
1280
  usage: "npx @lumy-pack/syncpoint status [options]",
1145
1281
  options: [
1146
- { flag: "--cleanup", description: "Enter interactive cleanup mode" }
1282
+ { flag: "--cleanup", description: "Enter interactive cleanup mode", type: "boolean" }
1147
1283
  ],
1148
1284
  examples: [
1149
1285
  "npx @lumy-pack/syncpoint status",
@@ -1157,7 +1293,8 @@ var COMMANDS = {
1157
1293
  options: [
1158
1294
  {
1159
1295
  flag: "--dry-run",
1160
- description: "Preview changes without writing"
1296
+ description: "Preview changes without writing",
1297
+ type: "boolean"
1161
1298
  }
1162
1299
  ],
1163
1300
  examples: [
@@ -1334,6 +1471,33 @@ function registerBackupCommand(program2) {
1334
1471
  });
1335
1472
  cmd.action(
1336
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
+ }
1337
1501
  const { waitUntilExit } = render(
1338
1502
  /* @__PURE__ */ jsx2(
1339
1503
  BackupView,
@@ -1358,46 +1522,7 @@ import { Box as Box2, Text as Text3, useApp as useApp2 } from "ink";
1358
1522
  import { render as render2 } from "ink";
1359
1523
  import Spinner from "ink-spinner";
1360
1524
  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
- }
1525
+ init_wizard_template();
1401
1526
 
1402
1527
  // assets/schemas/template.schema.json
1403
1528
  var template_schema_default = {
@@ -1473,6 +1598,9 @@ function validateTemplate(data) {
1473
1598
  return { valid: false, errors };
1474
1599
  }
1475
1600
 
1601
+ // src/commands/CreateTemplate.tsx
1602
+ init_assets();
1603
+
1476
1604
  // src/utils/claude-code-runner.ts
1477
1605
  import { spawn } from "child_process";
1478
1606
  async function isClaudeCodeAvailable() {
@@ -1797,6 +1925,30 @@ ${formatValidationErrors(validation.errors || [])}`
1797
1925
  };
1798
1926
  function registerCreateTemplateCommand(program2) {
1799
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
+ }
1800
1952
  const { waitUntilExit } = render2(
1801
1953
  /* @__PURE__ */ jsx3(
1802
1954
  CreateTemplateView,
@@ -1949,6 +2101,21 @@ var HelpView = ({ commandName }) => {
1949
2101
  };
1950
2102
  function registerHelpCommand(program2) {
1951
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
+ }
1952
2119
  const { waitUntilExit } = render3(/* @__PURE__ */ jsx4(HelpView, { commandName }));
1953
2120
  await waitUntilExit();
1954
2121
  });
@@ -1959,6 +2126,7 @@ import { join as join9 } from "path";
1959
2126
  import { Box as Box4, Text as Text5, useApp as useApp3 } from "ink";
1960
2127
  import { render as render4 } from "ink";
1961
2128
  import { useEffect as useEffect3, useState as useState3 } from "react";
2129
+ init_assets();
1962
2130
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1963
2131
  var InitView = () => {
1964
2132
  const { exit } = useApp3();
@@ -2062,6 +2230,18 @@ function registerInitCommand(program2) {
2062
2230
  program2.command("init").description(
2063
2231
  `Initialize ~/.${APP_NAME}/ directory structure and default config`
2064
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
+ }
2065
2245
  const { waitUntilExit } = render4(/* @__PURE__ */ jsx5(InitView, {}));
2066
2246
  await waitUntilExit();
2067
2247
  });
@@ -2844,11 +3024,72 @@ var ListView = ({ type, deleteIndex }) => {
2844
3024
  return null;
2845
3025
  };
2846
3026
  function registerListCommand(program2) {
2847
- 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
+ }
2848
3086
  const deleteIndex = opts.delete ? parseInt(opts.delete, 10) : void 0;
2849
3087
  if (deleteIndex !== void 0 && isNaN(deleteIndex)) {
2850
- console.error(`Invalid delete index: ${opts.delete}`);
2851
- process.exit(1);
3088
+ const { waitUntilExit: waitUntilExit2 } = render5(
3089
+ /* @__PURE__ */ jsx8(ListView, { type, deleteIndex: void 0 })
3090
+ );
3091
+ await waitUntilExit2();
3092
+ return;
2852
3093
  }
2853
3094
  const { waitUntilExit } = render5(
2854
3095
  /* @__PURE__ */ jsx8(ListView, { type, deleteIndex })
@@ -2865,6 +3106,7 @@ import { useEffect as useEffect5, useState as useState6 } from "react";
2865
3106
  // src/core/migrate.ts
2866
3107
  import { copyFile as copyFile2, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
2867
3108
  import YAML4 from "yaml";
3109
+ init_assets();
2868
3110
  function extractSchemaPaths(schema, prefix = []) {
2869
3111
  const paths = [];
2870
3112
  const properties = schema.properties;
@@ -3085,6 +3327,18 @@ var MigrateView = ({ dryRun }) => {
3085
3327
  };
3086
3328
  function registerMigrateCommand(program2) {
3087
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
+ }
3088
3342
  const { waitUntilExit } = render6(
3089
3343
  /* @__PURE__ */ jsx9(MigrateView, { dryRun: opts.dryRun ?? false })
3090
3344
  );
@@ -3396,14 +3650,24 @@ function registerProvisionCommand(program2) {
3396
3650
  false
3397
3651
  ).option("-f, --file <path>", "Path to template file").action(
3398
3652
  async (templateName, opts) => {
3653
+ const globalOpts = program2.opts();
3654
+ const startTime = Date.now();
3399
3655
  let templatePath;
3400
3656
  if (opts.file) {
3401
3657
  templatePath = resolveTargetPath(opts.file);
3402
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
+ }
3403
3663
  console.error(`Template file not found: ${opts.file}`);
3404
3664
  process.exit(1);
3405
3665
  }
3406
3666
  if (!templatePath.endsWith(".yml") && !templatePath.endsWith(".yaml")) {
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
+ }
3407
3671
  console.error(
3408
3672
  `Template file must have .yml or .yaml extension: ${opts.file}`
3409
3673
  );
@@ -3415,11 +3679,19 @@ function registerProvisionCommand(program2) {
3415
3679
  (t) => t.name === templateName || t.name === `${templateName}.yml` || t.config.name === templateName
3416
3680
  );
3417
3681
  if (!match) {
3682
+ if (globalOpts.json) {
3683
+ respondError("provision", SyncpointErrorCode.TEMPLATE_NOT_FOUND, `Template not found: ${templateName}`, startTime, VERSION);
3684
+ return;
3685
+ }
3418
3686
  console.error(`Template not found: ${templateName}`);
3419
3687
  process.exit(1);
3420
3688
  }
3421
3689
  templatePath = match.path;
3422
3690
  } else {
3691
+ if (globalOpts.json) {
3692
+ respondError("provision", SyncpointErrorCode.MISSING_ARGUMENT, "Either <template> name or --file option must be provided", startTime, VERSION);
3693
+ return;
3694
+ }
3423
3695
  console.error(
3424
3696
  "Error: Either <template> name or --file option must be provided"
3425
3697
  );
@@ -3431,6 +3703,30 @@ function registerProvisionCommand(program2) {
3431
3703
  if (tmpl.sudo && !opts.dryRun) {
3432
3704
  ensureSudo(tmpl.name);
3433
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
+ }
3434
3730
  const { waitUntilExit } = render7(
3435
3731
  /* @__PURE__ */ jsx11(
3436
3732
  ProvisionView,
@@ -3666,6 +3962,52 @@ var RestoreView = ({ filename, options }) => {
3666
3962
  };
3667
3963
  function registerRestoreCommand(program2) {
3668
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) => {
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;
3993
+ }
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
+ }
3669
4011
  const { waitUntilExit } = render8(
3670
4012
  /* @__PURE__ */ jsx12(RestoreView, { filename, options: { dryRun: opts.dryRun } })
3671
4013
  );
@@ -4080,6 +4422,34 @@ var StatusView = ({ cleanup }) => {
4080
4422
  };
4081
4423
  function registerStatusCommand(program2) {
4082
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
+ }
4083
4453
  const { waitUntilExit } = render9(/* @__PURE__ */ jsx13(StatusView, { cleanup: opts.cleanup }));
4084
4454
  await waitUntilExit();
4085
4455
  });
@@ -4137,6 +4507,9 @@ ${variables.defaultConfig}
4137
4507
  **Start by greeting the user and asking about their backup priorities. After understanding their needs, write the config.yml file directly.**`;
4138
4508
  }
4139
4509
 
4510
+ // src/commands/Wizard.tsx
4511
+ init_assets();
4512
+
4140
4513
  // src/utils/file-scanner.ts
4141
4514
  import { stat as stat3 } from "fs/promises";
4142
4515
  import { join as join13 } from "path";
@@ -4494,6 +4867,27 @@ function registerWizardCommand(program2) {
4494
4867
  cmd.option(opt.flag, opt.description);
4495
4868
  });
4496
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
+ }
4497
4891
  if (opts.print) {
4498
4892
  const { waitUntilExit } = render10(/* @__PURE__ */ jsx14(WizardView, { printMode: true }));
4499
4893
  await waitUntilExit();
@@ -4530,7 +4924,7 @@ function registerWizardCommand(program2) {
4530
4924
  var program = new Command();
4531
4925
  program.name("syncpoint").description(
4532
4926
  "Personal Environment Manager \u2014 Config backup/restore and machine provisioning CLI"
4533
- ).version(VERSION);
4927
+ ).version(VERSION).option("--json", "Output structured JSON to stdout").option("--yes", "Skip confirmation prompts (non-interactive mode)");
4534
4928
  registerInitCommand(program);
4535
4929
  registerWizardCommand(program);
4536
4930
  registerBackupCommand(program);
@@ -4541,7 +4935,34 @@ registerListCommand(program);
4541
4935
  registerMigrateCommand(program);
4542
4936
  registerStatusCommand(program);
4543
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
+ }
4544
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
+ }
4545
4966
  console.error("Fatal error:", error.message);
4546
4967
  process.exit(1);
4547
4968
  });
@@ -0,0 +1,16 @@
1
+ export declare const SyncpointErrorCode: {
2
+ readonly CONFIG_NOT_FOUND: "CONFIG_NOT_FOUND";
3
+ readonly CONFIG_INVALID: "CONFIG_INVALID";
4
+ readonly BACKUP_FAILED: "BACKUP_FAILED";
5
+ readonly RESTORE_FAILED: "RESTORE_FAILED";
6
+ readonly TEMPLATE_NOT_FOUND: "TEMPLATE_NOT_FOUND";
7
+ readonly PROVISION_FAILED: "PROVISION_FAILED";
8
+ readonly MISSING_ARGUMENT: "MISSING_ARGUMENT";
9
+ readonly INVALID_ARGUMENT: "INVALID_ARGUMENT";
10
+ readonly UNKNOWN: "UNKNOWN";
11
+ };
12
+ export type SyncpointErrorCode = (typeof SyncpointErrorCode)[keyof typeof SyncpointErrorCode];
13
+ /**
14
+ * Classify an error into a SyncpointErrorCode based on the error message.
15
+ */
16
+ export declare function classifyError(err: unknown): SyncpointErrorCode;
package/dist/index.cjs CHANGED
@@ -646,7 +646,7 @@ function validateMetadata(data) {
646
646
  }
647
647
 
648
648
  // src/version.ts
649
- var VERSION = "0.0.8";
649
+ var VERSION = "0.0.10";
650
650
 
651
651
  // src/core/metadata.ts
652
652
  var METADATA_VERSION = "1.0.0";
package/dist/index.mjs CHANGED
@@ -596,7 +596,7 @@ function validateMetadata(data) {
596
596
  }
597
597
 
598
598
  // src/version.ts
599
- var VERSION = "0.0.8";
599
+ var VERSION = "0.0.10";
600
600
 
601
601
  // src/core/metadata.ts
602
602
  var METADATA_VERSION = "1.0.0";
@@ -5,6 +5,7 @@
5
5
  export interface CommandOption {
6
6
  flag: string;
7
7
  description: string;
8
+ type?: 'boolean' | 'string' | 'number';
8
9
  default?: string;
9
10
  }
10
11
  export interface CommandArgument {
package/dist/version.d.ts CHANGED
@@ -2,4 +2,4 @@
2
2
  * Current package version from package.json
3
3
  * Automatically synchronized during build process
4
4
  */
5
- export declare const VERSION = "0.0.8";
5
+ export declare const VERSION = "0.0.10";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumy-pack/syncpoint",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "description": "CLI tool for project synchronization and scaffolding",
5
5
  "keywords": [
6
6
  "cli",
@@ -56,7 +56,6 @@
56
56
  "version:patch": "yarn version patch"
57
57
  },
58
58
  "dependencies": {
59
- "@lumy-pack/shared": "0.0.1",
60
59
  "ajv": "^8.0.0",
61
60
  "ajv-formats": "^3.0.0",
62
61
  "commander": "^12.1.0",
@@ -71,6 +70,7 @@
71
70
  "yaml": "^2.0.0"
72
71
  },
73
72
  "devDependencies": {
73
+ "@lumy-pack/shared": "0.0.1",
74
74
  "@types/micromatch": "^4.0.9",
75
75
  "@types/node": "^20.11.0",
76
76
  "@types/react": "^18.0.0",