@udondan/avanti 0.24.0 → 0.26.0

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.
Files changed (100) hide show
  1. package/README.md +488 -64
  2. package/dist/commands/diff.d.ts.map +1 -1
  3. package/dist/commands/diff.js +82 -18
  4. package/dist/commands/diff.js.map +1 -1
  5. package/dist/commands/lock.d.ts.map +1 -1
  6. package/dist/commands/lock.js +6 -4
  7. package/dist/commands/lock.js.map +1 -1
  8. package/dist/commands/log.d.ts.map +1 -1
  9. package/dist/commands/log.js +7 -5
  10. package/dist/commands/log.js.map +1 -1
  11. package/dist/commands/pull.d.ts.map +1 -1
  12. package/dist/commands/pull.js +682 -42
  13. package/dist/commands/pull.js.map +1 -1
  14. package/dist/commands/reset.d.ts.map +1 -1
  15. package/dist/commands/reset.js +43 -11
  16. package/dist/commands/reset.js.map +1 -1
  17. package/dist/commands/revert.d.ts.map +1 -1
  18. package/dist/commands/revert.js +68 -14
  19. package/dist/commands/revert.js.map +1 -1
  20. package/dist/condition.d.ts.map +1 -1
  21. package/dist/condition.js +9 -6
  22. package/dist/condition.js.map +1 -1
  23. package/dist/config-writeback.d.ts.map +1 -1
  24. package/dist/config-writeback.js +17 -0
  25. package/dist/config-writeback.js.map +1 -1
  26. package/dist/config.d.ts +12 -0
  27. package/dist/config.d.ts.map +1 -1
  28. package/dist/config.js +346 -3
  29. package/dist/config.js.map +1 -1
  30. package/dist/diff.d.ts +19 -0
  31. package/dist/diff.d.ts.map +1 -1
  32. package/dist/diff.js +317 -12
  33. package/dist/diff.js.map +1 -1
  34. package/dist/extract.d.ts +4 -0
  35. package/dist/extract.d.ts.map +1 -0
  36. package/dist/extract.js +142 -0
  37. package/dist/extract.js.map +1 -0
  38. package/dist/filter.d.ts +3 -0
  39. package/dist/filter.d.ts.map +1 -0
  40. package/dist/filter.js +126 -0
  41. package/dist/filter.js.map +1 -0
  42. package/dist/history.d.ts +7 -1
  43. package/dist/history.d.ts.map +1 -1
  44. package/dist/history.js +89 -9
  45. package/dist/history.js.map +1 -1
  46. package/dist/paths.d.ts +4 -0
  47. package/dist/paths.d.ts.map +1 -1
  48. package/dist/paths.js +97 -0
  49. package/dist/paths.js.map +1 -1
  50. package/dist/processors/ini.d.ts +33 -0
  51. package/dist/processors/ini.d.ts.map +1 -0
  52. package/dist/processors/ini.js +500 -0
  53. package/dist/processors/ini.js.map +1 -0
  54. package/dist/processors/insert.d.ts.map +1 -1
  55. package/dist/processors/insert.js +338 -15
  56. package/dist/processors/insert.js.map +1 -1
  57. package/dist/processors/on.d.ts +4 -0
  58. package/dist/processors/on.d.ts.map +1 -0
  59. package/dist/processors/on.js +54 -0
  60. package/dist/processors/on.js.map +1 -0
  61. package/dist/ref.d.ts +21 -0
  62. package/dist/ref.d.ts.map +1 -0
  63. package/dist/ref.js +65 -0
  64. package/dist/ref.js.map +1 -0
  65. package/dist/sources/bitbucket.d.ts.map +1 -1
  66. package/dist/sources/bitbucket.js +51 -12
  67. package/dist/sources/bitbucket.js.map +1 -1
  68. package/dist/sources/git.d.ts.map +1 -1
  69. package/dist/sources/git.js +54 -6
  70. package/dist/sources/git.js.map +1 -1
  71. package/dist/sources/github.d.ts.map +1 -1
  72. package/dist/sources/github.js +188 -51
  73. package/dist/sources/github.js.map +1 -1
  74. package/dist/sources/gitlab.d.ts.map +1 -1
  75. package/dist/sources/gitlab.js +242 -44
  76. package/dist/sources/gitlab.js.map +1 -1
  77. package/dist/sources/index.d.ts +4 -2
  78. package/dist/sources/index.d.ts.map +1 -1
  79. package/dist/sources/index.js +220 -49
  80. package/dist/sources/index.js.map +1 -1
  81. package/dist/sources/local.d.ts +3 -0
  82. package/dist/sources/local.d.ts.map +1 -1
  83. package/dist/sources/local.js +44 -0
  84. package/dist/sources/local.js.map +1 -1
  85. package/dist/types.d.ts +38 -2
  86. package/dist/types.d.ts.map +1 -1
  87. package/dist/types.js.map +1 -1
  88. package/dist/variables-remote.d.ts +1 -1
  89. package/dist/variables-remote.d.ts.map +1 -1
  90. package/dist/variables-remote.js +4 -3
  91. package/dist/variables-remote.js.map +1 -1
  92. package/dist/variables.d.ts +1 -0
  93. package/dist/variables.d.ts.map +1 -1
  94. package/dist/variables.js +37 -2
  95. package/dist/variables.js.map +1 -1
  96. package/dist/writer.d.ts +17 -0
  97. package/dist/writer.d.ts.map +1 -1
  98. package/dist/writer.js +848 -20
  99. package/dist/writer.js.map +1 -1
  100. package/package.json +14 -10
