tracksmith 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # tracksmith
2
2
 
3
- Tracksmith is a CLI for MKV files. It inspects tracks, extracts an audio track losslessly, creates short clips for checking audio/video sync, and muxes aligned audio into another MKV. It wraps FFmpeg and MKVToolNix. Audio tracks are always stream-copied, so Tracksmith never transcodes or re-encodes audio.
3
+ Tracksmith is a CLI for MKV files. It inspects tracks, edits metadata in place, extracts an audio track losslessly, creates short clips for checking audio/video sync, and muxes aligned audio into another MKV. It wraps FFmpeg and MKVToolNix. Audio tracks are always stream-copied, so Tracksmith never transcodes or re-encodes audio.
4
4
 
5
5
  ## Requirements
6
6
 
7
7
  - [Node.js](https://nodejs.org) 22 or newer, or [Bun](https://bun.com) 1.3.14 or newer
8
- - `ffmpeg` and `mkvmerge` available on `PATH`
8
+ - `ffmpeg`, `mkvmerge`, and `mkvpropedit` available on `PATH`
9
9
 
10
10
  `ffmpeg`:
11
11
 
@@ -24,7 +24,7 @@ sudo apt install ffmpeg
24
24
  sudo pacman -S ffmpeg
25
25
  ```
26
26
 
27
- `mkvmerge`:
27
+ `mkvmerge` and `mkvpropedit` (both part of MKVToolNix):
28
28
 
29
29
  ```powershell
30
30
  # Windows (choose one)
@@ -66,11 +66,15 @@ Release process and approval boundaries: [docs/releasing.md](docs/releasing.md).
66
66
  ```powershell
67
67
  tracksmith inspect <file>
68
68
  tracksmith extract <file> --track <id> [--output <file>] [--force]
69
+ tracksmith edit <file> --track <id> [--name <text>] [--language <lang>] [--default | --no-default] [--forced | --no-forced]
70
+ tracksmith edit <file> --title <text>
69
71
  tracksmith test-clip --video <target> --audio <donor> --track <id> --start <time> [--duration 60] [--delay-ms 0] [--output <file>] [--force]
70
- tracksmith mux --video <target> --audio <mka-or-mkv> --output <file> [--track <id>] [--delay-ms 0] [--language eng] [--name <title>] [--default] [--force]
72
+ tracksmith mux --video <target> --output <file> --audio <mka-or-mkv> [--track <id>] [--delay-ms 0] [--language eng] [--name <title>] [--default] [--audio <next> ...] [--dry-run] [--force]
71
73
  ```
72
74
 
73
- `--track` is always the MKVToolNix track ID shown by `tracksmith inspect`. Times accept seconds such as `90` or `90.5`, or `HH:MM:SS[.ms]`.
75
+ `--track` is always the MKVToolNix track ID shown by `tracksmith inspect`. For `mux`, repeat the full `--audio` group for each new track; its per-track flags apply to the most recent `--audio`, and `--default` may be used on at most one group. Using `--default` also clears the default flag on the target's existing audio tracks, so the new track really becomes the default. Times accept seconds such as `90` or `90.5`, or `HH:MM:SS[.ms]`.
76
+
77
+ `edit` changes metadata in place without remuxing. `--name ""` and `--title ""` clear the value, and `--title` (the file title) cannot be combined with `--track` edits. In Windows PowerShell, use the equals forms `--name=` and `--title=` to clear — PowerShell drops empty `""` arguments before they reach the CLI.
74
78
 
75
79
  ## PowerShell walkthrough
76
80
 
@@ -84,13 +88,16 @@ tracksmith extract "D:\rips\Movie DonorCut.mkv" --track 2
84
88
  # 3. Make a 60-second sync clip at the 10-minute mark, audio delayed 250 ms
85
89
  tracksmith test-clip --video "D:\rips\Movie TargetCut.mkv" --audio "D:\rips\Movie DonorCut.track2.mka" --track 0 --start 00:10:00 --delay-ms 250
86
90
 
87
- # 4. Once the delay is right, mux it into the target
88
- tracksmith mux --video "D:\rips\Movie TargetCut.mkv" --audio "D:\rips\Movie DonorCut.track2.mka" --delay-ms=250 --name "Donor E-AC-3 7.1" --default --output "D:\rips\Movie Final.mkv"
91
+ # 4. Preview two tracks from the same donor without writing an output file
92
+ tracksmith mux --video "D:\rips\Movie TargetCut.mkv" --audio "D:\rips\Movie DonorTracks.mka" --track 0 --delay-ms 250 --language eng --name "Original E-AC-3 7.1" --default --audio "D:\rips\Movie DonorTracks.mka" --track 1 --delay-ms 0 --language eng --name "Commentary" --output "D:\rips\Movie Final.mkv" --dry-run
93
+
94
+ # 5. Once the plan looks right, mux both tracks into the target
95
+ tracksmith mux --video "D:\rips\Movie TargetCut.mkv" --audio "D:\rips\Movie DonorTracks.mka" --track 0 --delay-ms 250 --language eng --name "Original E-AC-3 7.1" --default --audio "D:\rips\Movie DonorTracks.mka" --track 1 --delay-ms 0 --language eng --name "Commentary" --output "D:\rips\Movie Final.mkv"
89
96
  ```
90
97
 
91
98
  Positive `--delay-ms` delays audio; negative values advance it (e.g. `--delay-ms -250` or `--delay-ms=-250`). Check sync near the beginning, middle, and end of the file. If the required offset changes, the files differ by frame rate or edits and one constant delay cannot fix them.
92
99
 
93
- If an output already exists, an interactive terminal asks for confirmation with a y/N prompt. Pass `--force` to overwrite without prompting. Non-interactive sessions refuse to overwrite unless `--force` is provided.
100
+ For `mux`, an interactive terminal shows the planned output track table and asks one combined y/N confirmation; if the output already exists, that same confirmation also covers overwriting it. Use `--dry-run` to print the plan, listing every track already in the target plus the new tracks with `SOURCE` and `DELAY` columns, then exit without writing. Pass `--force` to skip all mux prompting; non-interactive mux sessions never prompt and still require `--force` to overwrite an existing output. `extract` and `test-clip` keep the old overwrite-only prompt behavior.
94
101
 
95
102
  ## Local development
96
103
 
@@ -38,8 +38,12 @@ function sameFile(left, right) {
38
38
  return false;
39
39
  }
40
40
  }
41
+ /** Returns the input path the output aliases on disk (same dev/inode), if any. */
42
+ function findAliasedInput(output, inputs) {
43
+ return inputs.find((input) => sameFile(output, input));
44
+ }
41
45
  async function ensureWritable(path, ctx) {
42
- const aliasedInput = ctx.inputs?.find((input) => sameFile(path, input));
46
+ const aliasedInput = findAliasedInput(path, ctx.inputs ?? []);
43
47
  if (aliasedInput !== void 0) throw new CliError(`Output "${path}" is the same file as input "${aliasedInput}". Choose a different output path.`);
44
48
  if (!(ctx.exists ?? existsSync)(path)) return;
45
49
  if (ctx.force) return;
@@ -57,9 +61,11 @@ function toolPath(path) {
57
61
  //#region src/probe.ts
58
62
  const mkvmergePropertiesSchema = z.object({
59
63
  language: z.optional(z.string()),
64
+ language_ietf: z.optional(z.string()),
60
65
  track_name: z.optional(z.string()),
61
66
  audio_channels: z.optional(z.number()),
62
- default_track: z.optional(z.boolean())
67
+ default_track: z.optional(z.boolean()),
68
+ forced_track: z.optional(z.boolean())
63
69
  });
64
70
  const mkvmergeTrackSchema = z.object({
65
71
  id: z.int(),
@@ -67,8 +73,16 @@ const mkvmergeTrackSchema = z.object({
67
73
  codec: z.string(),
68
74
  properties: z.optional(mkvmergePropertiesSchema)
69
75
  });
70
- const mkvmergeOutputSchema = z.object({ tracks: z.array(mkvmergeTrackSchema) });
71
- function parseMkvmergeJson(json) {
76
+ const mkvmergeContainerSchema = z.object({ properties: z.optional(z.object({ title: z.optional(z.string()) })) });
77
+ const mkvmergeOutputSchema = z.object({
78
+ container: z.optional(z.unknown()),
79
+ tracks: z.array(mkvmergeTrackSchema)
80
+ });
81
+ function containerTitle(container) {
82
+ const result = z.safeParse(mkvmergeContainerSchema, container);
83
+ return result.success ? result.data.properties?.title : void 0;
84
+ }
85
+ function parseMkvmergeFile(json) {
72
86
  let parsed;
73
87
  try {
74
88
  parsed = JSON.parse(json);
@@ -77,24 +91,31 @@ function parseMkvmergeJson(json) {
77
91
  }
78
92
  const result = z.safeParse(mkvmergeOutputSchema, parsed);
79
93
  if (!result.success) throw new CliError("mkvmerge -J output has an unexpected structure.");
80
- return result.data.tracks.map((track) => ({
81
- id: track.id,
82
- type: track.type,
83
- codec: track.codec,
84
- language: track.properties?.language,
85
- name: track.properties?.track_name,
86
- channels: track.properties?.audio_channels,
87
- isDefault: track.properties?.default_track ?? false
88
- }));
94
+ return {
95
+ title: containerTitle(result.data.container),
96
+ tracks: result.data.tracks.map((track) => ({
97
+ id: track.id,
98
+ type: track.type,
99
+ codec: track.codec,
100
+ language: track.properties?.language_ietf || track.properties?.language || void 0,
101
+ name: track.properties?.track_name,
102
+ channels: track.properties?.audio_channels,
103
+ isDefault: track.properties?.default_track ?? false,
104
+ isForced: track.properties?.forced_track ?? false
105
+ }))
106
+ };
89
107
  }
90
- async function probeTracks(runner, file) {
108
+ async function probeFile(runner, file) {
91
109
  const result = await runner.run([
92
110
  "mkvmerge",
93
111
  "-J",
94
112
  toolPath(file)
95
113
  ]);
96
114
  if (result.exitCode >= 2) throw new CliError(`mkvmerge could not read "${file}" (exit ${result.exitCode}):\n${result.stderr || result.stdout}`);
97
- return parseMkvmergeJson(result.stdout);
115
+ return parseMkvmergeFile(result.stdout);
116
+ }
117
+ async function probeTracks(runner, file) {
118
+ return (await probeFile(runner, file)).tracks;
98
119
  }
99
120
  function validAudioIds(tracks) {
100
121
  const ids = tracks.filter((track) => track.type === "audio").map((track) => track.id);
@@ -111,6 +132,54 @@ function audioRelativeIndex(tracks, id) {
111
132
  if (index === -1) throw new CliError(`Track ${id} is not an audio track. ${validAudioIds(tracks)}`);
112
133
  return index;
113
134
  }
135
+ function requireTrack(tracks, id) {
136
+ const track = tracks.find((candidate) => candidate.id === id);
137
+ if (!track) {
138
+ const ids = tracks.map((candidate) => candidate.id);
139
+ throw new CliError(`Track ${id} does not exist. Valid track IDs: ${ids.length > 0 ? ids.join(", ") : "none"}`);
140
+ }
141
+ return track;
142
+ }
143
+ //#endregion
144
+ //#region src/commands/edit.ts
145
+ function hasTrackEdit(opts) {
146
+ return opts.name !== void 0 || opts.language !== void 0 || opts.isDefault !== void 0 || opts.isForced !== void 0;
147
+ }
148
+ function buildEditArgs(opts) {
149
+ const args = [
150
+ "mkvpropedit",
151
+ "--command-line-charset",
152
+ "UTF-8",
153
+ toolPath(opts.file)
154
+ ];
155
+ switch (opts.kind) {
156
+ case "title":
157
+ args.push("--edit", "info");
158
+ if (opts.title === "") args.push("--delete", "title");
159
+ else args.push("--set", `title=${opts.title}`);
160
+ return args;
161
+ case "track":
162
+ args.push("--edit", `track:${opts.track + 1}`);
163
+ if (opts.name === "") args.push("--delete", "name");
164
+ else if (opts.name !== void 0) args.push("--set", `name=${opts.name}`);
165
+ if (opts.language !== void 0) args.push("--set", `language=${opts.language}`);
166
+ if (opts.isDefault !== void 0) args.push("--set", `flag-default=${opts.isDefault ? "1" : "0"}`);
167
+ if (opts.isForced !== void 0) args.push("--set", `flag-forced=${opts.isForced ? "1" : "0"}`);
168
+ return args;
169
+ default: return opts;
170
+ }
171
+ }
172
+ /** Returns mkvpropedit's diagnostic output when it exited 1 (modified with warnings). */
173
+ async function editCommand(opts, deps) {
174
+ requireInputFile(opts.file, deps.exists);
175
+ if (opts.kind === "track") {
176
+ if (!hasTrackEdit(opts)) throw new CliError("Nothing to edit: pass --name, --language, --default/--no-default, or --forced/--no-forced.");
177
+ requireTrack(await probeTracks(deps.runner, opts.file), opts.track);
178
+ }
179
+ const result = await deps.runner.run(buildEditArgs(opts));
180
+ if (result.exitCode >= 2) throw new CliError(`mkvpropedit failed (exit ${result.exitCode}):\n${result.stderr || result.stdout}`);
181
+ if (result.exitCode === 1) return `mkvpropedit finished with warnings — check the file in a player:\n${[result.stdout, result.stderr].map((stream) => stream.trim()).filter(Boolean).join("\n")}`;
182
+ }
114
183
  //#endregion
115
184
  //#region src/commands/extract.ts
116
185
  function buildExtractArgs(opts) {
@@ -150,34 +219,44 @@ async function extractCommand(opts, deps) {
150
219
  }
151
220
  //#endregion
152
221
  //#region src/commands/inspect.ts
222
+ function formatTable(header, rows) {
223
+ const all = [header, ...rows];
224
+ const widths = header.map((_, column) => Math.max(...all.map((row) => row[column].length)));
225
+ return all.map((row) => row.map((cell, column) => cell.padEnd(widths[column])).join(" ").trimEnd()).join("\n");
226
+ }
153
227
  function formatTrackTable(tracks) {
154
- const header = [
228
+ return formatTable([
155
229
  "ID",
156
230
  "TYPE",
157
231
  "CODEC",
158
232
  "LANG",
159
233
  "CH",
160
234
  "DEFAULT",
235
+ "FORCED",
161
236
  "NAME"
162
- ];
163
- const all = [header, ...tracks.map((track) => [
237
+ ], tracks.map((track) => [
164
238
  String(track.id),
165
239
  track.type,
166
240
  track.codec,
167
241
  track.language ?? "-",
168
242
  track.channels !== void 0 ? String(track.channels) : "-",
169
243
  track.isDefault ? "yes" : "-",
244
+ track.isForced ? "yes" : "-",
170
245
  track.name ?? "-"
171
- ])];
172
- const widths = header.map((_, column) => Math.max(...all.map((row) => row[column].length)));
173
- return all.map((row) => row.map((cell, column) => cell.padEnd(widths[column])).join(" ").trimEnd()).join("\n");
246
+ ]));
174
247
  }
175
248
  async function inspectCommand(file, deps) {
176
249
  requireInputFile(file, deps.exists);
177
- return formatTrackTable(await probeTracks(deps.runner, file));
250
+ const { title, tracks } = await probeFile(deps.runner, file);
251
+ const table = formatTrackTable(tracks);
252
+ return title === void 0 ? table : `Title: ${title}\n\n${table}`;
178
253
  }
179
254
  //#endregion
180
255
  //#region src/commands/mux.ts
256
+ /** Labels one `--audio` group by position and path, e.g. `--audio #2 ("donor.mka")`. */
257
+ function describeAudioGroup(index, audio) {
258
+ return `--audio #${index + 1} ("${audio}")`;
259
+ }
181
260
  function resolveAudioTrackId(tracks, requested) {
182
261
  if (requested !== void 0) return requireAudioTrack(tracks, requested).id;
183
262
  const audio = tracks.filter((track) => track.type === "audio");
@@ -186,51 +265,127 @@ function resolveAudioTrackId(tracks, requested) {
186
265
  throw new CliError(`The audio input contains ${audio.length} audio tracks; pass --track <id> (see: tracksmith inspect <file>).`);
187
266
  }
188
267
  function buildMuxArgs(a) {
189
- const id = String(a.audioTrackId);
268
+ const trackOrder = [...a.videoTrackIds.map((trackId) => `0:${trackId}`), ...a.groups.map((group, index) => `${index + 1}:${group.trackId}`)].join(",");
190
269
  const args = [
191
270
  "mkvmerge",
271
+ "--command-line-charset",
272
+ "UTF-8",
192
273
  "-o",
193
274
  toolPath(a.output),
194
- toolPath(a.video),
195
- "--audio-tracks",
196
- id,
197
- "--no-video",
198
- "--no-subtitles",
199
- "--no-buttons",
200
- "--no-chapters",
201
- "--no-attachments",
202
- "--no-global-tags"
275
+ "--track-order",
276
+ trackOrder
203
277
  ];
204
- if (a.delayMs !== 0) args.push("--sync", `${id}:${a.delayMs}`);
205
- if (a.language !== void 0) args.push("--language", `${id}:${a.language}`);
206
- if (a.trackName !== void 0) args.push("--track-name", `${id}:${a.trackName}`);
207
- args.push("--default-track-flag", `${id}:${a.makeDefault ? "yes" : "no"}`);
208
- args.push(toolPath(a.audio));
278
+ for (const id of a.clearDefaultAudioIds) args.push("--default-track-flag", `${id}:no`);
279
+ args.push(toolPath(a.video));
280
+ for (const group of a.groups) {
281
+ const id = String(group.trackId);
282
+ args.push("--audio-tracks", id, "--no-video", "--no-subtitles", "--no-buttons", "--no-chapters", "--no-attachments", "--no-global-tags");
283
+ if (group.delayMs !== 0) args.push("--sync", `${id}:${group.delayMs}`);
284
+ if (group.language !== void 0) args.push("--language", `${id}:${group.language}`);
285
+ if (group.trackName !== void 0) args.push("--track-name", `${id}:${group.trackName}`);
286
+ args.push("--default-track-flag", `${id}:${group.makeDefault ? "yes" : "no"}`);
287
+ args.push(toolPath(group.audio));
288
+ }
209
289
  return args;
210
290
  }
291
+ function buildPlan(a) {
292
+ const cleared = new Set(a.clearDefaultAudioIds);
293
+ const existingRows = a.videoTracks.map((track) => [
294
+ String(track.id),
295
+ track.type,
296
+ track.codec,
297
+ track.language ?? "-",
298
+ track.channels !== void 0 ? String(track.channels) : "-",
299
+ track.isDefault && !cleared.has(track.id) ? "yes" : "-",
300
+ "-",
301
+ track.name ?? "-",
302
+ a.video
303
+ ]);
304
+ const newRows = a.groups.map((group, index) => {
305
+ const donor = a.sourceTracks.get(group.audio)?.find((track) => track.id === group.trackId);
306
+ return [
307
+ String(a.videoTracks.length + index),
308
+ "audio",
309
+ donor?.codec ?? "-",
310
+ group.language ?? donor?.language ?? "-",
311
+ donor?.channels !== void 0 ? String(donor.channels) : "-",
312
+ group.makeDefault ? "yes" : "-",
313
+ group.delayMs !== 0 ? `${group.delayMs}ms` : "-",
314
+ group.trackName ?? donor?.name ?? "-",
315
+ group.audio
316
+ ];
317
+ });
318
+ const table = formatTable([
319
+ "ID",
320
+ "TYPE",
321
+ "CODEC",
322
+ "LANG",
323
+ "CH",
324
+ "DEFAULT",
325
+ "DELAY",
326
+ "NAME",
327
+ "SOURCE"
328
+ ], [...existingRows, ...newRows]);
329
+ return `Planned tracks for "${a.output}":\n${table}`;
330
+ }
211
331
  async function muxCommand(opts, deps) {
212
332
  requireInputFile(opts.video, deps.exists);
213
- requireInputFile(opts.audio, deps.exists);
214
- const audioTrackId = resolveAudioTrackId(await probeTracks(deps.runner, opts.audio), opts.track);
215
- await ensureWritable(opts.output, {
216
- force: opts.force,
217
- isTTY: deps.isTTY,
218
- confirm: deps.confirm,
219
- exists: deps.exists,
220
- inputs: [opts.video, opts.audio]
333
+ const uniqueAudio = [...new Set(opts.tracks.map((track) => track.audio))];
334
+ for (const audio of uniqueAudio) requireInputFile(audio, deps.exists);
335
+ const videoTracks = await probeTracks(deps.runner, opts.video);
336
+ const sourceTracks = new Map(await Promise.all(uniqueAudio.map(async (audio) => [audio, await probeTracks(deps.runner, audio)])));
337
+ const groups = opts.tracks.map((track, index) => {
338
+ let trackId;
339
+ try {
340
+ trackId = resolveAudioTrackId(sourceTracks.get(track.audio), track.track);
341
+ } catch (error) {
342
+ if (!(error instanceof CliError)) throw error;
343
+ throw new CliError(`${describeAudioGroup(index, track.audio)}: ${error.message}`);
344
+ }
345
+ return {
346
+ audio: track.audio,
347
+ trackId,
348
+ delayMs: track.delayMs,
349
+ language: track.language,
350
+ trackName: track.name,
351
+ makeDefault: track.makeDefault
352
+ };
353
+ });
354
+ if (groups.filter((group) => group.makeDefault).length > 1) throw new CliError("--default can be set on at most one --audio input.");
355
+ const aliasedInput = findAliasedInput(opts.output, [opts.video, ...uniqueAudio]);
356
+ if (aliasedInput !== void 0) throw new CliError(`Output "${opts.output}" is the same file as input "${aliasedInput}". Choose a different output path.`);
357
+ const clearDefaultAudioIds = groups.some((group) => group.makeDefault) ? videoTracks.filter((track) => track.type === "audio" && track.isDefault).map((track) => track.id) : [];
358
+ const plan = buildPlan({
359
+ video: opts.video,
360
+ videoTracks,
361
+ groups,
362
+ sourceTracks,
363
+ clearDefaultAudioIds,
364
+ output: opts.output
221
365
  });
366
+ if (opts.dryRun) return {
367
+ kind: "dry-run",
368
+ plan
369
+ };
370
+ const outputExists = (deps.exists ?? existsSync)(opts.output);
371
+ if (!opts.force) {
372
+ if (deps.isTTY) {
373
+ const overwrite = outputExists ? `\nOutput "${opts.output}" already exists and will be overwritten.` : "";
374
+ if (!await deps.confirm(`${plan}${overwrite}\nWrite "${opts.output}"?`)) throw new CliError("Aborted: nothing written.");
375
+ } else if (outputExists) throw new CliError(`Output "${opts.output}" already exists. Pass --force to overwrite (cannot prompt in a non-interactive session).`);
376
+ }
222
377
  const result = await deps.runner.run(buildMuxArgs({
223
378
  video: opts.video,
224
- audio: opts.audio,
225
- audioTrackId,
226
- delayMs: opts.delayMs,
227
- language: opts.language,
228
- trackName: opts.name,
229
- makeDefault: opts.makeDefault,
379
+ videoTrackIds: videoTracks.map((track) => track.id),
380
+ clearDefaultAudioIds,
381
+ groups,
230
382
  output: opts.output
231
383
  }), deps.stderrIsTTY ? { stream: "stdout" } : void 0);
232
384
  if (result.exitCode >= 2) throw new CliError(`mkvmerge failed (exit ${result.exitCode}):\n${result.stderr || result.stdout}`);
233
- return opts.output;
385
+ return {
386
+ kind: "written",
387
+ output: opts.output
388
+ };
234
389
  }
235
390
  //#endregion
236
391
  //#region src/commands/test-clip.ts
@@ -306,23 +461,32 @@ async function testClipCommand(opts, deps) {
306
461
  }
307
462
  //#endregion
308
463
  //#region src/cli.ts
309
- const USAGE = `tracksmith — inspect, extract, sync-test, and mux Matroska audio tracks
464
+ const USAGE = `tracksmith — inspect, edit, extract, sync-test, and mux Matroska audio tracks
310
465
 
311
466
  Usage:
312
467
  tracksmith inspect <file>
313
468
  tracksmith extract <file> --track <id> [--output <file>] [--force]
469
+ tracksmith edit <file> --track <id> [--name <text>] [--language <lang>]
470
+ [--default | --no-default] [--forced | --no-forced]
471
+ tracksmith edit <file> --title <text>
314
472
  tracksmith test-clip --video <target> --audio <donor> --track <id> --start <time>
315
473
  [--duration 60] [--delay-ms 0] [--output <file>] [--force]
316
- tracksmith mux --video <target> --audio <mka-or-mkv> --output <file>
317
- [--track <id>] [--delay-ms 0] [--language eng] [--name <title>]
318
- [--default] [--force]
474
+ tracksmith mux --video <target> --output <file> [--dry-run] [--force]
475
+ --audio <mka-or-mkv> [--track <id>] [--delay-ms 0] [--language eng] [--name <title>] [--default]
476
+ [--audio <next> ...]
319
477
 
320
478
  Notes:
479
+ Repeat --audio to add several tracks; per-track flags apply to the most recent --audio.
480
+ --default may be set on at most one --audio; it also clears the target's existing default audio flags.
481
+ --dry-run prints the planned track layout without writing.
321
482
  --track is always the MKVToolNix track ID shown by "tracksmith inspect".
483
+ edit changes metadata in place without remuxing. --name "" and --title "" clear the value
484
+ (in Windows PowerShell use --name= and --title=; empty "" arguments get dropped).
485
+ --title sets the file title and cannot be combined with --track edits.
322
486
  Positive --delay-ms delays audio; negative advances it (e.g. --delay-ms -250).
323
487
  Times accept seconds (90, 90.5) or HH:MM:SS[.ms].
324
488
  Check sync near the beginning, middle, and end; a changing offset cannot be fixed by one delay.
325
- Requires ffmpeg and MKVToolNix (mkvmerge) on PATH.`;
489
+ Requires ffmpeg and MKVToolNix (mkvmerge, mkvpropedit) on PATH.`;
326
490
  function parseIntStrict(value, flag) {
327
491
  if (value === void 0) throw new CliError(`${flag} is required.`);
328
492
  if (!/^-?\d+$/.test(value)) throw new CliError(`${flag} must be an integer, got "${value}".`);
@@ -440,46 +604,141 @@ async function runCli(argv, deps, stdout = console.log) {
440
604
  return 0;
441
605
  }
442
606
  case "mux": {
443
- const { values } = parseOrCliError(() => parseArgs({
607
+ const { values, tokens } = parseOrCliError(() => parseArgs({
444
608
  args: rest,
445
609
  options: {
446
610
  video: { type: "string" },
447
- audio: { type: "string" },
448
- track: { type: "string" },
611
+ output: { type: "string" },
612
+ force: {
613
+ type: "boolean",
614
+ default: false
615
+ },
616
+ "dry-run": {
617
+ type: "boolean",
618
+ default: false
619
+ },
620
+ audio: {
621
+ type: "string",
622
+ multiple: true
623
+ },
624
+ track: {
625
+ type: "string",
626
+ multiple: true
627
+ },
449
628
  "delay-ms": {
450
629
  type: "string",
451
- default: "0"
630
+ multiple: true
452
631
  },
453
632
  language: {
454
633
  type: "string",
455
- default: "eng"
634
+ multiple: true
456
635
  },
457
- name: { type: "string" },
458
- default: {
459
- type: "boolean",
460
- default: false
636
+ name: {
637
+ type: "string",
638
+ multiple: true
461
639
  },
462
- output: { type: "string" },
463
- force: {
640
+ default: {
464
641
  type: "boolean",
465
- default: false
642
+ multiple: true
466
643
  }
467
644
  },
468
- strict: true
645
+ strict: true,
646
+ tokens: true
469
647
  }));
470
- if (!values.video || !values.audio) throw new CliError("mux requires --video and --audio.");
648
+ const groups = [];
649
+ for (const token of tokens) {
650
+ if (token.kind !== "option") continue;
651
+ if (token.name === "audio") {
652
+ groups.push({ audio: token.value });
653
+ continue;
654
+ }
655
+ if (token.name !== "track" && token.name !== "delay-ms" && token.name !== "language" && token.name !== "name" && token.name !== "default") continue;
656
+ const group = groups.at(-1);
657
+ if (group === void 0) throw new CliError(`--${token.name} must come after an --audio input.`);
658
+ switch (token.name) {
659
+ case "track":
660
+ if (group.track !== void 0) throw new CliError(`Duplicate --track for ${describeAudioGroup(groups.length - 1, group.audio)}.`);
661
+ group.track = token.value;
662
+ break;
663
+ case "delay-ms":
664
+ if (group.delayMs !== void 0) throw new CliError(`Duplicate --delay-ms for ${describeAudioGroup(groups.length - 1, group.audio)}.`);
665
+ group.delayMs = token.value;
666
+ break;
667
+ case "language":
668
+ if (group.language !== void 0) throw new CliError(`Duplicate --language for ${describeAudioGroup(groups.length - 1, group.audio)}.`);
669
+ group.language = token.value;
670
+ break;
671
+ case "name":
672
+ if (group.name !== void 0) throw new CliError(`Duplicate --name for ${describeAudioGroup(groups.length - 1, group.audio)}.`);
673
+ group.name = token.value;
674
+ break;
675
+ case "default":
676
+ if (group.makeDefault !== void 0) throw new CliError(`Duplicate --default for ${describeAudioGroup(groups.length - 1, group.audio)}.`);
677
+ group.makeDefault = true;
678
+ }
679
+ }
680
+ if (!values.video || groups.length === 0) throw new CliError("mux requires --video and --audio.");
471
681
  if (!values.output) throw new CliError("mux requires --output (no default output name for the final file).");
472
- stdout(`Wrote ${await muxCommand({
682
+ const result = await muxCommand({
473
683
  video: values.video,
474
- audio: values.audio,
475
- track: values.track === void 0 ? void 0 : parseTrack(values.track),
476
- delayMs: parseIntStrict(values["delay-ms"] ?? "0", "--delay-ms"),
477
- language: values.language,
478
- name: values.name,
479
- makeDefault: values.default ?? false,
684
+ tracks: groups.map((group) => ({
685
+ audio: group.audio,
686
+ track: group.track === void 0 ? void 0 : parseTrack(group.track),
687
+ delayMs: parseIntStrict(group.delayMs ?? "0", "--delay-ms"),
688
+ language: group.language ?? "eng",
689
+ name: group.name,
690
+ makeDefault: group.makeDefault ?? false
691
+ })),
480
692
  output: values.output,
481
- force: values.force ?? false
482
- }, deps)}`);
693
+ force: values.force ?? false,
694
+ dryRun: values["dry-run"] ?? false
695
+ }, deps);
696
+ if (result.kind === "dry-run") stdout(result.plan);
697
+ else stdout(`Wrote ${result.output}`);
698
+ return 0;
699
+ }
700
+ case "edit": {
701
+ const { values, positionals } = parseOrCliError(() => parseArgs({
702
+ args: rest,
703
+ options: {
704
+ track: { type: "string" },
705
+ name: { type: "string" },
706
+ language: { type: "string" },
707
+ default: { type: "boolean" },
708
+ "no-default": { type: "boolean" },
709
+ forced: { type: "boolean" },
710
+ "no-forced": { type: "boolean" },
711
+ title: { type: "string" }
712
+ },
713
+ allowPositionals: true,
714
+ strict: true
715
+ }));
716
+ const file = requireFilePositional(positionals, "edit");
717
+ const hasTrackFlags = values.track !== void 0 || values.name !== void 0 || values.language !== void 0 || values.default !== void 0 || values["no-default"] !== void 0 || values.forced !== void 0 || values["no-forced"] !== void 0;
718
+ if (values.title !== void 0 && hasTrackFlags) throw new CliError("--title cannot be combined with --track edits. Run them as separate invocations.");
719
+ if (values.default !== void 0 && values["no-default"] !== void 0) throw new CliError("Pass either --default or --no-default, not both.");
720
+ if (values.forced !== void 0 && values["no-forced"] !== void 0) throw new CliError("Pass either --forced or --no-forced, not both.");
721
+ if (values.language === "") throw new CliError("--language cannot be empty. Use --language und for undetermined.");
722
+ let warning;
723
+ if (values.title !== void 0) warning = await editCommand({
724
+ kind: "title",
725
+ file,
726
+ title: values.title
727
+ }, deps);
728
+ else {
729
+ if (values.track === void 0) throw new CliError("edit requires --track (or --title for the file title).");
730
+ warning = await editCommand({
731
+ kind: "track",
732
+ file,
733
+ track: parseTrack(values.track),
734
+ name: values.name,
735
+ language: values.language,
736
+ isDefault: values.default ?? (values["no-default"] !== void 0 ? false : void 0),
737
+ isForced: values.forced ?? (values["no-forced"] !== void 0 ? false : void 0)
738
+ }, deps);
739
+ }
740
+ if (warning !== void 0) console.error(`tracksmith: ${warning}`);
741
+ stdout(`Edited ${file}`);
483
742
  return 0;
484
743
  }
485
744
  default: throw new CliError(`Unknown command "${command}".\n\n${USAGE}`);
@@ -517,13 +776,19 @@ function createConfirmPrompt(options = {}) {
517
776
  }
518
777
  //#endregion
519
778
  //#region src/tools.ts
520
- const TOOLS = ["ffmpeg", "mkvmerge"];
779
+ const TOOLS = [
780
+ "ffmpeg",
781
+ "mkvmerge",
782
+ "mkvpropedit"
783
+ ];
521
784
  function isToolName(value) {
522
785
  return TOOLS.some((tool) => tool === value);
523
786
  }
787
+ const MKVTOOLNIX_HINT = "Windows: winget install MoritzBunkus.MKVToolNix or scoop install mkvtoolnix | macOS: brew install mkvtoolnix | Linux: sudo apt install mkvtoolnix or sudo pacman -S mkvtoolnix-cli";
524
788
  const INSTALL_HINTS = {
525
789
  ffmpeg: "Windows: winget install Gyan.FFmpeg or scoop install ffmpeg | macOS: brew install ffmpeg | Linux: sudo apt install ffmpeg or sudo pacman -S ffmpeg",
526
- mkvmerge: "Windows: winget install MoritzBunkus.MKVToolNix or scoop install mkvtoolnix | macOS: brew install mkvtoolnix | Linux: sudo apt install mkvtoolnix or sudo pacman -S mkvtoolnix-cli"
790
+ mkvmerge: MKVTOOLNIX_HINT,
791
+ mkvpropedit: MKVTOOLNIX_HINT
527
792
  };
528
793
  function installHint(tool) {
529
794
  return INSTALL_HINTS[tool];
@@ -566,7 +831,13 @@ var ProcessRunner = class {
566
831
  process.once("error", (error) => {
567
832
  reject(processStartError(executable, error));
568
833
  });
569
- process.once("close", (exitCode) => {
834
+ process.once("close", (exitCode, signal) => {
835
+ if (signal !== null) {
836
+ const output = [stdout, stderr].map((stream) => stream.trim()).filter(Boolean).join("\n");
837
+ const diagnostics = output === "" ? "" : `\nOutput before termination:\n${output}`;
838
+ reject(new CliError(`${executable} was terminated by signal ${signal}.${diagnostics}`));
839
+ return;
840
+ }
570
841
  resolve({
571
842
  exitCode: exitCode ?? 1,
572
843
  stdout,
@@ -582,7 +853,7 @@ process.exitCode = await runCli(process.argv.slice(2), {
582
853
  runner: new ProcessRunner(),
583
854
  isTTY: process.stdin.isTTY ?? false,
584
855
  stderrIsTTY: process.stderr.isTTY ?? false,
585
- confirm: createConfirmPrompt()
856
+ confirm: createConfirmPrompt({ output: process.stderr })
586
857
  });
587
858
  //#endregion
588
859
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"tracksmith.js","names":[],"sources":["../src/types.ts","../src/output.ts","../src/paths.ts","../src/probe.ts","../src/commands/extract.ts","../src/commands/inspect.ts","../src/commands/mux.ts","../src/commands/test-clip.ts","../src/cli.ts","../src/prompt.ts","../src/tools.ts","../src/runner.ts","../src/tracksmith.ts"],"sourcesContent":["export interface RunResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\nexport interface RunOptions {\n stream?: 'stdout' | 'stderr';\n}\n\nexport interface Runner {\n run(argv: readonly string[], options?: RunOptions): Promise<RunResult>;\n}\n\nexport interface Track {\n id: number;\n type: string;\n codec: string;\n language?: string;\n name?: string;\n channels?: number;\n isDefault: boolean;\n}\n\nexport class CliError extends Error {\n constructor(\n message: string,\n readonly exitCode: number = 1\n ) {\n super(message);\n this.name = 'CliError';\n }\n}\n\nexport interface CommandDeps {\n runner: Runner;\n /** stdin is interactive — gates confirmation prompts. */\n isTTY: boolean;\n /** stderr is interactive — gates live tool progress output. */\n stderrIsTTY: boolean;\n confirm: (message: string) => Promise<boolean>;\n exists?: (path: string) => boolean;\n}\n","import { existsSync, statSync } from 'node:fs';\nimport { basename, dirname, extname, join } from 'node:path';\n\nimport { CliError } from './types.ts';\n\nexport function stemPath(file: string): string {\n return join(dirname(file), basename(file, extname(file)));\n}\n\nexport function defaultExtractOutput(source: string, trackId: number): string {\n return `${stemPath(source)}.track${trackId}.mka`;\n}\n\nexport function defaultTestClipOutput(video: string): string {\n return `${stemPath(video)}.sync-test.mkv`;\n}\n\nexport function requireInputFile(\n path: string,\n exists: (path: string) => boolean = existsSync\n): void {\n if (!exists(path)) throw new CliError(`Input file not found: \"${path}\".`);\n}\n\nexport interface OverwriteContext {\n force: boolean;\n isTTY: boolean;\n confirm: (message: string) => Promise<boolean>;\n exists?: (path: string) => boolean;\n inputs?: readonly string[];\n}\n\nfunction sameFile(left: string, right: string): boolean {\n try {\n const leftStat = statSync(left, { bigint: true });\n const rightStat = statSync(right, { bigint: true });\n return leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino;\n } catch {\n return false;\n }\n}\n\nexport async function ensureWritable(path: string, ctx: OverwriteContext): Promise<void> {\n const aliasedInput = ctx.inputs?.find((input) => sameFile(path, input));\n if (aliasedInput !== undefined) {\n throw new CliError(\n `Output \"${path}\" is the same file as input \"${aliasedInput}\". Choose a different output path.`\n );\n }\n\n const exists = ctx.exists ?? existsSync;\n if (!exists(path)) return;\n if (ctx.force) return;\n if (!ctx.isTTY) {\n throw new CliError(\n `Output \"${path}\" already exists. Pass --force to overwrite (cannot prompt in a non-interactive session).`\n );\n }\n const approved = await ctx.confirm(`Output \"${path}\" already exists. Overwrite?`);\n if (!approved) throw new CliError('Aborted: existing output left untouched.');\n}\n","import { isAbsolute, resolve } from 'node:path';\n\nconst TOOL_OPERATOR_PREFIXES = '-+@=()[]';\n\nexport function toolPath(path: string): string {\n if (isAbsolute(path) || !TOOL_OPERATOR_PREFIXES.includes(path[0] ?? '')) return path;\n return resolve(path);\n}\n","import type { Runner, Track } from './types.ts';\n\nimport * as z from 'zod/mini';\n\nimport { toolPath } from './paths.ts';\nimport { CliError } from './types.ts';\n\nconst mkvmergePropertiesSchema = z.object({\n language: z.optional(z.string()),\n track_name: z.optional(z.string()),\n audio_channels: z.optional(z.number()),\n default_track: z.optional(z.boolean())\n});\n\nconst mkvmergeTrackSchema = z.object({\n id: z.int(),\n type: z.string(),\n codec: z.string(),\n properties: z.optional(mkvmergePropertiesSchema)\n});\n\nconst mkvmergeOutputSchema = z.object({\n tracks: z.array(mkvmergeTrackSchema)\n});\n\nexport function parseMkvmergeJson(json: string): Track[] {\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n throw new CliError('Could not parse mkvmerge -J output as JSON.');\n }\n const result = z.safeParse(mkvmergeOutputSchema, parsed);\n if (!result.success) throw new CliError('mkvmerge -J output has an unexpected structure.');\n\n return result.data.tracks.map((track) => ({\n id: track.id,\n type: track.type,\n codec: track.codec,\n language: track.properties?.language,\n name: track.properties?.track_name,\n channels: track.properties?.audio_channels,\n isDefault: track.properties?.default_track ?? false\n }));\n}\n\nexport async function probeTracks(runner: Runner, file: string): Promise<Track[]> {\n const result = await runner.run(['mkvmerge', '-J', toolPath(file)]);\n if (result.exitCode >= 2) {\n throw new CliError(\n `mkvmerge could not read \"${file}\" (exit ${result.exitCode}):\\n${result.stderr || result.stdout}`\n );\n }\n return parseMkvmergeJson(result.stdout);\n}\n\nfunction validAudioIds(tracks: Track[]): string {\n const ids = tracks.filter((track) => track.type === 'audio').map((track) => track.id);\n return `Valid audio track IDs: ${ids.length > 0 ? ids.join(', ') : 'none'}`;\n}\n\nexport function requireAudioTrack(tracks: Track[], id: number): Track {\n const track = tracks.find((candidate) => candidate.id === id);\n if (!track) throw new CliError(`Track ${id} does not exist. ${validAudioIds(tracks)}`);\n if (track.type !== 'audio')\n throw new CliError(`Track ${id} is ${track.type}, not audio. ${validAudioIds(tracks)}`);\n return track;\n}\n\nexport function audioRelativeIndex(tracks: Track[], id: number): number {\n const index = tracks\n .filter((track) => track.type === 'audio')\n .findIndex((track) => track.id === id);\n if (index === -1)\n throw new CliError(`Track ${id} is not an audio track. ${validAudioIds(tracks)}`);\n return index;\n}\n","import type { CommandDeps } from '../types.ts';\n\nimport { defaultExtractOutput, ensureWritable, requireInputFile } from '../output.ts';\nimport { toolPath } from '../paths.ts';\nimport { probeTracks, requireAudioTrack } from '../probe.ts';\nimport { CliError } from '../types.ts';\n\nexport function buildExtractArgs(opts: { file: string; track: number; output: string }): string[] {\n return [\n 'mkvmerge',\n '-o',\n toolPath(opts.output),\n '--audio-tracks',\n String(opts.track),\n '--no-video',\n '--no-subtitles',\n '--no-buttons',\n '--no-chapters',\n '--no-attachments',\n '--no-global-tags',\n toolPath(opts.file)\n ];\n}\n\nexport async function extractCommand(\n opts: { file: string; track: number; output?: string; force: boolean },\n deps: CommandDeps\n): Promise<string> {\n requireInputFile(opts.file, deps.exists);\n const tracks = await probeTracks(deps.runner, opts.file);\n requireAudioTrack(tracks, opts.track);\n const output = opts.output ?? defaultExtractOutput(opts.file, opts.track);\n await ensureWritable(output, {\n force: opts.force,\n isTTY: deps.isTTY,\n confirm: deps.confirm,\n exists: deps.exists,\n inputs: [opts.file]\n });\n const result = await deps.runner.run(\n buildExtractArgs({ file: opts.file, track: opts.track, output }),\n deps.stderrIsTTY ? { stream: 'stdout' } : undefined\n );\n if (result.exitCode >= 2) {\n throw new CliError(\n `mkvmerge failed (exit ${result.exitCode}):\\n${result.stderr || result.stdout}`\n );\n }\n return output;\n}\n","import type { CommandDeps, Track } from '../types.ts';\n\nimport { requireInputFile } from '../output.ts';\nimport { probeTracks } from '../probe.ts';\n\nexport function formatTrackTable(tracks: Track[]): string {\n const header = ['ID', 'TYPE', 'CODEC', 'LANG', 'CH', 'DEFAULT', 'NAME'];\n const rows = tracks.map((track) => [\n String(track.id),\n track.type,\n track.codec,\n track.language ?? '-',\n track.channels !== undefined ? String(track.channels) : '-',\n track.isDefault ? 'yes' : '-',\n track.name ?? '-'\n ]);\n const all = [header, ...rows];\n const widths = header.map((_, column) => Math.max(...all.map((row) => row[column]!.length)));\n return all\n .map((row) =>\n row\n .map((cell, column) => cell.padEnd(widths[column]!))\n .join(' ')\n .trimEnd()\n )\n .join('\\n');\n}\n\nexport async function inspectCommand(file: string, deps: CommandDeps): Promise<string> {\n requireInputFile(file, deps.exists);\n return formatTrackTable(await probeTracks(deps.runner, file));\n}\n","import type { CommandDeps, Track } from '../types.ts';\n\nimport { ensureWritable, requireInputFile } from '../output.ts';\nimport { toolPath } from '../paths.ts';\nimport { probeTracks, requireAudioTrack } from '../probe.ts';\nimport { CliError } from '../types.ts';\n\nexport function resolveAudioTrackId(tracks: Track[], requested: number | undefined): number {\n if (requested !== undefined) return requireAudioTrack(tracks, requested).id;\n const audio = tracks.filter((track) => track.type === 'audio');\n if (audio.length === 1) return audio[0]!.id;\n if (audio.length === 0) throw new CliError('The audio input contains no audio tracks.');\n throw new CliError(\n `The audio input contains ${audio.length} audio tracks; pass --track <id> (see: tracksmith inspect <file>).`\n );\n}\n\nexport function buildMuxArgs(a: {\n video: string;\n audio: string;\n audioTrackId: number;\n delayMs: number;\n language?: string;\n trackName?: string;\n makeDefault: boolean;\n output: string;\n}): string[] {\n const id = String(a.audioTrackId);\n const args = [\n 'mkvmerge',\n '-o',\n toolPath(a.output),\n toolPath(a.video),\n '--audio-tracks',\n id,\n '--no-video',\n '--no-subtitles',\n '--no-buttons',\n '--no-chapters',\n '--no-attachments',\n '--no-global-tags'\n ];\n if (a.delayMs !== 0) args.push('--sync', `${id}:${a.delayMs}`);\n if (a.language !== undefined) args.push('--language', `${id}:${a.language}`);\n if (a.trackName !== undefined) args.push('--track-name', `${id}:${a.trackName}`);\n args.push('--default-track-flag', `${id}:${a.makeDefault ? 'yes' : 'no'}`);\n args.push(toolPath(a.audio));\n return args;\n}\n\nexport async function muxCommand(\n opts: {\n video: string;\n audio: string;\n track?: number;\n delayMs: number;\n language?: string;\n name?: string;\n makeDefault: boolean;\n output: string;\n force: boolean;\n },\n deps: CommandDeps\n): Promise<string> {\n requireInputFile(opts.video, deps.exists);\n requireInputFile(opts.audio, deps.exists);\n const audioTracks = await probeTracks(deps.runner, opts.audio);\n const audioTrackId = resolveAudioTrackId(audioTracks, opts.track);\n await ensureWritable(opts.output, {\n force: opts.force,\n isTTY: deps.isTTY,\n confirm: deps.confirm,\n exists: deps.exists,\n inputs: [opts.video, opts.audio]\n });\n const result = await deps.runner.run(\n buildMuxArgs({\n video: opts.video,\n audio: opts.audio,\n audioTrackId,\n delayMs: opts.delayMs,\n language: opts.language,\n trackName: opts.name,\n makeDefault: opts.makeDefault,\n output: opts.output\n }),\n deps.stderrIsTTY ? { stream: 'stdout' } : undefined\n );\n if (result.exitCode >= 2) {\n throw new CliError(\n `mkvmerge failed (exit ${result.exitCode}):\\n${result.stderr || result.stdout}`\n );\n }\n return opts.output;\n}\n","import type { CommandDeps } from '../types.ts';\n\nimport { defaultTestClipOutput, ensureWritable, requireInputFile } from '../output.ts';\nimport { toolPath } from '../paths.ts';\nimport { audioRelativeIndex, probeTracks, requireAudioTrack } from '../probe.ts';\nimport { CliError } from '../types.ts';\n\nexport function parseTimeToSeconds(value: string): number {\n if (/^\\d+(\\.\\d+)?$/.test(value)) return Number(value);\n const match = /^(\\d+):([0-5]?\\d):([0-5]?\\d(?:\\.\\d+)?)$/.exec(value);\n if (!match)\n throw new CliError(`Invalid time \"${value}\". Use seconds (90 or 90.5) or HH:MM:SS[.ms].`);\n return Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3]);\n}\n\nexport function buildTestClipArgs(a: {\n video: string;\n audio: string;\n audioStreamIndex: number;\n startSeconds: number;\n durationSeconds: number;\n delayMs: number;\n output: string;\n}): string[] {\n // -y is safe: our own overwrite policy (ensureWritable) has already run.\n return [\n 'ffmpeg',\n '-hide_banner',\n '-nostdin',\n '-ss',\n String(a.startSeconds),\n '-i',\n toolPath(a.video),\n '-itsoffset',\n String(a.delayMs / 1000),\n '-ss',\n String(a.startSeconds),\n '-i',\n toolPath(a.audio),\n '-map',\n '0:v:0',\n '-map',\n `1:a:${a.audioStreamIndex}`,\n '-t',\n String(a.durationSeconds),\n '-c:v',\n 'libx264',\n '-preset',\n 'veryfast',\n '-crf',\n '18',\n '-c:a',\n 'copy',\n '-sn',\n '-abort_on',\n 'empty_output_stream',\n '-y',\n toolPath(a.output)\n ];\n}\n\nexport async function testClipCommand(\n opts: {\n video: string;\n audio: string;\n track: number;\n start: string;\n duration: string;\n delayMs: number;\n output?: string;\n force: boolean;\n },\n deps: CommandDeps\n): Promise<string> {\n requireInputFile(opts.video, deps.exists);\n requireInputFile(opts.audio, deps.exists);\n const startSeconds = parseTimeToSeconds(opts.start);\n const durationSeconds = parseTimeToSeconds(opts.duration);\n if (durationSeconds <= 0) throw new CliError('--duration must be greater than zero.');\n const donorTracks = await probeTracks(deps.runner, opts.audio);\n requireAudioTrack(donorTracks, opts.track);\n const output = opts.output ?? defaultTestClipOutput(opts.video);\n await ensureWritable(output, {\n force: opts.force,\n isTTY: deps.isTTY,\n confirm: deps.confirm,\n exists: deps.exists,\n inputs: [opts.video, opts.audio]\n });\n const result = await deps.runner.run(\n buildTestClipArgs({\n video: opts.video,\n audio: opts.audio,\n audioStreamIndex: audioRelativeIndex(donorTracks, opts.track),\n startSeconds,\n durationSeconds,\n delayMs: opts.delayMs,\n output\n }),\n deps.stderrIsTTY ? { stream: 'stderr' } : undefined\n );\n if (result.exitCode !== 0) {\n throw new CliError(\n `ffmpeg failed (exit ${result.exitCode}):\\n${result.stderr || result.stdout}`\n );\n }\n return output;\n}\n","import { parseArgs } from 'node:util';\n\nimport type { CommandDeps } from './types.ts';\n\nimport { extractCommand } from './commands/extract.ts';\nimport { inspectCommand } from './commands/inspect.ts';\nimport { muxCommand } from './commands/mux.ts';\nimport { testClipCommand } from './commands/test-clip.ts';\nimport { CliError } from './types.ts';\n\nconst USAGE = `tracksmith — inspect, extract, sync-test, and mux Matroska audio tracks\n\nUsage:\n tracksmith inspect <file>\n tracksmith extract <file> --track <id> [--output <file>] [--force]\n tracksmith test-clip --video <target> --audio <donor> --track <id> --start <time>\n [--duration 60] [--delay-ms 0] [--output <file>] [--force]\n tracksmith mux --video <target> --audio <mka-or-mkv> --output <file>\n [--track <id>] [--delay-ms 0] [--language eng] [--name <title>]\n [--default] [--force]\n\nNotes:\n --track is always the MKVToolNix track ID shown by \"tracksmith inspect\".\n Positive --delay-ms delays audio; negative advances it (e.g. --delay-ms -250).\n Times accept seconds (90, 90.5) or HH:MM:SS[.ms].\n Check sync near the beginning, middle, and end; a changing offset cannot be fixed by one delay.\n Requires ffmpeg and MKVToolNix (mkvmerge) on PATH.`;\n\nfunction parseIntStrict(value: string | undefined, flag: string): number {\n if (value === undefined) throw new CliError(`${flag} is required.`);\n if (!/^-?\\d+$/.test(value)) throw new CliError(`${flag} must be an integer, got \"${value}\".`);\n return Number(value);\n}\n\nfunction parseTrack(value: string | undefined): number {\n const track = parseIntStrict(value, '--track');\n if (track < 0) throw new CliError(`--track must be >= 0, got ${track}.`);\n return track;\n}\n\nfunction requireFilePositional(positionals: string[], command: 'inspect' | 'extract'): string {\n if (positionals.length === 0) throw new CliError(`${command} requires a file argument.`);\n if (positionals.length > 1) throw new CliError(`${command} accepts exactly one file argument.`);\n return positionals[0]!;\n}\n\n// Wraps a parseArgs call so parse failures become CliErrors. Takes a thunk\n// (rather than the options object) so parseArgs keeps its precise generic\n// inference over each command's inline options literal.\nfunction parseOrCliError<T>(parseFn: () => T): T {\n try {\n return parseFn();\n } catch (error) {\n throw new CliError(error instanceof Error ? error.message : String(error));\n }\n}\n\n// parseArgs treats a leading-dash value token (\"-250\") as an option, so fold\n// `--delay-ms -250` into the equals form it does accept.\nfunction normalizeDelayMs(args: string[]): string[] {\n const out: string[] = [];\n for (let i = 0; i < args.length; i += 1) {\n const arg = args[i]!;\n const next = args[i + 1];\n if (arg === '--delay-ms' && next !== undefined && /^-\\d+$/.test(next)) {\n out.push(`--delay-ms=${next}`);\n i += 1;\n } else {\n out.push(arg);\n }\n }\n return out;\n}\n\nexport async function runCli(\n argv: string[],\n deps: CommandDeps,\n stdout: (line: string) => void = console.log\n): Promise<number> {\n const [command, ...rawRest] = argv;\n const rest = normalizeDelayMs(rawRest);\n try {\n switch (command) {\n case undefined: {\n stdout(USAGE);\n return 1;\n }\n case '--help':\n case '-h': {\n stdout(USAGE);\n return 0;\n }\n case 'inspect': {\n const { positionals } = parseOrCliError(() =>\n parseArgs({ args: rest, options: {}, allowPositionals: true, strict: true })\n );\n const file = requireFilePositional(positionals, 'inspect');\n stdout(await inspectCommand(file, deps));\n return 0;\n }\n case 'extract': {\n const { values, positionals } = parseOrCliError(() =>\n parseArgs({\n args: rest,\n options: {\n track: { type: 'string' },\n output: { type: 'string' },\n force: { type: 'boolean', default: false }\n },\n allowPositionals: true,\n strict: true\n })\n );\n const file = requireFilePositional(positionals, 'extract');\n const output = await extractCommand(\n {\n file,\n track: parseTrack(values.track),\n output: values.output,\n force: values.force ?? false\n },\n deps\n );\n stdout(`Wrote ${output}`);\n return 0;\n }\n case 'test-clip': {\n const { values } = parseOrCliError(() =>\n parseArgs({\n args: rest,\n options: {\n video: { type: 'string' },\n audio: { type: 'string' },\n track: { type: 'string' },\n start: { type: 'string' },\n duration: { type: 'string', default: '60' },\n 'delay-ms': { type: 'string', default: '0' },\n output: { type: 'string' },\n force: { type: 'boolean', default: false }\n },\n strict: true\n })\n );\n if (!values.video || !values.audio)\n throw new CliError('test-clip requires --video and --audio.');\n if (!values.start) throw new CliError('test-clip requires --start.');\n const output = await testClipCommand(\n {\n video: values.video,\n audio: values.audio,\n track: parseTrack(values.track),\n start: values.start,\n duration: values.duration ?? '60',\n delayMs: parseIntStrict(values['delay-ms'] ?? '0', '--delay-ms'),\n output: values.output,\n force: values.force ?? false\n },\n deps\n );\n stdout(`Wrote ${output}`);\n return 0;\n }\n case 'mux': {\n const { values } = parseOrCliError(() =>\n parseArgs({\n args: rest,\n options: {\n video: { type: 'string' },\n audio: { type: 'string' },\n track: { type: 'string' },\n 'delay-ms': { type: 'string', default: '0' },\n language: { type: 'string', default: 'eng' },\n name: { type: 'string' },\n default: { type: 'boolean', default: false },\n output: { type: 'string' },\n force: { type: 'boolean', default: false }\n },\n strict: true\n })\n );\n if (!values.video || !values.audio) throw new CliError('mux requires --video and --audio.');\n if (!values.output)\n throw new CliError('mux requires --output (no default output name for the final file).');\n const output = await muxCommand(\n {\n video: values.video,\n audio: values.audio,\n track: values.track === undefined ? undefined : parseTrack(values.track),\n delayMs: parseIntStrict(values['delay-ms'] ?? '0', '--delay-ms'),\n language: values.language,\n name: values.name,\n makeDefault: values.default ?? false,\n output: values.output,\n force: values.force ?? false\n },\n deps\n );\n stdout(`Wrote ${output}`);\n return 0;\n }\n default:\n throw new CliError(`Unknown command \"${command}\".\\n\\n${USAGE}`);\n }\n } catch (error) {\n if (error instanceof CliError) {\n console.error(`tracksmith: ${error.message}`);\n return error.exitCode;\n }\n throw error;\n }\n}\n","import { once } from 'node:events';\nimport { createInterface } from 'node:readline/promises';\n\nimport type { Readable, Writable } from 'node:stream';\n\nexport interface ConfirmPromptOptions {\n input?: Readable;\n output?: Writable;\n}\n\nexport function createConfirmPrompt(\n options: ConfirmPromptOptions = {}\n): (message: string) => Promise<boolean> {\n const input = options.input ?? process.stdin;\n const output = options.output ?? process.stdout;\n\n return async (message: string): Promise<boolean> => {\n const readline = createInterface({ input, output });\n\n try {\n const closed = once(readline, 'close').then(() => ({ kind: 'eof' }) as const);\n const answered = readline.question(`${message} [y/N] `).then(\n (value) => ({ kind: 'answer', value }) as const,\n () => ({ kind: 'eof' }) as const\n );\n const result = await Promise.race([answered, closed]);\n\n return result.kind === 'answer' && /^y(?:es)?$/i.test(result.value.trim());\n } finally {\n readline.close();\n }\n };\n}\n","export const TOOLS = ['ffmpeg', 'mkvmerge'] as const;\n\nexport type ToolName = (typeof TOOLS)[number];\n\nexport function isToolName(value: string): value is ToolName {\n return TOOLS.some((tool) => tool === value);\n}\n\nconst INSTALL_HINTS = {\n ffmpeg:\n 'Windows: winget install Gyan.FFmpeg or scoop install ffmpeg | macOS: brew install ffmpeg | Linux: sudo apt install ffmpeg or sudo pacman -S ffmpeg',\n mkvmerge:\n 'Windows: winget install MoritzBunkus.MKVToolNix or scoop install mkvtoolnix | macOS: brew install mkvtoolnix | Linux: sudo apt install mkvtoolnix or sudo pacman -S mkvtoolnix-cli'\n} satisfies Record<ToolName, string>;\n\nexport function installHint(tool: ToolName): string {\n return INSTALL_HINTS[tool];\n}\n","import { spawn } from 'node:child_process';\n\nimport type { RunOptions, Runner, RunResult } from './types.ts';\n\nimport { installHint, isToolName } from './tools.ts';\nimport { CliError } from './types.ts';\n\nexport function processStartError(program: string, error: unknown): CliError {\n if (\n typeof error === 'object' &&\n error !== null &&\n 'code' in error &&\n error.code === 'ENOENT' &&\n isToolName(program)\n ) {\n return new CliError(\n `${program} is required but was not found on PATH. ${installHint(program)}`\n );\n }\n\n const message = error instanceof Error ? error.message : String(error);\n return new CliError(`Failed to start ${program}: ${message}`);\n}\n\nconst writeToCliStderr = (chunk: string): void => {\n globalThis.process.stderr.write(chunk);\n};\n\nexport class ProcessRunner implements Runner {\n private readonly sink: (chunk: string) => void;\n\n constructor(options: { sink?: (chunk: string) => void } = {}) {\n this.sink = options.sink ?? writeToCliStderr;\n }\n\n async run(argv: readonly string[], options?: RunOptions): Promise<RunResult> {\n const [executable, ...args] = argv;\n if (executable === undefined) {\n throw new CliError('Could not start process: missing executable');\n }\n\n return await new Promise((resolve, reject) => {\n const process = spawn(executable, args, {\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let stdout = '';\n let stderr = '';\n\n process.stdout.setEncoding('utf8');\n process.stderr.setEncoding('utf8');\n process.stdout.on('data', (chunk: string) => {\n stdout += chunk;\n if (options?.stream === 'stdout') this.sink(chunk);\n });\n process.stderr.on('data', (chunk: string) => {\n stderr += chunk;\n if (options?.stream === 'stderr') this.sink(chunk);\n });\n process.once('error', (error) => {\n reject(processStartError(executable, error));\n });\n process.once('close', (exitCode) => {\n resolve({ exitCode: exitCode ?? 1, stdout, stderr });\n });\n });\n }\n}\n","#!/usr/bin/env node\n\nimport { runCli } from './cli.ts';\nimport { createConfirmPrompt } from './prompt.ts';\nimport { ProcessRunner } from './runner.ts';\n\nprocess.exitCode = await runCli(process.argv.slice(2), {\n runner: new ProcessRunner(),\n isTTY: process.stdin.isTTY ?? false,\n stderrIsTTY: process.stderr.isTTY ?? false,\n confirm: createConfirmPrompt()\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"tracksmith.js","names":[],"sources":["../src/types.ts","../src/output.ts","../src/paths.ts","../src/probe.ts","../src/commands/edit.ts","../src/commands/extract.ts","../src/commands/inspect.ts","../src/commands/mux.ts","../src/commands/test-clip.ts","../src/cli.ts","../src/prompt.ts","../src/tools.ts","../src/runner.ts","../src/tracksmith.ts"],"sourcesContent":["export interface RunResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\nexport interface RunOptions {\n stream?: 'stdout' | 'stderr';\n}\n\nexport interface Runner {\n run(argv: readonly string[], options?: RunOptions): Promise<RunResult>;\n}\n\nexport interface Track {\n id: number;\n type: string;\n codec: string;\n language?: string;\n name?: string;\n channels?: number;\n isDefault: boolean;\n isForced: boolean;\n}\n\nexport class CliError extends Error {\n constructor(\n message: string,\n readonly exitCode: number = 1\n ) {\n super(message);\n this.name = 'CliError';\n }\n}\n\nexport interface CommandDeps {\n runner: Runner;\n /** stdin is interactive — gates confirmation prompts. */\n isTTY: boolean;\n /** stderr is interactive — gates live tool progress output. */\n stderrIsTTY: boolean;\n confirm: (message: string) => Promise<boolean>;\n exists?: (path: string) => boolean;\n}\n","import { existsSync, statSync } from 'node:fs';\nimport { basename, dirname, extname, join } from 'node:path';\n\nimport { CliError } from './types.ts';\n\nexport function stemPath(file: string): string {\n return join(dirname(file), basename(file, extname(file)));\n}\n\nexport function defaultExtractOutput(source: string, trackId: number): string {\n return `${stemPath(source)}.track${trackId}.mka`;\n}\n\nexport function defaultTestClipOutput(video: string): string {\n return `${stemPath(video)}.sync-test.mkv`;\n}\n\nexport function requireInputFile(\n path: string,\n exists: (path: string) => boolean = existsSync\n): void {\n if (!exists(path)) throw new CliError(`Input file not found: \"${path}\".`);\n}\n\nexport interface OverwriteContext {\n force: boolean;\n isTTY: boolean;\n confirm: (message: string) => Promise<boolean>;\n exists?: (path: string) => boolean;\n inputs?: readonly string[];\n}\n\nfunction sameFile(left: string, right: string): boolean {\n try {\n const leftStat = statSync(left, { bigint: true });\n const rightStat = statSync(right, { bigint: true });\n return leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino;\n } catch {\n return false;\n }\n}\n\n/** Returns the input path the output aliases on disk (same dev/inode), if any. */\nexport function findAliasedInput(output: string, inputs: readonly string[]): string | undefined {\n return inputs.find((input) => sameFile(output, input));\n}\n\nexport async function ensureWritable(path: string, ctx: OverwriteContext): Promise<void> {\n const aliasedInput = findAliasedInput(path, ctx.inputs ?? []);\n if (aliasedInput !== undefined) {\n throw new CliError(\n `Output \"${path}\" is the same file as input \"${aliasedInput}\". Choose a different output path.`\n );\n }\n\n const exists = ctx.exists ?? existsSync;\n if (!exists(path)) return;\n if (ctx.force) return;\n if (!ctx.isTTY) {\n throw new CliError(\n `Output \"${path}\" already exists. Pass --force to overwrite (cannot prompt in a non-interactive session).`\n );\n }\n const approved = await ctx.confirm(`Output \"${path}\" already exists. Overwrite?`);\n if (!approved) throw new CliError('Aborted: existing output left untouched.');\n}\n","import { isAbsolute, resolve } from 'node:path';\n\nconst TOOL_OPERATOR_PREFIXES = '-+@=()[]';\n\nexport function toolPath(path: string): string {\n if (isAbsolute(path) || !TOOL_OPERATOR_PREFIXES.includes(path[0] ?? '')) return path;\n return resolve(path);\n}\n","import type { Runner, Track } from './types.ts';\n\nimport * as z from 'zod/mini';\n\nimport { toolPath } from './paths.ts';\nimport { CliError } from './types.ts';\n\nconst mkvmergePropertiesSchema = z.object({\n language: z.optional(z.string()),\n language_ietf: z.optional(z.string()),\n track_name: z.optional(z.string()),\n audio_channels: z.optional(z.number()),\n default_track: z.optional(z.boolean()),\n forced_track: z.optional(z.boolean())\n});\n\nconst mkvmergeTrackSchema = z.object({\n id: z.int(),\n type: z.string(),\n codec: z.string(),\n properties: z.optional(mkvmergePropertiesSchema)\n});\n\nconst mkvmergeContainerSchema = z.object({\n properties: z.optional(z.object({ title: z.optional(z.string()) }))\n});\n\nconst mkvmergeOutputSchema = z.object({\n // Container metadata is ancillary: an unexpected shape must not fail track\n // parsing, so it is validated separately and dropped on mismatch.\n container: z.optional(z.unknown()),\n tracks: z.array(mkvmergeTrackSchema)\n});\n\nfunction containerTitle(container: unknown): string | undefined {\n const result = z.safeParse(mkvmergeContainerSchema, container);\n return result.success ? result.data.properties?.title : undefined;\n}\n\nexport interface MkvFile {\n title?: string;\n tracks: Track[];\n}\n\nexport function parseMkvmergeFile(json: string): MkvFile {\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n throw new CliError('Could not parse mkvmerge -J output as JSON.');\n }\n const result = z.safeParse(mkvmergeOutputSchema, parsed);\n if (!result.success) throw new CliError('mkvmerge -J output has an unexpected structure.');\n\n return {\n title: containerTitle(result.data.container),\n tracks: result.data.tracks.map((track) => ({\n id: track.id,\n type: track.type,\n codec: track.codec,\n language: track.properties?.language_ietf || track.properties?.language || undefined,\n name: track.properties?.track_name,\n channels: track.properties?.audio_channels,\n isDefault: track.properties?.default_track ?? false,\n isForced: track.properties?.forced_track ?? false\n }))\n };\n}\n\nexport function parseMkvmergeJson(json: string): Track[] {\n return parseMkvmergeFile(json).tracks;\n}\n\nexport async function probeFile(runner: Runner, file: string): Promise<MkvFile> {\n const result = await runner.run(['mkvmerge', '-J', toolPath(file)]);\n if (result.exitCode >= 2) {\n throw new CliError(\n `mkvmerge could not read \"${file}\" (exit ${result.exitCode}):\\n${result.stderr || result.stdout}`\n );\n }\n return parseMkvmergeFile(result.stdout);\n}\n\nexport async function probeTracks(runner: Runner, file: string): Promise<Track[]> {\n return (await probeFile(runner, file)).tracks;\n}\n\nfunction validAudioIds(tracks: Track[]): string {\n const ids = tracks.filter((track) => track.type === 'audio').map((track) => track.id);\n return `Valid audio track IDs: ${ids.length > 0 ? ids.join(', ') : 'none'}`;\n}\n\nexport function requireAudioTrack(tracks: Track[], id: number): Track {\n const track = tracks.find((candidate) => candidate.id === id);\n if (!track) throw new CliError(`Track ${id} does not exist. ${validAudioIds(tracks)}`);\n if (track.type !== 'audio')\n throw new CliError(`Track ${id} is ${track.type}, not audio. ${validAudioIds(tracks)}`);\n return track;\n}\n\nexport function audioRelativeIndex(tracks: Track[], id: number): number {\n const index = tracks\n .filter((track) => track.type === 'audio')\n .findIndex((track) => track.id === id);\n if (index === -1)\n throw new CliError(`Track ${id} is not an audio track. ${validAudioIds(tracks)}`);\n return index;\n}\n\nexport function requireTrack(tracks: Track[], id: number): Track {\n const track = tracks.find((candidate) => candidate.id === id);\n if (!track) {\n const ids = tracks.map((candidate) => candidate.id);\n throw new CliError(\n `Track ${id} does not exist. Valid track IDs: ${ids.length > 0 ? ids.join(', ') : 'none'}`\n );\n }\n return track;\n}\n","import type { CommandDeps } from '../types.ts';\n\nimport { requireInputFile } from '../output.ts';\nimport { toolPath } from '../paths.ts';\nimport { probeTracks, requireTrack } from '../probe.ts';\nimport { CliError } from '../types.ts';\n\nexport type EditOpts =\n | { kind: 'title'; file: string; title: string }\n | {\n kind: 'track';\n file: string;\n track: number;\n name?: string;\n language?: string;\n isDefault?: boolean;\n isForced?: boolean;\n };\n\ntype TrackEditOpts = Extract<EditOpts, { kind: 'track' }>;\n\nfunction hasTrackEdit(opts: TrackEditOpts): boolean {\n return (\n opts.name !== undefined ||\n opts.language !== undefined ||\n opts.isDefault !== undefined ||\n opts.isForced !== undefined\n );\n}\n\nexport function buildEditArgs(opts: EditOpts): string[] {\n // Node and Bun always pass argv as UTF-8 bytes; without this flag mkvpropedit\n // decodes them with the system locale and silently mangles non-ASCII values\n // (e.g. drops a Japanese title under LC_ALL=C) while still exiting 0.\n const args = ['mkvpropedit', '--command-line-charset', 'UTF-8', toolPath(opts.file)];\n switch (opts.kind) {\n case 'title': {\n args.push('--edit', 'info');\n if (opts.title === '') args.push('--delete', 'title');\n else args.push('--set', `title=${opts.title}`);\n return args;\n }\n case 'track': {\n // mkvpropedit's track:n selector is 1-based over the same track order\n // mkvmerge --identify reports, so the inspect ID maps to id + 1.\n args.push('--edit', `track:${opts.track + 1}`);\n if (opts.name === '') args.push('--delete', 'name');\n else if (opts.name !== undefined) args.push('--set', `name=${opts.name}`);\n if (opts.language !== undefined) args.push('--set', `language=${opts.language}`);\n if (opts.isDefault !== undefined)\n args.push('--set', `flag-default=${opts.isDefault ? '1' : '0'}`);\n if (opts.isForced !== undefined)\n args.push('--set', `flag-forced=${opts.isForced ? '1' : '0'}`);\n return args;\n }\n default:\n return opts satisfies never;\n }\n}\n\n/** Returns mkvpropedit's diagnostic output when it exited 1 (modified with warnings). */\nexport async function editCommand(opts: EditOpts, deps: CommandDeps): Promise<string | undefined> {\n requireInputFile(opts.file, deps.exists);\n if (opts.kind === 'track') {\n if (!hasTrackEdit(opts)) {\n throw new CliError(\n 'Nothing to edit: pass --name, --language, --default/--no-default, or --forced/--no-forced.'\n );\n }\n requireTrack(await probeTracks(deps.runner, opts.file), opts.track);\n }\n const result = await deps.runner.run(buildEditArgs(opts));\n if (result.exitCode >= 2) {\n throw new CliError(\n `mkvpropedit failed (exit ${result.exitCode}):\\n${result.stderr || result.stdout}`\n );\n }\n if (result.exitCode === 1) {\n const output = [result.stdout, result.stderr]\n .map((stream) => stream.trim())\n .filter(Boolean)\n .join('\\n');\n return `mkvpropedit finished with warnings — check the file in a player:\\n${output}`;\n }\n return undefined;\n}\n","import type { CommandDeps } from '../types.ts';\n\nimport { defaultExtractOutput, ensureWritable, requireInputFile } from '../output.ts';\nimport { toolPath } from '../paths.ts';\nimport { probeTracks, requireAudioTrack } from '../probe.ts';\nimport { CliError } from '../types.ts';\n\nexport function buildExtractArgs(opts: { file: string; track: number; output: string }): string[] {\n return [\n 'mkvmerge',\n '-o',\n toolPath(opts.output),\n '--audio-tracks',\n String(opts.track),\n '--no-video',\n '--no-subtitles',\n '--no-buttons',\n '--no-chapters',\n '--no-attachments',\n '--no-global-tags',\n toolPath(opts.file)\n ];\n}\n\nexport async function extractCommand(\n opts: { file: string; track: number; output?: string; force: boolean },\n deps: CommandDeps\n): Promise<string> {\n requireInputFile(opts.file, deps.exists);\n const tracks = await probeTracks(deps.runner, opts.file);\n requireAudioTrack(tracks, opts.track);\n const output = opts.output ?? defaultExtractOutput(opts.file, opts.track);\n await ensureWritable(output, {\n force: opts.force,\n isTTY: deps.isTTY,\n confirm: deps.confirm,\n exists: deps.exists,\n inputs: [opts.file]\n });\n const result = await deps.runner.run(\n buildExtractArgs({ file: opts.file, track: opts.track, output }),\n deps.stderrIsTTY ? { stream: 'stdout' } : undefined\n );\n if (result.exitCode >= 2) {\n throw new CliError(\n `mkvmerge failed (exit ${result.exitCode}):\\n${result.stderr || result.stdout}`\n );\n }\n return output;\n}\n","import type { CommandDeps, Track } from '../types.ts';\n\nimport { requireInputFile } from '../output.ts';\nimport { probeFile } from '../probe.ts';\n\nexport function formatTable(header: readonly string[], rows: readonly string[][]): string {\n const all = [header, ...rows];\n const widths = header.map((_, column) => Math.max(...all.map((row) => row[column]!.length)));\n return all\n .map((row) =>\n row\n .map((cell, column) => cell.padEnd(widths[column]!))\n .join(' ')\n .trimEnd()\n )\n .join('\\n');\n}\n\nexport function formatTrackTable(tracks: Track[]): string {\n return formatTable(\n ['ID', 'TYPE', 'CODEC', 'LANG', 'CH', 'DEFAULT', 'FORCED', 'NAME'],\n tracks.map((track) => [\n String(track.id),\n track.type,\n track.codec,\n track.language ?? '-',\n track.channels !== undefined ? String(track.channels) : '-',\n track.isDefault ? 'yes' : '-',\n track.isForced ? 'yes' : '-',\n track.name ?? '-'\n ])\n );\n}\n\nexport async function inspectCommand(file: string, deps: CommandDeps): Promise<string> {\n requireInputFile(file, deps.exists);\n const { title, tracks } = await probeFile(deps.runner, file);\n const table = formatTrackTable(tracks);\n return title === undefined ? table : `Title: ${title}\\n\\n${table}`;\n}\n","import { existsSync } from 'node:fs';\n\nimport type { CommandDeps, Track } from '../types.ts';\n\nimport { findAliasedInput, requireInputFile } from '../output.ts';\nimport { toolPath } from '../paths.ts';\nimport { probeTracks, requireAudioTrack } from '../probe.ts';\nimport { CliError } from '../types.ts';\nimport { formatTable } from './inspect.ts';\n\n/** One `--audio` block from the CLI: a source file plus its per-track options. */\nexport interface MuxTrackRequest {\n audio: string;\n track?: number;\n delayMs: number;\n language?: string;\n name?: string;\n makeDefault: boolean;\n}\n\nexport interface MuxOptions {\n video: string;\n tracks: readonly MuxTrackRequest[];\n output: string;\n force: boolean;\n dryRun: boolean;\n}\n\n/** A request whose track id has been resolved against the probed source. */\nexport interface ResolvedMuxTrack {\n audio: string;\n trackId: number;\n delayMs: number;\n language?: string;\n trackName?: string;\n makeDefault: boolean;\n}\n\nexport type MuxResult = { kind: 'dry-run'; plan: string } | { kind: 'written'; output: string };\n\n/** Labels one `--audio` group by position and path, e.g. `--audio #2 (\"donor.mka\")`. */\nexport function describeAudioGroup(index: number, audio: string): string {\n return `--audio #${index + 1} (\"${audio}\")`;\n}\n\nexport function resolveAudioTrackId(tracks: Track[], requested: number | undefined): number {\n if (requested !== undefined) return requireAudioTrack(tracks, requested).id;\n const audio = tracks.filter((track) => track.type === 'audio');\n if (audio.length === 1) return audio[0]!.id;\n if (audio.length === 0) throw new CliError('The audio input contains no audio tracks.');\n throw new CliError(\n `The audio input contains ${audio.length} audio tracks; pass --track <id> (see: tracksmith inspect <file>).`\n );\n}\n\nexport function buildMuxArgs(a: {\n video: string;\n videoTrackIds: readonly number[];\n clearDefaultAudioIds: readonly number[];\n groups: readonly ResolvedMuxTrack[];\n output: string;\n}): string[] {\n const trackOrder = [\n ...a.videoTrackIds.map((trackId) => `0:${trackId}`),\n ...a.groups.map((group, index) => `${index + 1}:${group.trackId}`)\n ].join(',');\n const args = [\n 'mkvmerge',\n '--command-line-charset',\n 'UTF-8',\n '-o',\n toolPath(a.output),\n '--track-order',\n trackOrder\n ];\n for (const id of a.clearDefaultAudioIds) args.push('--default-track-flag', `${id}:no`);\n args.push(toolPath(a.video));\n for (const group of a.groups) {\n const id = String(group.trackId);\n args.push(\n '--audio-tracks',\n id,\n '--no-video',\n '--no-subtitles',\n '--no-buttons',\n '--no-chapters',\n '--no-attachments',\n '--no-global-tags'\n );\n if (group.delayMs !== 0) args.push('--sync', `${id}:${group.delayMs}`);\n if (group.language !== undefined) args.push('--language', `${id}:${group.language}`);\n if (group.trackName !== undefined) args.push('--track-name', `${id}:${group.trackName}`);\n args.push('--default-track-flag', `${id}:${group.makeDefault ? 'yes' : 'no'}`);\n args.push(toolPath(group.audio));\n }\n return args;\n}\n\nfunction buildPlan(a: {\n video: string;\n videoTracks: readonly Track[];\n groups: readonly ResolvedMuxTrack[];\n sourceTracks: ReadonlyMap<string, Track[]>;\n clearDefaultAudioIds: readonly number[];\n output: string;\n}): string {\n const cleared = new Set(a.clearDefaultAudioIds);\n const existingRows = a.videoTracks.map((track) => [\n String(track.id),\n track.type,\n track.codec,\n track.language ?? '-',\n track.channels !== undefined ? String(track.channels) : '-',\n track.isDefault && !cleared.has(track.id) ? 'yes' : '-',\n '-',\n track.name ?? '-',\n a.video\n ]);\n const newRows = a.groups.map((group, index) => {\n const donor = a.sourceTracks.get(group.audio)?.find((track) => track.id === group.trackId);\n return [\n String(a.videoTracks.length + index),\n 'audio',\n donor?.codec ?? '-',\n group.language ?? donor?.language ?? '-',\n donor?.channels !== undefined ? String(donor.channels) : '-',\n group.makeDefault ? 'yes' : '-',\n group.delayMs !== 0 ? `${group.delayMs}ms` : '-',\n group.trackName ?? donor?.name ?? '-',\n group.audio\n ];\n });\n const table = formatTable(\n ['ID', 'TYPE', 'CODEC', 'LANG', 'CH', 'DEFAULT', 'DELAY', 'NAME', 'SOURCE'],\n [...existingRows, ...newRows]\n );\n return `Planned tracks for \"${a.output}\":\\n${table}`;\n}\n\nexport async function muxCommand(opts: MuxOptions, deps: CommandDeps): Promise<MuxResult> {\n requireInputFile(opts.video, deps.exists);\n const uniqueAudio = [...new Set(opts.tracks.map((track) => track.audio))];\n for (const audio of uniqueAudio) requireInputFile(audio, deps.exists);\n\n const videoTracks = await probeTracks(deps.runner, opts.video);\n const sourceTracks = new Map(\n await Promise.all(\n uniqueAudio.map(async (audio): Promise<[string, Track[]]> => [\n audio,\n await probeTracks(deps.runner, audio)\n ])\n )\n );\n\n const groups: ResolvedMuxTrack[] = opts.tracks.map((track, index) => {\n let trackId: number;\n try {\n trackId = resolveAudioTrackId(sourceTracks.get(track.audio)!, track.track);\n } catch (error) {\n if (!(error instanceof CliError)) throw error;\n throw new CliError(`${describeAudioGroup(index, track.audio)}: ${error.message}`);\n }\n return {\n audio: track.audio,\n trackId,\n delayMs: track.delayMs,\n language: track.language,\n trackName: track.name,\n makeDefault: track.makeDefault\n };\n });\n if (groups.filter((group) => group.makeDefault).length > 1) {\n throw new CliError('--default can be set on at most one --audio input.');\n }\n\n const aliasedInput = findAliasedInput(opts.output, [opts.video, ...uniqueAudio]);\n if (aliasedInput !== undefined) {\n throw new CliError(\n `Output \"${opts.output}\" is the same file as input \"${aliasedInput}\". Choose a different output path.`\n );\n }\n\n const clearDefaultAudioIds = groups.some((group) => group.makeDefault)\n ? videoTracks\n .filter((track) => track.type === 'audio' && track.isDefault)\n .map((track) => track.id)\n : [];\n const plan = buildPlan({\n video: opts.video,\n videoTracks,\n groups,\n sourceTracks,\n clearDefaultAudioIds,\n output: opts.output\n });\n if (opts.dryRun) return { kind: 'dry-run', plan };\n\n const exists = deps.exists ?? existsSync;\n const outputExists = exists(opts.output);\n if (!opts.force) {\n if (deps.isTTY) {\n const overwrite = outputExists\n ? `\\nOutput \"${opts.output}\" already exists and will be overwritten.`\n : '';\n const approved = await deps.confirm(`${plan}${overwrite}\\nWrite \"${opts.output}\"?`);\n if (!approved) throw new CliError('Aborted: nothing written.');\n } else if (outputExists) {\n throw new CliError(\n `Output \"${opts.output}\" already exists. Pass --force to overwrite (cannot prompt in a non-interactive session).`\n );\n }\n }\n\n const result = await deps.runner.run(\n buildMuxArgs({\n video: opts.video,\n videoTrackIds: videoTracks.map((track) => track.id),\n clearDefaultAudioIds,\n groups,\n output: opts.output\n }),\n deps.stderrIsTTY ? { stream: 'stdout' } : undefined\n );\n if (result.exitCode >= 2) {\n throw new CliError(\n `mkvmerge failed (exit ${result.exitCode}):\\n${result.stderr || result.stdout}`\n );\n }\n return { kind: 'written', output: opts.output };\n}\n","import type { CommandDeps } from '../types.ts';\n\nimport { defaultTestClipOutput, ensureWritable, requireInputFile } from '../output.ts';\nimport { toolPath } from '../paths.ts';\nimport { audioRelativeIndex, probeTracks, requireAudioTrack } from '../probe.ts';\nimport { CliError } from '../types.ts';\n\nexport function parseTimeToSeconds(value: string): number {\n if (/^\\d+(\\.\\d+)?$/.test(value)) return Number(value);\n const match = /^(\\d+):([0-5]?\\d):([0-5]?\\d(?:\\.\\d+)?)$/.exec(value);\n if (!match)\n throw new CliError(`Invalid time \"${value}\". Use seconds (90 or 90.5) or HH:MM:SS[.ms].`);\n return Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3]);\n}\n\nexport function buildTestClipArgs(a: {\n video: string;\n audio: string;\n audioStreamIndex: number;\n startSeconds: number;\n durationSeconds: number;\n delayMs: number;\n output: string;\n}): string[] {\n // -y is safe: our own overwrite policy (ensureWritable) has already run.\n return [\n 'ffmpeg',\n '-hide_banner',\n '-nostdin',\n '-ss',\n String(a.startSeconds),\n '-i',\n toolPath(a.video),\n '-itsoffset',\n String(a.delayMs / 1000),\n '-ss',\n String(a.startSeconds),\n '-i',\n toolPath(a.audio),\n '-map',\n '0:v:0',\n '-map',\n `1:a:${a.audioStreamIndex}`,\n '-t',\n String(a.durationSeconds),\n '-c:v',\n 'libx264',\n '-preset',\n 'veryfast',\n '-crf',\n '18',\n '-c:a',\n 'copy',\n '-sn',\n '-abort_on',\n 'empty_output_stream',\n '-y',\n toolPath(a.output)\n ];\n}\n\nexport async function testClipCommand(\n opts: {\n video: string;\n audio: string;\n track: number;\n start: string;\n duration: string;\n delayMs: number;\n output?: string;\n force: boolean;\n },\n deps: CommandDeps\n): Promise<string> {\n requireInputFile(opts.video, deps.exists);\n requireInputFile(opts.audio, deps.exists);\n const startSeconds = parseTimeToSeconds(opts.start);\n const durationSeconds = parseTimeToSeconds(opts.duration);\n if (durationSeconds <= 0) throw new CliError('--duration must be greater than zero.');\n const donorTracks = await probeTracks(deps.runner, opts.audio);\n requireAudioTrack(donorTracks, opts.track);\n const output = opts.output ?? defaultTestClipOutput(opts.video);\n await ensureWritable(output, {\n force: opts.force,\n isTTY: deps.isTTY,\n confirm: deps.confirm,\n exists: deps.exists,\n inputs: [opts.video, opts.audio]\n });\n const result = await deps.runner.run(\n buildTestClipArgs({\n video: opts.video,\n audio: opts.audio,\n audioStreamIndex: audioRelativeIndex(donorTracks, opts.track),\n startSeconds,\n durationSeconds,\n delayMs: opts.delayMs,\n output\n }),\n deps.stderrIsTTY ? { stream: 'stderr' } : undefined\n );\n if (result.exitCode !== 0) {\n throw new CliError(\n `ffmpeg failed (exit ${result.exitCode}):\\n${result.stderr || result.stdout}`\n );\n }\n return output;\n}\n","import { parseArgs } from 'node:util';\n\nimport type { CommandDeps } from './types.ts';\n\nimport { editCommand } from './commands/edit.ts';\nimport { extractCommand } from './commands/extract.ts';\nimport { inspectCommand } from './commands/inspect.ts';\nimport { describeAudioGroup, muxCommand } from './commands/mux.ts';\nimport { testClipCommand } from './commands/test-clip.ts';\nimport { CliError } from './types.ts';\n\nconst USAGE = `tracksmith — inspect, edit, extract, sync-test, and mux Matroska audio tracks\n\nUsage:\n tracksmith inspect <file>\n tracksmith extract <file> --track <id> [--output <file>] [--force]\n tracksmith edit <file> --track <id> [--name <text>] [--language <lang>]\n [--default | --no-default] [--forced | --no-forced]\n tracksmith edit <file> --title <text>\n tracksmith test-clip --video <target> --audio <donor> --track <id> --start <time>\n [--duration 60] [--delay-ms 0] [--output <file>] [--force]\n tracksmith mux --video <target> --output <file> [--dry-run] [--force]\n --audio <mka-or-mkv> [--track <id>] [--delay-ms 0] [--language eng] [--name <title>] [--default]\n [--audio <next> ...]\n\nNotes:\n Repeat --audio to add several tracks; per-track flags apply to the most recent --audio.\n --default may be set on at most one --audio; it also clears the target's existing default audio flags.\n --dry-run prints the planned track layout without writing.\n --track is always the MKVToolNix track ID shown by \"tracksmith inspect\".\n edit changes metadata in place without remuxing. --name \"\" and --title \"\" clear the value\n (in Windows PowerShell use --name= and --title=; empty \"\" arguments get dropped).\n --title sets the file title and cannot be combined with --track edits.\n Positive --delay-ms delays audio; negative advances it (e.g. --delay-ms -250).\n Times accept seconds (90, 90.5) or HH:MM:SS[.ms].\n Check sync near the beginning, middle, and end; a changing offset cannot be fixed by one delay.\n Requires ffmpeg and MKVToolNix (mkvmerge, mkvpropedit) on PATH.`;\n\nfunction parseIntStrict(value: string | undefined, flag: string): number {\n if (value === undefined) throw new CliError(`${flag} is required.`);\n if (!/^-?\\d+$/.test(value)) throw new CliError(`${flag} must be an integer, got \"${value}\".`);\n return Number(value);\n}\n\nfunction parseTrack(value: string | undefined): number {\n const track = parseIntStrict(value, '--track');\n if (track < 0) throw new CliError(`--track must be >= 0, got ${track}.`);\n return track;\n}\n\nfunction requireFilePositional(\n positionals: string[],\n command: 'inspect' | 'extract' | 'edit'\n): string {\n if (positionals.length === 0) throw new CliError(`${command} requires a file argument.`);\n if (positionals.length > 1) throw new CliError(`${command} accepts exactly one file argument.`);\n return positionals[0]!;\n}\n\n// Wraps a parseArgs call so parse failures become CliErrors. Takes a thunk\n// (rather than the options object) so parseArgs keeps its precise generic\n// inference over each command's inline options literal.\nfunction parseOrCliError<T>(parseFn: () => T): T {\n try {\n return parseFn();\n } catch (error) {\n throw new CliError(error instanceof Error ? error.message : String(error));\n }\n}\n\n// parseArgs treats a leading-dash value token (\"-250\") as an option, so fold\n// `--delay-ms -250` into the equals form it does accept.\nfunction normalizeDelayMs(args: string[]): string[] {\n const out: string[] = [];\n for (let i = 0; i < args.length; i += 1) {\n const arg = args[i]!;\n const next = args[i + 1];\n if (arg === '--delay-ms' && next !== undefined && /^-\\d+$/.test(next)) {\n out.push(`--delay-ms=${next}`);\n i += 1;\n } else {\n out.push(arg);\n }\n }\n return out;\n}\n\nexport async function runCli(\n argv: string[],\n deps: CommandDeps,\n stdout: (line: string) => void = console.log\n): Promise<number> {\n const [command, ...rawRest] = argv;\n const rest = normalizeDelayMs(rawRest);\n try {\n switch (command) {\n case undefined: {\n stdout(USAGE);\n return 1;\n }\n case '--help':\n case '-h': {\n stdout(USAGE);\n return 0;\n }\n case 'inspect': {\n const { positionals } = parseOrCliError(() =>\n parseArgs({ args: rest, options: {}, allowPositionals: true, strict: true })\n );\n const file = requireFilePositional(positionals, 'inspect');\n stdout(await inspectCommand(file, deps));\n return 0;\n }\n case 'extract': {\n const { values, positionals } = parseOrCliError(() =>\n parseArgs({\n args: rest,\n options: {\n track: { type: 'string' },\n output: { type: 'string' },\n force: { type: 'boolean', default: false }\n },\n allowPositionals: true,\n strict: true\n })\n );\n const file = requireFilePositional(positionals, 'extract');\n const output = await extractCommand(\n {\n file,\n track: parseTrack(values.track),\n output: values.output,\n force: values.force ?? false\n },\n deps\n );\n stdout(`Wrote ${output}`);\n return 0;\n }\n case 'test-clip': {\n const { values } = parseOrCliError(() =>\n parseArgs({\n args: rest,\n options: {\n video: { type: 'string' },\n audio: { type: 'string' },\n track: { type: 'string' },\n start: { type: 'string' },\n duration: { type: 'string', default: '60' },\n 'delay-ms': { type: 'string', default: '0' },\n output: { type: 'string' },\n force: { type: 'boolean', default: false }\n },\n strict: true\n })\n );\n if (!values.video || !values.audio)\n throw new CliError('test-clip requires --video and --audio.');\n if (!values.start) throw new CliError('test-clip requires --start.');\n const output = await testClipCommand(\n {\n video: values.video,\n audio: values.audio,\n track: parseTrack(values.track),\n start: values.start,\n duration: values.duration ?? '60',\n delayMs: parseIntStrict(values['delay-ms'] ?? '0', '--delay-ms'),\n output: values.output,\n force: values.force ?? false\n },\n deps\n );\n stdout(`Wrote ${output}`);\n return 0;\n }\n case 'mux': {\n const { values, tokens } = parseOrCliError(() =>\n parseArgs({\n args: rest,\n options: {\n video: { type: 'string' },\n output: { type: 'string' },\n force: { type: 'boolean', default: false },\n 'dry-run': { type: 'boolean', default: false },\n audio: { type: 'string', multiple: true },\n track: { type: 'string', multiple: true },\n 'delay-ms': { type: 'string', multiple: true },\n language: { type: 'string', multiple: true },\n name: { type: 'string', multiple: true },\n default: { type: 'boolean', multiple: true }\n },\n strict: true,\n tokens: true\n })\n );\n const groups: {\n audio: string;\n track?: string;\n delayMs?: string;\n language?: string;\n name?: string;\n makeDefault?: true;\n }[] = [];\n for (const token of tokens) {\n if (token.kind !== 'option') continue;\n if (token.name === 'audio') {\n groups.push({ audio: token.value });\n continue;\n }\n if (\n token.name !== 'track' &&\n token.name !== 'delay-ms' &&\n token.name !== 'language' &&\n token.name !== 'name' &&\n token.name !== 'default'\n ) {\n continue;\n }\n const group = groups.at(-1);\n if (group === undefined) {\n throw new CliError(`--${token.name} must come after an --audio input.`);\n }\n switch (token.name) {\n case 'track':\n if (group.track !== undefined)\n throw new CliError(\n `Duplicate --track for ${describeAudioGroup(groups.length - 1, group.audio)}.`\n );\n group.track = token.value;\n break;\n case 'delay-ms':\n if (group.delayMs !== undefined)\n throw new CliError(\n `Duplicate --delay-ms for ${describeAudioGroup(groups.length - 1, group.audio)}.`\n );\n group.delayMs = token.value;\n break;\n case 'language':\n if (group.language !== undefined)\n throw new CliError(\n `Duplicate --language for ${describeAudioGroup(groups.length - 1, group.audio)}.`\n );\n group.language = token.value;\n break;\n case 'name':\n if (group.name !== undefined)\n throw new CliError(\n `Duplicate --name for ${describeAudioGroup(groups.length - 1, group.audio)}.`\n );\n group.name = token.value;\n break;\n case 'default':\n if (group.makeDefault !== undefined)\n throw new CliError(\n `Duplicate --default for ${describeAudioGroup(groups.length - 1, group.audio)}.`\n );\n group.makeDefault = true;\n break;\n }\n }\n if (!values.video || groups.length === 0)\n throw new CliError('mux requires --video and --audio.');\n if (!values.output)\n throw new CliError('mux requires --output (no default output name for the final file).');\n const result = await muxCommand(\n {\n video: values.video,\n tracks: groups.map((group) => ({\n audio: group.audio,\n track: group.track === undefined ? undefined : parseTrack(group.track),\n delayMs: parseIntStrict(group.delayMs ?? '0', '--delay-ms'),\n language: group.language ?? 'eng',\n name: group.name,\n makeDefault: group.makeDefault ?? false\n })),\n output: values.output,\n force: values.force ?? false,\n dryRun: values['dry-run'] ?? false\n },\n deps\n );\n if (result.kind === 'dry-run') stdout(result.plan);\n else stdout(`Wrote ${result.output}`);\n return 0;\n }\n case 'edit': {\n const { values, positionals } = parseOrCliError(() =>\n parseArgs({\n args: rest,\n options: {\n track: { type: 'string' },\n name: { type: 'string' },\n language: { type: 'string' },\n default: { type: 'boolean' },\n 'no-default': { type: 'boolean' },\n forced: { type: 'boolean' },\n 'no-forced': { type: 'boolean' },\n title: { type: 'string' }\n },\n allowPositionals: true,\n strict: true\n })\n );\n const file = requireFilePositional(positionals, 'edit');\n const hasTrackFlags =\n values.track !== undefined ||\n values.name !== undefined ||\n values.language !== undefined ||\n values.default !== undefined ||\n values['no-default'] !== undefined ||\n values.forced !== undefined ||\n values['no-forced'] !== undefined;\n if (values.title !== undefined && hasTrackFlags)\n throw new CliError(\n '--title cannot be combined with --track edits. Run them as separate invocations.'\n );\n if (values.default !== undefined && values['no-default'] !== undefined)\n throw new CliError('Pass either --default or --no-default, not both.');\n if (values.forced !== undefined && values['no-forced'] !== undefined)\n throw new CliError('Pass either --forced or --no-forced, not both.');\n if (values.language === '')\n throw new CliError('--language cannot be empty. Use --language und for undetermined.');\n let warning: string | undefined;\n if (values.title !== undefined) {\n warning = await editCommand({ kind: 'title', file, title: values.title }, deps);\n } else {\n if (values.track === undefined)\n throw new CliError('edit requires --track (or --title for the file title).');\n warning = await editCommand(\n {\n kind: 'track',\n file,\n track: parseTrack(values.track),\n name: values.name,\n language: values.language,\n isDefault: values.default ?? (values['no-default'] !== undefined ? false : undefined),\n isForced: values.forced ?? (values['no-forced'] !== undefined ? false : undefined)\n },\n deps\n );\n }\n if (warning !== undefined) console.error(`tracksmith: ${warning}`);\n stdout(`Edited ${file}`);\n return 0;\n }\n default:\n throw new CliError(`Unknown command \"${command}\".\\n\\n${USAGE}`);\n }\n } catch (error) {\n if (error instanceof CliError) {\n console.error(`tracksmith: ${error.message}`);\n return error.exitCode;\n }\n throw error;\n }\n}\n","import { once } from 'node:events';\nimport { createInterface } from 'node:readline/promises';\n\nimport type { Readable, Writable } from 'node:stream';\n\nexport interface ConfirmPromptOptions {\n input?: Readable;\n output?: Writable;\n}\n\nexport function createConfirmPrompt(\n options: ConfirmPromptOptions = {}\n): (message: string) => Promise<boolean> {\n const input = options.input ?? process.stdin;\n const output = options.output ?? process.stdout;\n\n return async (message: string): Promise<boolean> => {\n const readline = createInterface({ input, output });\n\n try {\n const closed = once(readline, 'close').then(() => ({ kind: 'eof' }) as const);\n const answered = readline.question(`${message} [y/N] `).then(\n (value) => ({ kind: 'answer', value }) as const,\n () => ({ kind: 'eof' }) as const\n );\n const result = await Promise.race([answered, closed]);\n\n return result.kind === 'answer' && /^y(?:es)?$/i.test(result.value.trim());\n } finally {\n readline.close();\n }\n };\n}\n","export const TOOLS = ['ffmpeg', 'mkvmerge', 'mkvpropedit'] as const;\n\nexport type ToolName = (typeof TOOLS)[number];\n\nexport function isToolName(value: string): value is ToolName {\n return TOOLS.some((tool) => tool === value);\n}\n\nconst MKVTOOLNIX_HINT =\n 'Windows: winget install MoritzBunkus.MKVToolNix or scoop install mkvtoolnix | macOS: brew install mkvtoolnix | Linux: sudo apt install mkvtoolnix or sudo pacman -S mkvtoolnix-cli';\n\nconst INSTALL_HINTS = {\n ffmpeg:\n 'Windows: winget install Gyan.FFmpeg or scoop install ffmpeg | macOS: brew install ffmpeg | Linux: sudo apt install ffmpeg or sudo pacman -S ffmpeg',\n mkvmerge: MKVTOOLNIX_HINT,\n mkvpropedit: MKVTOOLNIX_HINT\n} satisfies Record<ToolName, string>;\n\nexport function installHint(tool: ToolName): string {\n return INSTALL_HINTS[tool];\n}\n","import { spawn } from 'node:child_process';\n\nimport type { RunOptions, Runner, RunResult } from './types.ts';\n\nimport { installHint, isToolName } from './tools.ts';\nimport { CliError } from './types.ts';\n\nexport function processStartError(program: string, error: unknown): CliError {\n if (\n typeof error === 'object' &&\n error !== null &&\n 'code' in error &&\n error.code === 'ENOENT' &&\n isToolName(program)\n ) {\n return new CliError(\n `${program} is required but was not found on PATH. ${installHint(program)}`\n );\n }\n\n const message = error instanceof Error ? error.message : String(error);\n return new CliError(`Failed to start ${program}: ${message}`);\n}\n\nconst writeToCliStderr = (chunk: string): void => {\n globalThis.process.stderr.write(chunk);\n};\n\nexport class ProcessRunner implements Runner {\n private readonly sink: (chunk: string) => void;\n\n constructor(options: { sink?: (chunk: string) => void } = {}) {\n this.sink = options.sink ?? writeToCliStderr;\n }\n\n async run(argv: readonly string[], options?: RunOptions): Promise<RunResult> {\n const [executable, ...args] = argv;\n if (executable === undefined) {\n throw new CliError('Could not start process: missing executable');\n }\n\n return await new Promise((resolve, reject) => {\n const process = spawn(executable, args, {\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let stdout = '';\n let stderr = '';\n\n process.stdout.setEncoding('utf8');\n process.stderr.setEncoding('utf8');\n process.stdout.on('data', (chunk: string) => {\n stdout += chunk;\n if (options?.stream === 'stdout') this.sink(chunk);\n });\n process.stderr.on('data', (chunk: string) => {\n stderr += chunk;\n if (options?.stream === 'stderr') this.sink(chunk);\n });\n process.once('error', (error) => {\n reject(processStartError(executable, error));\n });\n // Windows loses the signal: a killed child reports a plain numeric exit\n // code (usually 1) with signal null, so this detection is POSIX-only and\n // Windows cannot distinguish a killed tool from a warning exit.\n process.once('close', (exitCode, signal) => {\n if (signal !== null) {\n const output = [stdout, stderr]\n .map((stream) => stream.trim())\n .filter(Boolean)\n .join('\\n');\n const diagnostics = output === '' ? '' : `\\nOutput before termination:\\n${output}`;\n reject(new CliError(`${executable} was terminated by signal ${signal}.${diagnostics}`));\n return;\n }\n resolve({ exitCode: exitCode ?? 1, stdout, stderr });\n });\n });\n }\n}\n","#!/usr/bin/env node\n\nimport { runCli } from './cli.ts';\nimport { createConfirmPrompt } from './prompt.ts';\nimport { ProcessRunner } from './runner.ts';\n\nprocess.exitCode = await runCli(process.argv.slice(2), {\n runner: new ProcessRunner(),\n isTTY: process.stdin.isTTY ?? false,\n stderrIsTTY: process.stderr.isTTY ?? false,\n confirm: createConfirmPrompt({ output: process.stderr })\n});\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tracksmith",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "CLI for inspecting, extracting, syncing, and muxing Matroska audio tracks.",
5
5
  "type": "module",
6
6
  "bin": {