@reliverse/dler 1.7.2 → 1.7.4

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 (45) hide show
  1. package/LICENSES +16 -0
  2. package/README.md +93 -71
  3. package/bin/app/agg/run.js +2 -2
  4. package/bin/app/check/cmd.d.ts +37 -0
  5. package/bin/app/check/cmd.js +74 -4
  6. package/bin/app/cmds.d.ts +15 -20
  7. package/bin/app/cmds.js +16 -76
  8. package/bin/app/copy/cmd.d.ts +10 -5
  9. package/bin/app/copy/cmd.js +77 -47
  10. package/bin/app/inject/cmd.d.ts +13 -0
  11. package/bin/app/inject/cmd.js +33 -21
  12. package/bin/app/merge/cmd.d.ts +63 -5
  13. package/bin/app/merge/cmd.js +220 -25
  14. package/bin/app/migrate/cmd.d.ts +5 -0
  15. package/bin/app/migrate/cmd.js +17 -1
  16. package/bin/app/rempts/{cmdsTs/cmd.d.ts → cmd.d.ts} +13 -4
  17. package/bin/app/rempts/cmd.js +304 -0
  18. package/bin/cli.js +6 -11
  19. package/bin/libs/sdk/sdk-impl/build/build-library.js +1 -1
  20. package/bin/libs/sdk/sdk-impl/build/build-regular.js +1 -1
  21. package/bin/libs/sdk/sdk-impl/cfg/info.js +1 -1
  22. package/bin/libs/sdk/sdk-impl/utils/{utils-pkg-json-libs.js → utils-package-json-libraries.js} +18 -10
  23. package/bin/libs/sdk/sdk-impl/utils/{utils-pkg-json-reg.js → utils-package-json-regular.js} +19 -7
  24. package/bin/libs/sdk/sdk-mod.d.ts +2 -2
  25. package/bin/libs/sdk/sdk-mod.js +2 -2
  26. package/package.json +4 -3
  27. package/bin/app/deps/cmd.d.ts +0 -45
  28. package/bin/app/deps/cmd.js +0 -84
  29. package/bin/app/inject/README.md +0 -139
  30. package/bin/app/inject/expect/cmd.d.ts +0 -20
  31. package/bin/app/inject/expect/cmd.js +0 -43
  32. package/bin/app/rempts/README.md +0 -121
  33. package/bin/app/rempts/cmd/cmd.d.ts +0 -16
  34. package/bin/app/rempts/cmd/cmd.js +0 -157
  35. package/bin/app/rempts/cmd/templates.d.ts +0 -2
  36. package/bin/app/rempts/cmd/templates.js +0 -30
  37. package/bin/app/rempts/cmdsTs/cmd.js +0 -96
  38. package/bin/app/rempts/migrate/cmd.d.ts +0 -14
  39. package/bin/app/rempts/migrate/cmd.js +0 -38
  40. /package/bin/app/inject/{expect/impl.d.ts → impl.d.ts} +0 -0
  41. /package/bin/app/inject/{expect/impl.js → impl.js} +0 -0
  42. /package/bin/app/{rempts/migrate/impl/commander.d.ts → migrate/codemods/commander-rempts.d.ts} +0 -0
  43. /package/bin/app/{rempts/migrate/impl/commander.js → migrate/codemods/commander-rempts.js} +0 -0
  44. /package/bin/libs/sdk/sdk-impl/utils/{utils-pkg-json-libs.d.ts → utils-package-json-libraries.d.ts} +0 -0
  45. /package/bin/libs/sdk/sdk-impl/utils/{utils-pkg-json-reg.d.ts → utils-package-json-regular.d.ts} +0 -0
@@ -1,8 +1,14 @@
1
1
  import { relinka } from "@reliverse/relinka";
2
2
  import { defineCommand, selectPrompt, inputPrompt } from "@reliverse/rempts";
3
3
  import { copyFile, access, mkdir } from "node:fs/promises";
4
- import { join, dirname } from "node:path";
4
+ import { join, dirname, basename } from "node:path";
5
+ import pMap from "p-map";
6
+ import prettyMilliseconds from "pretty-ms";
5
7
  import { glob } from "tinyglobby";
