@intecoag/inteco-cli 1.3.0 → 1.4.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.
@@ -1,7 +1,15 @@
1
1
  version: 2
2
2
  updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
7
+ day: "monday"
8
+ time: "03:00"
9
+ timezone: "UTC"
3
10
  - package-ecosystem: "npm"
4
11
  directory: "/"
12
+ versioning-strategy: increase
5
13
  schedule:
6
14
  interval: "weekly"
7
15
  day: "monday"
@@ -14,3 +22,7 @@ updates:
14
22
  patterns:
15
23
  - "*"
16
24
  open-pull-requests-limit: 10
25
+ commit-message:
26
+ prefix: fix
27
+ prefix-development: chore
28
+ include: scope
@@ -14,11 +14,11 @@ jobs:
14
14
  runs-on: ubuntu-latest
15
15
  steps:
16
16
  - name: Checkout
17
- uses: actions/checkout@v4
17
+ uses: actions/checkout@v6
18
18
  with:
19
19
  fetch-depth: 0
20
20
  - name: Setup Node.js
21
- uses: actions/setup-node@v4
21
+ uses: actions/setup-node@v6
22
22
  with:
23
23
  node-version: "lts/*"
24
24
  - name: Install dependencies
@@ -0,0 +1,6 @@
1
+ {
2
+ "conventionalCommits.scopes": [
3
+ "dependabot",
4
+ "actions"
5
+ ]
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intecoag/inteco-cli",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "CLI-Tools for Inteco",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -19,6 +19,7 @@
19
19
  "cli-meow-help": "^4.0.0",
20
20
  "cli-table3": "^0.6.5",
21
21
  "csv-parser": "^3.0.0",
22
+ "fast-glob": "^3.3.3",
22
23
  "fuzzysort": "^3.1.0",
23
24
  "graphql": "^16.6.0",
24
25
  "meow": "^14.0.0",
@@ -36,10 +37,10 @@
36
37
  "inteco": "src/index.js"
37
38
  },
38
39
  "devDependencies": {
39
- "@commitlint/cli": "^20.2.0",
40
- "@commitlint/config-conventional": "^20.2.0",
40
+ "@commitlint/cli": "^20.4.0",
41
+ "@commitlint/config-conventional": "^20.4.0",
41
42
  "husky": "^9.1.7",
42
- "semantic-release": "^25.0.2"
43
+ "semantic-release": "^25.0.3"
43
44
  },