package/dist/config.d.ts CHANGED
@@ -1,8 +1,20 @@
1
1
  import { AvantiConfig, Via } from './types';
2
2
  export declare const SELF_KEY = "$self";
3
3
  export declare function isRemoteConfigSpec(s: string): boolean;
4
+ export declare function deriveConfigBase(configPath: string): string;
5
+ export declare function resolveRelativeSrc(src: string, configBase: string): string;
4
6
  export declare function normalizeConfigKey(spec: string): string;
5
7
  export declare function resolveConfigPath(explicit?: string): string;
8
+ export declare function parseGitHubSpec(spec: string): {
9
+ repo: string;
10
+ file: string;
11
+ ref: string | undefined;
12
+ };
13
+ export declare function parseGitLabSpec(spec: string): {
14
+ project: string;
15
+ file: string;
16
+ ref: string | undefined;
17
+ };
6
18
  export declare function parseConfigContent(content: string): AvantiConfig;
7
19
  export declare function loadConfig(configPath: string, via?: Via | Via[]): Promise<AvantiConfig>;
8
20
  export declare function parseVia(value: unknown, loc: string): Via | Via[] | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,YAAY,EA4BZ,GAAG,EACJ,MAAM,SAAS,CAAC;AAQjB,eAAO,MAAM,QAAQ,UAAU,CAAC;AAShC,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAQrD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAcvD;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAgB3D;AAsGD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,CA+NhE;AAED,wBAAsB,UAAU,CAC9B,UAAU,EAAE,MAAM,EAClB,GAAG,CAAC,EAAE,GAAG,GAAG,GAAG,EAAE,GAChB,OAAO,CAAC,YAAY,CAAC,CAQvB;AAqJD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,GAAG,GAAG,GAAG,EAAE,GAAG,SAAS,CAgC7E"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,YAAY,EAiCZ,GAAG,EACJ,MAAM,SAAS,CAAC;AASjB,eAAO,MAAM,QAAQ,UAAU,CAAC;AAiBhC,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAQrD;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAkC3D;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAkC1E;AA6CD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAcvD;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAgB3D;AAGD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;CACzB,CAmBA;AAGD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;CACzB,CAmBA;AAkDD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,CAkdhE;AAED,wBAAsB,UAAU,CAC9B,UAAU,EAAE,MAAM,EAClB,GAAG,CAAC,EAAE,GAAG,GAAG,GAAG,EAAE,GAChB,OAAO,CAAC,YAAY,CAAC,CAQvB;AA8JD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,GAAG,GAAG,GAAG,EAAE,GAAG,SAAS,CAgC7E"}
package/dist/config.js CHANGED
@@ -35,8 +35,12 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.SELF_KEY = void 0;
37
37
  exports.isRemoteConfigSpec = isRemoteConfigSpec;
38
+ exports.deriveConfigBase = deriveConfigBase;
39
+ exports.resolveRelativeSrc = resolveRelativeSrc;
38
40
  exports.normalizeConfigKey = normalizeConfigKey;
39
41
  exports.resolveConfigPath = resolveConfigPath;
42
+ exports.parseGitHubSpec = parseGitHubSpec;
43
+ exports.parseGitLabSpec = parseGitLabSpec;
40
44
  exports.parseConfigContent = parseConfigContent;
41
45
  exports.loadConfig = loadConfig;
42
46
  exports.parseVia = parseVia;