8
+ import {
9
+ createPerfTimer,
10
+ getElapsedPerfTime
11
+ } from "../../libs/sdk/sdk-impl/utils/utils-perf.js";
6
12
  async function fileExists(path) {
7
13
  try {
8
14
  await access(path);
@@ -11,12 +17,6 @@ async function fileExists(path) {
11
17
  return false;
12
18
  }
13
19
  }
14
- async function safeCopy(source, destination) {
15
- if (await fileExists(destination)) {
16
- throw new Error(`Destination file already exists: ${destination}`);
17
- }
18
- await copyFile(source, destination);
19
- }
20
20
  async function ensureDir(path) {
21
21
  try {
22
22
  await access(path);
@@ -31,11 +31,11 @@ export default defineCommand({
31
31
  description: "Copy files and directories"
32
32
  },
33
33
  args: {
34
- source: {
34
+ s: {
35
35
  type: "string",
36
36
  description: "Source file or directory to copy (supports glob patterns)"
37
37
  },
38
- destination: {
38
+ d: {
39
39
  type: "string",
40
40
  description: "Destination path for the copy operation"
41
41
  },
@@ -44,27 +44,33 @@ export default defineCommand({
44
44
  description: "Recursively process all files in subdirectories (default: true)",
45
45
  default: true
46
46
  },
47
- force: {
48
- type: "boolean",
49
- description: "Overwrite existing files (default: false)",
50
- default: false
51
- },
52
47
  preserveStructure: {
53
48
  type: "boolean",
54
49
  description: "Preserve source directory structure in destination (default: true)",
55
50
  default: true
51
+ },
52
+ increment: {
53
+ type: "boolean",
54
+ description: "Attach an incrementing index to each destination filename before the extension if set (default: true)",
55
+ default: true
56
+ },
57
+ concurrency: {
58
+ type: "number",
59
+ description: "Number of concurrent copy operations (default: 8)",
60
+ default: 8
56
61
  }
57
62
  },
58
63
  async run({ args }) {
59
64
  const {
60
- source,
61
- destination,
65
+ s,
66
+ d,
62
67
  recursive = true,
63
- force = false,
64
- preserveStructure = true
68
+ preserveStructure = true,
69
+ increment = false,
70
+ concurrency = 8
65
71
  } = args;
66
- let finalSource = source;
67
- let finalDestination = destination;
72
+ let finalSource = s;
73
+ let finalDestination = d;
68
74
  if (!finalSource) {
69
75
  finalSource = await inputPrompt({
70
76
  title: "Enter source file or directory (supports glob patterns)",
@@ -78,7 +84,7 @@ export default defineCommand({
78
84
  });
79
85
  }
80
86
  if (!finalSource || !finalDestination) {
81
- relinka("error", "Usage: dler copy <source> <destination>");
87
+ relinka("error", "Usage: dler copy --s <source> --d <destination>");
82
88
  process.exit(1);
83
89
  }
84
90
  try {
@@ -103,37 +109,61 @@ export default defineCommand({
103
109
  return;
104
110
  }
105
111
  }
106
- for (const file of files) {
107
- let destPath;
108
- if (preserveStructure) {
109
- const match = file.match(/packages\/([^/]+)\/lib\/(.*)/);
110
- if (match?.[1] && match?.[2]) {
111
- const packageName = match[1];
112
- const relativePath = match[2];
113
- destPath = join(finalDestination, packageName, relativePath);
112
+ const timer = createPerfTimer();
113
+ const fileNameCounts = /* @__PURE__ */ new Map();
114
+ await pMap(
115
+ files,
116
+ async (file) => {
117
+ let destPath;
118
+ if (preserveStructure) {
119
+ const match = file.match(/packages\/([^/]+)\/lib\/(.*)/);
120
+ if (match?.[1] && match?.[2]) {
121
+ const packageName = match[1];
122
+ const relativePath = match[2];
123
+ destPath = join(finalDestination, packageName, relativePath);
124
+ } else {
125
+ destPath = join(finalDestination, file);
126
+ }
114
127
  } else {
115
- destPath = join(finalDestination, file);
128
+ destPath = join(finalDestination, basename(file));
129
+ }
130
+ if (increment) {
131
+ const dir = dirname(destPath);
132
+ const base = basename(destPath);
133
+ let dirMap = fileNameCounts.get(dir);
134
+ if (!dirMap) {
135
+ dirMap = /* @__PURE__ */ new Map();
136
+ fileNameCounts.set(dir, dirMap);
137
+ }
138
+ const count = dirMap.get(base) || 0;
139
+ if (count > 0) {
140
+ const extMatch = base.match(/(.*)(\.[^./\\]+)$/);
141
+ let newBase;
142
+ if (extMatch) {
143
+ newBase = `${extMatch[1]}-${count + 1}${extMatch[2]}`;
144
+ } else {
145
+ newBase = `${base}-${count + 1}`;
146
+ }
147
+ destPath = join(dir, newBase);
148
+ }
149
+ dirMap.set(base, count + 1);
116
150
  }
117
- } else {
118
- destPath = join(finalDestination, file);
119
- }
120
- try {
121
151
  await ensureDir(dirname(destPath));
122
- if (force) {
123
- await copyFile(file, destPath);
124
- } else {
125
- await safeCopy(file, destPath);
152
+ if (await fileExists(destPath)) {
153
+ throw new Error(`Destination file already exists: ${destPath}`);
126
154
  }
155
+ await copyFile(file, destPath);
127
156
  relinka("log", `Copied '${file}' to '${destPath}'`);
128
- } catch (error) {
129
- const errorMessage = error instanceof Error ? error.message : String(error);
130
- relinka("error", `Error copying '${file}': ${errorMessage}`);
131
- if (!force) {
132
- process.exit(1);
133
- }
134
- }
135
- }
136
- relinka("log", `Successfully copied ${files.length} file(s)`);
157
+ },
158
+ { concurrency, stopOnError: true }
159
+ );
160
+ const elapsed = getElapsedPerfTime(timer);
161
+ relinka(
162
+ "log",
163
+ `Successfully copied ${files.length} file(s) in ${prettyMilliseconds(
164
+ elapsed
165
+ )}`
166
+ );
137
167
  } catch (error) {
138
168
  const errorMessage = error instanceof Error ? error.message : String(error);
139
169
  relinka("error", `Error during copy operation: ${errorMessage}`);
@@ -7,5 +7,18 @@ declare const _default: import("@reliverse/rempts").Command<{
7
7
  type: "string";
8
8
  description: string;
9
9
  };
10
+ files: {
11
+ type: "positional";
12
+ description: string;
13
+ default: string;
14
+ };
15
+ comment: {
16
+ type: "string";
17
+ description: string;
18
+ };
19
+ tscPaths: {
20
+ type: "string";
21
+ description: string;
22
+ };
10
23
  }>;
11
24
  export default _default;
@@ -1,35 +1,47 @@
1
1
  import { relinka } from "@reliverse/relinka";
2
- import { defineCommand, runCmd, selectPrompt } from "@reliverse/rempts";
3
- import { getCmdInjectExpect } from "../cmds.js";
2
+ import { defineArgs, defineCommand } from "@reliverse/rempts";
3
+ import { useTsExpectError } from "./impl.js";
4
4
  export default defineCommand({
5
5
  meta: {
6
- name: "cli",
7
- description: "Runs the Inject command interactive menu (displays list of available commands)"
6
+ name: "expect",
7
+ version: "1.0.0",
8
+ description: "Inject `@ts-expect-error` above lines where TS errors occur"
8
9
  },
9
- args: {
10
+ args: defineArgs({
10
11
  dev: {
11
12
  type: "boolean",
12
- description: "Runs the CLI in dev mode"
13
+ description: "Run the CLI in dev mode"
13
14
  },
14
15
  cwd: {
15
16
  type: "string",
16
17
  description: "The working directory to run the CLI in"
18
+ },
19
+ files: {
20
+ type: "positional",
21
+ description: `'auto' or path(s) to line references file(s)`,
22
+ default: "auto"
23
+ },
24
+ comment: {
25
+ type: "string",
26
+ description: "Override the comment line to insert. Default is `// @ts-expect-error TODO: fix ts`"
27
+ },
28
+ tscPaths: {
29
+ type: "string",
30
+ description: "Optional: specify path(s) to restrict TSC processing (only effective when using 'auto')"
17
31
  }
18
- },
19
- run: async ({ args }) => {
20
- const isDev = args.dev;
21
- relinka("verbose", `Running in ${isDev ? "dev" : "prod"} mode`);
22
- const cmd = await selectPrompt({
23
- title: "Select a command",
24
- options: [
25
- {
26
- value: "ts-expect-error",
27
- label: "Inject `@ts-expect-error` above lines where TS errors occur"
28
- }
29
- ]
30
- });
31
- if (cmd === "ts-expect-error") {
32
- await runCmd(await getCmdInjectExpect(), []);
32
+ }),
33
+ async run({ args }) {
34
+ if (args.dev) {
35
+ relinka("verbose", "Using dev mode");
33
36
  }
37
+ let pathsTsc = args.tscPaths;
38
+ if (pathsTsc === void 0 && args.files === "auto") {
39
+ pathsTsc = "./tsconfig.json";
40
+ }
41
+ await useTsExpectError({
42
+ files: [args.files],
43
+ comment: args.comment,
44
+ tscPaths: [pathsTsc]
45
+ });
34
46
  }
35
47
  });
@@ -1,14 +1,14 @@
1
1
  declare const _default: import("@reliverse/rempts").Command<{
2
- in: {
2
+ s: {
3
3
  type: "array";
4
4
  description: string;
5
5
  };
6
- ignore: {
7
- type: "array";
6
+ d: {
7
+ type: "string";
8
8
  description: string;
9
9
  };
10
- out: {
11
- type: "string";
10
+ ignore: {
11
+ type: "array";
12
12
  description: string;
13
13
  };
14
14
  format: {
@@ -24,6 +24,11 @@ declare const _default: import("@reliverse/rempts").Command<{
24
24
  type: "boolean";
25
25
  description: string;
26
26
  };
27
+ pathAbove: {
28
+ type: "boolean";
29
+ description: string;
30
+ default: true;
31
+ };
27
32
  separator: {
28
33
  type: "string";
29
34
  description: string;
@@ -40,5 +45,58 @@ declare const _default: import("@reliverse/rempts").Command<{
40
45
  type: "boolean";
41
46
  description: string;
42
47
  };
48
+ recursive: {
49
+ type: "boolean";
50
+ description: string;
51
+ default: true;
52
+ };
53
+ preserveStructure: {
54
+ type: "boolean";
55
+ description: string;
56
+ default: true;
57
+ };
58
+ increment: {
59
+ type: "boolean";
60
+ description: string;
61
+ default: false;
62
+ };
63
+ concurrency: {
64
+ type: "number";
65
+ description: string;
66
+ default: number;
67
+ };
68
+ sort: {
69
+ type: "string";
70
+ description: string;
71
+ default: string;
72
+ };
73
+ dryRun: {
74
+ type: "boolean";
75
+ description: string;
76
+ default: false;
77
+ };
78
+ backup: {
79
+ type: "boolean";
80
+ description: string;
81
+ default: false;
82
+ };
83
+ dedupe: {
84
+ type: "boolean";
85
+ description: string;
86
+ default: false;
87
+ };
88
+ header: {
89
+ type: "string";
90
+ description: string;
91
+ };
92
+ footer: {
93
+ type: "string";
94
+ description: string;
95
+ };
96
+ interactive: {
97
+ type: "boolean";
98
+ description: string;
99
+ default: false;
100
+ };
43
101
  }>;
44
102
  export default _default;
@@ -1,7 +1,19 @@
1
1
  import path from "@reliverse/pathkit";
2
2
  import { glob } from "@reliverse/reglob";
3
3
  import fs from "@reliverse/relifso";
4
- import { defineCommand, inputPrompt, confirmPrompt } from "@reliverse/rempts";
4
+ import { relinka } from "@reliverse/relinka";
5
+ import {
6
+ defineCommand,
7
+ inputPrompt,
8
+ confirmPrompt,
9
+ multiselectPrompt
10
+ } from "@reliverse/rempts";
11
+ import pMap from "p-map";
12
+ import prettyMilliseconds from "pretty-ms";
13
+ import {
14
+ createPerfTimer,
15
+ getElapsedPerfTime
16
+ } from "../../libs/sdk/sdk-impl/utils/utils-perf.js";
5
17
  const DEFAULT_IGNORES = ["**/.git/**", "**/node_modules/**"];
6
18
  const BINARY_EXTS = [
7
19
  "png",
@@ -85,24 +97,28 @@ const maybePrompt = async (batch, value, promptFn) => {
85
97
  if (batch || value !== void 0) return value;
86
98
  return promptFn();
87
99
  };
88
- const collectFiles = async (include, extraIgnore) => {
100
+ const collectFiles = async (include, extraIgnore, recursive, sortBy) => {
89
101
  const files = await glob(include, {
90
102
  ignore: [...DEFAULT_IGNORES, ...extraIgnore],
91
- absolute: true
103
+ absolute: true,
104
+ onlyFiles: true,
105
+ deep: recursive ? void 0 : 1
92
106
  });
93
- return [...new Set(files)].filter((f) => !isBinaryExt(f)).sort();
94
- };
95
- const readSections = async (files, injectPath, getPrefix) => {
96
- const cwd = process.cwd();
97
- const reads = files.map(async (f) => {
98
- const raw = await fs.readFile(f, "utf8");
99
- if (!injectPath) return raw;
100
- const rel = path.relative(cwd, f);
101
- return `${ensureTrailingNL(raw)}${getPrefix(f)}${rel}`;
102
- });
103
- return Promise.all(reads);
107
+ let filtered = [...new Set(files)].filter((f) => !isBinaryExt(f));
108
+ if (sortBy === "name") {
109
+ filtered.sort((a, b) => path.basename(a).localeCompare(path.basename(b)));
110
+ } else if (sortBy === "path") {
111
+ filtered.sort();
112
+ } else if (sortBy === "mtime") {
113
+ filtered = await pMap(
114
+ filtered,
115
+ async (f) => ({ f, mtime: (await fs.stat(f)).mtimeMs }),
116
+ { concurrency: 8 }
117
+ ).then((arr) => arr.sort((a, b) => a.mtime - b.mtime).map((x) => x.f));
118
+ }
119
+ return filtered;
104
120
  };
105
- const writeResult = async (sections, separator, toFile, toStdout) => {
121
+ const writeResult = async (sections, separator, toFile, toStdout, dryRun, backup) => {
106
122
  const content = `${sections.join(separator)}
107
123
  `;
108
124
  if (toStdout || !toFile) {
@@ -111,18 +127,65 @@ const writeResult = async (sections, separator, toFile, toStdout) => {
111
127
  }
112
128
  const dir = path.dirname(toFile);
113
129
  if (dir && dir !== ".") await fs.ensureDir(dir);
114
- await fs.writeFile(toFile, content, "utf8");
130
+ if (backup && await fs.pathExists(toFile)) {
131
+ const backupPath = `${toFile}.${Date.now()}.bak`;
132
+ await fs.copyFile(toFile, backupPath);
133
+ }
134
+ if (!dryRun) {
135
+ await fs.writeFile(toFile, content, "utf8");
136
+ }
137
+ };
138
+ const writeFilesPreserveStructure = async (files, outDir, preserveStructure, increment, concurrency, dryRun, backup) => {
139
+ const cwd = process.cwd();
140
+ const fileNameCounts = /* @__PURE__ */ new Map();
141
+ await pMap(
142
+ files,
143
+ async (file) => {
144
+ const relPath = preserveStructure ? path.relative(cwd, file) : path.basename(file);
145
+ let destPath = path.join(outDir, relPath);
146
+ if (increment) {
147
+ const dir = path.dirname(destPath);
148
+ const base = path.basename(destPath);
149
+ let dirMap = fileNameCounts.get(dir);
150
+ if (!dirMap) {
151
+ dirMap = /* @__PURE__ */ new Map();
152
+ fileNameCounts.set(dir, dirMap);
153
+ }
154
+ const count = dirMap.get(base) || 0;
155
+ if (count > 0) {
156
+ const extMatch = base.match(/(.*)(\.[^./\\]+)$/);
157
+ let newBase;
158
+ if (extMatch) {
159
+ newBase = `${extMatch[1]}-${count + 1}${extMatch[2]}`;
160
+ } else {
161
+ newBase = `${base}-${count + 1}`;
162
+ }
163
+ destPath = path.join(dir, newBase);
164
+ }
165
+ dirMap.set(base, count + 1);
166
+ }
167
+ await fs.ensureDir(path.dirname(destPath));
168
+ if (backup && await fs.pathExists(destPath)) {
169
+ const backupPath = `${destPath}.${Date.now()}.bak`;
170
+ await fs.copyFile(destPath, backupPath);
171
+ }
172
+ if (!dryRun) {
173
+ await fs.copyFile(file, destPath);
174
+ }
175
+ },
176
+ { concurrency }
177
+ );
115
178
  };
116
179
  export default defineCommand({
117
180
  meta: {
118
- name: "remege",
181
+ name: "merge",
119
182
  version: "1.0.0",
120
- description: "Merge text files with optional commented path footer, skips binaries/media, built for CI & interactive use."
183
+ description: "Merge text files with optional commented path header/footer, skips binaries/media, built for CI & interactive use. Supports copy-like patterns and advanced options."
121
184
  },
122
185
  args: {
123
- in: { type: "array", description: "Input glob patterns" },
186
+ s: { type: "array", description: "Input glob patterns" },
187
+ d: { type: "string", description: "Output file path or directory" },
124
188
  ignore: { type: "array", description: "Extra ignore patterns" },
125
- out: { type: "string", description: "Output file path" },
126
189
  format: {
127
190
  type: "string",
128
191
  default: "txt",
@@ -133,6 +196,11 @@ export default defineCommand({
133
196
  type: "boolean",
134
197
  description: "Don't inject relative path below each file"
135
198
  },
199
+ pathAbove: {
200
+ type: "boolean",
201
+ description: "Print file path above each file's content (default: true)",
202
+ default: true
203
+ },
136
204
  separator: {
137
205
  type: "string",
138
206
  description: `Custom separator (default ${DEFAULT_SEPARATOR_RAW})`
@@ -148,11 +216,65 @@ export default defineCommand({
148
216
  batch: {
149
217
  type: "boolean",
150
218
  description: "Disable interactive prompts (CI/non-interactive mode)"
219
+ },
220
+ recursive: {
221
+ type: "boolean",
222
+ description: "Recursively process all files in subdirectories (default: true)",
223
+ default: true
224
+ },
225
+ preserveStructure: {
226
+ type: "boolean",
227
+ description: "Preserve source directory structure in output (default: true)",
228
+ default: true
229
+ },
230
+ increment: {
231
+ type: "boolean",
232
+ description: "Attach an incrementing index to each output filename if set (default: false)",
233
+ default: false
234
+ },
235
+ concurrency: {
236
+ type: "number",
237
+ description: "Number of concurrent file operations (default: 8)",
238
+ default: 8
239
+ },
240
+ sort: {
241
+ type: "string",
242
+ description: "Sort files by: name, path, mtime, none (default: path)",
243
+ default: "path"
244
+ },
245
+ dryRun: {
246
+ type: "boolean",
247
+ description: "Show what would be done, but don't write files",
248
+ default: false
249
+ },
250
+ backup: {
251
+ type: "boolean",
252
+ description: "Backup output files before overwriting",
253
+ default: false
254
+ },
255
+ dedupe: {
256
+ type: "boolean",
257
+ description: "Remove duplicate file contents in merge",
258
+ default: false
259
+ },
260
+ header: {
261
+ type: "string",
262
+ description: "Header text to add at the start of merged output"
263
+ },
264
+ footer: {
265
+ type: "string",
266
+ description: "Footer text to add at the end of merged output"
267
+ },
268
+ interactive: {
269
+ type: "boolean",
270
+ description: "Prompt for file selection before merging",
271
+ default: false
151
272
  }
152
273
  },
153
274
  async run({ args }) {
275
+ const timer = createPerfTimer();
154
276
  const batch = Boolean(args.batch);
155
- let include = args.in ?? [];
277
+ let include = args.s ?? [];
156
278
  if (include.length === 0) {
157
279
  const raw = await maybePrompt(
158
280
  batch,
@@ -197,6 +319,7 @@ export default defineCommand({
197
319
  }
198
320
  const forceComment = args.forceComment ?? false;
199
321
  const injectPath = !args.noPath;
322
+ const pathAbove = args.pathAbove ?? true;
200
323
  const sepRaw = args.separator ?? await maybePrompt(
201
324
  batch,
202
325
  void 0,
@@ -207,7 +330,7 @@ export default defineCommand({
207
330
  ) ?? DEFAULT_SEPARATOR_RAW;
208
331
  const separator = unescape(sepRaw);
209
332
  let stdoutFlag = args.stdout ?? false;
210
- let outFile = args.out;
333
+ let outFile = args.d;
211
334
  if (!stdoutFlag && !outFile && !batch) {
212
335
  stdoutFlag = await confirmPrompt({
213
336
  title: "Print result to stdout?",
@@ -227,18 +350,90 @@ export default defineCommand({
227
350
  }
228
351
  }
229
352
  }
230
- const files = await collectFiles(include, ignore);
353
+ const recursive = args.recursive ?? true;
354
+ const preserveStructure = args.preserveStructure ?? true;
355
+ const increment = args.increment ?? false;
356
+ const concurrency = args.concurrency ?? 8;
357
+ const sortBy = args.sort;
358
+ const dryRun = args.dryRun ?? false;
359
+ const backup = args.backup ?? false;
360
+ const dedupe = args.dedupe ?? false;
361
+ const header = args.header;
362
+ const footer = args.footer;
363
+ const interactive = args.interactive ?? false;
364
+ let files = await collectFiles(include, ignore, recursive, sortBy);
231
365
  if (files.length === 0) {
232
366
  throw new Error(
233
367
  "No text files matched given patterns (binary/media files are skipped)"
234
368
  );
235
369
  }
370
+ if (interactive && !batch) {
371
+ const selected = await multiselectPrompt({
372
+ title: "Select files to merge",
373
+ options: files.map((f) => ({
374
+ label: path.relative(process.cwd(), f),
375
+ value: f
376
+ }))
377
+ });
378
+ files = Array.isArray(selected) ? selected : [selected];
379
+ if (files.length === 0) {
380
+ throw new Error("No files selected for merging");
381
+ }
382
+ }
236
383
  const getPrefix = (filePath) => {
237
384
  if (forceComment && customComment) return customComment;
238
385
  const ext = path.extname(filePath).slice(1).toLowerCase();
239
386
  return COMMENT_MAP[ext] ?? customComment ?? DEFAULT_COMMENT;
240
387
  };
241
- const sections = await readSections(files, injectPath, getPrefix);
242
- await writeResult(sections, separator, outFile, stdoutFlag);
388
+ if (outFile && await fs.pathExists(outFile) && (await fs.stat(outFile)).isDirectory()) {
389
+ await writeFilesPreserveStructure(
390
+ files,
391
+ outFile,
392
+ preserveStructure,
393
+ increment,
394
+ concurrency,
395
+ dryRun,
396
+ backup
397
+ );
398
+ return;
399
+ }
400
+ const cwd = process.cwd();
401
+ const seen = /* @__PURE__ */ new Set();
402
+ const sections = await pMap(
403
+ files,
404
+ async (f) => {
405
+ const raw = await fs.readFile(f, "utf8");
406
+ if (dedupe) {
407
+ const hash = raw.trim();
408
+ if (seen.has(hash)) return null;
409
+ seen.add(hash);
410
+ }
411
+ const rel = path.relative(cwd, f);
412
+ const prefix = getPrefix(f);
413
+ let section = raw;
414
+ if (pathAbove) {
415
+ section = `${prefix}${rel}
416
+ ${section}`;
417
+ }
418
+ if (injectPath) {
419
+ section = `${ensureTrailingNL(section)}${prefix}${rel}`;
420
+ }
421
+ return section;
422
+ },
423
+ { concurrency }
424
+ );
425
+ const filteredSections = sections.filter(Boolean);
426
+ if (header) filteredSections.unshift(header);
427
+ if (footer) filteredSections.push(footer);
428
+ await writeResult(
429
+ filteredSections,
430
+ separator,
431
+ outFile,
432
+ stdoutFlag,
433
+ dryRun,
434
+ backup
435
+ );
436
+ const elapsed = getElapsedPerfTime(timer);
437
+ relinka("success", `Merge completed in ${prettyMilliseconds(elapsed)}`);
243
438
  }
244
439
  });