44
45
  "release": {
45
46
  "branches": [
package/src/index.js CHANGED
@@ -18,6 +18,7 @@ import commands from "./ressources/cmds.json" with {type: 'json'};
18
18
  import packageJson from "../package.json" with {type: 'json'}
19
19
  import extdSearch from './modules/extdSearch.js';
20
20
  import syncConfig from './modules/syncConfig.js';
21
+ import configMutation from './modules/configMutation.js';
21
22
  import bundleProduct from './modules/bundleProduct.js';
22
23
 
23
24
  import updateNotifier from 'update-notifier';
@@ -87,6 +88,9 @@ switch (cli.input[0]) {
87
88
  case "sync_config":
88
89
  syncConfig();
89
90
  break;
91
+ case "config_mutation":
92
+ configMutation();
93
+ break;
90
94
  case "bundle_product":
91
95
  bundleProduct(cli);
92
96
  break;
@@ -0,0 +1,360 @@
1
+ import prompts from "prompts";
2
+ import chalk from "chalk";
3
+ import { Config } from "../utils/config/config.js";
4
+ import FS from "fs"
5
+ import * as CliFS from "../utils/fs/FS.js";
6
+ import YAML from "yaml";
7
+ import { YAMLMap, YAMLSeq } from "yaml";
8
+ import fg from "fast-glob";
9
+ import path from "path";
10
+
11
+ export default async function mutateConfig() {
12
+ console.log()
13
+
14
+ const config = await Config.getConfig();
15
+ const configDirectories = [
16
+ { title: '[Update all]', value: '*' },
17
+ ...FS.readdirSync(config.configIndividualPathEclipse, { withFileTypes: true }).filter(dirent => dirent.isDirectory())
18
+ .map(dirent => { return { title: dirent.name, value: dirent.name } })
19
+ ]
20
+
21
+ let success = true;
22
+
23
+ const responses = await prompts([
24
+ {
25
+ type: 'select',
26
+ name: 'mergeType',
27
+ message: 'Select Merge Type:',
28
+ choices: [
29
+ { title: 'Only Create missing Keys (add missing keys from source to target)', value: 'only_create' },
30
+ { title: 'Create, Update and Overwrite Keys (copy everything except keys that only exist in target)', value: 'create_update_overwrite' },
31
+ { title: 'Remove missing Keys (remove all keys from target that don\'t exist in source)', value: 'remove_missing' }
32
+ ]
33
+ },
34
+ {
35
+ type: 'toggle',
36
+ name: 'mergeClients',
37
+ message: 'Include Mand? (Apply changes to 1_, 2_, ...)',
38
+ active: false
39
+ },
40
+ {
41
+ type: 'autocomplete',
42
+ name: 'configDest',
43
+ message: 'Update Target?',
44
+ choices: configDirectories
45
+ },
46
+ {
47
+ type: 'toggle',
48
+ name: 'dryRun',
49
+ message: 'Dry run? (show what would happen without making changes)',
50
+ initial: false,
51
+ active: 'yes',
52
+ inactive: 'no'
53
+ }
54
+ ], {
55
+ onCancel: () => {
56
+ console.log();
57
+ console.log(chalk.red("Cancelled Operation!"));
58
+ console.log();
59
+ success = false;
60
+ }
61
+ });
62
+
63
+
64
+ if (success) {
65
+ const sourceConfigs = await CliFS.FS.filePicker(path.join(path.dirname(config.configIndividualPathEclipse), "config", "yaml"),
66
+ path.join(path.dirname(config.configIndividualPathEclipse), "config", "yaml"));
67
+
68
+ const configsToUpdate = responses.configDest == '*'
69
+ ? FS.readdirSync(config.configIndividualPathEclipse, { withFileTypes: true }).filter(f => f.isDirectory()).map(f => f.name)
70
+ : [responses.configDest];
71
+
72
+ switch (responses.mergeType) {
73
+ case 'only_create':
74
+ await processAddMissing(sourceConfigs, config.configIndividualPathEclipse, configsToUpdate, responses.mergeClients, responses.dryRun);
75
+ break;
76
+ case 'remove_missing':
77
+ await processRemoveMissing(sourceConfigs, config.configIndividualPathEclipse, configsToUpdate, responses.mergeClients, responses.dryRun);
78
+ break;
79
+ case 'create_update_overwrite':
80
+ await processMergeOverwrite(sourceConfigs, config.configIndividualPathEclipse, configsToUpdate, responses.mergeClients, responses.dryRun);
81
+ break;
82
+ }
83
+
84
+ if (responses.dryRun) {
85
+ console.log()
86
+ const confirmationResults = await prompts([
87
+ {
88
+ type: 'confirm',
89
+ name: 'confirmation',
90
+ message: 'Would you like to execute the dry-run?',
91
+ initial: true
92
+ }
93
+ ], {
94
+ onCancel: () => {
95
+ console.log();
96
+ console.log(chalk.red("Cancelled Operation!"));
97
+ console.log();
98
+ success = false;
99
+ }
100
+ })
101
+
102
+ if(confirmationResults.confirmation){
103
+ switch (responses.mergeType) {
104
+ case 'only_create':
105
+ await processAddMissing(sourceConfigs, config.configIndividualPathEclipse, configsToUpdate, responses.mergeClients, false);
106
+ break;
107
+ case 'remove_missing':
108
+ await processRemoveMissing(sourceConfigs, config.configIndividualPathEclipse, configsToUpdate, responses.mergeClients, false);
109
+ break;
110
+ case 'create_update_overwrite':
111
+ await processMergeOverwrite(sourceConfigs, config.configIndividualPathEclipse, configsToUpdate, responses.mergeClients, false);
112
+ break;
113
+ }
114
+ }
115
+ }
116
+ console.log();
117
+ }
118
+ }
119
+
120
+ async function processMergeOverwrite(filesToUpdate, configIndividualPathEclipse, configsToUpdate, mergeClients, dryRun) {
121
+ await processInEachFile(filesToUpdate, configIndividualPathEclipse,
122
+ configsToUpdate, mergeClients, dryRun, mergeOverwriteNodes);
123
+ }
124
+
125
+ async function processRemoveMissing(filesToUpdate, configIndividualPathEclipse, configsToUpdate, mergeClients, dryRun) {
126
+ await processInEachFile(filesToUpdate, configIndividualPathEclipse,
127
+ configsToUpdate, mergeClients, dryRun, removeMissingNodes);
128
+ }
129
+
130
+ async function processAddMissing(filesToUpdate, configIndividualPathEclipse, configsToUpdate, mergeClients, dryRun) {
131
+ await processInEachFile(filesToUpdate, configIndividualPathEclipse,
132
+ configsToUpdate, mergeClients, dryRun, addMissingNodes);
133
+ }
134
+
135
+ async function processEachFile(filesToUpdate, configIndividualPathEclipse, configsToUpdate, mergeClients, dryRun, processAction) {
136
+ let updatedFiles = 0;
137
+ const globalConfig = path.join(path.dirname(configIndividualPathEclipse), "config");
138
+ for(const file of filesToUpdate) {
139
+ for(const c of configsToUpdate) {
140
+ const individualConfigPath = path.join(configIndividualPathEclipse, c, "yaml", file);
141
+ const relatedConfigsForFile = await getRelatedConfigs(individualConfigPath, mergeClients);
142
+ for(const config of relatedConfigsForFile) {
143
+ const hasUpdates = processAction(path.join(globalConfig, "yaml", file), config, dryRun);
144
+ if(hasUpdates) updatedFiles++;
145
+ }
146
+ }
147
+ }
148
+
149
+ console.log();
150
+ console.log(chalk.green(`Summary: Updated ${updatedFiles} files.`));
151
+ if (dryRun) {
152
+ console.log(chalk.yellow("Dry run complete — no changes were made."));
153
+ } else {
154
+ console.log(chalk.green("Config mutate completed successfully."));
155
+ }
156
+ }
157
+
158
+ // Loop through each file in filesToUpdate and executes processAction
159
+ async function processInEachFile(filesToUpdate, configIndividualPathEclipse, configsToUpdate, mergeClients, dryRun, processAction) {
160
+ return await processEachFile(filesToUpdate, configIndividualPathEclipse,
161
+ configsToUpdate, mergeClients, dryRun,
162
+ (globalConfig, individualConfig, dryRun) => processInFile(globalConfig, individualConfig, dryRun, processAction));
163
+ }
164
+
165
+ // Executes processAction for a yaml config file
166
+ function processInFile(compareFrom, compareTo, dryRun, processAction) {
167
+ try {
168
+ if(!FS.existsSync(compareFrom) || !FS.existsSync(compareTo)) return;
169
+ const fromDocument = YAML.parseDocument(FS.readFileSync(compareFrom, "utf-8"));
170
+ const toDocument = YAML.parseDocument(FS.readFileSync(compareTo, "utf-8"));
171
+
172
+ const hasChanges = processAction(fromDocument.contents, toDocument.contents);
173
+ const resultText = toDocument.toString();
174
+
175
+
176
+ if(hasChanges) {
177
+ if(dryRun) {
178
+ console.debug(chalk.gray(`[Debug] From file ${compareFrom}`));
179
+ console.log(chalk.green(`[DryRun] Would Update file: ${compareTo}`));
180
+ }
181
+ else {
182
+ console.log(chalk.green(`Updating file ${compareTo}`));
183
+ FS.writeFileSync(compareTo, resultText);
184
+ }
185
+ }
186
+ return hasChanges;
187
+ }
188
+ catch(error) {
189
+ if(dryRun) {
190
+ console.log(chalk.red(`[DryRun] Error in file: ${compareFrom} <-> ${compareTo}: ${error.message}`));
191
+ }
192
+ else {
193
+ console.log(chalk.red(`Could not update file ${compareFrom} <-> ${compareTo}: ${error.message}`));
194
+ }
195
+ return false;
196
+ }
197
+ }
198
+
199
+ // Returns related configs for a base config file (config.yaml, 1_config.yaml, 2_config.yaml, ...)
200
+ async function getRelatedConfigs(file, withClients) {
201
+ const dir = path.dirname(file);
202
+ const ext = path.extname(file);
203
+ const name = path.basename(file, ext);
204
+
205
+ let patterns = [
206
+ path.join(dir, `${name}${ext}`), // name.yaml
207
+ ];
208
+
209
+ if(withClients)
210
+ patterns.push(path.join(dir, `*_${name}${ext}`)); // *_name.yaml (für mandanten)
211
+
212
+ patterns = patterns.map(p => p.replaceAll(path.sep, '/'));
213
+
214
+ return (await fg(patterns, {
215
+ onlyFiles: true,
216
+ unique: true
217
+ })).map(f => f.replaceAll('/', path.sep));
218
+ }
219
+
220
+ // only_create
221
+ function addMissingNodes(fromNode, toNode) {
222
+ let hasChanges = false;
223
+
224
+ if (!fromNode || !toNode) return;
225
+
226
+ if (fromNode instanceof YAMLMap && toNode instanceof YAMLMap) {
227
+ for (const pair of fromNode.items) {
228
+ const keyNode = pair.key;
229
+ const fromVal = pair.value;
230
+
231
+ if (!toNode.has(keyNode)) {
232
+ toNode.items.push(pair);
233
+ hasChanges = true;
234
+ continue;
235
+ }
236
+
237
+ const toVal = toNode.get(keyNode, true);
238
+ hasChanges |= addMissingNodes(fromVal, toVal);
239
+ }
240
+ return hasChanges;
241
+ }
242
+
243
+ if (fromNode instanceof YAMLSeq && toNode instanceof YAMLSeq) {
244
+ for (const fromItem of fromNode.items) {
245
+ const fromJson = nodeToJs(fromItem);
246
+ let exists = false;
247
+
248
+ for (const toItem of toNode.items) {
249
+ if (deepEqual(fromJson, nodeToJs(toItem))) {
250
+ exists = true;
251
+ break;
252
+ }
253
+ }
254
+
255
+ if (!exists) {
256
+ toNode.items.push(fromItem);
257
+ hasChanges = true;
258
+ }
259
+ }
260
+ return hasChanges;
261
+ }
262
+ }
263
+
264
+ // remove_missing
265
+ function removeMissingNodes(fromNode, toNode) {
266
+ let hasChanges = false;
267
+
268
+ if (!fromNode || !toNode) return false;
269
+
270
+ if (fromNode instanceof YAMLMap && toNode instanceof YAMLMap) {
271
+ for (let i = toNode.items.length - 1; i >= 0; i--) {
272
+ const toPair = toNode.items[i];
273
+ const keyNode = toPair.key;
274
+
275
+ if (!fromNode.has(keyNode)) {
276
+ toNode.items.splice(i, 1);
277
+ hasChanges = true;
278
+ continue;
279
+ }
280
+
281
+ const fromVal = fromNode.get(keyNode, true);
282
+ hasChanges |= removeMissingNodes(fromVal, toPair.value);
283
+ }
284
+ return hasChanges;
285
+ }
286
+
287
+ if (fromNode instanceof YAMLSeq && toNode instanceof YAMLSeq) {
288
+ for (let i = toNode.items.length - 1; i >= 0; i--) {
289
+ const toItem = toNode.items[i];
290
+ const toJson = nodeToJs(toItem);
291
+
292
+ let exists = false;
293
+ for (const fromItem of fromNode.items) {
294
+ if (deepEqual(toJson, nodeToJs(fromItem))) {
295
+ exists = true;
296
+ break;
297
+ }
298
+ }
299
+
300
+ if (!exists) {
301
+ toNode.items.splice(i, 1);
302
+ hasChanges = true;
303
+ }
304
+ }
305
+ return hasChanges;
306
+ }
307
+
308
+ return false;
309
+ }
310
+
311
+ // create_update_overwrite
312
+ function mergeOverwriteNodes(fromNode, toNode) {
313
+ let hasChanges = false;
314
+
315
+ if (!fromNode || !toNode) return false;
316
+
317
+ if (fromNode instanceof YAMLMap && toNode instanceof YAMLMap) {
318
+ for (const fromPair of fromNode.items) {
319
+ const keyNode = fromPair.key;
320
+ const fromVal = fromPair.value;
321
+
322
+ if (!toNode.has(keyNode)) {
323
+ toNode.items.push(fromPair);
324
+ hasChanges = true;
325
+ continue;
326
+ }
327
+
328
+ const toVal = toNode.get(keyNode, true);
329
+
330
+ if (
331
+ fromVal?.constructor === toVal?.constructor &&
332
+ (fromVal instanceof YAMLMap || fromVal instanceof YAMLSeq)
333
+ ) {
334
+ hasChanges |= mergeOverwriteNodes(fromVal, toVal);
335
+ } else {
336
+ toNode.set(keyNode, fromVal);
337
+ hasChanges = true;
338
+ }
339
+ }
340
+ return hasChanges;
341
+ }
342
+
343
+ if (fromNode instanceof YAMLSeq && toNode instanceof YAMLSeq) {
344
+ toNode.items = [...fromNode.items];
345
+ return true;
346
+ }
347
+
348
+ return false;
349
+ }
350
+
351
+
352
+ function nodeToJs(node) {
353
+ return node && typeof node.toJSON === "function"
354
+ ? node.toJSON()
355
+ : node;
356
+ }
357
+
358
+ function deepEqual(a, b) {
359
+ return JSON.stringify(a) === JSON.stringify(b);
360
+ }
@@ -1,5 +1,5 @@
1
1
  import prompts from "prompts";
2
- import { mkdirSync, existsSync, readdirSync, rmSync } from "fs";
2
+ import { mkdirSync, existsSync, readdirSync, rmSync, copyFileSync, statSync } from "fs";
3
3
  import path from "path";
4
4
  import chalk from "chalk";
5
5
  import { Config } from "../utils/config/config.js";
@@ -9,9 +9,7 @@ export default async function syncConfig() {
9
9
  console.log()
10
10
 
11
11
  const config = await Config.getConfig();
12
-
13
12
  const configDirectoriesEclipse = readdirSync(config.configIndividualPathEclipse, { withFileTypes: true }).filter(dirent => dirent.isDirectory()).map(dirent => { return { title: dirent.name } })
14
-
15
13
  const configDirectories = readdirSync(config.configIndividualPath, { withFileTypes: true }).filter(dirent => dirent.isDirectory()).map(dirent => { return { title: dirent.name } })
16
14
 
17
15
  let success = true;
@@ -29,7 +27,8 @@ export default async function syncConfig() {
29
27
  { title: 'Import Everything (Repository → Work)', value: 'import_all' },
30
28
  { title: 'Export Everything (Work → Repository)', value: 'export_all' },
31
29
  { title: 'Import All ConfigIndividual (Repository → Work)', value: 'import_all_individuals' },
32
- { title: 'Export All ConfigIndividual (Work → Repository)', value: 'export_all_individuals' }
30
+ { title: 'Export All ConfigIndividual (Work → Repository)', value: 'export_all_individuals' },
31
+ { title: 'Sync from Config to ConfigIndividual (Repository → Repository)', value: 'sync_to_configIndividual' },
33
32
  ]
34
33
  },
35
34
  {
@@ -61,6 +60,16 @@ export default async function syncConfig() {
61
60
  return filtered;
62
61
  }
63
62
  },
63
+ {
64
+ type: (prev, values) => values.direction == 'sync_to_configIndividual' ? 'autocomplete' : null,
65
+ name: 'configIndividualSelection',
66
+ message: 'ConfigIndividual (Destination)?',
67
+ choices: readdirSync(config.configIndividualPathEclipse, { withFileTypes: true }).filter(e => e.isDirectory())
68
+ .map(e => ({
69
+ title: e.name,
70
+ value: path.join(config.configIndividualPathEclipse, e.name)
71
+ }))
72
+ },
64
73
  {
65
74
  type: 'toggle',
66
75
  name: 'dryRun',
@@ -73,8 +82,8 @@ export default async function syncConfig() {
73
82
  type: 'select',
74
83
  name: 'type',
75
84
  message: 'Sync Type?',
76
- choices: [
77
- { title: 'UPDATE', value: 'UPDATE' },
85
+ choices: (prev, values) => [
86
+ values.direction == 'sync_to_configIndividual' ? { title: 'CREATE (if not exists)', value: 'CREATE_IF_NOT_EXISTS' } : { title: 'UPDATE', value: 'UPDATE' },
78
87
  { title: 'OVERWRITE', value: 'OVERWRITE' }
79
88
  ]
80
89
  }
@@ -90,7 +99,7 @@ export default async function syncConfig() {
90
99
 
91
100
 
92
101
  if (success) {
93
- let sourcePaths, destPaths;
102
+ let sourcePaths, destPaths, syncFiles;
94
103
 
95
104
  switch (responses.direction) {
96
105
  case 'import':
@@ -140,9 +149,24 @@ export default async function syncConfig() {
140
149
  destPaths = [findConfigDirNamedConfigIn(destParent)];
141
150
  break;
142
151
  }
152
+
153
+ case 'sync_to_configIndividual': {
154
+ const sourceParent = path.resolve(config.configIndividualPathEclipse, '..');
155
+
156
+ sourcePaths = [findConfigDirNamedConfigIn(sourceParent)];
157
+ destPaths = [path.resolve(config.configIndividualPathEclipse, responses.configIndividualSelection)];
158
+
159
+ syncFiles = await FS.filePicker(path.resolve(config.configIndividualPathEclipse, '..', "config"), path.resolve(config.configIndividualPathEclipse, '..', "config"));
160
+ }
143
161
  }
144
162
 
145
- processMultiple(responses, responses.dryRun, sourcePaths, destPaths);
163
+ if(responses.direction != "sync_to_configIndividual")
164
+ processMultiple(responses, responses.dryRun, sourcePaths, destPaths);
165
+ else
166
+ processSyncToIndividual(responses.dryRun, syncFiles,
167
+ responses.configIndividualSelection,
168
+ findConfigDirNamedConfigIn(path.resolve(config.configIndividualPathEclipse, '..')),
169
+ responses.type == "OVERWRITE");
146
170
 
147
171
  if (responses.dryRun) {
148
172
  console.log()
@@ -163,13 +187,61 @@ export default async function syncConfig() {
163
187
  })
164
188
 
165
189
  if(confirmationResults.confirmation){
166
- processMultiple(responses, false, sourcePaths, destPaths);
190
+ if(responses.direction != "sync_to_configIndividual")
191
+ processMultiple(responses, false, sourcePaths, destPaths);
192
+ else
193
+ processSyncToIndividual(false, syncFiles,
194
+ responses.configIndividualSelection,
195
+ findConfigDirNamedConfigIn(path.resolve(config.configIndividualPathEclipse, '..')),
196
+ responses.type == "OVERWRITE");
167
197
  }
168
198
  }
169
199
  console.log();
170
200
  }
171
201
  }
172
202
 
203
+ function processSyncToIndividual(dryRun, sourceFiles, targetConfigIndividual, configPath, overwrite) {
204
+ console.log();
205
+ let changed = 0;
206
+
207
+ sourceFiles.forEach(sourceFile => {
208
+ let shouldCopy = false;
209
+
210
+ let destPath = path.join(targetConfigIndividual, sourceFile);
211
+ let sourcePath = path.join(configPath, sourceFile);
212
+
213
+ if (overwrite || !existsSync(destPath)) {
214
+ shouldCopy = true;
215
+ }
216
+
217
+ let shouldCreateDirectory = shouldCopy && !existsSync(path.dirname(destPath));
218
+
219
+ if (shouldCopy) {
220
+ if (dryRun) {
221
+ if(shouldCreateDirectory) {
222
+ console.log(chalk.blue(`[DryRun] Would create directory: ${path.dirname(destPath)}`));
223
+ }
224
+ console.log(chalk.blue(`[DryRun] Would ${overwrite ? "overwrite" : "create"} file: ${destPath}`));
225
+ } else {
226
+ if(shouldCreateDirectory){
227
+ mkdirSync(path.dirname(destPath));
228
+ }
229
+ copyFileSync(sourcePath, destPath);
230
+ }
231
+ changed++;
232
+ }
233
+ });
234
+
235
+ console.log();
236
+ console.log(chalk.green(`Summary: Changed ${changed} items.`));
237
+
238
+ console.log();
239
+ if (dryRun) {
240
+ console.log(chalk.yellow("Dry run complete — no changes were made."));
241
+ } else {
242
+ console.log(chalk.green("Config sync completed successfully."));
243
+ }
244
+ }
173
245
 
174
246
  function processMultiple(responses, dryRun, sourcePaths, destPaths) {
175
247
  console.log();
@@ -8,6 +8,9 @@
8
8
  "config_rewrite": {
9
9
  "desc": "Rewrites WEGAS-Config"
10
10
  },
11
+ "config_mutation": {
12
+ "desc": "Synchronize Configs with new/changed properties"
13
+ },
11
14
  "sync_config": {
12
15
  "desc": "Synchronize Config/ConfigIndividual-Folders between Work and Repository (Eclipse-Repo)"
13
16
  },
@@ -1,10 +1,10 @@
1
1
  import { statSync, mkdirSync, existsSync, copyFileSync, readdirSync } from "fs";
2
2
  import chalk from "chalk";
3
- import path from "path";
4
-
3
+ import path, { relative } from "path";
4
+ import prompts from "prompts";
5
5
 
6
6
  export class FS {
7
- static copyUpdatedFiles(sourceDir, destDir, dryRun = false, stats = { added: 0, updated: 0 }, filenameBlacklist = []) {
7
+ static copyUpdatedFiles(sourceDir, destDir, dryRun = false, stats = { added: 0, updated: 0 }, filenameBlacklist = [], onlyCopyNonExistant = false) {
8
8
  if (!existsSync(destDir)) {
9
9
  if (dryRun) {
10
10
  console.log(chalk.gray(`[DryRun] Would create directory: ${destDir}`));
@@ -30,7 +30,7 @@ export class FS {
30
30
 
31
31
  if (!existsSync(destPath)) {
32
32
  shouldCopy = true;
33
- } else {
33
+ } else if(!onlyCopyNonExistant) {
34
34
  const sourceStat = statSync(sourcePath);
35
35
  const destStat = statSync(destPath);
36
36
 
@@ -84,5 +84,61 @@ export class FS {
84
84
  }
85
85
  }
86
86
 
87
+
88
+ static async filePicker(startDir = process.cwd(), navRootDir = process.cwd()) {
89
+ let current = startDir;
90
+
91
+ while(true) {
92
+ const entries = readdirSync(current, { withFileTypes: true });
93
+
94
+ const choices = [
95
+ { title: '[Dir] . (Select Current Directory)', value: '.', isDirectory: true },
96
+ ...entries.map(e => ({
97
+ title: e.isDirectory() ? `${e.name} [Dir]` : e.name,
98
+ value: e.name,
99
+ isDirectory: e.isDirectory()
100
+ })).sort((a, b) => a.isDirectory && !b.isDirectory ? -1 : (!a.isDirectory && b.isDirectory ? 1 : a.value.localeCompare(b.value)))
101
+ ]
102
+
103
+ if(path.relative(navRootDir, current) !== "") {
104
+ choices.unshift({ title: '[Dir] .. (Go Up one Directory)', value: '..', isDirectory: true });
105
+ }
106
+
107
+ const { file } = await prompts({
108
+ type: 'autocomplete',
109
+ name: "file",
110
+ message: `Pick Files: ${current}`,
111
+ choices
112
+ });
113
+
114
+ const picked = choices.find(c => c.value === file);
115
+
116
+ if(picked.isDirectory) {
117
+ const dir = picked.value;
118
+ if(dir === ".") return FS.getAllFiles(current).map(e => path.relative(navRootDir, e));
119
+ current = dir === ".." ? path.dirname(current) : path.join(current, dir);
120
+ }
121
+ else {
122
+ return [path.relative(navRootDir, path.join(current, picked.value))];
123
+ }
124
+ }
125
+ }
126
+
127
+ static getAllFiles(dir) {
128
+ const entries = readdirSync(dir, { withFileTypes: true });
129
+ const files = [];
130
+
131
+ for(const entry of entries) {
132
+ const fullPath = path.join(dir, entry.name);
87
133
 
134
+ if(entry.isDirectory()) {
135
+ files.push(...FS.getAllFiles(fullPath));
136
+ }
137
+ else {
138
+ files.push(fullPath);
139
+ }
140
+ }
141
+
142
+ return files;
143
+ }
88
144
  }