@lumy-pack/syncpoint 0.0.1 → 0.0.2
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/README.md +921 -0
- package/assets/config.default.yml +12 -2
- package/dist/cli.mjs +2750 -1440
- package/dist/commands/Backup.d.ts +1 -1
- package/dist/commands/CreateTemplate.d.ts +2 -0
- package/dist/commands/Help.d.ts +2 -0
- package/dist/commands/Wizard.d.ts +2 -0
- package/dist/constants.d.ts +1 -2
- package/dist/core/backup.d.ts +8 -1
- package/dist/core/config.d.ts +1 -1
- package/dist/core/metadata.d.ts +1 -1
- package/dist/core/provision.d.ts +1 -1
- package/dist/core/restore.d.ts +1 -1
- package/dist/index.cjs +215 -46
- package/dist/index.d.ts +5 -5
- package/dist/index.mjs +219 -50
- package/dist/prompts/wizard-config.d.ts +9 -0
- package/dist/prompts/wizard-template.d.ts +7 -0
- package/dist/schemas/ajv.d.ts +1 -1
- package/dist/utils/claude-code-runner.d.ts +29 -0
- package/dist/utils/command-registry.d.ts +23 -0
- package/dist/utils/error-formatter.d.ts +16 -0
- package/dist/utils/file-scanner.d.ts +25 -0
- package/dist/utils/paths.d.ts +5 -0
- package/dist/utils/pattern.d.ts +46 -0
- package/dist/utils/types.d.ts +4 -3
- package/dist/utils/yaml-parser.d.ts +19 -0
- package/dist/version.d.ts +5 -0
- package/package.json +5 -3
package/dist/index.mjs
CHANGED
|
@@ -49,6 +49,14 @@ async function fileExists(filePath) {
|
|
|
49
49
|
return false;
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
+
async function isDirectory(filePath) {
|
|
53
|
+
try {
|
|
54
|
+
const stats = await stat(filePath);
|
|
55
|
+
return stats.isDirectory();
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
52
60
|
|
|
53
61
|
// src/constants.ts
|
|
54
62
|
var APP_NAME = "syncpoint";
|
|
@@ -64,7 +72,6 @@ var LOGS_DIR = "logs";
|
|
|
64
72
|
function getAppDir() {
|
|
65
73
|
return join2(getHomeDir(), APP_DIR);
|
|
66
74
|
}
|
|
67
|
-
var APP_VERSION = "0.0.1";
|
|
68
75
|
function getSubDir(sub) {
|
|
69
76
|
return join2(getAppDir(), sub);
|
|
70
77
|
}
|
|
@@ -72,8 +79,106 @@ function getSubDir(sub) {
|
|
|
72
79
|
// src/schemas/ajv.ts
|
|
73
80
|
import Ajv from "ajv";
|
|
74
81
|
import addFormats from "ajv-formats";
|
|
82
|
+
|
|
83
|
+
// src/utils/pattern.ts
|
|
84
|
+
import micromatch from "micromatch";
|
|
85
|
+
function detectPatternType(pattern) {
|
|
86
|
+
if (pattern.startsWith("/") && pattern.endsWith("/") && pattern.length > 2) {
|
|
87
|
+
const inner = pattern.slice(1, -1);
|
|
88
|
+
const hasUnescapedSlash = /(?<!\\)\//.test(inner);
|
|
89
|
+
if (!hasUnescapedSlash) {
|
|
90
|
+
return "regex";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (pattern.includes("*") || pattern.includes("?") || pattern.includes("{")) {
|
|
94
|
+
return "glob";
|
|
95
|
+
}
|
|
96
|
+
return "literal";
|
|
97
|
+
}
|
|
98
|
+
function parseRegexPattern(pattern) {
|
|
99
|
+
if (!pattern.startsWith("/") || !pattern.endsWith("/")) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Invalid regex pattern format: ${pattern}. Must be enclosed in slashes like /pattern/`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const regexBody = pattern.slice(1, -1);
|
|
105
|
+
try {
|
|
106
|
+
return new RegExp(regexBody);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Invalid regex pattern: ${pattern}. ${error instanceof Error ? error.message : String(error)}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function createExcludeMatcher(excludePatterns) {
|
|
114
|
+
if (excludePatterns.length === 0) {
|
|
115
|
+
return () => false;
|
|
116
|
+
}
|
|
117
|
+
const regexPatterns = [];
|
|
118
|
+
const globPatterns = [];
|
|
119
|
+
const literalPatterns = /* @__PURE__ */ new Set();
|
|
120
|
+
for (const pattern of excludePatterns) {
|
|
121
|
+
const type = detectPatternType(pattern);
|
|
122
|
+
if (type === "regex") {
|
|
123
|
+
try {
|
|
124
|
+
regexPatterns.push(parseRegexPattern(pattern));
|
|
125
|
+
} catch {
|
|
126
|
+
console.warn(`Skipping invalid regex pattern: ${pattern}`);
|
|
127
|
+
}
|
|
128
|
+
} else if (type === "glob") {
|
|
129
|
+
globPatterns.push(pattern);
|
|
130
|
+
} else {
|
|
131
|
+
literalPatterns.add(pattern);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return (filePath) => {
|
|
135
|
+
if (literalPatterns.has(filePath)) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
if (globPatterns.length > 0 && micromatch.isMatch(filePath, globPatterns, {
|
|
139
|
+
dot: true,
|
|
140
|
+
// Match dotfiles
|
|
141
|
+
matchBase: false
|
|
142
|
+
// Don't use basename matching, match full path
|
|
143
|
+
})) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
for (const regex of regexPatterns) {
|
|
147
|
+
if (regex.test(filePath)) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function isValidPattern(pattern) {
|
|
155
|
+
if (typeof pattern !== "string" || pattern.length === 0) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
const type = detectPatternType(pattern);
|
|
159
|
+
if (type === "regex") {
|
|
160
|
+
try {
|
|
161
|
+
parseRegexPattern(pattern);
|
|
162
|
+
return true;
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/schemas/ajv.ts
|
|
75
171
|
var ajv = new Ajv({ allErrors: true });
|
|
76
172
|
addFormats(ajv);
|
|
173
|
+
ajv.addKeyword({
|
|
174
|
+
keyword: "validPattern",
|
|
175
|
+
type: "string",
|
|
176
|
+
validate: function validate(schema, data) {
|
|
177
|
+
if (!schema) return true;
|
|
178
|
+
return isValidPattern(data);
|
|
179
|
+
},
|
|
180
|
+
errors: true
|
|
181
|
+
});
|
|
77
182
|
|
|
78
183
|
// src/schemas/config.schema.ts
|
|
79
184
|
var configSchema = {
|
|
@@ -86,11 +191,11 @@ var configSchema = {
|
|
|
86
191
|
properties: {
|
|
87
192
|
targets: {
|
|
88
193
|
type: "array",
|
|
89
|
-
items: { type: "string" }
|
|
194
|
+
items: { type: "string", validPattern: true }
|
|
90
195
|
},
|
|
91
196
|
exclude: {
|
|
92
197
|
type: "array",
|
|
93
|
-
items: { type: "string" }
|
|
198
|
+
items: { type: "string", validPattern: true }
|
|
94
199
|
},
|
|
95
200
|
filename: {
|
|
96
201
|
type: "string",
|
|
@@ -114,11 +219,11 @@ var configSchema = {
|
|
|
114
219
|
},
|
|
115
220
|
additionalProperties: false
|
|
116
221
|
};
|
|
117
|
-
var
|
|
222
|
+
var validate2 = ajv.compile(configSchema);
|
|
118
223
|
function validateConfig(data) {
|
|
119
|
-
const valid =
|
|
224
|
+
const valid = validate2(data);
|
|
120
225
|
if (valid) return { valid: true };
|
|
121
|
-
const errors =
|
|
226
|
+
const errors = validate2.errors?.map(
|
|
122
227
|
(e) => `${e.instancePath || "/"} ${e.message ?? "unknown error"}`
|
|
123
228
|
);
|
|
124
229
|
return { valid: false, errors };
|
|
@@ -170,20 +275,16 @@ Run "syncpoint init" first.`
|
|
|
170
275
|
const data = stripDangerousKeys(YAML.parse(raw));
|
|
171
276
|
const result = validateConfig(data);
|
|
172
277
|
if (!result.valid) {
|
|
173
|
-
throw new Error(
|
|
174
|
-
|
|
175
|
-
${(result.errors ?? []).join("\n")}`
|
|
176
|
-
);
|
|
278
|
+
throw new Error(`Invalid config:
|
|
279
|
+
${(result.errors ?? []).join("\n")}`);
|
|
177
280
|
}
|
|
178
281
|
return data;
|
|
179
282
|
}
|
|
180
283
|
async function saveConfig(config) {
|
|
181
284
|
const result = validateConfig(config);
|
|
182
285
|
if (!result.valid) {
|
|
183
|
-
throw new Error(
|
|
184
|
-
|
|
185
|
-
${(result.errors ?? []).join("\n")}`
|
|
186
|
-
);
|
|
286
|
+
throw new Error(`Invalid config:
|
|
287
|
+
${(result.errors ?? []).join("\n")}`);
|
|
187
288
|
}
|
|
188
289
|
const configPath = getConfigPath();
|
|
189
290
|
await ensureDir(getAppDir());
|
|
@@ -223,11 +324,11 @@ async function initDefaultConfig() {
|
|
|
223
324
|
|
|
224
325
|
// src/core/backup.ts
|
|
225
326
|
import { readdir } from "fs/promises";
|
|
226
|
-
import { join as join7
|
|
327
|
+
import { basename, join as join7 } from "path";
|
|
227
328
|
import fg from "fast-glob";
|
|
228
329
|
|
|
229
330
|
// src/utils/system.ts
|
|
230
|
-
import { hostname as osHostname, platform, release
|
|
331
|
+
import { arch, hostname as osHostname, platform, release } from "os";
|
|
231
332
|
function getHostname() {
|
|
232
333
|
return osHostname();
|
|
233
334
|
}
|
|
@@ -240,7 +341,7 @@ function getSystemInfo() {
|
|
|
240
341
|
}
|
|
241
342
|
function formatHostname(name) {
|
|
242
343
|
const raw = name ?? getHostname();
|
|
243
|
-
return raw.replace(/\s+/g, "-").replace(/\./g, "-").replace(/[^a-zA-Z0-9
|
|
344
|
+
return raw.replace(/\s+/g, "-").replace(/\./g, "-").replace(/[^a-zA-Z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
244
345
|
}
|
|
245
346
|
|
|
246
347
|
// src/utils/format.ts
|
|
@@ -332,7 +433,7 @@ var logger = {
|
|
|
332
433
|
|
|
333
434
|
// src/core/metadata.ts
|
|
334
435
|
import { createHash } from "crypto";
|
|
335
|
-
import { readFile as readFile2
|
|
436
|
+
import { lstat, readFile as readFile2 } from "fs/promises";
|
|
336
437
|
|
|
337
438
|
// src/schemas/metadata.schema.ts
|
|
338
439
|
var metadataSchema = {
|
|
@@ -398,23 +499,26 @@ var metadataSchema = {
|
|
|
398
499
|
},
|
|
399
500
|
additionalProperties: false
|
|
400
501
|
};
|
|
401
|
-
var
|
|
502
|
+
var validate3 = ajv.compile(metadataSchema);
|
|
402
503
|
function validateMetadata(data) {
|
|
403
|
-
const valid =
|
|
504
|
+
const valid = validate3(data);
|
|
404
505
|
if (valid) return { valid: true };
|
|
405
|
-
const errors =
|
|
506
|
+
const errors = validate3.errors?.map(
|
|
406
507
|
(e) => `${e.instancePath || "/"} ${e.message ?? "unknown error"}`
|
|
407
508
|
);
|
|
408
509
|
return { valid: false, errors };
|
|
409
510
|
}
|
|
410
511
|
|
|
512
|
+
// src/version.ts
|
|
513
|
+
var VERSION = "0.0.2";
|
|
514
|
+
|
|
411
515
|
// src/core/metadata.ts
|
|
412
516
|
var METADATA_VERSION = "1.0.0";
|
|
413
517
|
function createMetadata(files, config) {
|
|
414
518
|
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
415
519
|
return {
|
|
416
520
|
version: METADATA_VERSION,
|
|
417
|
-
toolVersion:
|
|
521
|
+
toolVersion: VERSION,
|
|
418
522
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
419
523
|
hostname: getHostname(),
|
|
420
524
|
system: getSystemInfo(),
|
|
@@ -434,10 +538,8 @@ function parseMetadata(data) {
|
|
|
434
538
|
const parsed = JSON.parse(str);
|
|
435
539
|
const result = validateMetadata(parsed);
|
|
436
540
|
if (!result.valid) {
|
|
437
|
-
throw new Error(
|
|
438
|
-
|
|
439
|
-
${(result.errors ?? []).join("\n")}`
|
|
440
|
-
);
|
|
541
|
+
throw new Error(`Invalid metadata:
|
|
542
|
+
${(result.errors ?? []).join("\n")}`);
|
|
441
543
|
}
|
|
442
544
|
return parsed;
|
|
443
545
|
}
|
|
@@ -453,6 +555,9 @@ async function collectFileInfo(absolutePath, logicalPath) {
|
|
|
453
555
|
type = "symlink";
|
|
454
556
|
} else if (lstats.isDirectory()) {
|
|
455
557
|
type = "directory";
|
|
558
|
+
throw new Error(
|
|
559
|
+
`Cannot collect file info for directory: ${logicalPath}. Directories should be converted to glob patterns before calling collectFileInfo().`
|
|
560
|
+
);
|
|
456
561
|
}
|
|
457
562
|
let hash;
|
|
458
563
|
if (lstats.isSymbolicLink()) {
|
|
@@ -480,7 +585,10 @@ async function createArchive(files, outputPath) {
|
|
|
480
585
|
const fileNames = [];
|
|
481
586
|
for (const file of files) {
|
|
482
587
|
const targetPath = join6(tmpDir, file.name);
|
|
483
|
-
const parentDir = join6(
|
|
588
|
+
const parentDir = join6(
|
|
589
|
+
tmpDir,
|
|
590
|
+
file.name.split("/").slice(0, -1).join("/")
|
|
591
|
+
);
|
|
484
592
|
if (parentDir !== tmpDir) {
|
|
485
593
|
await mkdir3(parentDir, { recursive: true });
|
|
486
594
|
}
|
|
@@ -557,18 +665,48 @@ function isSensitiveFile(filePath) {
|
|
|
557
665
|
async function scanTargets(config) {
|
|
558
666
|
const found = [];
|
|
559
667
|
const missing = [];
|
|
668
|
+
const isExcluded = createExcludeMatcher(config.backup.exclude);
|
|
560
669
|
for (const target of config.backup.targets) {
|
|
561
670
|
const expanded = expandTilde(target);
|
|
562
|
-
|
|
671
|
+
const patternType = detectPatternType(expanded);
|
|
672
|
+
if (patternType === "regex") {
|
|
673
|
+
try {
|
|
674
|
+
const regex = parseRegexPattern(expanded);
|
|
675
|
+
const homeDir = expandTilde("~/");
|
|
676
|
+
const homeDirNormalized = homeDir.endsWith("/") ? homeDir : `${homeDir}/`;
|
|
677
|
+
const allFiles = await fg(`${homeDirNormalized}**`, {
|
|
678
|
+
dot: true,
|
|
679
|
+
absolute: true,
|
|
680
|
+
onlyFiles: true,
|
|
681
|
+
deep: 5
|
|
682
|
+
// Limit depth for performance
|
|
683
|
+
});
|
|
684
|
+
for (const match of allFiles) {
|
|
685
|
+
if (regex.test(match) && !isExcluded(match)) {
|
|
686
|
+
const entry = await collectFileInfo(match, match);
|
|
687
|
+
found.push(entry);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
} catch (error) {
|
|
691
|
+
logger.warn(
|
|
692
|
+
`Invalid regex pattern "${target}": ${error instanceof Error ? error.message : String(error)}`
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
} else if (patternType === "glob") {
|
|
696
|
+
const globExcludes = config.backup.exclude.filter(
|
|
697
|
+
(p) => detectPatternType(p) === "glob"
|
|
698
|
+
);
|
|
563
699
|
const matches = await fg(expanded, {
|
|
564
700
|
dot: true,
|
|
565
701
|
absolute: true,
|
|
566
|
-
ignore:
|
|
702
|
+
ignore: globExcludes,
|
|
567
703
|
onlyFiles: true
|
|
568
704
|
});
|
|
569
705
|
for (const match of matches) {
|
|
570
|
-
|
|
571
|
-
|
|
706
|
+
if (!isExcluded(match)) {
|
|
707
|
+
const entry = await collectFileInfo(match, match);
|
|
708
|
+
found.push(entry);
|
|
709
|
+
}
|
|
572
710
|
}
|
|
573
711
|
} else {
|
|
574
712
|
const absPath = resolveTargetPath(target);
|
|
@@ -577,16 +715,43 @@ async function scanTargets(config) {
|
|
|
577
715
|
missing.push(target);
|
|
578
716
|
continue;
|
|
579
717
|
}
|
|
580
|
-
const
|
|
581
|
-
if (
|
|
582
|
-
|
|
583
|
-
|
|
718
|
+
const isDir = await isDirectory(absPath);
|
|
719
|
+
if (isDir) {
|
|
720
|
+
const dirGlob = `${expanded}/**/*`;
|
|
721
|
+
const globExcludes = config.backup.exclude.filter(
|
|
722
|
+
(p) => detectPatternType(p) === "glob"
|
|
584
723
|
);
|
|
724
|
+
const matches = await fg(dirGlob, {
|
|
725
|
+
dot: true,
|
|
726
|
+
absolute: true,
|
|
727
|
+
ignore: globExcludes,
|
|
728
|
+
onlyFiles: true
|
|
729
|
+
// Only include files, not subdirectories
|
|
730
|
+
});
|
|
731
|
+
for (const match of matches) {
|
|
732
|
+
if (!isExcluded(match)) {
|
|
733
|
+
const entry = await collectFileInfo(match, match);
|
|
734
|
+
found.push(entry);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (matches.length === 0) {
|
|
738
|
+
logger.warn(`Directory is empty or fully excluded: ${target}`);
|
|
739
|
+
}
|
|
740
|
+
} else {
|
|
741
|
+
if (isExcluded(absPath)) {
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
const entry = await collectFileInfo(absPath, absPath);
|
|
745
|
+
if (entry.size > LARGE_FILE_THRESHOLD) {
|
|
746
|
+
logger.warn(
|
|
747
|
+
`Large file (>${Math.round(LARGE_FILE_THRESHOLD / 1024 / 1024)}MB): ${target}`
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
if (isSensitiveFile(absPath)) {
|
|
751
|
+
logger.warn(`Sensitive file detected: ${target}`);
|
|
752
|
+
}
|
|
753
|
+
found.push(entry);
|
|
585
754
|
}
|
|
586
|
-
if (isSensitiveFile(absPath)) {
|
|
587
|
-
logger.warn(`Sensitive file detected: ${target}`);
|
|
588
|
-
}
|
|
589
|
-
found.push(entry);
|
|
590
755
|
}
|
|
591
756
|
}
|
|
592
757
|
return { found, missing };
|
|
@@ -612,8 +777,10 @@ async function collectScripts() {
|
|
|
612
777
|
}
|
|
613
778
|
async function createBackup(config, options = {}) {
|
|
614
779
|
const { found, missing } = await scanTargets(config);
|
|
615
|
-
|
|
616
|
-
|
|
780
|
+
if (options.verbose && missing.length > 0) {
|
|
781
|
+
for (const m of missing) {
|
|
782
|
+
logger.warn(`File not found, skipping: ${m}`);
|
|
783
|
+
}
|
|
617
784
|
}
|
|
618
785
|
let allFiles = [...found];
|
|
619
786
|
if (config.scripts.includeInBackup) {
|
|
@@ -652,7 +819,7 @@ async function createBackup(config, options = {}) {
|
|
|
652
819
|
|
|
653
820
|
// src/core/restore.ts
|
|
654
821
|
import { copyFile, lstat as lstat2, readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
655
|
-
import {
|
|
822
|
+
import { dirname as dirname2, join as join8 } from "path";
|
|
656
823
|
async function getBackupList(config) {
|
|
657
824
|
const backupDir = config?.backup.destination ? resolveTargetPath(config.backup.destination) : getSubDir(BACKUPS_DIR);
|
|
658
825
|
const exists = await fileExists(backupDir);
|
|
@@ -842,11 +1009,11 @@ var templateSchema = {
|
|
|
842
1009
|
},
|
|
843
1010
|
additionalProperties: false
|
|
844
1011
|
};
|
|
845
|
-
var
|
|
1012
|
+
var validate4 = ajv.compile(templateSchema);
|
|
846
1013
|
function validateTemplate(data) {
|
|
847
|
-
const valid =
|
|
1014
|
+
const valid = validate4(data);
|
|
848
1015
|
if (valid) return { valid: true };
|
|
849
|
-
const errors =
|
|
1016
|
+
const errors = validate4.errors?.map(
|
|
850
1017
|
(e) => `${e.instancePath || "/"} ${e.message ?? "unknown error"}`
|
|
851
1018
|
);
|
|
852
1019
|
return { valid: false, errors };
|
|
@@ -930,7 +1097,9 @@ function execAsync(command) {
|
|
|
930
1097
|
}
|
|
931
1098
|
async function evaluateSkipIf(command, stepName) {
|
|
932
1099
|
if (containsRemoteScriptPattern(command)) {
|
|
933
|
-
throw new Error(
|
|
1100
|
+
throw new Error(
|
|
1101
|
+
`Blocked dangerous remote script pattern in skip_if: ${stepName}`
|
|
1102
|
+
);
|
|
934
1103
|
}
|
|
935
1104
|
try {
|
|
936
1105
|
await execAsync(command);
|
|
@@ -942,7 +1111,9 @@ async function evaluateSkipIf(command, stepName) {
|
|
|
942
1111
|
async function executeStep(step) {
|
|
943
1112
|
const startTime = Date.now();
|
|
944
1113
|
if (containsRemoteScriptPattern(step.command)) {
|
|
945
|
-
throw new Error(
|
|
1114
|
+
throw new Error(
|
|
1115
|
+
`Blocked dangerous remote script pattern in command: ${step.name}`
|
|
1116
|
+
);
|
|
946
1117
|
}
|
|
947
1118
|
if (step.skip_if) {
|
|
948
1119
|
const shouldSkip = await evaluateSkipIf(step.skip_if, step.name);
|
|
@@ -996,9 +1167,7 @@ async function* runProvision(templatePath, options = {}) {
|
|
|
996
1167
|
const result = await executeStep(step);
|
|
997
1168
|
yield result;
|
|
998
1169
|
if (result.status === "failed" && !step.continue_on_error) {
|
|
999
|
-
logger.error(
|
|
1000
|
-
`Step "${step.name}" failed. Stopping provisioning.`
|
|
1001
|
-
);
|
|
1170
|
+
logger.error(`Step "${step.name}" failed. Stopping provisioning.`);
|
|
1002
1171
|
return;
|
|
1003
1172
|
}
|
|
1004
1173
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FileStructure } from '../utils/file-scanner.js';
|
|
2
|
+
export interface ConfigPromptVariables {
|
|
3
|
+
fileStructure: FileStructure;
|
|
4
|
+
defaultConfig: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Generate the LLM prompt for config wizard
|
|
8
|
+
*/
|
|
9
|
+
export declare function generateConfigWizardPrompt(variables: ConfigPromptVariables): string;
|
package/dist/schemas/ajv.d.ts
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface ClaudeCodeResult {
|
|
2
|
+
success: boolean;
|
|
3
|
+
output: string;
|
|
4
|
+
error?: string;
|
|
5
|
+
sessionId?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Check if Claude Code CLI is available
|
|
9
|
+
*/
|
|
10
|
+
export declare function isClaudeCodeAvailable(): Promise<boolean>;
|
|
11
|
+
/**
|
|
12
|
+
* Invoke Claude Code in edit mode with a prompt
|
|
13
|
+
*/
|
|
14
|
+
export declare function invokeClaudeCode(prompt: string, options?: {
|
|
15
|
+
sessionId?: string;
|
|
16
|
+
timeout?: number;
|
|
17
|
+
}): Promise<ClaudeCodeResult>;
|
|
18
|
+
/**
|
|
19
|
+
* Resume a Claude Code session with additional context
|
|
20
|
+
*/
|
|
21
|
+
export declare function resumeClaudeCodeSession(sessionId: string, prompt: string, options?: {
|
|
22
|
+
timeout?: number;
|
|
23
|
+
}): Promise<ClaudeCodeResult>;
|
|
24
|
+
/**
|
|
25
|
+
* Invoke Claude Code in interactive mode
|
|
26
|
+
* User can directly interact with Claude Code UI
|
|
27
|
+
* @param prompt - Context prompt with file structure
|
|
28
|
+
*/
|
|
29
|
+
export declare function invokeClaudeCodeInteractive(prompt: string): Promise<ClaudeCodeResult>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central command registry for syncpoint CLI
|
|
3
|
+
* This ensures consistency between custom help and Commander.js --help
|
|
4
|
+
*/
|
|
5
|
+
export interface CommandOption {
|
|
6
|
+
flag: string;
|
|
7
|
+
description: string;
|
|
8
|
+
default?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface CommandArgument {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
required: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface CommandInfo {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
usage: string;
|
|
19
|
+
arguments?: CommandArgument[];
|
|
20
|
+
options?: CommandOption[];
|
|
21
|
+
examples?: string[];
|
|
22
|
+
}
|
|
23
|
+
export declare const COMMANDS: Record<string, CommandInfo>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format validation errors for human-readable feedback to LLM
|
|
3
|
+
*/
|
|
4
|
+
export declare function formatValidationErrors(errors: string[]): string;
|
|
5
|
+
/**
|
|
6
|
+
* Create a retry prompt with validation error context
|
|
7
|
+
*/
|
|
8
|
+
export declare function createRetryPrompt(originalPrompt: string, errors: string[], attemptNumber: number): string;
|
|
9
|
+
/**
|
|
10
|
+
* Format error for display in terminal UI
|
|
11
|
+
*/
|
|
12
|
+
export declare function formatErrorForDisplay(error: string | Error): string;
|
|
13
|
+
/**
|
|
14
|
+
* Create user-friendly error messages
|
|
15
|
+
*/
|
|
16
|
+
export declare function createUserFriendlyError(context: string, error: unknown): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface FileCategory {
|
|
2
|
+
category: string;
|
|
3
|
+
files: string[];
|
|
4
|
+
}
|
|
5
|
+
export interface FileStructure {
|
|
6
|
+
homeDir: string;
|
|
7
|
+
categories: FileCategory[];
|
|
8
|
+
totalFiles: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Scan home directory and categorize files for backup suggestions
|
|
12
|
+
*/
|
|
13
|
+
export declare function scanHomeDirectory(options?: {
|
|
14
|
+
maxDepth?: number;
|
|
15
|
+
maxFiles?: number;
|
|
16
|
+
ignorePatterns?: string[];
|
|
17
|
+
}): Promise<FileStructure>;
|
|
18
|
+
/**
|
|
19
|
+
* Convert file structure to JSON format for LLM prompt
|
|
20
|
+
*/
|
|
21
|
+
export declare function fileStructureToJSON(structure: FileStructure): string;
|
|
22
|
+
/**
|
|
23
|
+
* Get a list of recommended backup targets based on scan results
|
|
24
|
+
*/
|
|
25
|
+
export declare function getRecommendedTargets(structure: FileStructure): string[];
|
package/dist/utils/paths.d.ts
CHANGED
|
@@ -19,4 +19,9 @@ export declare function ensureDir(dirPath: string): Promise<void>;
|
|
|
19
19
|
* Check if a file exists.
|
|
20
20
|
*/
|
|
21
21
|
export declare function fileExists(filePath: string): Promise<boolean>;
|
|
22
|
+
/**
|
|
23
|
+
* Check if a path is a directory.
|
|
24
|
+
* Returns false if path doesn't exist or is not a directory.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isDirectory(filePath: string): Promise<boolean>;
|
|
22
27
|
export declare function isInsideDir(filePath: string, dir: string): boolean;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern type classification for targets and excludes.
|
|
3
|
+
*/
|
|
4
|
+
export type PatternType = 'literal' | 'glob' | 'regex';
|
|
5
|
+
/**
|
|
6
|
+
* Detect the type of a pattern string.
|
|
7
|
+
*
|
|
8
|
+
* Rules:
|
|
9
|
+
* - Regex: Starts and ends with `/`, and the content between doesn't contain unescaped `/`
|
|
10
|
+
* Example: `/\.conf$/`, `/test/` (simple)
|
|
11
|
+
* - Glob: Contains glob metacharacters: `*`, `?`, `{`
|
|
12
|
+
* Example: `*.conf`, `~/.config/{a,b}`
|
|
13
|
+
* - Literal: Everything else
|
|
14
|
+
* Example: `~/.zshrc`, `/usr/local/bin`
|
|
15
|
+
*
|
|
16
|
+
* Special handling for absolute paths vs regex:
|
|
17
|
+
* - `/usr/local/bin` is literal (multiple `/` inside, not at boundaries)
|
|
18
|
+
* - `/test/` is regex (starts and ends with `/`, single segment inside)
|
|
19
|
+
* - `/\.conf$/` is regex (starts and ends with `/`, no unescaped `/` inside)
|
|
20
|
+
*/
|
|
21
|
+
export declare function detectPatternType(pattern: string): PatternType;
|
|
22
|
+
/**
|
|
23
|
+
* Parse a regex pattern string (e.g., `/\.conf$/`) into a RegExp object.
|
|
24
|
+
*
|
|
25
|
+
* @throws {Error} If the pattern is invalid regex syntax
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseRegexPattern(pattern: string): RegExp;
|
|
28
|
+
/**
|
|
29
|
+
* Create an optimized exclude matcher function.
|
|
30
|
+
*
|
|
31
|
+
* This function pre-compiles all patterns for efficient repeated matching.
|
|
32
|
+
* It handles three pattern types: glob, regex, and literal.
|
|
33
|
+
*
|
|
34
|
+
* @param excludePatterns - Array of exclude pattern strings
|
|
35
|
+
* @returns A function that tests if a file path should be excluded
|
|
36
|
+
*/
|
|
37
|
+
export declare function createExcludeMatcher(excludePatterns: string[]): (filePath: string) => boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Validate that a pattern string is well-formed.
|
|
40
|
+
*
|
|
41
|
+
* Used for schema validation at config load time.
|
|
42
|
+
*
|
|
43
|
+
* @param pattern - Pattern string to validate
|
|
44
|
+
* @returns true if valid, false otherwise
|
|
45
|
+
*/
|
|
46
|
+
export declare function isValidPattern(pattern: string): boolean;
|
package/dist/utils/types.d.ts
CHANGED
|
@@ -65,7 +65,7 @@ export interface RestorePlan {
|
|
|
65
65
|
}
|
|
66
66
|
export interface RestoreAction {
|
|
67
67
|
path: string;
|
|
68
|
-
action:
|
|
68
|
+
action: 'overwrite' | 'skip' | 'create';
|
|
69
69
|
currentSize?: number;
|
|
70
70
|
backupSize?: number;
|
|
71
71
|
reason: string;
|
|
@@ -76,7 +76,7 @@ export interface ProvisionResult {
|
|
|
76
76
|
}
|
|
77
77
|
export interface StepResult {
|
|
78
78
|
name: string;
|
|
79
|
-
status:
|
|
79
|
+
status: 'success' | 'skipped' | 'failed' | 'running' | 'pending';
|
|
80
80
|
duration?: number;
|
|
81
81
|
error?: string;
|
|
82
82
|
output?: string;
|
|
@@ -84,6 +84,7 @@ export interface StepResult {
|
|
|
84
84
|
export interface BackupOptions {
|
|
85
85
|
dryRun?: boolean;
|
|
86
86
|
tag?: string;
|
|
87
|
+
verbose?: boolean;
|
|
87
88
|
}
|
|
88
89
|
export interface RestoreOptions {
|
|
89
90
|
dryRun?: boolean;
|
|
@@ -121,7 +122,7 @@ export interface StatusInfo {
|
|
|
121
122
|
oldestBackup?: Date;
|
|
122
123
|
}
|
|
123
124
|
export interface CleanupPolicy {
|
|
124
|
-
type:
|
|
125
|
+
type: 'keep-recent' | 'older-than' | 'select';
|
|
125
126
|
count?: number;
|
|
126
127
|
days?: number;
|
|
127
128
|
indices?: number[];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract YAML content from LLM response
|
|
3
|
+
* Handles markdown code blocks and pure YAML
|
|
4
|
+
* Rejects plain text and scalar values
|
|
5
|
+
*/
|
|
6
|
+
export declare function extractYAML(response: string): string | null;
|
|
7
|
+
/**
|
|
8
|
+
* Parse YAML string to object
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseYAML<T = unknown>(yamlString: string): T;
|
|
11
|
+
/**
|
|
12
|
+
* Extract and parse YAML from LLM response
|
|
13
|
+
*/
|
|
14
|
+
export declare function extractAndParseYAML<T = unknown>(response: string): T | null;
|
|
15
|
+
/**
|
|
16
|
+
* Extract config-specific YAML (requires 'backup:' key)
|
|
17
|
+
* More strict validation for Syncpoint config files
|
|
18
|
+
*/
|
|
19
|
+
export declare function extractConfigYAML(response: string): string | null;
|