@crafter/trx 0.1.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.
@@ -0,0 +1,373 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { resolve } from "node:path";
3
+
4
+ const CLI = resolve(import.meta.dir, "../bin/trx.ts");
5
+
6
+ async function run(args: string[]): Promise<{ stdout: string; stderr: string; exitCode: number }> {
7
+ const proc = Bun.spawn(["bun", "run", CLI, ...args], {
8
+ stdout: "pipe",
9
+ stderr: "pipe",
10
+ env: { ...process.env, FORCE_COLOR: "0" },
11
+ });
12
+ const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
13
+ const exitCode = await proc.exited;
14
+ return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode };
15
+ }
16
+
17
+ function parseJSON(output: string): unknown {
18
+ return JSON.parse(output);
19
+ }
20
+
21
+ describe("trx --help", () => {
22
+ test("prints usage and exits 0", async () => {
23
+ const { stdout, exitCode } = await run(["--help"]);
24
+ expect(exitCode).toBe(0);
25
+ expect(stdout).toContain("Agent-first CLI");
26
+ expect(stdout).toContain("transcribe");
27
+ expect(stdout).toContain("doctor");
28
+ expect(stdout).toContain("schema");
29
+ expect(stdout).toContain("init");
30
+ });
31
+
32
+ test("prints version", async () => {
33
+ const { stdout, exitCode } = await run(["--version"]);
34
+ expect(exitCode).toBe(0);
35
+ expect(stdout).toMatch(/^\d+\.\d+\.\d+$/);
36
+ });
37
+ });
38
+
39
+ describe("trx doctor", () => {
40
+ test("returns healthy JSON with all deps", async () => {
41
+ const { stdout, exitCode } = await run(["doctor", "--output", "json"]);
42
+ expect(exitCode).toBe(0);
43
+ const data = parseJSON(stdout) as Record<string, unknown>;
44
+ expect(data).toHaveProperty("healthy");
45
+ expect(data).toHaveProperty("dependencies");
46
+ expect(data).toHaveProperty("config");
47
+
48
+ const deps = data.dependencies as Record<string, Record<string, unknown>>;
49
+ expect(deps).toHaveProperty("whisper-cli");
50
+ expect(deps).toHaveProperty("yt-dlp");
51
+ expect(deps).toHaveProperty("ffmpeg");
52
+
53
+ for (const dep of Object.values(deps)) {
54
+ expect(dep).toHaveProperty("installed");
55
+ expect(dep).toHaveProperty("path");
56
+ }
57
+ });
58
+
59
+ test("config section reports model info", async () => {
60
+ const { stdout } = await run(["doctor", "--output", "json"]);
61
+ const data = parseJSON(stdout) as Record<string, unknown>;
62
+ const config = data.config as Record<string, unknown>;
63
+ expect(config).toHaveProperty("exists");
64
+ expect(config).toHaveProperty("path");
65
+ expect(config).toHaveProperty("modelsDir");
66
+ });
67
+ });
68
+
69
+ describe("trx schema", () => {
70
+ test("transcribe schema returns valid JSON with command info", async () => {
71
+ const { stdout, exitCode } = await run(["schema", "transcribe"]);
72
+ expect(exitCode).toBe(0);
73
+ const data = parseJSON(stdout) as Record<string, unknown>;
74
+ expect(data.command).toBe("transcribe");
75
+ expect(data).toHaveProperty("arguments");
76
+ expect(data).toHaveProperty("flags");
77
+ expect(data).toHaveProperty("output");
78
+ expect(data).toHaveProperty("examples");
79
+
80
+ const flags = data.flags as Record<string, unknown>;
81
+ expect(flags).toHaveProperty("--language");
82
+ expect(flags).toHaveProperty("--model");
83
+ expect(flags).toHaveProperty("--dry-run");
84
+ expect(flags).toHaveProperty("--fields");
85
+ expect(flags).toHaveProperty("--output");
86
+ });
87
+
88
+ test("init schema returns valid JSON with deps info", async () => {
89
+ const { stdout, exitCode } = await run(["schema", "init"]);
90
+ expect(exitCode).toBe(0);
91
+ const data = parseJSON(stdout) as Record<string, unknown>;
92
+ expect(data.command).toBe("init");
93
+ expect(data).toHaveProperty("dependencies");
94
+ expect(data).toHaveProperty("flags");
95
+
96
+ const deps = data.dependencies as Record<string, unknown>;
97
+ expect(deps).toHaveProperty("whisper-cli");
98
+ expect(deps).toHaveProperty("yt-dlp");
99
+ expect(deps).toHaveProperty("ffmpeg");
100
+ });
101
+
102
+ test("unknown schema exits with error", async () => {
103
+ const { stderr, exitCode } = await run(["schema", "nonexistent"]);
104
+ expect(exitCode).toBe(1);
105
+ expect(stderr).toContain("Unknown schema");
106
+ });
107
+ });
108
+
109
+ describe("trx transcribe --dry-run", () => {
110
+ test("validates URL input and shows execution plan", async () => {
111
+ const { stdout, exitCode } = await run([
112
+ "transcribe",
113
+ "https://youtube.com/watch?v=test123",
114
+ "--dry-run",
115
+ "--output",
116
+ "json",
117
+ ]);
118
+ expect(exitCode).toBe(0);
119
+ const data = parseJSON(stdout) as Record<string, unknown>;
120
+ expect(data.dryRun).toBe(true);
121
+ expect(data.inputType).toBe("url");
122
+ expect(data.input).toBe("https://youtube.com/watch?v=test123");
123
+ expect(data).toHaveProperty("language");
124
+ expect(data).toHaveProperty("model");
125
+ expect(data).toHaveProperty("steps");
126
+
127
+ const steps = data.steps as string[];
128
+ expect(steps).toContain("download via yt-dlp");
129
+ expect(steps).toContain("clean audio via ffmpeg");
130
+ expect(steps).toContain("transcribe via whisper-cli");
131
+ });
132
+
133
+ test("validates local file input (nonexistent file fails)", async () => {
134
+ const { stdout, exitCode } = await run([
135
+ "transcribe",
136
+ "/tmp/nonexistent-file.mp4",
137
+ "--dry-run",
138
+ "--output",
139
+ "json",
140
+ ]);
141
+ expect(exitCode).toBe(1);
142
+ const data = parseJSON(stdout) as Record<string, unknown>;
143
+ expect(data.success).toBe(false);
144
+ expect(data.error).toContain("File not found");
145
+ });
146
+
147
+ test("--no-download removes download step", async () => {
148
+ const { stdout, exitCode } = await run([
149
+ "transcribe",
150
+ "https://example.com/video.mp4",
151
+ "--dry-run",
152
+ "--no-download",
153
+ "--output",
154
+ "json",
155
+ ]);
156
+ expect(exitCode).toBe(0);
157
+ const data = parseJSON(stdout) as Record<string, unknown>;
158
+ const steps = data.steps as string[];
159
+ expect(steps).not.toContain("download via yt-dlp");
160
+ expect(steps).toContain("clean audio via ffmpeg");
161
+ });
162
+
163
+ test("--no-clean removes ffmpeg step", async () => {
164
+ const { stdout, exitCode } = await run([
165
+ "transcribe",
166
+ "https://example.com/video.mp4",
167
+ "--dry-run",
168
+ "--no-clean",
169
+ "--output",
170
+ "json",
171
+ ]);
172
+ expect(exitCode).toBe(0);
173
+ const data = parseJSON(stdout) as Record<string, unknown>;
174
+ const steps = data.steps as string[];
175
+ expect(steps).toContain("download via yt-dlp");
176
+ expect(steps).not.toContain("clean audio via ffmpeg");
177
+ });
178
+ });
179
+
180
+ describe("input validation", () => {
181
+ test("rejects path traversal in URL", async () => {
182
+ const { stdout, exitCode } = await run([
183
+ "transcribe",
184
+ "https://evil.com/../../etc/passwd",
185
+ "--dry-run",
186
+ "--output",
187
+ "json",
188
+ ]);
189
+ expect(exitCode).toBe(1);
190
+ const data = parseJSON(stdout) as Record<string, unknown>;
191
+ expect(data.success).toBe(false);
192
+ expect(data.error).toContain("path traversal");
193
+ });
194
+
195
+ test("rejects path traversal in file path", async () => {
196
+ const { stdout, exitCode } = await run([
197
+ "transcribe",
198
+ "../../etc/passwd",
199
+ "--dry-run",
200
+ "--output",
201
+ "json",
202
+ ]);
203
+ expect(exitCode).toBe(1);
204
+ const data = parseJSON(stdout) as Record<string, unknown>;
205
+ expect(data.success).toBe(false);
206
+ expect(data.error).toContain("traversal");
207
+ });
208
+
209
+ test("rejects URL-encoded file paths", async () => {
210
+ const { stdout, exitCode } = await run([
211
+ "transcribe",
212
+ "/tmp/%2e%2e/etc/passwd",
213
+ "--dry-run",
214
+ "--output",
215
+ "json",
216
+ ]);
217
+ expect(exitCode).toBe(1);
218
+ const data = parseJSON(stdout) as Record<string, unknown>;
219
+ expect(data.success).toBe(false);
220
+ expect(data.error).toContain("URL-encoded");
221
+ });
222
+
223
+ test("rejects invalid language code", async () => {
224
+ const { stdout, exitCode } = await run([
225
+ "transcribe",
226
+ "https://example.com/video.mp4",
227
+ "--language",
228
+ "klingon",
229
+ "--dry-run",
230
+ "--output",
231
+ "json",
232
+ ]);
233
+ expect(exitCode).toBe(1);
234
+ const data = parseJSON(stdout) as Record<string, unknown>;
235
+ expect(data.success).toBe(false);
236
+ expect(data.error).toContain("Unsupported language");
237
+ });
238
+
239
+ test("rejects invalid model name", async () => {
240
+ const { stdout, exitCode } = await run([
241
+ "transcribe",
242
+ "https://example.com/video.mp4",
243
+ "--model",
244
+ "gigantic",
245
+ "--dry-run",
246
+ "--output",
247
+ "json",
248
+ ]);
249
+ expect(exitCode).toBe(1);
250
+ const data = parseJSON(stdout) as Record<string, unknown>;
251
+ expect(data.success).toBe(false);
252
+ expect(data.error).toContain("Unknown model");
253
+ });
254
+
255
+ test("accepts valid language codes", async () => {
256
+ for (const lang of ["es", "en", "pt", "fr", "auto"]) {
257
+ const { exitCode } = await run([
258
+ "transcribe",
259
+ "https://example.com/video.mp4",
260
+ "--language",
261
+ lang,
262
+ "--dry-run",
263
+ "--output",
264
+ "json",
265
+ ]);
266
+ expect(exitCode).toBe(0);
267
+ }
268
+ });
269
+
270
+ test("accepts valid model names", async () => {
271
+ for (const model of ["tiny", "base", "small", "medium", "large"]) {
272
+ const { exitCode } = await run([
273
+ "transcribe",
274
+ "https://example.com/video.mp4",
275
+ "--model",
276
+ model,
277
+ "--dry-run",
278
+ "--output",
279
+ "json",
280
+ ]);
281
+ expect(exitCode).toBe(0);
282
+ }
283
+ });
284
+ });
285
+
286
+ describe("trx transcribe (real file)", () => {
287
+ const testWav = resolve(import.meta.dir, "fixtures/silence.wav");
288
+
289
+ test("transcribes a real WAV file", async () => {
290
+ const { existsSync } = await import("node:fs");
291
+ if (!existsSync(testWav)) {
292
+ console.log("Generating test fixture: 2s silence WAV");
293
+ const fixturesDir = resolve(import.meta.dir, "fixtures");
294
+ await Bun.spawn(["mkdir", "-p", fixturesDir]).exited;
295
+ const proc = Bun.spawn([
296
+ "ffmpeg",
297
+ "-f",
298
+ "lavfi",
299
+ "-i",
300
+ "anullsrc=r=16000:cl=mono",
301
+ "-t",
302
+ "2",
303
+ "-c:a",
304
+ "pcm_s16le",
305
+ testWav,
306
+ "-y",
307
+ ]);
308
+ await proc.exited;
309
+ }
310
+
311
+ const { stdout, exitCode } = await run([
312
+ "transcribe",
313
+ testWav,
314
+ "--no-clean",
315
+ "--output",
316
+ "json",
317
+ "--output-dir",
318
+ "/tmp",
319
+ ]);
320
+ expect(exitCode).toBe(0);
321
+ const data = parseJSON(stdout) as Record<string, unknown>;
322
+ expect(data.success).toBe(true);
323
+ expect(data).toHaveProperty("files");
324
+ expect(data).toHaveProperty("metadata");
325
+ expect(data).toHaveProperty("text");
326
+
327
+ const files = data.files as Record<string, string>;
328
+ expect(files).toHaveProperty("srt");
329
+ expect(files).toHaveProperty("txt");
330
+
331
+ const metadata = data.metadata as Record<string, string>;
332
+ expect(metadata).toHaveProperty("model");
333
+ expect(metadata).toHaveProperty("language");
334
+ }, 30000);
335
+
336
+ test("--fields text returns only text", async () => {
337
+ const { existsSync } = await import("node:fs");
338
+ if (!existsSync(testWav)) return;
339
+
340
+ const { stdout, exitCode } = await run([
341
+ "transcribe",
342
+ testWav,
343
+ "--no-clean",
344
+ "--fields",
345
+ "text",
346
+ "--output",
347
+ "json",
348
+ "--output-dir",
349
+ "/tmp",
350
+ ]);
351
+ expect(exitCode).toBe(0);
352
+ const data = parseJSON(stdout) as Record<string, unknown>;
353
+ expect(data.success).toBe(true);
354
+ expect(data).toHaveProperty("text");
355
+ expect(data).not.toHaveProperty("files");
356
+ expect(data).not.toHaveProperty("metadata");
357
+ }, 30000);
358
+ });
359
+
360
+ describe("trx shorthand", () => {
361
+ test("trx <url> delegates to transcribe", async () => {
362
+ const { stdout, exitCode } = await run([
363
+ "https://example.com/video.mp4",
364
+ "--dry-run",
365
+ "--output",
366
+ "json",
367
+ ]);
368
+ expect(exitCode).toBe(0);
369
+ const data = parseJSON(stdout) as Record<string, unknown>;
370
+ expect(data.dryRun).toBe(true);
371
+ expect(data.inputType).toBe("url");
372
+ });
373
+ });
@@ -0,0 +1,2 @@
1
+ 1
2
+ you
Binary file
@@ -0,0 +1,4 @@
1
+ 1
2
+ 00:00:00,000 --> 00:00:02,060
3
+ you
4
+
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": ["ESNext"],
4
+ "target": "ESNext",
5
+ "module": "ESNext",
6
+ "moduleDetection": "force",
7
+ "allowJs": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "verbatimModuleSyntax": true,
11
+ "noEmit": true,
12
+ "strict": true,
13
+ "skipLibCheck": true,
14
+ "noFallthroughCasesInSwitch": true,
15
+ "noUnusedLocals": false,
16
+ "noUnusedParameters": false,
17
+ "noPropertyAccessFromIndexSignature": false
18
+ }
19
+ }