@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.
@@ -1,2 +1,2 @@
1
- import { Command } from "commander";
1
+ import { Command } from 'commander';
2
2
  export declare function registerBackupCommand(program: Command): void;
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerCreateTemplateCommand(program: Command): void;
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerHelpCommand(program: Command): void;
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerWizardCommand(program: Command): void;
@@ -11,6 +11,5 @@ export declare const TEMPLATES_DIR = "templates";
11
11
  export declare const SCRIPTS_DIR = "scripts";
12
12
  export declare const LOGS_DIR = "logs";
13
13
  export declare function getAppDir(): string;
14
- export declare const APP_VERSION = "0.0.1";
15
- export type SubDirName = "backups" | "templates" | "scripts" | "logs";
14
+ export type SubDirName = 'backups' | 'templates' | 'scripts' | 'logs';
16
15
  export declare function getSubDir(sub: SubDirName): string;
@@ -1,7 +1,14 @@
1
- import type { BackupOptions, BackupResult, FileEntry, SyncpointConfig } from "../utils/types.js";
1
+ import type { BackupOptions, BackupResult, FileEntry, SyncpointConfig } from '../utils/types.js';
2
2
  /**
3
3
  * Scan config targets, resolve globs, filter excludes.
4
4
  * Returns found FileEntry[] and missing path strings.
5
+ *
6
+ * Supports three pattern types:
7
+ * - Regex: /pattern/ format (e.g., /\.conf$/)
8
+ * - Glob: *.ext, **\/pattern (e.g., ~/.config/*.yml)
9
+ * - Literal: Direct file paths (e.g., ~/.zshrc)
10
+ *
11
+ * Exclude patterns are applied consistently across all target types.
5
12
  */
6
13
  export declare function scanTargets(config: SyncpointConfig): Promise<{
7
14
  found: FileEntry[];
@@ -1,4 +1,4 @@
1
- import type { SyncpointConfig } from "../utils/types.js";
1
+ import type { SyncpointConfig } from '../utils/types.js';
2
2
  /**
3
3
  * Get the path to the config.yml file.
4
4
  */
@@ -1,4 +1,4 @@
1
- import type { BackupMetadata, FileEntry, SyncpointConfig } from "../utils/types.js";
1
+ import type { BackupMetadata, FileEntry, SyncpointConfig } from '../utils/types.js';
2
2
  /**
3
3
  * Create a full metadata object for a backup.
4
4
  */
@@ -1,4 +1,4 @@
1
- import type { ProvisionOptions, StepResult, TemplateConfig, TemplateStep } from "../utils/types.js";
1
+ import type { ProvisionOptions, StepResult, TemplateConfig, TemplateStep } from '../utils/types.js';
2
2
  /**
3
3
  * Load and validate a template from a YAML file.
4
4
  */
@@ -1,4 +1,4 @@
1
- import type { BackupInfo, RestoreOptions, RestorePlan, RestoreResult, SyncpointConfig } from "../utils/types.js";
1
+ import type { BackupInfo, RestoreOptions, RestorePlan, RestoreResult, SyncpointConfig } from '../utils/types.js';
2
2
  /**
3
3
  * List all backup archives in the backup directory, sorted by date desc.
4
4
  */
package/dist/index.cjs CHANGED
@@ -99,6 +99,14 @@ async function fileExists(filePath) {
99
99
  return false;
100
100
  }
101
101
  }
