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