@@ -51,6 +55,7 @@ const http_1 = require("./sources/http");
51
55
  const github_1 = require("./sources/github");
52
56
  const gitlab_1 = require("./sources/gitlab");
53
57
  const git_1 = require("./sources/git");
58
+ const local_1 = require("./sources/local");
54
59
  exports.SELF_KEY = '$self';
55
60
  const CONFIG_CANDIDATES = [
56
61
  '.avanti.yml',
@@ -58,6 +63,14 @@ const CONFIG_CANDIDATES = [
58
63
  'avanti.yml',
59
64
  'avanti.yaml',
60
65
  ];
66
+ function isLocalFileSrc(src) {
67
+ if (typeof src === 'string') {
68
+ return !(0, local_1.isNonLocalSrcString)(src);
69
+ }
70
+ if (!('path' in src))
71
+ return false;
72
+ return !(0, local_1.isNonLocalSrcString)(src.path);
73
+ }
61
74
  function isRemoteConfigSpec(s) {
62
75
  return (s.startsWith('http://') ||
63
76
  s.startsWith('https://') ||
@@ -65,6 +78,104 @@ function isRemoteConfigSpec(s) {
65
78
  s.startsWith('github:') ||
66
79
  s.startsWith('gitlab:'));
67
80
  }
81
+ function deriveConfigBase(configPath) {
82
+ if (configPath.startsWith('http://') || configPath.startsWith('https://')) {
83
+ const url = new URL(configPath);
84
+ const lastSlash = url.pathname.lastIndexOf('/');
85
+ url.pathname = lastSlash >= 0 ? url.pathname.slice(0, lastSlash + 1) : '/';
86
+ return url.toString();
87
+ }
88
+ if (configPath.startsWith('github:')) {
89
+ const { repo, file, ref } = parseGitHubSpec(configPath);
90
+ const normalizedFile = file.replace(/\\/g, '/');
91
+ const dir = normalizedFile.includes('/')
92
+ ? normalizedFile.slice(0, normalizedFile.lastIndexOf('/'))
93
+ : '';
94
+ const refSuffix = ref !== undefined ? `@${ref}` : '';
95
+ return `github:${repo}:${dir}${refSuffix}`;
96
+ }
97
+ if (configPath.startsWith('gitlab:')) {
98
+ const { project, file, ref } = parseGitLabSpec(configPath);
99
+ const normalizedFile = file.replace(/\\/g, '/');
100
+ const dir = normalizedFile.includes('/')
101
+ ? normalizedFile.slice(0, normalizedFile.lastIndexOf('/'))
102
+ : '';
103
+ const refSuffix = ref !== undefined ? `@${ref}` : '';
104
+ return `gitlab:${project}:${dir}${refSuffix}`;
105
+ }
106
+ if ((0, git_1.isGitRemoteUrl)(configPath)) {
107
+ const { repo, file, ref } = (0, git_1.parseGitRemoteSpec)(configPath);
108
+ const normalizedFile = file.replace(/\\/g, '/');
109
+ const dir = path.posix.dirname(normalizedFile);
110
+ const dirPart = dir === '.' ? '' : dir;
111
+ const refSuffix = ref !== undefined ? `@${ref}` : '';
112
+ return `${repo}//${dirPart}${refSuffix}`;
113
+ }
114
+ return path.dirname(configPath);
115
+ }
116
+ function resolveRelativeSrc(src, configBase) {
117
+ if (src.startsWith('/') ||
118
+ path.isAbsolute(src) ||
119
+ src.startsWith('~/') ||
120
+ (0, local_1.isNonLocalSrcString)(src)) {
121
+ return src;
122
+ }
123
+ if (configBase.startsWith('http://') || configBase.startsWith('https://')) {
124
+ const normalizedSrc = src.replace(/\\/g, '/');
125
+ const resolvedUrl = new URL(normalizedSrc, configBase);
126
+ const baseUrl = new URL(configBase);
127
+ if (baseUrl.search) {
128
+ const baseParams = new URLSearchParams(baseUrl.search);
129
+ const resolvedParams = new URLSearchParams(resolvedUrl.search);
130
+ const overriddenKeys = new Set(resolvedParams.keys());
131
+ for (const [key, val] of baseParams.entries()) {
132
+ if (!overriddenKeys.has(key))
133
+ resolvedParams.append(key, val);
134
+ }
135
+ resolvedUrl.search = resolvedParams.toString();
136
+ }
137
+ return resolvedUrl.href;
138
+ }
139
+ if (configBase.startsWith('github:')) {
140
+ return resolveRelativeGitHostSpec(src, configBase, 'github');
141
+ }
142
+ if (configBase.startsWith('gitlab:')) {
143
+ return resolveRelativeGitHostSpec(src, configBase, 'gitlab');
144
+ }
145
+ if ((0, git_1.isGitRemoteUrl)(configBase)) {
146
+ return resolveRelativeGitRemoteSpec(src, configBase);
147
+ }
148
+ return path.resolve(configBase, src);
149
+ }
150
+ function resolveGitRelativePath(rest, src, configBase) {
151
+ const atIdx = rest.lastIndexOf('@');
152
+ const dir = atIdx === -1 ? rest : rest.slice(0, atIdx);
153
+ const ref = atIdx === -1 ? undefined : rest.slice(atIdx + 1);
154
+ const normalizedSrc = src.replace(/\\/g, '/');
155
+ const file = path.posix.join(dir, normalizedSrc);
156
+ if (file.startsWith('../') || file === '..') {
157
+ throw new Error(`Relative source path '${src}' escapes the repository root for config base '${configBase}'`);
158
+ }
159
+ const refSuffix = ref !== undefined ? `@${ref}` : '';
160
+ return { file, refSuffix };
161
+ }
162
+ function resolveRelativeGitHostSpec(src, configBase, scheme) {
163
+ const prefix = `${scheme}:`;
164
+ const body = configBase.slice(prefix.length);
165
+ const colonIdx = body.indexOf(':');
166
+ const id = colonIdx >= 0 ? body.slice(0, colonIdx) : body;
167
+ const rest = colonIdx >= 0 ? body.slice(colonIdx + 1) : '';
168
+ const { file, refSuffix } = resolveGitRelativePath(rest, src, configBase);
169
+ return `${prefix}${id}:${file}${refSuffix}`;
170
+ }
171
+ function resolveRelativeGitRemoteSpec(src, configBase) {
172
+ const schemeEnd = configBase.indexOf('://') + 3;
173
+ const separatorIdx = configBase.indexOf('//', schemeEnd);
174
+ const repo = separatorIdx >= 0 ? configBase.slice(0, separatorIdx) : configBase;
175
+ const rest = separatorIdx >= 0 ? configBase.slice(separatorIdx + 2) : '';
176
+ const { file, refSuffix } = resolveGitRelativePath(rest, src, configBase);
177
+ return `${repo}//${file}${refSuffix}`;
178
+ }
68
179
  function normalizeConfigKey(spec) {
69
180
  if (spec.startsWith('github:') || spec.startsWith('gitlab:')) {
70
181
  const atIdx = spec.lastIndexOf('@');
@@ -260,16 +371,76 @@ function parseConfigContent(content) {
260
371
  }
261
372
  if (typeof e['backup'] === 'string')
262
373
  fileEntry.backup = e['backup'];
263
- if (typeof e['post'] === 'string')
264
- fileEntry.post = e['post'];
374
+ if (e['post'] !== undefined) {
375
+ throw new Error(`files["${target}"].post: removed — use on.write instead`);
376
+ }
377
+ if (e['on'] !== undefined) {
378
+ if (typeof e['on'] !== 'object' ||
379
+ e['on'] === null ||
380
+ Array.isArray(e['on']) ||
381
+ (Object.getPrototypeOf(e['on']) !== Object.prototype &&
382
+ Object.getPrototypeOf(e['on']) !== null)) {
383
+ throw new Error(`files["${target}"].on: must be a mapping`);
384
+ }
385
+ const validKeys = new Set([
386
+ 'write',
387
+ 'beforeWrite',
388
+ 'beforeCreate',
389
+ 'beforeUpdate',
390
+ 'create',
391
+ 'update',
392
+ ]);
393
+ const onObj = e['on'];
394
+ const onHooks = Object.create(null);
395
+ for (const key of Object.keys(onObj)) {
396
+ if (!validKeys.has(key)) {
397
+ throw new Error(`files["${target}"].on: unknown key "${key}"`);
398
+ }
399
+ if (typeof onObj[key] !== 'string') {
400
+ throw new Error(`files["${target}"].on.${key}: must be a string`);
401
+ }
402
+ onHooks[key] = onObj[key];
403
+ }
404
+ fileEntry.on = onHooks;
405
+ }
265
406
  if (typeof e['writeInPlace'] === 'boolean')
266
407
  fileEntry.writeInPlace = e['writeInPlace'];
408
+ if (typeof e['followSymlink'] === 'boolean')
409
+ fileEntry.followSymlink = e['followSymlink'];
410
+ if (e['symlink'] !== undefined) {
411
+ if (e['symlink'] !== true &&
412
+ e['symlink'] !== 'absolute' &&
413
+ e['symlink'] !== 'relative') {
414
+ throw new Error(`files["${target}"].symlink: must be true, "absolute", or "relative"`);
415
+ }
416
+ fileEntry.symlink = e['symlink'];
417
+ }
418
+ if (e['sudo'] !== undefined) {
419
+ if (e['sudo'] === true) {
420
+ fileEntry.sudo = true;
421
+ }
422
+ else if (typeof e['sudo'] === 'string' && e['sudo'].trim()) {
423
+ if (e['sudo'].trim().startsWith('-')) {
424
+ throw new Error(`files["${target}"].sudo: username must not start with '-'`);
425
+ }
426
+ fileEntry.sudo = e['sudo'].trim();
427
+ }
428
+ else if (e['sudo'] !== false) {
429
+ throw new Error(`files["${target}"].sudo: must be true or a non-empty username string`);
430
+ }
431
+ }
267
432
  if (e['strategy'] !== undefined) {
268
433
  if (e['strategy'] !== 'replace' && e['strategy'] !== 'insert') {
269
434
  throw new Error(`files["${target}"].strategy: must be "replace" or "insert"`);
270
435
  }
271
436
  fileEntry.strategy = e['strategy'];
272
437
  }
438
+ if (fileEntry.strategy === 'insert' && fileEntry.sudo) {
439
+ throw new Error(`files["${target}"]: strategy "insert" cannot be combined with sudo — ` +
440
+ `insert mode reads the existing file without privilege escalation, ` +
441
+ `which silently treats an unreadable privileged file as absent. ` +
442
+ `Use a non-insert strategy, or manage the file without sudo.`);
443
+ }
273
444
  if (e['replace'] !== undefined) {
274
445
  if (!Array.isArray(e['replace'])) {
275
446
  throw new Error(`files["${target}"]: "replace" must be an array`);
@@ -303,6 +474,15 @@ function parseConfigContent(content) {
303
474
  fileEntry.toml = parseTomlMergeOptions(rawToml, `files["${target}"]`);
304
475
  }
305
476
  }
477
+ if (e['ini'] !== undefined) {
478
+ const rawIni = e['ini'];
479
+ if (rawIni === true || rawIni === false) {
480
+ fileEntry.ini = rawIni;
481
+ }
482
+ else {
483
+ fileEntry.ini = parseIniMergeOptions(rawIni, `files["${target}"]`);
484
+ }
485
+ }
306
486
  if (e['template'] !== undefined) {
307
487
  const rawTemplate = e['template'];
308
488
  if (rawTemplate === true) {
@@ -316,6 +496,106 @@ function parseConfigContent(content) {
316
496
  fileEntry.template = rawTemplate;
317
497
  }
318
498
  }
499
+ if (e['extract'] !== undefined) {
500
+ if (fileEntry.symlink) {
501
+ throw new Error(`files["${target}"].symlink: cannot be combined with extract:`);
502
+ }
503
+ if (Array.isArray(src)) {
504
+ throw new Error(`files["${target}"].extract: cannot be used with a list of sources`);
505
+ }
506
+ if (!target.endsWith('/') && !target.endsWith(path.sep)) {
507
+ throw new Error(`files["${target}"].extract: target must be a directory (end with "/") — archive extraction writes multiple files`);
508
+ }
509
+ const rawExtract = e['extract'];
510
+ if (rawExtract === true) {
511
+ fileEntry.extract = true;
512
+ }
513
+ else if (Array.isArray(rawExtract)) {
514
+ if (rawExtract.length === 0) {
515
+ throw new Error(`files["${target}"].extract: must not be an empty array`);
516
+ }
517
+ fileEntry.extract = rawExtract.map((pat, i) => {
518
+ if (typeof pat !== 'string' || !pat) {
519
+ throw new Error(`files["${target}"].extract[${i}]: must be a non-empty string`);
520
+ }
521
+ if (pat.length > 2 && pat.startsWith('/') && pat.endsWith('/')) {
522
+ try {
523
+ new RegExp(pat.slice(1, -1));
524
+ }
525
+ catch (err) {
526
+ throw new Error(`files["${target}"].extract[${i}]: invalid regex`, { cause: err });
527
+ }
528
+ }
529
+ return pat;
530
+ });
531
+ }
532
+ else {
533
+ throw new Error(`files["${target}"].extract: must be true or a non-empty array of patterns`);
534
+ }
535
+ }
536
+ if (fileEntry.symlink) {
537
+ if (target === exports.SELF_KEY) {
538
+ throw new Error(`files["${exports.SELF_KEY}"].symlink: $self cannot be a symlink entry`);
539
+ }
540
+ if (target.endsWith('/') || target.endsWith(path.sep)) {
541
+ throw new Error(`files["${target}"].symlink: target must be a single file path, not a directory pattern (remove the trailing slash)`);
542
+ }
543
+ if (Array.isArray(fileEntry.src)) {
544
+ throw new Error(`files["${target}"].symlink: cannot be combined with a list of sources`);
545
+ }
546
+ if (!isLocalFileSrc(fileEntry.src)) {
547
+ throw new Error(`files["${target}"].symlink: src must be a local path — ` +
548
+ `http, exec, github, gitlab, and other remote sources are not supported`);
549
+ }
550
+ if (typeof fileEntry.src !== 'string' && 'path' in fileEntry.src) {
551
+ const localSrc = fileEntry.src;
552
+ if (localSrc.sha) {
553
+ throw new Error(`files["${target}"].symlink: cannot be combined with src.sha`);
554
+ }
555
+ if (localSrc.filter) {
556
+ throw new Error(`files["${target}"].symlink: cannot be combined with src.filter`);
557
+ }
558
+ if (localSrc.if) {
559
+ throw new Error(`files["${target}"].symlink: cannot be combined with src.if`);
560
+ }
561
+ if (localSrc.ifAny) {
562
+ throw new Error(`files["${target}"].symlink: cannot be combined with src.ifAny`);
563
+ }
564
+ }
565
+ if (fileEntry.replace) {
566
+ throw new Error(`files["${target}"].symlink: cannot be combined with replace:`);
567
+ }
568
+ if (fileEntry.template) {
569
+ throw new Error(`files["${target}"].symlink: cannot be combined with template:`);
570
+ }
571
+ if (fileEntry.json) {
572
+ throw new Error(`files["${target}"].symlink: cannot be combined with json:`);
573
+ }
574
+ if (fileEntry.yaml) {
575
+ throw new Error(`files["${target}"].symlink: cannot be combined with yaml:`);
576
+ }
577
+ if (fileEntry.toml) {
578
+ throw new Error(`files["${target}"].symlink: cannot be combined with toml:`);
579
+ }
580
+ if (fileEntry.ini) {
581
+ throw new Error(`files["${target}"].symlink: cannot be combined with ini:`);
582
+ }
583
+ if (fileEntry.on?.write) {
584
+ throw new Error(`files["${target}"].symlink: cannot be combined with on.write:`);
585
+ }
586
+ if (fileEntry.writeInPlace) {
587
+ throw new Error(`files["${target}"].symlink: cannot be combined with writeInPlace:`);
588
+ }
589
+ if (fileEntry.strategy) {
590
+ throw new Error(`files["${target}"].symlink: cannot be combined with strategy:`);
591
+ }
592
+ if (fileEntry.followSymlink) {
593
+ throw new Error(`files["${target}"].symlink: cannot be combined with followSymlink:`);
594
+ }
595
+ if (fileEntry.mode) {
596
+ throw new Error(`files["${target}"].symlink: cannot be combined with mode — symlinks do not have independent permission bits on POSIX`);
597
+ }
598
+ }
319
599
  let expandedTargets;
320
600
  try {
321
601
  expandedTargets = (0, paths_1.expandBraces)(target);
@@ -336,6 +616,11 @@ function parseConfigContent(content) {
336
616
  const suffix = parts.length > 0 ? ` (${parts.join('; ')})` : '';
337
617
  throw new Error(`files["${expandedTarget}"]: duplicate target${suffix}`);
338
618
  }
619
+ if (fileEntry.symlink &&
620
+ expandedTarget !== target &&
621
+ (expandedTarget.endsWith('/') || expandedTarget.endsWith(path.sep))) {
622
+ throw new Error(`files["${expandedTarget}"] (expanded from "${target}").symlink: target must be a single file path, not a directory pattern (remove the trailing slash)`);
623
+ }
339
624
  files[expandedTarget] = { ...fileEntry, target: expandedTarget };
340
625
  if (expandedTarget !== target) {
341
626
  fileOrigins[expandedTarget] = target;
@@ -460,6 +745,15 @@ function parseVariableEntry(obj, varName) {
460
745
  entry.toml = parseTomlMergeOptions(rawToml, loc);
461
746
  }
462
747
  }
748
+ if (obj['ini'] !== undefined) {
749
+ const rawIni = obj['ini'];
750
+ if (rawIni === true || rawIni === false) {
751
+ entry.ini = rawIni;
752
+ }
753
+ else {
754
+ entry.ini = parseIniMergeOptions(rawIni, loc);
755
+ }
756
+ }
463
757
  if (obj['template'] !== undefined) {
464
758
  const rawTemplate = obj['template'];
465
759
  if (rawTemplate === true) {
@@ -519,7 +813,13 @@ function parseVia(value, loc) {
519
813
  }
520
814
  throw new Error(`${loc}.via: must be a string or array`);
521
815
  }
522
- const VALID_OS_PLATFORMS = ['linux', 'mac', 'windows'];
816
+ const VALID_OS_PLATFORMS = [
817
+ 'linux',
818
+ 'mac',
819
+ 'windows',
820
+ 'darwin',
821
+ 'win32',
822
+ ];
523
823
  function parseCondition(raw, loc) {
524
824
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
525
825
  throw new Error(`${loc}: must be an object`);
@@ -600,6 +900,30 @@ function parseConditionField(obj, loc) {
600
900
  }
601
901
  return result;
602
902
  }
903
+ function parseFilter(obj, loc) {
904
+ if (obj['filter'] === undefined)
905
+ return undefined;
906
+ if (!Array.isArray(obj['filter'])) {
907
+ throw new Error(`${loc}.filter: must be an array`);
908
+ }
909
+ if (obj['filter'].length === 0) {
910
+ throw new Error(`${loc}.filter: must not be an empty array`);
911
+ }
912
+ return obj['filter'].map((entry, i) => {
913
+ if (typeof entry !== 'string' || !entry) {
914
+ throw new Error(`${loc}.filter[${i}]: must be a non-empty string`);
915
+ }
916
+ if (entry.length > 2 && entry.startsWith('/') && entry.endsWith('/')) {
917
+ try {
918
+ new RegExp(entry.slice(1, -1));
919
+ }
920
+ catch (err) {
921
+ throw new Error(`${loc}.filter[${i}]: invalid regex: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
922
+ }
923
+ }
924
+ return entry;
925
+ });
926
+ }
603
927
  function parseSingleSrc(raw, loc) {
604
928
  // Plain string → http/https URL or local path
605
929
  if (typeof raw === 'string') {
@@ -620,6 +944,9 @@ function parseSingleSrc(raw, loc) {
620
944
  }
621
945
  result.optional = obj['optional'];
622
946
  }
947
+ const pathFilter = parseFilter(obj, loc);
948
+ if (pathFilter !== undefined)
949
+ result.filter = pathFilter;
623
950
  const pathSha = parseSha(obj['sha'], loc);
624
951
  if (pathSha !== undefined)
625
952
  result.sha = pathSha;
@@ -735,6 +1062,7 @@ function parseSingleSrc(raw, loc) {
735
1062
  throw new Error(`${loc}.gitlab.host: must be a non-empty string`);
736
1063
  }
737
1064
  const gitlabConds = parseConditionField(obj, loc);
1065
+ const gitlabFilter = parseFilter(obj, loc);
738
1066
  if (hasRelease) {
739
1067
  return {
740
1068
  gitlab: {
@@ -744,6 +1072,7 @@ function parseSingleSrc(raw, loc) {
744
1072
  host: typeof g['host'] === 'string' ? g['host'] : undefined,
745
1073
  via: parseVia(g['via'], `${loc}.gitlab`),
746
1074
  },
1075
+ ...(gitlabFilter !== undefined ? { filter: gitlabFilter } : {}),
747
1076
  ...gitlabConds,
748
1077
  };
749
1078
  }
@@ -756,6 +1085,7 @@ function parseSingleSrc(raw, loc) {
756
1085
  host: typeof g['host'] === 'string' ? g['host'] : undefined,
757
1086
  via: parseVia(g['via'], `${loc}.gitlab`),
758
1087
  },
1088
+ ...(gitlabFilter !== undefined ? { filter: gitlabFilter } : {}),
759
1089
  ...gitlabConds,
760
1090
  };
761
1091
  }
@@ -799,6 +1129,7 @@ function parseSingleSrc(raw, loc) {
799
1129
  throw new Error(`${loc}.github.host: must be a non-empty string`);
800
1130
  }
801
1131
  const githubConds = parseConditionField(obj, loc);
1132
+ const githubFilter = parseFilter(obj, loc);
802
1133
  if (hasRelease) {
803
1134
  return {
804
1135
  github: {
@@ -808,6 +1139,7 @@ function parseSingleSrc(raw, loc) {
808
1139
  host: typeof g['host'] === 'string' ? g['host'] : undefined,
809
1140
  via: parseVia(g['via'], `${loc}.github`),
810
1141
  },
1142
+ ...(githubFilter !== undefined ? { filter: githubFilter } : {}),
811
1143
  ...githubConds,
812
1144
  };
813
1145
  }
@@ -820,6 +1152,7 @@ function parseSingleSrc(raw, loc) {
820
1152
  host: typeof g['host'] === 'string' ? g['host'] : undefined,
821
1153
  via: parseVia(g['via'], `${loc}.github`),
822
1154
  },
1155
+ ...(githubFilter !== undefined ? { filter: githubFilter } : {}),
823
1156
  ...githubConds,
824
1157
  };
825
1158
  }
@@ -843,6 +1176,7 @@ function parseSingleSrc(raw, loc) {
843
1176
  throw new Error(`${loc}.bitbucket.host: must be a non-empty string`);
844
1177
  }
845
1178
  const bitbucketConds = parseConditionField(obj, loc);
1179
+ const bitbucketFilter = parseFilter(obj, loc);
846
1180
  return {
847
1181
  bitbucket: {
848
1182
  workspace: b['workspace'],
@@ -852,6 +1186,7 @@ function parseSingleSrc(raw, loc) {
852
1186
  sha: parseSha(b['sha'], `${loc}.bitbucket`),
853
1187
  host: typeof b['host'] === 'string' ? b['host'] : undefined,
854
1188
  },
1189
+ ...(bitbucketFilter !== undefined ? { filter: bitbucketFilter } : {}),
855
1190
  ...bitbucketConds,
856
1191
  };
857
1192
  }
@@ -868,6 +1203,7 @@ function parseSingleSrc(raw, loc) {
868
1203
  throw new Error(`${loc}.git.file: required string`);
869
1204
  }
870
1205
  const gitConds = parseConditionField(obj, loc);
1206
+ const gitFilter = parseFilter(obj, loc);
871
1207
  return {
872
1208
  git: {
873
1209
  repo: gt['repo'],
@@ -875,6 +1211,7 @@ function parseSingleSrc(raw, loc) {
875
1211
  ref: typeof gt['ref'] === 'string' ? gt['ref'] : undefined,
876
1212
  sha: parseSha(gt['sha'], `${loc}.git`),
877
1213
  },
1214
+ ...(gitFilter !== undefined ? { filter: gitFilter } : {}),
878
1215
  ...gitConds,
879
1216
  };
880
1217
  }
@@ -883,6 +1220,9 @@ function parseSingleSrc(raw, loc) {
883
1220
  throw new Error(`${loc}.aws_s3: must be a non-empty string (s3:// URI)`);
884
1221
  }
885
1222
  const result = { aws_s3: obj['aws_s3'] };
1223
+ const s3Filter = parseFilter(obj, loc);
1224
+ if (s3Filter !== undefined)
1225
+ result.filter = s3Filter;
886
1226
  const s3Sha = parseSha(obj['sha'], loc);
887
1227
  if (s3Sha !== undefined)
888
1228
  result.sha = s3Sha;
@@ -1030,6 +1370,9 @@ function parseYamlMergeOptions(raw, loc) {
1030
1370
  function parseTomlMergeOptions(raw, loc) {
1031
1371
  return parseMergeOptions(raw, loc, 'toml', ['abort', 'first_wins', 'last_wins'], ['replace', 'concat', 'dedupe'], ['replace', 'merge']);
1032
1372
  }
1373
+ function parseIniMergeOptions(raw, loc) {
1374
+ return parseMergeOptions(raw, loc, 'ini', ['abort', 'first_wins', 'last_wins'], ['replace', 'concat', 'dedupe'], ['replace', 'merge']);
1375
+ }
1033
1376
  function parseReplaceRule(r, target, j) {
1034
1377
  if (!r || typeof r !== 'object' || Array.isArray(r)) {
1035
1378
  throw new Error(`files["${target}"].replace[${j}]: must be an object`);