@sushanglewis/lincoln 1.5.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 +193 -0
- package/dist/main.js +1009 -0
- package/dist/main.js.map +1 -0
- package/package.json +48 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,1009 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.tsx
|
|
4
|
+
import { Box as Box8, Text as Text7 } from "ink";
|
|
5
|
+
|
|
6
|
+
// src/config/args.ts
|
|
7
|
+
import { parseArgs as nodeParseArgs } from "util";
|
|
8
|
+
function parseArgs(argv) {
|
|
9
|
+
const { values } = nodeParseArgs({
|
|
10
|
+
args: argv,
|
|
11
|
+
options: {
|
|
12
|
+
topic: { type: "string" },
|
|
13
|
+
"design-id": { type: "string" },
|
|
14
|
+
branch: { type: "string" },
|
|
15
|
+
"session-id": { type: "string" },
|
|
16
|
+
"no-tui": { type: "boolean", default: false },
|
|
17
|
+
help: { type: "boolean", short: "h", default: false }
|
|
18
|
+
},
|
|
19
|
+
strict: false,
|
|
20
|
+
allowPositionals: true
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
topic: values.topic,
|
|
24
|
+
designId: values["design-id"],
|
|
25
|
+
branch: values.branch,
|
|
26
|
+
sessionId: values["session-id"],
|
|
27
|
+
noTui: values["no-tui"],
|
|
28
|
+
help: values.help
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/config/resolveConfig.ts
|
|
33
|
+
import { existsSync } from "fs";
|
|
34
|
+
import { homedir } from "os";
|
|
35
|
+
import { join } from "path";
|
|
36
|
+
|
|
37
|
+
// src/config/loadConfig.ts
|
|
38
|
+
import { readFileSync } from "fs";
|
|
39
|
+
import yaml from "js-yaml";
|
|
40
|
+
function parseConfig(content, path) {
|
|
41
|
+
const trimmed = content.trim();
|
|
42
|
+
if (!trimmed) {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
const isJson = trimmed.startsWith("{") || trimmed.startsWith("[");
|
|
46
|
+
const raw = isJson ? JSON.parse(trimmed) : yaml.load(trimmed);
|
|
47
|
+
if (raw === null || typeof raw !== "object") {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
const data = raw;
|
|
51
|
+
return {
|
|
52
|
+
defaultTopic: asString(data.default_topic),
|
|
53
|
+
defaultDesignId: asString(data.default_design_id),
|
|
54
|
+
defaultBranch: asString(data.default_branch),
|
|
55
|
+
autoProcess: asBoolean(data.auto_process),
|
|
56
|
+
showAudioMeter: asBoolean(data.show_audio_meter),
|
|
57
|
+
audioMeterStyle: asMeterStyle(data.audio_meter_style)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function asString(value) {
|
|
61
|
+
return typeof value === "string" ? value : void 0;
|
|
62
|
+
}
|
|
63
|
+
function asBoolean(value) {
|
|
64
|
+
return typeof value === "boolean" ? value : void 0;
|
|
65
|
+
}
|
|
66
|
+
function asMeterStyle(value) {
|
|
67
|
+
if (value === "bar" || value === "dot" || value === "wave") {
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
return void 0;
|
|
71
|
+
}
|
|
72
|
+
function loadConfig(path) {
|
|
73
|
+
let content;
|
|
74
|
+
try {
|
|
75
|
+
content = readFileSync(path, "utf-8");
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw new Error(`Config file not found: ${String(path)}`);
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
return parseConfig(content, path);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
const cause = error instanceof Error ? error.message : String(error);
|
|
83
|
+
throw new Error(`Failed to parse config ${String(path)}: ${cause}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function loadConfigOptional(path) {
|
|
87
|
+
try {
|
|
88
|
+
return loadConfig(path);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
91
|
+
if (message.includes("Config file not found")) {
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/utils/sessionId.ts
|
|
99
|
+
function generateSessionId(options = {}) {
|
|
100
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
101
|
+
const date = now.toISOString().slice(0, 10);
|
|
102
|
+
const slug = normalizeDescriptor(options.descriptor);
|
|
103
|
+
return `${date}-${slug}`;
|
|
104
|
+
}
|
|
105
|
+
function normalizeDescriptor(descriptor) {
|
|
106
|
+
if (!descriptor) {
|
|
107
|
+
return "recording";
|
|
108
|
+
}
|
|
109
|
+
const normalized = descriptor.trim().toLowerCase().replace(/[^a-z0-9一-龥]+/g, "-").replace(/^-+|-+$/g, "");
|
|
110
|
+
return normalized.slice(0, 80) || "recording";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/config/resolveConfig.ts
|
|
114
|
+
function pickFirst(...values) {
|
|
115
|
+
for (const value of values) {
|
|
116
|
+
if (value !== void 0 && value !== "") {
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return "";
|
|
121
|
+
}
|
|
122
|
+
function uniqueSessionId(workspaceRoot, now, descriptor) {
|
|
123
|
+
const baseSessionId = generateSessionId({ now, descriptor });
|
|
124
|
+
const recordingPath = join(workspaceRoot, "recordings", `${baseSessionId}.m4a`);
|
|
125
|
+
if (!existsSync(recordingPath)) {
|
|
126
|
+
return baseSessionId;
|
|
127
|
+
}
|
|
128
|
+
for (let counter = 1; counter <= 999; counter += 1) {
|
|
129
|
+
const candidate = `${baseSessionId}-${String(counter).padStart(3, "0")}`;
|
|
130
|
+
const candidatePath = join(workspaceRoot, "recordings", `${candidate}.m4a`);
|
|
131
|
+
if (!existsSync(candidatePath)) {
|
|
132
|
+
return candidate;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return baseSessionId;
|
|
136
|
+
}
|
|
137
|
+
function resolveConfig(args, options = {}) {
|
|
138
|
+
const cwd = options.cwd ?? process.cwd();
|
|
139
|
+
const home = options.home ?? homedir();
|
|
140
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
141
|
+
const globalConfig = loadConfigOptional(join(home, ".lincolnrc"));
|
|
142
|
+
const localConfig = loadConfigOptional(join(cwd, ".lincolnrc"));
|
|
143
|
+
const topic = pickFirst(args.topic, localConfig.defaultTopic, globalConfig.defaultTopic);
|
|
144
|
+
const designId = pickFirst(args.designId, localConfig.defaultDesignId, globalConfig.defaultDesignId);
|
|
145
|
+
const branch = pickFirst(args.branch, localConfig.defaultBranch, globalConfig.defaultBranch);
|
|
146
|
+
const descriptor = designId || topic || void 0;
|
|
147
|
+
const sessionId = args.sessionId || uniqueSessionId(cwd, now, descriptor);
|
|
148
|
+
return {
|
|
149
|
+
workspaceRoot: cwd,
|
|
150
|
+
sessionId,
|
|
151
|
+
topic,
|
|
152
|
+
designId,
|
|
153
|
+
branch,
|
|
154
|
+
noTui: args.noTui ?? false,
|
|
155
|
+
autoProcess: localConfig.autoProcess ?? globalConfig.autoProcess ?? false,
|
|
156
|
+
showAudioMeter: localConfig.showAudioMeter ?? globalConfig.showAudioMeter ?? true,
|
|
157
|
+
audioMeterStyle: localConfig.audioMeterStyle ?? globalConfig.audioMeterStyle ?? "bar"
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/components/RecordingApp.tsx
|
|
162
|
+
import { Box as Box7, useApp } from "ink";
|
|
163
|
+
import { useCallback as useCallback2, useEffect as useEffect4, useMemo, useState as useState5 } from "react";
|
|
164
|
+
|
|
165
|
+
// src/components/StatusPanel.tsx
|
|
166
|
+
import { Box, Text } from "ink";
|
|
167
|
+
|
|
168
|
+
// src/utils/formatDuration.ts
|
|
169
|
+
function formatDuration(totalSeconds) {
|
|
170
|
+
const safeSeconds = Math.max(0, Math.floor(totalSeconds));
|
|
171
|
+
const minutes = Math.floor(safeSeconds / 60);
|
|
172
|
+
const seconds = safeSeconds % 60;
|
|
173
|
+
return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/components/StatusPanel.tsx
|
|
177
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
178
|
+
function StatusPanel({
|
|
179
|
+
sessionId,
|
|
180
|
+
topic,
|
|
181
|
+
designId,
|
|
182
|
+
branch,
|
|
183
|
+
status,
|
|
184
|
+
duration
|
|
185
|
+
}) {
|
|
186
|
+
const statusLabel = {
|
|
187
|
+
idle: "Ready",
|
|
188
|
+
recording: `Recording ${formatDuration(duration)}`,
|
|
189
|
+
stopped: "Stopped",
|
|
190
|
+
cancelled: "Cancelled",
|
|
191
|
+
error: "Error"
|
|
192
|
+
}[status];
|
|
193
|
+
const statusColor = status === "recording" ? "red" : status === "error" ? "red" : status === "cancelled" ? "yellow" : "green";
|
|
194
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, paddingTop: 1, children: [
|
|
195
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "row", justifyContent: "space-between", alignItems: "center", children: [
|
|
196
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "white", children: "Lincoln Recorder" }),
|
|
197
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
|
|
198
|
+
/* @__PURE__ */ jsx(Text, { color: statusColor, children: status === "recording" ? "\u25CF" : "\u25CB" }),
|
|
199
|
+
/* @__PURE__ */ jsx(Text, { children: statusLabel })
|
|
200
|
+
] })
|
|
201
|
+
] }),
|
|
202
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingY: 1, children: [
|
|
203
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
204
|
+
"Session: ",
|
|
205
|
+
sessionId
|
|
206
|
+
] }),
|
|
207
|
+
topic ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
208
|
+
"Topic: ",
|
|
209
|
+
topic
|
|
210
|
+
] }) : null,
|
|
211
|
+
designId ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
212
|
+
"Design: ",
|
|
213
|
+
designId
|
|
214
|
+
] }) : null,
|
|
215
|
+
branch ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
216
|
+
"Branch: ",
|
|
217
|
+
branch
|
|
218
|
+
] }) : null
|
|
219
|
+
] })
|
|
220
|
+
] });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/components/LogView.tsx
|
|
224
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
225
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
226
|
+
var TYPE_COLORS = {
|
|
227
|
+
info: void 0,
|
|
228
|
+
success: "green",
|
|
229
|
+
error: "red",
|
|
230
|
+
command: "cyan"
|
|
231
|
+
};
|
|
232
|
+
function LogView({ logs }) {
|
|
233
|
+
const visible = logs.slice(-8);
|
|
234
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [
|
|
235
|
+
/* @__PURE__ */ jsx2(Text2, { underline: true, dimColor: true, children: "Log" }),
|
|
236
|
+
visible.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No events yet." }) : visible.map((log) => /* @__PURE__ */ jsx2(Text2, { color: TYPE_COLORS[log.type ?? "info"], children: log.message }, log.id))
|
|
237
|
+
] });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/components/HintBar.tsx
|
|
241
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
242
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
243
|
+
var HINTS = {
|
|
244
|
+
idle: "[r] record [d] devices [m] model [q] quit",
|
|
245
|
+
recording: "[s] stop [c] cancel",
|
|
246
|
+
stopped: "[any key] exit",
|
|
247
|
+
cancelled: "[q] exit",
|
|
248
|
+
error: "[q] exit",
|
|
249
|
+
menu: "[\u2191\u2193] select [enter] confirm [esc] back"
|
|
250
|
+
};
|
|
251
|
+
function HintBar({ mode }) {
|
|
252
|
+
return /* @__PURE__ */ jsx3(Box3, { paddingX: 1, paddingY: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: HINTS[mode] }) });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/components/DevicesMenu.tsx
|
|
256
|
+
import { Box as Box5, Text as Text5, useInput as useInput2 } from "ink";
|
|
257
|
+
import { useEffect, useState as useState2 } from "react";
|
|
258
|
+
|
|
259
|
+
// src/components/SelectMenu.tsx
|
|
260
|
+
import { Box as Box4, Text as Text4, useInput } from "ink";
|
|
261
|
+
import { useState } from "react";
|
|
262
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
263
|
+
function SelectMenu({ title, items, onSelect, onClose, footer }) {
|
|
264
|
+
const [index, setIndex] = useState(0);
|
|
265
|
+
useInput((_input, key) => {
|
|
266
|
+
if (key.escape) {
|
|
267
|
+
onClose();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (key.upArrow) {
|
|
271
|
+
setIndex((i) => i > 0 ? i - 1 : items.length - 1);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (key.downArrow) {
|
|
275
|
+
setIndex((i) => i < items.length - 1 ? i + 1 : 0);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (key.return && items.length > 0) {
|
|
279
|
+
onSelect(items[Math.min(index, items.length - 1)].key);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", paddingX: 1, children: [
|
|
283
|
+
/* @__PURE__ */ jsx4(Text4, { bold: true, children: title }),
|
|
284
|
+
items.length === 0 ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "(none)" }) : items.map((item, i) => /* @__PURE__ */ jsxs3(Text4, { color: i === index ? "cyan" : void 0, children: [
|
|
285
|
+
i === index ? "\u203A" : " ",
|
|
286
|
+
" ",
|
|
287
|
+
item.label
|
|
288
|
+
] }, item.key)),
|
|
289
|
+
footer ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: footer }) : null
|
|
290
|
+
] });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/components/DevicesMenu.tsx
|
|
294
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
295
|
+
function DevicesMenu({ listDevicesFn, onSelect, onClose }) {
|
|
296
|
+
const [loadState, setLoadState] = useState2({ status: "loading" });
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
let cancelled = false;
|
|
299
|
+
listDevicesFn().then((list2) => {
|
|
300
|
+
if (!cancelled) setLoadState({ status: "loaded", list: list2 });
|
|
301
|
+
}).catch((error) => {
|
|
302
|
+
if (!cancelled) {
|
|
303
|
+
setLoadState({
|
|
304
|
+
status: "error",
|
|
305
|
+
message: error instanceof Error ? error.message : String(error)
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
return () => {
|
|
310
|
+
cancelled = true;
|
|
311
|
+
};
|
|
312
|
+
}, [listDevicesFn]);
|
|
313
|
+
useInput2(
|
|
314
|
+
(_input, key) => {
|
|
315
|
+
if (key.escape) onClose();
|
|
316
|
+
},
|
|
317
|
+
{ isActive: loadState.status !== "loaded" }
|
|
318
|
+
);
|
|
319
|
+
if (loadState.status === "loading") {
|
|
320
|
+
return /* @__PURE__ */ jsx5(Box5, { paddingX: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Loading devices\u2026" }) });
|
|
321
|
+
}
|
|
322
|
+
if (loadState.status === "error") {
|
|
323
|
+
return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", paddingX: 1, children: [
|
|
324
|
+
/* @__PURE__ */ jsxs4(Text5, { color: "red", children: [
|
|
325
|
+
"Failed to list devices: ",
|
|
326
|
+
loadState.message
|
|
327
|
+
] }),
|
|
328
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "[esc] back" })
|
|
329
|
+
] });
|
|
330
|
+
}
|
|
331
|
+
const { list } = loadState;
|
|
332
|
+
const items = list.devices.map((name) => ({
|
|
333
|
+
key: name,
|
|
334
|
+
label: name === list.defaultDevice ? `${name} (default)` : name
|
|
335
|
+
}));
|
|
336
|
+
return /* @__PURE__ */ jsx5(
|
|
337
|
+
SelectMenu,
|
|
338
|
+
{
|
|
339
|
+
title: "Devices",
|
|
340
|
+
items,
|
|
341
|
+
onSelect,
|
|
342
|
+
onClose,
|
|
343
|
+
footer: "[\u2191\u2193] select [enter] confirm [esc] back"
|
|
344
|
+
}
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/components/ModelMenu.tsx
|
|
349
|
+
import { Box as Box6, Text as Text6, useInput as useInput3 } from "ink";
|
|
350
|
+
import { useEffect as useEffect2, useState as useState3 } from "react";
|
|
351
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
352
|
+
function ModelMenu({ listModelStatusesFn, runWarmupFn, onSelect, onClose }) {
|
|
353
|
+
const [menuState, setMenuState] = useState3({ status: "loading" });
|
|
354
|
+
useEffect2(() => {
|
|
355
|
+
let cancelled = false;
|
|
356
|
+
listModelStatusesFn().then((models) => {
|
|
357
|
+
if (!cancelled) setMenuState({ status: "loaded", models });
|
|
358
|
+
}).catch((error) => {
|
|
359
|
+
if (!cancelled) {
|
|
360
|
+
setMenuState({
|
|
361
|
+
status: "error",
|
|
362
|
+
message: error instanceof Error ? error.message : String(error)
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
return () => {
|
|
367
|
+
cancelled = true;
|
|
368
|
+
};
|
|
369
|
+
}, [listModelStatusesFn]);
|
|
370
|
+
useInput3(
|
|
371
|
+
(_input, key) => {
|
|
372
|
+
if (key.escape) onClose();
|
|
373
|
+
},
|
|
374
|
+
{ isActive: menuState.status === "error" }
|
|
375
|
+
);
|
|
376
|
+
const handleSelect = (name) => {
|
|
377
|
+
if (menuState.status !== "loaded") return;
|
|
378
|
+
const target = menuState.models.find((m) => m.name === name);
|
|
379
|
+
if (!target) return;
|
|
380
|
+
if (target.cached) {
|
|
381
|
+
onSelect(name);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
setMenuState({ status: "downloading", model: name, percent: null, downloaded: 0 });
|
|
385
|
+
runWarmupFn(name, (progress) => {
|
|
386
|
+
setMenuState({
|
|
387
|
+
status: "downloading",
|
|
388
|
+
model: name,
|
|
389
|
+
percent: progress.percent,
|
|
390
|
+
downloaded: progress.downloaded
|
|
391
|
+
});
|
|
392
|
+
}).then(() => onSelect(name)).catch((error) => {
|
|
393
|
+
setMenuState({
|
|
394
|
+
status: "error",
|
|
395
|
+
message: error instanceof Error ? error.message : String(error)
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
};
|
|
399
|
+
if (menuState.status === "loading") {
|
|
400
|
+
return /* @__PURE__ */ jsx6(Box6, { paddingX: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Loading models\u2026" }) });
|
|
401
|
+
}
|
|
402
|
+
if (menuState.status === "error") {
|
|
403
|
+
return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", paddingX: 1, children: [
|
|
404
|
+
/* @__PURE__ */ jsx6(Text6, { color: "red", children: menuState.message }),
|
|
405
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "[esc] back" })
|
|
406
|
+
] });
|
|
407
|
+
}
|
|
408
|
+
if (menuState.status === "downloading") {
|
|
409
|
+
const progressText = menuState.percent !== null ? `${menuState.percent.toFixed(1)}%` : `${menuState.downloaded} bytes`;
|
|
410
|
+
return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", paddingX: 1, children: [
|
|
411
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
412
|
+
"Downloading model '",
|
|
413
|
+
menuState.model,
|
|
414
|
+
"'\u2026 ",
|
|
415
|
+
progressText
|
|
416
|
+
] }),
|
|
417
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "please wait" })
|
|
418
|
+
] });
|
|
419
|
+
}
|
|
420
|
+
const items = menuState.models.map((m) => ({
|
|
421
|
+
key: m.name,
|
|
422
|
+
label: m.cached ? `\u2713 ${m.name}` : `\xB7 ${m.name} (not downloaded)`
|
|
423
|
+
}));
|
|
424
|
+
return /* @__PURE__ */ jsx6(
|
|
425
|
+
SelectMenu,
|
|
426
|
+
{
|
|
427
|
+
title: "Model",
|
|
428
|
+
items,
|
|
429
|
+
onSelect: handleSelect,
|
|
430
|
+
onClose,
|
|
431
|
+
footer: "[\u2191\u2193] select [enter] confirm [esc] back"
|
|
432
|
+
}
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/hooks/useKeyHandler.ts
|
|
437
|
+
import { useInput as useInput4 } from "ink";
|
|
438
|
+
function useKeyHandler({
|
|
439
|
+
enabled = true,
|
|
440
|
+
onRecord,
|
|
441
|
+
onStop,
|
|
442
|
+
onCancel,
|
|
443
|
+
onQuit,
|
|
444
|
+
onDevices,
|
|
445
|
+
onModel,
|
|
446
|
+
onAnyKey
|
|
447
|
+
}) {
|
|
448
|
+
useInput4(
|
|
449
|
+
(input, key) => {
|
|
450
|
+
if (key.escape || input === "q" || input === "Q") {
|
|
451
|
+
onQuit?.();
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (key.ctrl && input === "c") {
|
|
455
|
+
onQuit?.();
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
switch (input) {
|
|
459
|
+
case "r":
|
|
460
|
+
case "R":
|
|
461
|
+
onRecord?.();
|
|
462
|
+
return;
|
|
463
|
+
case "s":
|
|
464
|
+
case "S":
|
|
465
|
+
onStop?.();
|
|
466
|
+
return;
|
|
467
|
+
case "c":
|
|
468
|
+
case "C":
|
|
469
|
+
onCancel?.();
|
|
470
|
+
return;
|
|
471
|
+
case "d":
|
|
472
|
+
case "D":
|
|
473
|
+
onDevices?.();
|
|
474
|
+
return;
|
|
475
|
+
case "m":
|
|
476
|
+
case "M":
|
|
477
|
+
onModel?.();
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
onAnyKey?.();
|
|
481
|
+
},
|
|
482
|
+
{ isActive: enabled }
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// src/recording/useRecorder.ts
|
|
487
|
+
import { useCallback, useEffect as useEffect3, useRef, useState as useState4 } from "react";
|
|
488
|
+
|
|
489
|
+
// src/recording/spawnRecorder.ts
|
|
490
|
+
import { spawn } from "child_process";
|
|
491
|
+
import { EventEmitter } from "events";
|
|
492
|
+
var RecorderProcessImpl = class extends EventEmitter {
|
|
493
|
+
child;
|
|
494
|
+
stopped = false;
|
|
495
|
+
exitPromise;
|
|
496
|
+
resolveExit = () => {
|
|
497
|
+
};
|
|
498
|
+
exitSettled = false;
|
|
499
|
+
constructor(child) {
|
|
500
|
+
super();
|
|
501
|
+
this.child = child;
|
|
502
|
+
this.exitPromise = new Promise((resolve) => {
|
|
503
|
+
this.resolveExit = resolve;
|
|
504
|
+
});
|
|
505
|
+
this.child.on("error", (error) => {
|
|
506
|
+
this.emit("error", error);
|
|
507
|
+
this.settleExit();
|
|
508
|
+
});
|
|
509
|
+
this.child.on("exit", (code, signal) => {
|
|
510
|
+
this.emit("exit", code, signal);
|
|
511
|
+
this.settleExit();
|
|
512
|
+
});
|
|
513
|
+
this.child.stdout.on("data", (data) => {
|
|
514
|
+
process.stdout.write(data);
|
|
515
|
+
});
|
|
516
|
+
this.child.stderr.on("data", (data) => {
|
|
517
|
+
process.stderr.write(data);
|
|
518
|
+
});
|
|
519
|
+
process.nextTick(() => this.emit("ready"));
|
|
520
|
+
}
|
|
521
|
+
settleExit() {
|
|
522
|
+
if (!this.exitSettled) {
|
|
523
|
+
this.exitSettled = true;
|
|
524
|
+
this.resolveExit();
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
stop() {
|
|
528
|
+
return this._terminate("SIGINT");
|
|
529
|
+
}
|
|
530
|
+
cancel() {
|
|
531
|
+
return this._terminate("SIGKILL");
|
|
532
|
+
}
|
|
533
|
+
_terminate(signal) {
|
|
534
|
+
if (this.stopped) {
|
|
535
|
+
return this.exitPromise;
|
|
536
|
+
}
|
|
537
|
+
this.stopped = true;
|
|
538
|
+
if (this.child.killed) {
|
|
539
|
+
return this.exitPromise;
|
|
540
|
+
}
|
|
541
|
+
this.child.kill(signal);
|
|
542
|
+
const timeout = setTimeout(() => {
|
|
543
|
+
if (!this.child.killed) {
|
|
544
|
+
this.child.kill("SIGKILL");
|
|
545
|
+
}
|
|
546
|
+
}, 5e3);
|
|
547
|
+
this.exitPromise.finally(() => clearTimeout(timeout));
|
|
548
|
+
return this.exitPromise;
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
function spawnRecorder(options) {
|
|
552
|
+
const {
|
|
553
|
+
workspaceRoot,
|
|
554
|
+
sessionId,
|
|
555
|
+
lincolnRecordPath = "lincoln-record",
|
|
556
|
+
mic,
|
|
557
|
+
model
|
|
558
|
+
} = options;
|
|
559
|
+
const args = [
|
|
560
|
+
"record",
|
|
561
|
+
"--session-id",
|
|
562
|
+
sessionId,
|
|
563
|
+
"--output",
|
|
564
|
+
workspaceRoot
|
|
565
|
+
];
|
|
566
|
+
if (mic) {
|
|
567
|
+
args.push("--mic", mic);
|
|
568
|
+
}
|
|
569
|
+
if (model) {
|
|
570
|
+
args.push("--model", model);
|
|
571
|
+
}
|
|
572
|
+
const child = spawn(lincolnRecordPath, args, {
|
|
573
|
+
cwd: workspaceRoot,
|
|
574
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
575
|
+
});
|
|
576
|
+
return new RecorderProcessImpl(child);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/recording/useRecorder.ts
|
|
580
|
+
function mockAmplitude(duration) {
|
|
581
|
+
return 0.5 + 0.4 * Math.sin(duration * 0.8);
|
|
582
|
+
}
|
|
583
|
+
function useRecorder(options) {
|
|
584
|
+
const {
|
|
585
|
+
startOnMount = false,
|
|
586
|
+
workspaceRoot,
|
|
587
|
+
sessionId,
|
|
588
|
+
lincolnRecordPath,
|
|
589
|
+
mic,
|
|
590
|
+
model
|
|
591
|
+
} = options;
|
|
592
|
+
const [state, setState] = useState4({
|
|
593
|
+
status: "idle",
|
|
594
|
+
duration: 0,
|
|
595
|
+
amplitude: 0,
|
|
596
|
+
errorMessage: null
|
|
597
|
+
});
|
|
598
|
+
const recorderRef = useRef(null);
|
|
599
|
+
const intervalRef = useRef(null);
|
|
600
|
+
const durationRef = useRef(0);
|
|
601
|
+
const terminatingRef = useRef(false);
|
|
602
|
+
const clearRecordingInterval = useCallback(() => {
|
|
603
|
+
if (intervalRef.current) {
|
|
604
|
+
clearInterval(intervalRef.current);
|
|
605
|
+
intervalRef.current = null;
|
|
606
|
+
}
|
|
607
|
+
}, []);
|
|
608
|
+
const start = useCallback(() => {
|
|
609
|
+
if (recorderRef.current) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
terminatingRef.current = false;
|
|
613
|
+
setState({ status: "recording", duration: 0, amplitude: 0.3, errorMessage: null });
|
|
614
|
+
durationRef.current = 0;
|
|
615
|
+
try {
|
|
616
|
+
const recorder = spawnRecorder({
|
|
617
|
+
workspaceRoot,
|
|
618
|
+
sessionId,
|
|
619
|
+
lincolnRecordPath,
|
|
620
|
+
mic,
|
|
621
|
+
model
|
|
622
|
+
});
|
|
623
|
+
recorderRef.current = recorder;
|
|
624
|
+
recorder.on("ready", () => {
|
|
625
|
+
});
|
|
626
|
+
recorder.on("error", (error) => {
|
|
627
|
+
setState((s) => ({ ...s, status: "error", errorMessage: error.message }));
|
|
628
|
+
clearRecordingInterval();
|
|
629
|
+
});
|
|
630
|
+
recorder.on("exit", (code, signal) => {
|
|
631
|
+
recorderRef.current = null;
|
|
632
|
+
clearRecordingInterval();
|
|
633
|
+
if (terminatingRef.current) {
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const reason = code !== null && code !== void 0 ? `lincoln-record exited unexpectedly with code ${code}` : `lincoln-record exited unexpectedly with signal ${signal}`;
|
|
637
|
+
setState((s) => ({ ...s, status: "error", errorMessage: reason }));
|
|
638
|
+
});
|
|
639
|
+
intervalRef.current = setInterval(() => {
|
|
640
|
+
durationRef.current += 1;
|
|
641
|
+
setState((s) => ({
|
|
642
|
+
...s,
|
|
643
|
+
duration: durationRef.current,
|
|
644
|
+
amplitude: mockAmplitude(durationRef.current)
|
|
645
|
+
}));
|
|
646
|
+
}, 1e3);
|
|
647
|
+
} catch (error) {
|
|
648
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
649
|
+
setState((s) => ({ ...s, status: "error", errorMessage: message }));
|
|
650
|
+
}
|
|
651
|
+
}, [workspaceRoot, sessionId, lincolnRecordPath, mic, model, clearRecordingInterval]);
|
|
652
|
+
const stop = useCallback(async () => {
|
|
653
|
+
const recorder = recorderRef.current;
|
|
654
|
+
if (!recorder) return;
|
|
655
|
+
terminatingRef.current = true;
|
|
656
|
+
clearRecordingInterval();
|
|
657
|
+
setState((s) => ({ ...s, status: "stopped" }));
|
|
658
|
+
await recorder.stop();
|
|
659
|
+
}, [clearRecordingInterval]);
|
|
660
|
+
const cancel = useCallback(async () => {
|
|
661
|
+
const recorder = recorderRef.current;
|
|
662
|
+
if (!recorder) return;
|
|
663
|
+
terminatingRef.current = true;
|
|
664
|
+
clearRecordingInterval();
|
|
665
|
+
setState((s) => ({ ...s, status: "cancelled" }));
|
|
666
|
+
await recorder.cancel();
|
|
667
|
+
}, [clearRecordingInterval]);
|
|
668
|
+
useEffect3(() => {
|
|
669
|
+
if (startOnMount) {
|
|
670
|
+
start();
|
|
671
|
+
}
|
|
672
|
+
return () => {
|
|
673
|
+
clearRecordingInterval();
|
|
674
|
+
if (recorderRef.current) {
|
|
675
|
+
recorderRef.current.cancel().catch(() => {
|
|
676
|
+
});
|
|
677
|
+
recorderRef.current = null;
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
}, [startOnMount, start, clearRecordingInterval]);
|
|
681
|
+
return { state, start, stop, cancel };
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// src/devices/listDevices.ts
|
|
685
|
+
import { execFile } from "child_process";
|
|
686
|
+
import { promisify } from "util";
|
|
687
|
+
|
|
688
|
+
// src/devices/parseDevices.ts
|
|
689
|
+
var DEFAULT_PREFIX = "Default input: ";
|
|
690
|
+
function parseDevicesOutput(output) {
|
|
691
|
+
let defaultDevice = null;
|
|
692
|
+
const devices = [];
|
|
693
|
+
for (const rawLine of output.split("\n")) {
|
|
694
|
+
const line = rawLine.trim();
|
|
695
|
+
if (!line) continue;
|
|
696
|
+
if (line.startsWith(DEFAULT_PREFIX)) {
|
|
697
|
+
defaultDevice = line.slice(DEFAULT_PREFIX.length).trim() || null;
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
devices.push(line);
|
|
701
|
+
}
|
|
702
|
+
return { defaultDevice, devices };
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// src/devices/listDevices.ts
|
|
706
|
+
var execFileAsync = promisify(execFile);
|
|
707
|
+
async function listDevices(lincolnRecordPath = "lincoln-record") {
|
|
708
|
+
const { stdout } = await execFileAsync(lincolnRecordPath, ["devices"]);
|
|
709
|
+
return parseDevicesOutput(stdout);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// src/models/models.ts
|
|
713
|
+
import { existsSync as existsSync2 } from "fs";
|
|
714
|
+
import { homedir as homedir2 } from "os";
|
|
715
|
+
import { join as join2 } from "path";
|
|
716
|
+
var KNOWN_MODELS = [
|
|
717
|
+
"tiny",
|
|
718
|
+
"base",
|
|
719
|
+
"small",
|
|
720
|
+
"medium",
|
|
721
|
+
"large-v1",
|
|
722
|
+
"large-v2",
|
|
723
|
+
"large-v3",
|
|
724
|
+
"large-v3-turbo"
|
|
725
|
+
];
|
|
726
|
+
function defaultModelCacheDir() {
|
|
727
|
+
return join2(homedir2(), ".cache", "lincoln", "models");
|
|
728
|
+
}
|
|
729
|
+
function modelCachePath(name, cacheDir) {
|
|
730
|
+
return join2(cacheDir, "whisper", `ggml-${name}.bin`);
|
|
731
|
+
}
|
|
732
|
+
function listModelStatuses(cacheDir) {
|
|
733
|
+
return KNOWN_MODELS.map((name) => ({
|
|
734
|
+
name,
|
|
735
|
+
cached: existsSync2(modelCachePath(name, cacheDir))
|
|
736
|
+
}));
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// src/models/runWarmup.ts
|
|
740
|
+
import { spawn as spawn2 } from "child_process";
|
|
741
|
+
|
|
742
|
+
// src/models/warmupProgress.ts
|
|
743
|
+
var PROGRESS_WITH_TOTAL = /downloaded (\d+) \/ (\d+) bytes \(([\d.]+)%\)/;
|
|
744
|
+
var PROGRESS_WITHOUT_TOTAL = /downloaded (\d+) bytes/;
|
|
745
|
+
function parseWarmupProgress(chunk) {
|
|
746
|
+
const withTotal = PROGRESS_WITH_TOTAL.exec(chunk);
|
|
747
|
+
if (withTotal) {
|
|
748
|
+
return {
|
|
749
|
+
downloaded: Number(withTotal[1]),
|
|
750
|
+
total: Number(withTotal[2]),
|
|
751
|
+
percent: Number(withTotal[3])
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
const withoutTotal = PROGRESS_WITHOUT_TOTAL.exec(chunk);
|
|
755
|
+
if (withoutTotal) {
|
|
756
|
+
return {
|
|
757
|
+
downloaded: Number(withoutTotal[1]),
|
|
758
|
+
total: null,
|
|
759
|
+
percent: null
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// src/models/runWarmup.ts
|
|
766
|
+
var READY_LINE = /Model ready at (.+)/;
|
|
767
|
+
function runWarmup(options) {
|
|
768
|
+
const { model, lincolnRecordPath = "lincoln-record", onProgress } = options;
|
|
769
|
+
return new Promise((resolve, reject) => {
|
|
770
|
+
const child = spawn2(lincolnRecordPath, ["warmup", "--model", model], {
|
|
771
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
772
|
+
});
|
|
773
|
+
let readyPath = null;
|
|
774
|
+
child.stdout.on("data", (data) => {
|
|
775
|
+
const match = READY_LINE.exec(data.toString());
|
|
776
|
+
if (match) {
|
|
777
|
+
readyPath = match[1].trim();
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
child.stderr.on("data", (data) => {
|
|
781
|
+
const progress = parseWarmupProgress(data.toString());
|
|
782
|
+
if (progress) {
|
|
783
|
+
onProgress?.(progress);
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
child.on("error", reject);
|
|
787
|
+
child.on("exit", (code) => {
|
|
788
|
+
if (code === 0) {
|
|
789
|
+
resolve(readyPath ?? model);
|
|
790
|
+
} else {
|
|
791
|
+
reject(new Error(`lincoln-record warmup exited with code ${code}`));
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// src/components/RecordingApp.tsx
|
|
798
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
799
|
+
function createLogId() {
|
|
800
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
801
|
+
}
|
|
802
|
+
function RecordingApp({
|
|
803
|
+
workspaceRoot,
|
|
804
|
+
sessionId,
|
|
805
|
+
topic,
|
|
806
|
+
designId,
|
|
807
|
+
branch,
|
|
808
|
+
audioMeterStyle = "bar",
|
|
809
|
+
lincolnRecordPath,
|
|
810
|
+
listDevicesFn,
|
|
811
|
+
listModelStatusesFn,
|
|
812
|
+
runWarmupFn
|
|
813
|
+
}) {
|
|
814
|
+
const { exit } = useApp();
|
|
815
|
+
const [logs, setLogs] = useState5([]);
|
|
816
|
+
const [mode, setMode] = useState5("main");
|
|
817
|
+
const [mic, setMic] = useState5(null);
|
|
818
|
+
const [model, setModel] = useState5(null);
|
|
819
|
+
const addLog = useCallback2((message, type = "info") => {
|
|
820
|
+
setLogs((prev) => [...prev, { id: createLogId(), message, type }]);
|
|
821
|
+
}, []);
|
|
822
|
+
const { state, start, stop, cancel } = useRecorder({
|
|
823
|
+
workspaceRoot,
|
|
824
|
+
sessionId,
|
|
825
|
+
lincolnRecordPath,
|
|
826
|
+
mic,
|
|
827
|
+
model,
|
|
828
|
+
startOnMount: false
|
|
829
|
+
});
|
|
830
|
+
useEffect4(() => {
|
|
831
|
+
switch (state.status) {
|
|
832
|
+
case "recording":
|
|
833
|
+
addLog("Recording started.", "info");
|
|
834
|
+
break;
|
|
835
|
+
case "stopped":
|
|
836
|
+
addLog(`Saved to ${workspaceRoot}/${sessionId}/audio.wav`, "success");
|
|
837
|
+
addLog(`Run: claude process-interview ${sessionId}`, "command");
|
|
838
|
+
break;
|
|
839
|
+
case "cancelled":
|
|
840
|
+
addLog("Recording cancelled. No files were saved.", "error");
|
|
841
|
+
break;
|
|
842
|
+
case "error":
|
|
843
|
+
addLog(`Recording error: ${state.errorMessage ?? "unknown error"}`, "error");
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
}, [state.status, state.errorMessage, addLog, workspaceRoot, sessionId]);
|
|
847
|
+
const handleQuit = useCallback2(() => {
|
|
848
|
+
exit();
|
|
849
|
+
}, [exit]);
|
|
850
|
+
const handleRecord = useCallback2(() => {
|
|
851
|
+
if (state.status === "idle") {
|
|
852
|
+
start();
|
|
853
|
+
}
|
|
854
|
+
}, [state.status, start]);
|
|
855
|
+
const handleStop = useCallback2(() => {
|
|
856
|
+
if (state.status === "recording") {
|
|
857
|
+
stop().catch(() => {
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
}, [state.status, stop]);
|
|
861
|
+
const handleCancel = useCallback2(() => {
|
|
862
|
+
if (state.status === "recording") {
|
|
863
|
+
cancel().catch(() => {
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
}, [state.status, cancel]);
|
|
867
|
+
const handleDevices = useCallback2(() => {
|
|
868
|
+
if (state.status === "idle") {
|
|
869
|
+
setMode("devices");
|
|
870
|
+
}
|
|
871
|
+
}, [state.status]);
|
|
872
|
+
const handleModel = useCallback2(() => {
|
|
873
|
+
if (state.status === "idle") {
|
|
874
|
+
setMode("model");
|
|
875
|
+
}
|
|
876
|
+
}, [state.status]);
|
|
877
|
+
const handleDeviceSelect = useCallback2(
|
|
878
|
+
(device) => {
|
|
879
|
+
setMic(device);
|
|
880
|
+
addLog(`Input device: ${device}`, "success");
|
|
881
|
+
setMode("main");
|
|
882
|
+
},
|
|
883
|
+
[addLog]
|
|
884
|
+
);
|
|
885
|
+
const handleModelSelect = useCallback2(
|
|
886
|
+
(name) => {
|
|
887
|
+
setModel(name);
|
|
888
|
+
addLog(`Model: ${name}`, "success");
|
|
889
|
+
setMode("main");
|
|
890
|
+
},
|
|
891
|
+
[addLog]
|
|
892
|
+
);
|
|
893
|
+
const handleMenuClose = useCallback2(() => {
|
|
894
|
+
setMode("main");
|
|
895
|
+
}, []);
|
|
896
|
+
const isTerminal = state.status === "stopped" || state.status === "cancelled" || state.status === "error";
|
|
897
|
+
useKeyHandler({
|
|
898
|
+
enabled: mode === "main",
|
|
899
|
+
onRecord: handleRecord,
|
|
900
|
+
onStop: handleStop,
|
|
901
|
+
onCancel: handleCancel,
|
|
902
|
+
onQuit: handleQuit,
|
|
903
|
+
onDevices: handleDevices,
|
|
904
|
+
onModel: handleModel,
|
|
905
|
+
onAnyKey: isTerminal ? handleQuit : void 0
|
|
906
|
+
});
|
|
907
|
+
const hintMode = useMemo(() => {
|
|
908
|
+
if (mode !== "main") return "menu";
|
|
909
|
+
if (state.status === "idle") return "idle";
|
|
910
|
+
if (state.status === "recording") return "recording";
|
|
911
|
+
if (state.status === "stopped") return "stopped";
|
|
912
|
+
if (state.status === "cancelled") return "cancelled";
|
|
913
|
+
return "error";
|
|
914
|
+
}, [mode, state.status]);
|
|
915
|
+
const devicesLoader = useCallback2(
|
|
916
|
+
() => (listDevicesFn ?? (() => listDevices(lincolnRecordPath)))(),
|
|
917
|
+
[listDevicesFn, lincolnRecordPath]
|
|
918
|
+
);
|
|
919
|
+
const modelsLoader = useCallback2(
|
|
920
|
+
() => (listModelStatusesFn ?? (() => Promise.resolve(listModelStatuses(defaultModelCacheDir()))))(),
|
|
921
|
+
[listModelStatusesFn]
|
|
922
|
+
);
|
|
923
|
+
const warmupRunner = useCallback2(
|
|
924
|
+
(name, onProgress) => runWarmupFn ? runWarmupFn(name, onProgress) : runWarmup({ model: name, lincolnRecordPath, onProgress }),
|
|
925
|
+
[runWarmupFn, lincolnRecordPath]
|
|
926
|
+
);
|
|
927
|
+
return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "gray", height: 24, children: [
|
|
928
|
+
/* @__PURE__ */ jsx7(
|
|
929
|
+
StatusPanel,
|
|
930
|
+
{
|
|
931
|
+
sessionId,
|
|
932
|
+
topic,
|
|
933
|
+
designId,
|
|
934
|
+
branch,
|
|
935
|
+
status: state.status,
|
|
936
|
+
duration: state.duration
|
|
937
|
+
}
|
|
938
|
+
),
|
|
939
|
+
mode === "devices" && /* @__PURE__ */ jsx7(
|
|
940
|
+
DevicesMenu,
|
|
941
|
+
{
|
|
942
|
+
listDevicesFn: devicesLoader,
|
|
943
|
+
onSelect: handleDeviceSelect,
|
|
944
|
+
onClose: handleMenuClose
|
|
945
|
+
}
|
|
946
|
+
),
|
|
947
|
+
mode === "model" && /* @__PURE__ */ jsx7(
|
|
948
|
+
ModelMenu,
|
|
949
|
+
{
|
|
950
|
+
listModelStatusesFn: modelsLoader,
|
|
951
|
+
runWarmupFn: warmupRunner,
|
|
952
|
+
onSelect: handleModelSelect,
|
|
953
|
+
onClose: handleMenuClose
|
|
954
|
+
}
|
|
955
|
+
),
|
|
956
|
+
/* @__PURE__ */ jsx7(LogView, { logs }),
|
|
957
|
+
/* @__PURE__ */ jsx7(Box7, { flexGrow: 1 }),
|
|
958
|
+
/* @__PURE__ */ jsx7(HintBar, { mode: hintMode })
|
|
959
|
+
] });
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// src/cli.tsx
|
|
963
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
964
|
+
function App({ args, config }) {
|
|
965
|
+
if (args.help) {
|
|
966
|
+
return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", padding: 1, children: [
|
|
967
|
+
/* @__PURE__ */ jsx8(Text7, { bold: true, children: "lincoln" }),
|
|
968
|
+
/* @__PURE__ */ jsx8(Text7, { children: "Cross-tool TUI for Lincoln interview recording." }),
|
|
969
|
+
/* @__PURE__ */ jsx8(Text7, {}),
|
|
970
|
+
/* @__PURE__ */ jsx8(Text7, { bold: true, children: "Usage:" }),
|
|
971
|
+
/* @__PURE__ */ jsx8(Text7, { children: " lincoln [options]" }),
|
|
972
|
+
/* @__PURE__ */ jsx8(Text7, {}),
|
|
973
|
+
/* @__PURE__ */ jsx8(Text7, { bold: true, children: "Options:" }),
|
|
974
|
+
/* @__PURE__ */ jsx8(Text7, { children: " --topic Interview topic" }),
|
|
975
|
+
/* @__PURE__ */ jsx8(Text7, { children: " --design-id Design ID for the session" }),
|
|
976
|
+
/* @__PURE__ */ jsx8(Text7, { children: " --branch Current branch name" }),
|
|
977
|
+
/* @__PURE__ */ jsx8(Text7, { children: " --session-id Explicit session ID" }),
|
|
978
|
+
/* @__PURE__ */ jsx8(Text7, { children: " --no-tui Run without the terminal UI" }),
|
|
979
|
+
/* @__PURE__ */ jsx8(Text7, { children: " --help, -h Show this help message" }),
|
|
980
|
+
/* @__PURE__ */ jsx8(Text7, {}),
|
|
981
|
+
/* @__PURE__ */ jsx8(Text7, { dimColor: true, children: "Configuration files: ~/.lincolnrc, ./.lincolnrc" })
|
|
982
|
+
] });
|
|
983
|
+
}
|
|
984
|
+
return /* @__PURE__ */ jsx8(
|
|
985
|
+
RecordingApp,
|
|
986
|
+
{
|
|
987
|
+
workspaceRoot: config.workspaceRoot,
|
|
988
|
+
sessionId: config.sessionId,
|
|
989
|
+
topic: config.topic,
|
|
990
|
+
designId: config.designId,
|
|
991
|
+
branch: config.branch,
|
|
992
|
+
audioMeterStyle: config.audioMeterStyle
|
|
993
|
+
}
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
async function main(argv = process.argv.slice(2)) {
|
|
997
|
+
const args = parseArgs(argv);
|
|
998
|
+
const config = resolveConfig(args);
|
|
999
|
+
if (args.noTui) {
|
|
1000
|
+
console.log(JSON.stringify(config, null, 2));
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
const { render } = await import("ink");
|
|
1004
|
+
render(/* @__PURE__ */ jsx8(App, { args, config }));
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// src/main.tsx
|
|
1008
|
+
main();
|
|
1009
|
+
//# sourceMappingURL=main.js.map
|