102
+ async function isDirectory(filePath) {
103
+ try {
104
+ const stats = await (0, import_promises.stat)(filePath);
105
+ return stats.isDirectory();
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
102
110
 
103
111
  // src/constants.ts
104
112
  var APP_NAME = "syncpoint";
@@ -114,7 +122,6 @@ var LOGS_DIR = "logs";
114
122
  function getAppDir() {
115
123
  return (0, import_node_path2.join)(getHomeDir(), APP_DIR);
116
124
  }
117
- var APP_VERSION = "0.0.1";
118
125
  function getSubDir(sub) {
119
126
  return (0, import_node_path2.join)(getAppDir(), sub);
120
127
  }
@@ -122,8 +129,106 @@ function getSubDir(sub) {
122
129
  // src/schemas/ajv.ts
123
130
  var import_ajv = __toESM(require("ajv"), 1);
124
131
  var import_ajv_formats = __toESM(require("ajv-formats"), 1);
132
+
133
+ // src/utils/pattern.ts
134
+ var import_micromatch = __toESM(require("micromatch"), 1);
135
+ function detectPatternType(pattern) {
136
+ if (pattern.startsWith("/") && pattern.endsWith("/") && pattern.length > 2) {
137
+ const inner = pattern.slice(1, -1);
138
+ const hasUnescapedSlash = /(?<!\\)\//.test(inner);
139
+ if (!hasUnescapedSlash) {
140
+ return "regex";
141
+ }
142
+ }
143
+ if (pattern.includes("*") || pattern.includes("?") || pattern.includes("{")) {
144
+ return "glob";
145
+ }
146
+ return "literal";
147
+ }
148
+ function parseRegexPattern(pattern) {
149
+ if (!pattern.startsWith("/") || !pattern.endsWith("/")) {
150
+ throw new Error(
151
+ `Invalid regex pattern format: ${pattern}. Must be enclosed in slashes like /pattern/`
152
+ );
153
+ }
154
+ const regexBody = pattern.slice(1, -1);
155
+ try {
156
+ return new RegExp(regexBody);
157
+ } catch (error) {
158
+ throw new Error(
159
+ `Invalid regex pattern: ${pattern}. ${error instanceof Error ? error.message : String(error)}`
160
+ );
161
+ }
162
+ }
163
+ function createExcludeMatcher(excludePatterns) {
164
+ if (excludePatterns.length === 0) {
165
+ return () => false;
166
+ }
167
+ const regexPatterns = [];
168
+ const globPatterns = [];
169
+ const literalPatterns = /* @__PURE__ */ new Set();
170
+ for (const pattern of excludePatterns) {
171
+ const type = detectPatternType(pattern);
172
+ if (type === "regex") {
173
+ try {
174
+ regexPatterns.push(parseRegexPattern(pattern));
175
+ } catch {
176
+ console.warn(`Skipping invalid regex pattern: ${pattern}`);
177
+ }
178
+ } else if (type === "glob") {
179
+ globPatterns.push(pattern);
180
+ } else {
181
+ literalPatterns.add(pattern);
182
+ }
183
+ }
184
+ return (filePath) => {
185
+ if (literalPatterns.has(filePath)) {
186
+ return true;
187
+ }
188
+ if (globPatterns.length > 0 && import_micromatch.default.isMatch(filePath, globPatterns, {
189
+ dot: true,
190
+ // Match dotfiles
191
+ matchBase: false
192
+ // Don't use basename matching, match full path
193
+ })) {
194
+ return true;
195
+ }
196
+ for (const regex of regexPatterns) {
197
+ if (regex.test(filePath)) {
198
+ return true;
199
+ }
200
+ }
201
+ return false;
202
+ };
203
+ }
204
+ function isValidPattern(pattern) {
205
+ if (typeof pattern !== "string" || pattern.length === 0) {
206
+ return false;
207
+ }
208
+ const type = detectPatternType(pattern);
209
+ if (type === "regex") {
210
+ try {
211
+ parseRegexPattern(pattern);
212
+ return true;
213
+ } catch {
214
+ return false;
215
+ }
216
+ }
217
+ return true;
218
+ }
219
+
220
+ // src/schemas/ajv.ts
125
221
  var ajv = new import_ajv.default({ allErrors: true });
126
222
  (0, import_ajv_formats.default)(ajv);
223
+ ajv.addKeyword({
224
+ keyword: "validPattern",
225
+ type: "string",
226
+ validate: function validate(schema, data) {
227
+ if (!schema) return true;
228
+ return isValidPattern(data);
229
+ },
230
+ errors: true
231
+ });
127
232
 
128
233
  // src/schemas/config.schema.ts
129
234
  var configSchema = {
@@ -136,11 +241,11 @@ var configSchema = {
136
241
  properties: {
137
242
  targets: {
138
243
  type: "array",
139
- items: { type: "string" }
244
+ items: { type: "string", validPattern: true }
140
245
  },
141
246
  exclude: {
142
247
  type: "array",
143
- items: { type: "string" }
248
+ items: { type: "string", validPattern: true }
144
249
  },
145
250
  filename: {
146
251
  type: "string",
@@ -164,11 +269,11 @@ var configSchema = {
164
269
  },
165
270
  additionalProperties: false
166
271
  };
167
- var validate = ajv.compile(configSchema);
272
+ var validate2 = ajv.compile(configSchema);
168
273
  function validateConfig(data) {
169
- const valid = validate(data);
274
+ const valid = validate2(data);
170
275
  if (valid) return { valid: true };
171
- const errors = validate.errors?.map(
276
+ const errors = validate2.errors?.map(
172
277
  (e) => `${e.instancePath || "/"} ${e.message ?? "unknown error"}`
173
278
  );
174
279
  return { valid: false, errors };
@@ -220,20 +325,16 @@ Run "syncpoint init" first.`
220
325
  const data = stripDangerousKeys(import_yaml.default.parse(raw));
221
326
  const result = validateConfig(data);
222
327
  if (!result.valid) {
223
- throw new Error(
224
- `Invalid config:
225
- ${(result.errors ?? []).join("\n")}`
226
- );
328
+ throw new Error(`Invalid config:
329
+ ${(result.errors ?? []).join("\n")}`);
227
330
  }
228
331
  return data;
229
332
  }
230
333
  async function saveConfig(config) {
231
334
  const result = validateConfig(config);
232
335
  if (!result.valid) {
233
- throw new Error(
234
- `Invalid config:
235
- ${(result.errors ?? []).join("\n")}`
236
- );
336
+ throw new Error(`Invalid config:
337
+ ${(result.errors ?? []).join("\n")}`);
237
338
  }
238
339
  const configPath = getConfigPath();
239
340
  await ensureDir(getAppDir());
@@ -290,7 +391,7 @@ function getSystemInfo() {
290
391
  }
291
392
  function formatHostname(name) {
292
393
  const raw = name ?? getHostname();
293
- return raw.replace(/\s+/g, "-").replace(/\./g, "-").replace(/[^a-zA-Z0-9\-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
394
+ return raw.replace(/\s+/g, "-").replace(/\./g, "-").replace(/[^a-zA-Z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
294
395
  }
295
396
 
296
397
  // src/utils/format.ts
@@ -448,23 +549,26 @@ var metadataSchema = {
448
549
  },
449
550
  additionalProperties: false
450
551
  };
451
- var validate2 = ajv.compile(metadataSchema);
552
+ var validate3 = ajv.compile(metadataSchema);
452
553
  function validateMetadata(data) {
453
- const valid = validate2(data);
554
+ const valid = validate3(data);
454
555
  if (valid) return { valid: true };
455
- const errors = validate2.errors?.map(
556
+ const errors = validate3.errors?.map(
456
557
  (e) => `${e.instancePath || "/"} ${e.message ?? "unknown error"}`
457
558
  );
458
559
  return { valid: false, errors };
459
560
  }
460
561
 
562
+ // src/version.ts
563
+ var VERSION = "0.0.2";
564
+
461
565
  // src/core/metadata.ts
462
566
  var METADATA_VERSION = "1.0.0";
463
567
  function createMetadata(files, config) {
464
568
  const totalSize = files.reduce((sum, f) => sum + f.size, 0);
465
569
  return {
466
570
  version: METADATA_VERSION,
467
- toolVersion: APP_VERSION,
571
+ toolVersion: VERSION,
468
572
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
469
573
  hostname: getHostname(),
470
574
  system: getSystemInfo(),
@@ -484,10 +588,8 @@ function parseMetadata(data) {
484
588
  const parsed = JSON.parse(str);
485
589
  const result = validateMetadata(parsed);
486
590
  if (!result.valid) {
487
- throw new Error(
488
- `Invalid metadata:
489
- ${(result.errors ?? []).join("\n")}`
490
- );
591
+ throw new Error(`Invalid metadata:
592
+ ${(result.errors ?? []).join("\n")}`);
491
593
  }
492
594
  return parsed;
493
595
  }
@@ -503,6 +605,9 @@ async function collectFileInfo(absolutePath, logicalPath) {
503
605
  type = "symlink";
504
606
  } else if (lstats.isDirectory()) {
505
607
  type = "directory";
608
+ throw new Error(
609
+ `Cannot collect file info for directory: ${logicalPath}. Directories should be converted to glob patterns before calling collectFileInfo().`
610
+ );
506
611
  }
507
612
  let hash;
508
613
  if (lstats.isSymbolicLink()) {
@@ -530,7 +635,10 @@ async function createArchive(files, outputPath) {
530
635
  const fileNames = [];
531
636
  for (const file of files) {
532
637
  const targetPath = (0, import_node_path6.join)(tmpDir, file.name);
533
- const parentDir = (0, import_node_path6.join)(tmpDir, file.name.split("/").slice(0, -1).join("/"));
638
+ const parentDir = (0, import_node_path6.join)(
639
+ tmpDir,
640
+ file.name.split("/").slice(0, -1).join("/")
641
+ );
534
642
  if (parentDir !== tmpDir) {
535
643
  await (0, import_promises5.mkdir)(parentDir, { recursive: true });
536
644
  }
@@ -607,18 +715,48 @@ function isSensitiveFile(filePath) {
607
715
  async function scanTargets(config) {
608
716
  const found = [];
609
717
  const missing = [];
718
+ const isExcluded = createExcludeMatcher(config.backup.exclude);
610
719
  for (const target of config.backup.targets) {
611
720
  const expanded = expandTilde(target);
612
- if (expanded.includes("*") || expanded.includes("?") || expanded.includes("{")) {
721
+ const patternType = detectPatternType(expanded);
722
+ if (patternType === "regex") {
723
+ try {
724
+ const regex = parseRegexPattern(expanded);
725
+ const homeDir = expandTilde("~/");
726
+ const homeDirNormalized = homeDir.endsWith("/") ? homeDir : `${homeDir}/`;
727
+ const allFiles = await (0, import_fast_glob.default)(`${homeDirNormalized}**`, {
728
+ dot: true,
729
+ absolute: true,
730
+ onlyFiles: true,
731
+ deep: 5
732
+ // Limit depth for performance
733
+ });
734
+ for (const match of allFiles) {
735
+ if (regex.test(match) && !isExcluded(match)) {
736
+ const entry = await collectFileInfo(match, match);
737
+ found.push(entry);
738
+ }
739
+ }
740
+ } catch (error) {
741
+ logger.warn(
742
+ `Invalid regex pattern "${target}": ${error instanceof Error ? error.message : String(error)}`
743
+ );
744
+ }
745
+ } else if (patternType === "glob") {
746
+ const globExcludes = config.backup.exclude.filter(
747
+ (p) => detectPatternType(p) === "glob"
748
+ );
613
749
  const matches = await (0, import_fast_glob.default)(expanded, {
614
750
  dot: true,
615
751
  absolute: true,
616
- ignore: config.backup.exclude,
752
+ ignore: globExcludes,
617
753
  onlyFiles: true
618
754
  });
619
755
  for (const match of matches) {
620
- const entry = await collectFileInfo(match, match);
621
- found.push(entry);
756
+ if (!isExcluded(match)) {
757
+ const entry = await collectFileInfo(match, match);
758
+ found.push(entry);
759
+ }
622
760
  }
623
761
  } else {
624
762
  const absPath = resolveTargetPath(target);
@@ -627,16 +765,43 @@ async function scanTargets(config) {
627
765
  missing.push(target);
628
766
  continue;
629
767
  }
630
- const entry = await collectFileInfo(absPath, absPath);
631
- if (entry.size > LARGE_FILE_THRESHOLD) {
632
- logger.warn(
633
- `Large file (>${Math.round(LARGE_FILE_THRESHOLD / 1024 / 1024)}MB): ${target}`
768
+ const isDir = await isDirectory(absPath);
769
+ if (isDir) {
770
+ const dirGlob = `${expanded}/**/*`;
771
+ const globExcludes = config.backup.exclude.filter(
772
+ (p) => detectPatternType(p) === "glob"
634
773
  );
774
+ const matches = await (0, import_fast_glob.default)(dirGlob, {
775
+ dot: true,
776
+ absolute: true,
777
+ ignore: globExcludes,
778
+ onlyFiles: true
779
+ // Only include files, not subdirectories
780
+ });
781
+ for (const match of matches) {
782
+ if (!isExcluded(match)) {
783
+ const entry = await collectFileInfo(match, match);
784
+ found.push(entry);
785
+ }
786
+ }
787
+ if (matches.length === 0) {
788
+ logger.warn(`Directory is empty or fully excluded: ${target}`);
789
+ }
790
+ } else {
791
+ if (isExcluded(absPath)) {
792
+ continue;
793
+ }
794
+ const entry = await collectFileInfo(absPath, absPath);
795
+ if (entry.size > LARGE_FILE_THRESHOLD) {
796
+ logger.warn(
797
+ `Large file (>${Math.round(LARGE_FILE_THRESHOLD / 1024 / 1024)}MB): ${target}`
798
+ );
799
+ }
800
+ if (isSensitiveFile(absPath)) {
801
+ logger.warn(`Sensitive file detected: ${target}`);
802
+ }
803
+ found.push(entry);
635
804
  }
636
- if (isSensitiveFile(absPath)) {
637
- logger.warn(`Sensitive file detected: ${target}`);
638
- }
639
- found.push(entry);
640
805
  }
641
806
  }
642
807
  return { found, missing };
@@ -662,8 +827,10 @@ async function collectScripts() {
662
827
  }
663
828
  async function createBackup(config, options = {}) {
664
829
  const { found, missing } = await scanTargets(config);
665
- for (const m of missing) {
666
- logger.warn(`File not found, skipping: ${m}`);
830
+ if (options.verbose && missing.length > 0) {
831
+ for (const m of missing) {
832
+ logger.warn(`File not found, skipping: ${m}`);
833
+ }
667
834
  }
668
835
  let allFiles = [...found];
669
836
  if (config.scripts.includeInBackup) {
@@ -892,11 +1059,11 @@ var templateSchema = {
892
1059
  },
893
1060
  additionalProperties: false
894
1061
  };
895
- var validate3 = ajv.compile(templateSchema);
1062
+ var validate4 = ajv.compile(templateSchema);
896
1063
  function validateTemplate(data) {
897
- const valid = validate3(data);
1064
+ const valid = validate4(data);
898
1065
  if (valid) return { valid: true };
899
- const errors = validate3.errors?.map(
1066
+ const errors = validate4.errors?.map(
900
1067
  (e) => `${e.instancePath || "/"} ${e.message ?? "unknown error"}`
901
1068
  );
902
1069
  return { valid: false, errors };
@@ -980,7 +1147,9 @@ function execAsync(command) {
980
1147
  }
981
1148
  async function evaluateSkipIf(command, stepName) {
982
1149
  if (containsRemoteScriptPattern(command)) {
983
- throw new Error(`Blocked dangerous remote script pattern in skip_if: ${stepName}`);
1150
+ throw new Error(
1151
+ `Blocked dangerous remote script pattern in skip_if: ${stepName}`
1152
+ );
984
1153
  }
985
1154
  try {
986
1155
  await execAsync(command);
@@ -992,7 +1161,9 @@ async function evaluateSkipIf(command, stepName) {
992
1161
  async function executeStep(step) {
993
1162
  const startTime = Date.now();
994
1163
  if (containsRemoteScriptPattern(step.command)) {
995
- throw new Error(`Blocked dangerous remote script pattern in command: ${step.name}`);
1164
+ throw new Error(
1165
+ `Blocked dangerous remote script pattern in command: ${step.name}`
1166
+ );
996
1167
  }
997
1168
  if (step.skip_if) {
998
1169
  const shouldSkip = await evaluateSkipIf(step.skip_if, step.name);
@@ -1046,9 +1217,7 @@ async function* runProvision(templatePath, options = {}) {
1046
1217
  const result = await executeStep(step);
1047
1218
  yield result;
1048
1219
  if (result.status === "failed" && !step.continue_on_error) {
1049
- logger.error(
1050
- `Step "${step.name}" failed. Stopping provisioning.`
1051
- );
1220
+ logger.error(`Step "${step.name}" failed. Stopping provisioning.`);
1052
1221
  return;
1053
1222
  }
1054
1223
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export type { SyncpointConfig, BackupMetadata, FileEntry, TemplateConfig, TemplateStep, BackupResult, RestoreResult, RestorePlan, RestoreAction, StepResult, BackupOptions, RestoreOptions, ProvisionOptions, BackupInfo, StatusInfo, } from "./utils/types.js";
2
- export { loadConfig, saveConfig, initDefaultConfig } from "./core/config.js";
3
- export { createBackup, scanTargets } from "./core/backup.js";
4
- export { restoreBackup, getBackupList, getRestorePlan } from "./core/restore.js";
5
- export { runProvision, loadTemplate, listTemplates } from "./core/provision.js";
1
+ export type { SyncpointConfig, BackupMetadata, FileEntry, TemplateConfig, TemplateStep, BackupResult, RestoreResult, RestorePlan, RestoreAction, StepResult, BackupOptions, RestoreOptions, ProvisionOptions, BackupInfo, StatusInfo, } from './utils/types.js';
2
+ export { loadConfig, saveConfig, initDefaultConfig } from './core/config.js';
3
+ export { createBackup, scanTargets } from './core/backup.js';
4
+ export { restoreBackup, getBackupList, getRestorePlan, } from './core/restore.js';
5
+ export { runProvision, loadTemplate, listTemplates } from './core/provision.js';