@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/LICENSE +21 -0
- package/README.md +69 -38
- package/assets/schemas/config.schema.json +3 -9
- package/dist/cli.mjs +670 -200
- package/dist/commands/CreateTemplate.d.ts +1 -1
- package/dist/commands/Init.d.ts +1 -1
- package/dist/commands/Provision.d.ts +1 -1
- package/dist/commands/Restore.d.ts +1 -1
- package/dist/components/ProgressBar.d.ts +1 -1
- package/dist/components/StepRunner.d.ts +2 -2
- package/dist/components/Table.d.ts +1 -1
- package/dist/components/Viewer.d.ts +1 -1
- package/dist/errors.d.ts +16 -0
- package/dist/index.cjs +3 -9
- package/dist/index.mjs +3 -9
- package/dist/utils/command-registry.d.ts +1 -0
- package/dist/version.d.ts +1 -1
- package/package.json +4 -2
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.
|
|
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 <
|
|
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({
|
|
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(
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
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/
|
|
1428
|
-
|
|
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(
|
|
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: "
|
|
1766
|
-
/* @__PURE__ */
|
|
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(
|
|
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({
|
|
2171
|
+
completed.push({
|
|
2172
|
+
name: `Created ${CONFIG_FILENAME} (defaults)`,
|
|
2173
|
+
done: true
|
|
2174
|
+
});
|
|
1984
2175
|
setSteps([...completed]);
|
|
1985
|
-
const exampleTemplatePath = join9(
|
|
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(
|
|
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 {
|
|
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(
|
|
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 <
|
|
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
|
-
|
|
2821
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
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))
|
|
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))
|
|
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))
|
|
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))
|
|
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 {
|
|
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
|
});
|