@tienne/gestalt 0.5.1 → 0.7.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.backup.md +442 -0
- package/README.ko.md +487 -0
- package/README.md +324 -286
- package/dist/package.json +10 -3
- package/dist/review-agents/performance-reviewer/AGENT.md +31 -0
- package/dist/review-agents/quality-reviewer/AGENT.md +31 -0
- package/dist/review-agents/security-reviewer/AGENT.md +32 -0
- package/dist/role-agents/architect/AGENT.md +30 -0
- package/dist/role-agents/backend-developer/AGENT.md +30 -0
- package/dist/role-agents/designer/AGENT.md +30 -0
- package/dist/role-agents/devops-engineer/AGENT.md +30 -0
- package/dist/role-agents/frontend-developer/AGENT.md +30 -0
- package/dist/role-agents/product-planner/AGENT.md +30 -0
- package/dist/role-agents/qa-engineer/AGENT.md +30 -0
- package/dist/role-agents/researcher/AGENT.md +30 -0
- package/dist/role-agents/technical-writer/AGENT.md +212 -0
- package/dist/skills/agent/SKILL.md +102 -0
- package/dist/skills/execute/SKILL.md +274 -6
- package/dist/src/agent/role-agent-registry.d.ts +4 -2
- package/dist/src/agent/role-agent-registry.d.ts.map +1 -1
- package/dist/src/agent/role-agent-registry.js +12 -3
- package/dist/src/agent/role-agent-registry.js.map +1 -1
- package/dist/src/cli/commands/interview.d.ts +5 -1
- package/dist/src/cli/commands/interview.d.ts.map +1 -1
- package/dist/src/cli/commands/interview.js +15 -3
- package/dist/src/cli/commands/interview.js.map +1 -1
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +4 -2
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/core/config.d.ts +3 -0
- package/dist/src/core/config.d.ts.map +1 -1
- package/dist/src/core/config.js +4 -0
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/types.d.ts +28 -0
- package/dist/src/core/types.d.ts.map +1 -1
- package/dist/src/mcp/schemas.d.ts +3 -0
- package/dist/src/mcp/schemas.d.ts.map +1 -1
- package/dist/src/mcp/schemas.js +2 -0
- package/dist/src/mcp/schemas.js.map +1 -1
- package/dist/src/mcp/server.d.ts.map +1 -1
- package/dist/src/mcp/server.js +12 -1
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/mcp/tools/agent-passthrough.d.ts +7 -0
- package/dist/src/mcp/tools/agent-passthrough.d.ts.map +1 -0
- package/dist/src/mcp/tools/agent-passthrough.js +49 -0
- package/dist/src/mcp/tools/agent-passthrough.js.map +1 -0
- package/dist/src/mcp/tools/interview-passthrough.d.ts.map +1 -1
- package/dist/src/mcp/tools/interview-passthrough.js +26 -1
- package/dist/src/mcp/tools/interview-passthrough.js.map +1 -1
- package/dist/src/mcp/tools/interview.d.ts.map +1 -1
- package/dist/src/mcp/tools/interview.js +26 -1
- package/dist/src/mcp/tools/interview.js.map +1 -1
- package/dist/src/recording/agg-converter.d.ts +25 -0
- package/dist/src/recording/agg-converter.d.ts.map +1 -0
- package/dist/src/recording/agg-converter.js +80 -0
- package/dist/src/recording/agg-converter.js.map +1 -0
- package/dist/src/recording/agg-installer.d.ts +6 -0
- package/dist/src/recording/agg-installer.d.ts.map +1 -0
- package/dist/src/recording/agg-installer.js +50 -0
- package/dist/src/recording/agg-installer.js.map +1 -0
- package/dist/src/recording/asciinema-installer.d.ts +6 -0
- package/dist/src/recording/asciinema-installer.d.ts.map +1 -0
- package/dist/src/recording/asciinema-installer.js +50 -0
- package/dist/src/recording/asciinema-installer.js.map +1 -0
- package/dist/src/recording/asciinema-recorder.d.ts +26 -0
- package/dist/src/recording/asciinema-recorder.d.ts.map +1 -0
- package/dist/src/recording/asciinema-recorder.js +52 -0
- package/dist/src/recording/asciinema-recorder.js.map +1 -0
- package/dist/src/recording/cast-generator.d.ts +7 -0
- package/dist/src/recording/cast-generator.d.ts.map +1 -0
- package/dist/src/recording/cast-generator.js +72 -0
- package/dist/src/recording/cast-generator.js.map +1 -0
- package/dist/src/recording/filename-generator.d.ts +19 -0
- package/dist/src/recording/filename-generator.d.ts.map +1 -0
- package/dist/src/recording/filename-generator.js +67 -0
- package/dist/src/recording/filename-generator.js.map +1 -0
- package/dist/src/recording/gif-generator.d.ts +21 -0
- package/dist/src/recording/gif-generator.d.ts.map +1 -0
- package/dist/src/recording/gif-generator.js +121 -0
- package/dist/src/recording/gif-generator.js.map +1 -0
- package/dist/src/recording/recording-dir.d.ts +5 -0
- package/dist/src/recording/recording-dir.d.ts.map +1 -0
- package/dist/src/recording/recording-dir.js +13 -0
- package/dist/src/recording/recording-dir.js.map +1 -0
- package/dist/src/recording/recording-orchestrator.d.ts +50 -0
- package/dist/src/recording/recording-orchestrator.d.ts.map +1 -0
- package/dist/src/recording/recording-orchestrator.js +98 -0
- package/dist/src/recording/recording-orchestrator.js.map +1 -0
- package/dist/src/recording/resume-detector.d.ts +10 -0
- package/dist/src/recording/resume-detector.d.ts.map +1 -0
- package/dist/src/recording/resume-detector.js +14 -0
- package/dist/src/recording/resume-detector.js.map +1 -0
- package/dist/src/recording/segment-merger.d.ts +27 -0
- package/dist/src/recording/segment-merger.d.ts.map +1 -0
- package/dist/src/recording/segment-merger.js +65 -0
- package/dist/src/recording/segment-merger.js.map +1 -0
- package/dist/src/recording/terminal-recorder.d.ts +31 -0
- package/dist/src/recording/terminal-recorder.d.ts.map +1 -0
- package/dist/src/recording/terminal-recorder.js +111 -0
- package/dist/src/recording/terminal-recorder.js.map +1 -0
- package/dist/src/scripts/postinstall.d.ts +2 -0
- package/dist/src/scripts/postinstall.d.ts.map +1 -0
- package/dist/src/scripts/postinstall.js +27 -0
- package/dist/src/scripts/postinstall.js.map +1 -0
- package/package.json +10 -3
- package/review-agents/performance-reviewer/AGENT.md +31 -0
- package/review-agents/quality-reviewer/AGENT.md +31 -0
- package/review-agents/security-reviewer/AGENT.md +32 -0
- package/role-agents/architect/AGENT.md +30 -0
- package/role-agents/backend-developer/AGENT.md +30 -0
- package/role-agents/designer/AGENT.md +30 -0
- package/role-agents/devops-engineer/AGENT.md +30 -0
- package/role-agents/frontend-developer/AGENT.md +30 -0
- package/role-agents/product-planner/AGENT.md +30 -0
- package/role-agents/qa-engineer/AGENT.md +30 -0
- package/role-agents/researcher/AGENT.md +30 -0
- package/role-agents/technical-writer/AGENT.md +212 -0
- package/skills/agent/SKILL.md +102 -0
- package/skills/execute/SKILL.md +274 -6
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { createReadStream, createWriteStream, existsSync } from 'node:fs';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { Jimp, loadFont } from 'jimp';
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
7
|
+
import GIFEncoder from 'gifencoder';
|
|
8
|
+
// ANSI escape code 제거 정규식
|
|
9
|
+
const ANSI_RE = /\x1B\[[0-9;]*[a-zA-Z]|\x1B\][^\x07]*\x07|\x1B[()][A-Z0-9]|\r/g;
|
|
10
|
+
// 터미널 렌더링 설정
|
|
11
|
+
const CHAR_WIDTH = 9;
|
|
12
|
+
const CHAR_HEIGHT = 18;
|
|
13
|
+
const PADDING = 10;
|
|
14
|
+
const BG_COLOR = 0x1e1e2eff; // dark background
|
|
15
|
+
export class GifGenerator {
|
|
16
|
+
repeat;
|
|
17
|
+
quality;
|
|
18
|
+
frameDelay;
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
this.repeat = options.repeat ?? 0;
|
|
21
|
+
this.quality = options.quality ?? 10;
|
|
22
|
+
this.frameDelay = options.frameDelay ?? 100;
|
|
23
|
+
}
|
|
24
|
+
/** .frames NDJSON 파일을 읽어 GIF 파일로 변환 */
|
|
25
|
+
async generate(framesPath, outputPath) {
|
|
26
|
+
if (!existsSync(framesPath)) {
|
|
27
|
+
throw new Error(`Frames file not found: ${framesPath}`);
|
|
28
|
+
}
|
|
29
|
+
const frames = await this.readFrames(framesPath);
|
|
30
|
+
if (frames.length === 0) {
|
|
31
|
+
throw new Error('No frames found in recording');
|
|
32
|
+
}
|
|
33
|
+
return this.encodeGif(frames, outputPath);
|
|
34
|
+
}
|
|
35
|
+
/** TerminalFrame 배열을 직접 받아 GIF 생성 (SegmentMerger에서 병합된 결과 사용) */
|
|
36
|
+
async generateFromFrames(frames, outputPath) {
|
|
37
|
+
if (frames.length === 0) {
|
|
38
|
+
throw new Error('No frames to encode');
|
|
39
|
+
}
|
|
40
|
+
return this.encodeGif(frames, outputPath);
|
|
41
|
+
}
|
|
42
|
+
async readFrames(framesPath) {
|
|
43
|
+
const frames = [];
|
|
44
|
+
const rl = createInterface({
|
|
45
|
+
input: createReadStream(framesPath),
|
|
46
|
+
crlfDelay: Infinity,
|
|
47
|
+
});
|
|
48
|
+
for await (const line of rl) {
|
|
49
|
+
const trimmed = line.trim();
|
|
50
|
+
if (!trimmed)
|
|
51
|
+
continue;
|
|
52
|
+
try {
|
|
53
|
+
const frame = JSON.parse(trimmed);
|
|
54
|
+
frames.push(frame);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// 파싱 실패한 라인 무시
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return frames;
|
|
61
|
+
}
|
|
62
|
+
async encodeGif(frames, outputPath) {
|
|
63
|
+
const font = await this.loadTerminalFont();
|
|
64
|
+
const firstFrame = frames[0];
|
|
65
|
+
const width = firstFrame.cols * CHAR_WIDTH + PADDING * 2;
|
|
66
|
+
const height = firstFrame.rows * CHAR_HEIGHT + PADDING * 2;
|
|
67
|
+
const encoder = new GIFEncoder(width, height);
|
|
68
|
+
const outputStream = createWriteStream(outputPath);
|
|
69
|
+
encoder.createReadStream().pipe(outputStream);
|
|
70
|
+
encoder.start();
|
|
71
|
+
encoder.setRepeat(this.repeat);
|
|
72
|
+
encoder.setDelay(this.frameDelay);
|
|
73
|
+
encoder.setQuality(this.quality);
|
|
74
|
+
let prevTimestamp = frames[0].timestamp;
|
|
75
|
+
for (const frame of frames) {
|
|
76
|
+
const delay = Math.min(Math.max(frame.timestamp - prevTimestamp, 50), 3000);
|
|
77
|
+
encoder.setDelay(delay || this.frameDelay);
|
|
78
|
+
prevTimestamp = frame.timestamp;
|
|
79
|
+
const imageData = await this.renderFrame(frame, font, width, height);
|
|
80
|
+
encoder.addFrame(imageData);
|
|
81
|
+
}
|
|
82
|
+
encoder.finish();
|
|
83
|
+
await new Promise((resolve, reject) => {
|
|
84
|
+
outputStream.on('finish', resolve);
|
|
85
|
+
outputStream.on('error', reject);
|
|
86
|
+
});
|
|
87
|
+
const { statSync } = await import('node:fs');
|
|
88
|
+
const stat = statSync(outputPath);
|
|
89
|
+
return {
|
|
90
|
+
filePath: outputPath,
|
|
91
|
+
sizeBytes: stat.size,
|
|
92
|
+
frameCount: frames.length,
|
|
93
|
+
durationMs: frames[frames.length - 1].timestamp - frames[0].timestamp,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
async renderFrame(frame, font, width, height) {
|
|
97
|
+
const image = new Jimp({ width, height, color: BG_COLOR });
|
|
98
|
+
const text = stripAnsi(frame.data);
|
|
99
|
+
const lines = text.split('\n');
|
|
100
|
+
let y = PADDING;
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
if (y >= height - PADDING)
|
|
103
|
+
break;
|
|
104
|
+
if (line.length > 0) {
|
|
105
|
+
image.print({ font, x: PADDING, y, text: line });
|
|
106
|
+
}
|
|
107
|
+
y += CHAR_HEIGHT;
|
|
108
|
+
}
|
|
109
|
+
return image.bitmap.data;
|
|
110
|
+
}
|
|
111
|
+
async loadTerminalFont() {
|
|
112
|
+
const req = createRequire(import.meta.url);
|
|
113
|
+
const jimpPath = req.resolve('jimp');
|
|
114
|
+
const fontPath = join(dirname(jimpPath), '../../../@jimp/plugin-print/fonts/open-sans/open-sans-16-white/open-sans-16-white.fnt');
|
|
115
|
+
return loadFont(fontPath);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function stripAnsi(str) {
|
|
119
|
+
return str.replace(ANSI_RE, '');
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=gif-generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gif-generator.js","sourceRoot":"","sources":["../../../src/recording/gif-generator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const RECORDINGS_BASE_DIR = ".gestalt/recordings";
|
|
2
|
+
/** sessionId에 대한 .frames 파일 경로를 반환하고, 부모 디렉토리가 없으면 생성한다 */
|
|
3
|
+
export declare function ensureRecordingsDir(): void;
|
|
4
|
+
export declare function getFramesPath(sessionId: string): string;
|
|
5
|
+
//# sourceMappingURL=recording-dir.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recording-dir.d.ts","sourceRoot":"","sources":["../../../src/recording/recording-dir.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB,wBAAwB,CAAC;AAEzD,2DAA2D;AAC3D,wBAAgB,mBAAmB,IAAI,IAAI,CAI1C;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEvD"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { mkdirSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export const RECORDINGS_BASE_DIR = '.gestalt/recordings';
|
|
4
|
+
/** sessionId에 대한 .frames 파일 경로를 반환하고, 부모 디렉토리가 없으면 생성한다 */
|
|
5
|
+
export function ensureRecordingsDir() {
|
|
6
|
+
if (!existsSync(RECORDINGS_BASE_DIR)) {
|
|
7
|
+
mkdirSync(RECORDINGS_BASE_DIR, { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function getFramesPath(sessionId) {
|
|
11
|
+
return join(RECORDINGS_BASE_DIR, `${sessionId}.frames`);
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=recording-dir.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recording-dir.js","sourceRoot":"","sources":["../../../src/recording/recording-dir.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,CAAC,MAAM,mBAAmB,GAAG,qBAAqB,CAAC;AAEzD,2DAA2D;AAC3D,MAAM,UAAU,mBAAmB;IACjC,IAAI,CAAC,UAAU,CAAC,mBAAmB,CAAC,EAAE,CAAC;QACrC,SAAS,CAAC,mBAAmB,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtD,CAAC;AACH,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,SAAiB;IAC7C,OAAO,IAAI,CAAC,mBAAmB,EAAE,GAAG,SAAS,SAAS,CAAC,CAAC;AAC1D,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { LLMAdapter } from '../llm/types.js';
|
|
2
|
+
export interface RecordingOptions {
|
|
3
|
+
/** --record 또는 -r 플래그 */
|
|
4
|
+
record?: boolean;
|
|
5
|
+
/** --mp4 플래그 — GIF와 함께 mp4도 생성 */
|
|
6
|
+
mp4?: boolean;
|
|
7
|
+
/** 출력 디렉토리 (기본값: 현재 디렉토리) */
|
|
8
|
+
outputDir?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* RecordingOrchestrator: asciinema 기반 녹화의 전체 생명주기를 조율한다.
|
|
12
|
+
*
|
|
13
|
+
* 사용 패턴 (interview CLI):
|
|
14
|
+
*
|
|
15
|
+
* 1. startIfNeeded() — interview 시작 전 호출. --record 플래그가 있고
|
|
16
|
+
* 아직 asciinema로 감싸지지 않았으면 self-respawn으로 재실행.
|
|
17
|
+
*
|
|
18
|
+
* 2. stopAndConvert() — interview 완료 후 호출. 백그라운드 비동기로
|
|
19
|
+
* .cast → GIF (→ mp4) 변환을 트리거한다.
|
|
20
|
+
*/
|
|
21
|
+
export declare class RecordingOrchestrator {
|
|
22
|
+
private readonly llm;
|
|
23
|
+
private readonly asciinemaInstaller;
|
|
24
|
+
private readonly aggInstaller;
|
|
25
|
+
private readonly converter;
|
|
26
|
+
constructor(llm: LLMAdapter);
|
|
27
|
+
/**
|
|
28
|
+
* 필요하면 asciinema 녹화를 시작한다.
|
|
29
|
+
* - GESTALT_RECORDING=1이면 이미 asciinema 안에 있으므로 아무것도 하지 않음.
|
|
30
|
+
* - --record 플래그가 있으면 asciinema를 설치하고 self-respawn.
|
|
31
|
+
* - 재실행 후에는 process.exit()이 호출되므로 이 함수는 return하지 않을 수 있음.
|
|
32
|
+
*/
|
|
33
|
+
startIfNeeded(options: RecordingOptions): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* 현재 프로세스가 asciinema로 녹화 중인지 확인.
|
|
36
|
+
* GESTALT_CAST_PATH 환경변수가 있으면 true.
|
|
37
|
+
*/
|
|
38
|
+
isRecording(): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* 녹화를 종료하고 GIF (+ mp4) 변환을 백그라운드로 트리거한다.
|
|
41
|
+
* asciinema는 부모 프로세스(respawned)에서 자동 종료되므로
|
|
42
|
+
* 여기서는 cast 파일 경로를 읽어 변환만 시작한다.
|
|
43
|
+
*
|
|
44
|
+
* @param topic - 인터뷰 주제 (파일명 생성용)
|
|
45
|
+
* @param sessionId - 세션 ID (파일명 생성용)
|
|
46
|
+
* @param options - 녹화 옵션
|
|
47
|
+
*/
|
|
48
|
+
stopAndConvert(topic: string, sessionId: string, options?: RecordingOptions): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=recording-orchestrator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recording-orchestrator.d.ts","sourceRoot":"","sources":["../../../src/recording/recording-orchestrator.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,MAAM,WAAW,gBAAgB;IAC/B,yBAAyB;IACzB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,kCAAkC;IAClC,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;GAUG;AACH,qBAAa,qBAAqB;IAKpB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAJhC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA4B;IAC/D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAsB;IACnD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAsB;gBAEnB,GAAG,EAAE,UAAU;IAE5C;;;;;OAKG;IACG,aAAa,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAY7D;;;OAGG;IACH,WAAW,IAAI,OAAO;IAItB;;;;;;;;OAQG;IACG,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;CAyCtG"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { AsciinemaInstaller } from './asciinema-installer.js';
|
|
2
|
+
import { AsciinemaRecorder } from './asciinema-recorder.js';
|
|
3
|
+
import { AggInstaller } from './agg-installer.js';
|
|
4
|
+
import { AggConverter } from './agg-converter.js';
|
|
5
|
+
import { FilenameGenerator } from './filename-generator.js';
|
|
6
|
+
/**
|
|
7
|
+
* RecordingOrchestrator: asciinema 기반 녹화의 전체 생명주기를 조율한다.
|
|
8
|
+
*
|
|
9
|
+
* 사용 패턴 (interview CLI):
|
|
10
|
+
*
|
|
11
|
+
* 1. startIfNeeded() — interview 시작 전 호출. --record 플래그가 있고
|
|
12
|
+
* 아직 asciinema로 감싸지지 않았으면 self-respawn으로 재실행.
|
|
13
|
+
*
|
|
14
|
+
* 2. stopAndConvert() — interview 완료 후 호출. 백그라운드 비동기로
|
|
15
|
+
* .cast → GIF (→ mp4) 변환을 트리거한다.
|
|
16
|
+
*/
|
|
17
|
+
export class RecordingOrchestrator {
|
|
18
|
+
llm;
|
|
19
|
+
asciinemaInstaller = new AsciinemaInstaller();
|
|
20
|
+
aggInstaller = new AggInstaller();
|
|
21
|
+
converter = new AggConverter();
|
|
22
|
+
constructor(llm) {
|
|
23
|
+
this.llm = llm;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 필요하면 asciinema 녹화를 시작한다.
|
|
27
|
+
* - GESTALT_RECORDING=1이면 이미 asciinema 안에 있으므로 아무것도 하지 않음.
|
|
28
|
+
* - --record 플래그가 있으면 asciinema를 설치하고 self-respawn.
|
|
29
|
+
* - 재실행 후에는 process.exit()이 호출되므로 이 함수는 return하지 않을 수 있음.
|
|
30
|
+
*/
|
|
31
|
+
async startIfNeeded(options) {
|
|
32
|
+
if (!options.record)
|
|
33
|
+
return;
|
|
34
|
+
if (AsciinemaRecorder.isInsideRecording())
|
|
35
|
+
return;
|
|
36
|
+
await this.asciinemaInstaller.ensureInstalled();
|
|
37
|
+
const castPath = AsciinemaRecorder.createTempCastPath();
|
|
38
|
+
console.log('📹 Starting asciinema recording...\n');
|
|
39
|
+
AsciinemaRecorder.respawnWithAsciinema(castPath);
|
|
40
|
+
// respawnWithAsciinema calls process.exit() — 이 줄은 실행되지 않음
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 현재 프로세스가 asciinema로 녹화 중인지 확인.
|
|
44
|
+
* GESTALT_CAST_PATH 환경변수가 있으면 true.
|
|
45
|
+
*/
|
|
46
|
+
isRecording() {
|
|
47
|
+
return AsciinemaRecorder.isInsideRecording() && !!AsciinemaRecorder.getCurrentCastPath();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 녹화를 종료하고 GIF (+ mp4) 변환을 백그라운드로 트리거한다.
|
|
51
|
+
* asciinema는 부모 프로세스(respawned)에서 자동 종료되므로
|
|
52
|
+
* 여기서는 cast 파일 경로를 읽어 변환만 시작한다.
|
|
53
|
+
*
|
|
54
|
+
* @param topic - 인터뷰 주제 (파일명 생성용)
|
|
55
|
+
* @param sessionId - 세션 ID (파일명 생성용)
|
|
56
|
+
* @param options - 녹화 옵션
|
|
57
|
+
*/
|
|
58
|
+
async stopAndConvert(topic, sessionId, options = {}) {
|
|
59
|
+
const castPath = AsciinemaRecorder.getCurrentCastPath();
|
|
60
|
+
if (!castPath)
|
|
61
|
+
return;
|
|
62
|
+
try {
|
|
63
|
+
await this.aggInstaller.ensureInstalled();
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
67
|
+
console.error(`⚠️ agg installation failed: ${msg}`);
|
|
68
|
+
console.error(' GIF conversion skipped. The .cast file is preserved at:', castPath);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const filenameGen = new FilenameGenerator(this.llm, { outputDir: options.outputDir });
|
|
72
|
+
const gifPath = await filenameGen.generate(topic, sessionId);
|
|
73
|
+
console.log('\n🎬 Converting recording to GIF in background...');
|
|
74
|
+
// 백그라운드 비동기 — await하지 않음
|
|
75
|
+
void this.converter
|
|
76
|
+
.convertAsync(castPath, gifPath, {
|
|
77
|
+
deleteCastAfter: true,
|
|
78
|
+
onComplete: (outputPath) => {
|
|
79
|
+
console.log(`✅ GIF saved: ${outputPath}\n`);
|
|
80
|
+
if (options.mp4) {
|
|
81
|
+
const mp4Path = outputPath.replace(/\.gif$/, '.mp4');
|
|
82
|
+
void this.converter.convertGifToMp4Async(outputPath, mp4Path, {
|
|
83
|
+
onComplete: (p) => console.log(`✅ MP4 saved: ${p}\n`),
|
|
84
|
+
onError: (e) => console.error(`⚠️ MP4 conversion failed: ${e.message}`),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
onError: (err) => {
|
|
89
|
+
console.error(`⚠️ GIF conversion failed: ${err.message}`);
|
|
90
|
+
console.error(' The .cast file may be preserved at:', castPath);
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
.catch(() => {
|
|
94
|
+
// onError에서 처리됨
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=recording-orchestrator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recording-orchestrator.js","sourceRoot":"","sources":["../../../src/recording/recording-orchestrator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface ResumeInfo {
|
|
2
|
+
isResuming: boolean;
|
|
3
|
+
framesPath: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* sessionId에 해당하는 .frames 임시 파일이 존재하는지 확인한다.
|
|
7
|
+
* 존재하면 이전 녹화를 이어붙일 수 있음을 의미한다.
|
|
8
|
+
*/
|
|
9
|
+
export declare function detectResume(sessionId: string): ResumeInfo;
|
|
10
|
+
//# sourceMappingURL=resume-detector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resume-detector.d.ts","sourceRoot":"","sources":["../../../src/recording/resume-detector.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,CAM1D"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { getFramesPath } from './recording-dir.js';
|
|
3
|
+
/**
|
|
4
|
+
* sessionId에 해당하는 .frames 임시 파일이 존재하는지 확인한다.
|
|
5
|
+
* 존재하면 이전 녹화를 이어붙일 수 있음을 의미한다.
|
|
6
|
+
*/
|
|
7
|
+
export function detectResume(sessionId) {
|
|
8
|
+
const framesPath = getFramesPath(sessionId);
|
|
9
|
+
return {
|
|
10
|
+
isResuming: existsSync(framesPath),
|
|
11
|
+
framesPath,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=resume-detector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resume-detector.js","sourceRoot":"","sources":["../../../src/recording/resume-detector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAOnD;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,SAAiB;IAC5C,MAAM,UAAU,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;IAC5C,OAAO;QACL,UAAU,EAAE,UAAU,CAAC,UAAU,CAAC;QAClC,UAAU;KACX,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { TerminalFrame } from '../core/types.js';
|
|
2
|
+
import { GifGenerator } from './gif-generator.js';
|
|
3
|
+
/**
|
|
4
|
+
* SegmentMerger: 동일 sessionId의 여러 .frames 파일 세그먼트를
|
|
5
|
+
* 시간순으로 병합해 GifGenerator에 전달한다.
|
|
6
|
+
*
|
|
7
|
+
* 현재 아키텍처에서는 sessionId당 .frames 파일이 1개이므로,
|
|
8
|
+
* 단일 파일 읽기도 이 클래스를 경유해 일관된 인터페이스를 유지한다.
|
|
9
|
+
*/
|
|
10
|
+
export declare class SegmentMerger {
|
|
11
|
+
private readonly gifGenerator;
|
|
12
|
+
constructor(gifGenerator?: GifGenerator);
|
|
13
|
+
/**
|
|
14
|
+
* 여러 .frames 파일을 읽어 타임스탬프 기준 오름차순으로 병합한다.
|
|
15
|
+
* 각 세그먼트 사이의 긴 갭(5초 이상)은 3초로 압축한다.
|
|
16
|
+
*/
|
|
17
|
+
mergeFrameFiles(framesPaths: string[]): Promise<TerminalFrame[]>;
|
|
18
|
+
/**
|
|
19
|
+
* 단일 .frames 파일 읽기 (공통 인터페이스 유지)
|
|
20
|
+
*/
|
|
21
|
+
readSingleFile(framesPath: string): Promise<TerminalFrame[]>;
|
|
22
|
+
/**
|
|
23
|
+
* 타임스탬프를 오름차순 정렬하고 큰 갭을 압축한다.
|
|
24
|
+
*/
|
|
25
|
+
private normalizeTimestamps;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=segment-merger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"segment-merger.d.ts","sourceRoot":"","sources":["../../../src/recording/segment-merger.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD;;;;;;GAMG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;gBAEhC,YAAY,CAAC,EAAE,YAAY;IAIvC;;;OAGG;IACG,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IAYtE;;OAEG;IACG,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IAIlE;;OAEG;IACH,OAAO,CAAC,mBAAmB;CA4B5B"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { GifGenerator } from './gif-generator.js';
|
|
3
|
+
/**
|
|
4
|
+
* SegmentMerger: 동일 sessionId의 여러 .frames 파일 세그먼트를
|
|
5
|
+
* 시간순으로 병합해 GifGenerator에 전달한다.
|
|
6
|
+
*
|
|
7
|
+
* 현재 아키텍처에서는 sessionId당 .frames 파일이 1개이므로,
|
|
8
|
+
* 단일 파일 읽기도 이 클래스를 경유해 일관된 인터페이스를 유지한다.
|
|
9
|
+
*/
|
|
10
|
+
export class SegmentMerger {
|
|
11
|
+
gifGenerator;
|
|
12
|
+
constructor(gifGenerator) {
|
|
13
|
+
this.gifGenerator = gifGenerator ?? new GifGenerator();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 여러 .frames 파일을 읽어 타임스탬프 기준 오름차순으로 병합한다.
|
|
17
|
+
* 각 세그먼트 사이의 긴 갭(5초 이상)은 3초로 압축한다.
|
|
18
|
+
*/
|
|
19
|
+
async mergeFrameFiles(framesPaths) {
|
|
20
|
+
const allFrames = [];
|
|
21
|
+
for (const framesPath of framesPaths) {
|
|
22
|
+
if (!existsSync(framesPath))
|
|
23
|
+
continue;
|
|
24
|
+
const frames = await this.gifGenerator.readFrames(framesPath);
|
|
25
|
+
allFrames.push(...frames);
|
|
26
|
+
}
|
|
27
|
+
return this.normalizeTimestamps(allFrames);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 단일 .frames 파일 읽기 (공통 인터페이스 유지)
|
|
31
|
+
*/
|
|
32
|
+
async readSingleFile(framesPath) {
|
|
33
|
+
return this.mergeFrameFiles([framesPath]);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 타임스탬프를 오름차순 정렬하고 큰 갭을 압축한다.
|
|
37
|
+
*/
|
|
38
|
+
normalizeTimestamps(frames) {
|
|
39
|
+
if (frames.length === 0)
|
|
40
|
+
return [];
|
|
41
|
+
// 타임스탬프 오름차순 정렬
|
|
42
|
+
frames.sort((a, b) => a.timestamp - b.timestamp);
|
|
43
|
+
// 세그먼트 갭 압축: 5초 이상 빈 구간은 3초로 압축
|
|
44
|
+
const GAP_THRESHOLD_MS = 5000;
|
|
45
|
+
const MAX_GAP_MS = 3000;
|
|
46
|
+
const normalized = [frames[0]];
|
|
47
|
+
for (let i = 1; i < frames.length; i++) {
|
|
48
|
+
const curr = frames[i];
|
|
49
|
+
const prev_frame = frames[i - 1];
|
|
50
|
+
const gap = curr.timestamp - prev_frame.timestamp;
|
|
51
|
+
if (gap > GAP_THRESHOLD_MS) {
|
|
52
|
+
const prev = normalized[normalized.length - 1];
|
|
53
|
+
normalized.push({
|
|
54
|
+
...curr,
|
|
55
|
+
timestamp: prev.timestamp + MAX_GAP_MS,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
normalized.push(curr);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return normalized;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=segment-merger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"segment-merger.js","sourceRoot":"","sources":["../../../src/recording/segment-merger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAErC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD;;;;;;GAMG;AACH,MAAM,OAAO,aAAa;IACP,YAAY,CAAe;IAE5C,YAAY,YAA2B;QACrC,IAAI,CAAC,YAAY,GAAG,YAAY,IAAI,IAAI,YAAY,EAAE,CAAC;IACzD,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,eAAe,CAAC,WAAqB;QACzC,MAAM,SAAS,GAAoB,EAAE,CAAC;QAEtC,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;YACrC,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;gBAAE,SAAS;YACtC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;YAC9D,SAAS,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC;QAC5B,CAAC;QAED,OAAO,IAAI,CAAC,mBAAmB,CAAC,SAAS,CAAC,CAAC;IAC7C,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,cAAc,CAAC,UAAkB;QACrC,OAAO,IAAI,CAAC,eAAe,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;IAC5C,CAAC;IAED;;OAEG;IACK,mBAAmB,CAAC,MAAuB;QACjD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAEnC,gBAAgB;QAChB,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC;QAEjD,gCAAgC;QAChC,MAAM,gBAAgB,GAAG,IAAI,CAAC;QAC9B,MAAM,UAAU,GAAG,IAAI,CAAC;QAExB,MAAM,UAAU,GAAoB,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,CAAC;QACjD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACvC,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAE,CAAC;YACxB,MAAM,UAAU,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC;YAClC,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,GAAG,UAAU,CAAC,SAAS,CAAC;YAClD,IAAI,GAAG,GAAG,gBAAgB,EAAE,CAAC;gBAC3B,MAAM,IAAI,GAAG,UAAU,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC;gBAChD,UAAU,CAAC,IAAI,CAAC;oBACd,GAAG,IAAI;oBACP,SAAS,EAAE,IAAI,CAAC,SAAS,GAAG,UAAU;iBACvC,CAAC,CAAC;YACL,CAAC;iBAAM,CAAC;gBACN,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACxB,CAAC;QACH,CAAC;QAED,OAAO,UAAU,CAAC;IACpB,CAAC;CACF"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RecordingSegment } from '../core/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* TerminalRecorder: process.stdout.write를 인터셉트하여
|
|
4
|
+
* TerminalFrame을 .frames NDJSON 파일에 실시간 append한다.
|
|
5
|
+
*
|
|
6
|
+
* node-pty 없이도 동작하지만, PTY 기반 캡처가 필요한 경우
|
|
7
|
+
* 향후 node-pty 통합으로 확장 가능하도록 설계한다.
|
|
8
|
+
*/
|
|
9
|
+
export declare class TerminalRecorder {
|
|
10
|
+
private readonly sessionId;
|
|
11
|
+
private readonly framesPath;
|
|
12
|
+
private segment;
|
|
13
|
+
private isRecording;
|
|
14
|
+
private frameBuffer;
|
|
15
|
+
private originalStdoutWrite;
|
|
16
|
+
constructor(sessionId: string);
|
|
17
|
+
/** 녹화 시작. 기존 .frames 파일이 있으면 이어서 append (resume) */
|
|
18
|
+
start(): RecordingSegment;
|
|
19
|
+
/** 녹화 일시 중지 (세션 중단 시) */
|
|
20
|
+
pause(): void;
|
|
21
|
+
/** 녹화 완전 종료 */
|
|
22
|
+
stop(): RecordingSegment | null;
|
|
23
|
+
get recording(): boolean;
|
|
24
|
+
get framesFilePath(): string;
|
|
25
|
+
/** stdout.write 인터셉트 */
|
|
26
|
+
private interceptStdout;
|
|
27
|
+
private restoreStdout;
|
|
28
|
+
private captureFrame;
|
|
29
|
+
private flushBuffer;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=terminal-recorder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-recorder.d.ts","sourceRoot":"","sources":["../../../src/recording/terminal-recorder.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAExE;;;;;;GAMG;AACH,qBAAa,gBAAgB;IASf,OAAO,CAAC,QAAQ,CAAC,SAAS;IARtC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,OAAO,CAAiC;IAChD,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAuB;IAG1C,OAAO,CAAC,mBAAmB,CAA+B;gBAE7B,SAAS,EAAE,MAAM;IAK9C,oDAAoD;IACpD,KAAK,IAAI,gBAAgB;IAoBzB,yBAAyB;IACzB,KAAK,IAAI,IAAI;IAUb,eAAe;IACf,IAAI,IAAI,gBAAgB,GAAG,IAAI;IAO/B,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,wBAAwB;IACxB,OAAO,CAAC,eAAe;IA2BvB,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,YAAY;IAkBpB,OAAO,CAAC,WAAW;CAMpB"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { appendFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { ensureRecordingsDir, getFramesPath } from './recording-dir.js';
|
|
3
|
+
/**
|
|
4
|
+
* TerminalRecorder: process.stdout.write를 인터셉트하여
|
|
5
|
+
* TerminalFrame을 .frames NDJSON 파일에 실시간 append한다.
|
|
6
|
+
*
|
|
7
|
+
* node-pty 없이도 동작하지만, PTY 기반 캡처가 필요한 경우
|
|
8
|
+
* 향후 node-pty 통합으로 확장 가능하도록 설계한다.
|
|
9
|
+
*/
|
|
10
|
+
export class TerminalRecorder {
|
|
11
|
+
sessionId;
|
|
12
|
+
framesPath;
|
|
13
|
+
segment = null;
|
|
14
|
+
isRecording = false;
|
|
15
|
+
frameBuffer = [];
|
|
16
|
+
// 원본 stdout.write 저장
|
|
17
|
+
originalStdoutWrite;
|
|
18
|
+
constructor(sessionId) {
|
|
19
|
+
this.sessionId = sessionId;
|
|
20
|
+
ensureRecordingsDir();
|
|
21
|
+
this.framesPath = getFramesPath(sessionId);
|
|
22
|
+
}
|
|
23
|
+
/** 녹화 시작. 기존 .frames 파일이 있으면 이어서 append (resume) */
|
|
24
|
+
start() {
|
|
25
|
+
if (this.isRecording)
|
|
26
|
+
return this.segment;
|
|
27
|
+
const isResuming = existsSync(this.framesPath);
|
|
28
|
+
this.segment = {
|
|
29
|
+
sessionId: this.sessionId,
|
|
30
|
+
framesPath: this.framesPath,
|
|
31
|
+
startedAt: Date.now(),
|
|
32
|
+
};
|
|
33
|
+
this.isRecording = true;
|
|
34
|
+
this.interceptStdout();
|
|
35
|
+
if (!isResuming) {
|
|
36
|
+
// 새 녹화: 빈 파일로 시작
|
|
37
|
+
}
|
|
38
|
+
return this.segment;
|
|
39
|
+
}
|
|
40
|
+
/** 녹화 일시 중지 (세션 중단 시) */
|
|
41
|
+
pause() {
|
|
42
|
+
if (!this.isRecording)
|
|
43
|
+
return;
|
|
44
|
+
this.restoreStdout();
|
|
45
|
+
this.flushBuffer();
|
|
46
|
+
if (this.segment) {
|
|
47
|
+
this.segment.endedAt = Date.now();
|
|
48
|
+
}
|
|
49
|
+
this.isRecording = false;
|
|
50
|
+
}
|
|
51
|
+
/** 녹화 완전 종료 */
|
|
52
|
+
stop() {
|
|
53
|
+
if (this.isRecording) {
|
|
54
|
+
this.pause();
|
|
55
|
+
}
|
|
56
|
+
return this.segment;
|
|
57
|
+
}
|
|
58
|
+
get recording() {
|
|
59
|
+
return this.isRecording;
|
|
60
|
+
}
|
|
61
|
+
get framesFilePath() {
|
|
62
|
+
return this.framesPath;
|
|
63
|
+
}
|
|
64
|
+
/** stdout.write 인터셉트 */
|
|
65
|
+
interceptStdout() {
|
|
66
|
+
const self = this;
|
|
67
|
+
this.originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
68
|
+
// overwrite with interceptor
|
|
69
|
+
const intercepted = function (chunk, encodingOrCallback, callback) {
|
|
70
|
+
const data = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8');
|
|
71
|
+
self.captureFrame(data);
|
|
72
|
+
// 원본 write 호출
|
|
73
|
+
if (typeof encodingOrCallback === 'function') {
|
|
74
|
+
return self.originalStdoutWrite(chunk, encodingOrCallback);
|
|
75
|
+
}
|
|
76
|
+
if (typeof encodingOrCallback === 'string') {
|
|
77
|
+
return self.originalStdoutWrite(chunk, encodingOrCallback, callback);
|
|
78
|
+
}
|
|
79
|
+
return self.originalStdoutWrite(chunk);
|
|
80
|
+
};
|
|
81
|
+
process.stdout.write = intercepted;
|
|
82
|
+
}
|
|
83
|
+
restoreStdout() {
|
|
84
|
+
if (this.originalStdoutWrite) {
|
|
85
|
+
process.stdout.write = this.originalStdoutWrite;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
captureFrame(data) {
|
|
89
|
+
if (!data || !this.isRecording)
|
|
90
|
+
return;
|
|
91
|
+
const frame = {
|
|
92
|
+
timestamp: Date.now(),
|
|
93
|
+
data,
|
|
94
|
+
cols: process.stdout.columns ?? 80,
|
|
95
|
+
rows: process.stdout.rows ?? 24,
|
|
96
|
+
};
|
|
97
|
+
this.frameBuffer.push(frame);
|
|
98
|
+
// 버퍼가 10개 이상 쌓이면 즉시 flush
|
|
99
|
+
if (this.frameBuffer.length >= 10) {
|
|
100
|
+
this.flushBuffer();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
flushBuffer() {
|
|
104
|
+
if (this.frameBuffer.length === 0)
|
|
105
|
+
return;
|
|
106
|
+
const lines = this.frameBuffer.map((f) => JSON.stringify(f)).join('\n') + '\n';
|
|
107
|
+
appendFileSync(this.framesPath, lines, 'utf8');
|
|
108
|
+
this.frameBuffer = [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=terminal-recorder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-recorder.js","sourceRoot":"","sources":["../../../src/recording/terminal-recorder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postinstall.d.ts","sourceRoot":"","sources":["../../../src/scripts/postinstall.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postinstall script: auto-setup agg for GIF recording support.
|
|
3
|
+
* Runs after: npm install @tienne/gestalt
|
|
4
|
+
*
|
|
5
|
+
* - Silently skips in CI or if GESTALT_SKIP_DEPS=1
|
|
6
|
+
* - Always exits 0 (never fails the install)
|
|
7
|
+
*/
|
|
8
|
+
import { AggInstaller } from '../recording/agg-installer.js';
|
|
9
|
+
async function main() {
|
|
10
|
+
if (process.env['CI'] || process.env['GESTALT_SKIP_DEPS'] === '1') {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const installer = new AggInstaller();
|
|
14
|
+
if (installer.isInstalled())
|
|
15
|
+
return;
|
|
16
|
+
process.stdout.write('\n[gestalt] agg not found — setting up GIF recording support...\n');
|
|
17
|
+
try {
|
|
18
|
+
await installer.ensureInstalled();
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
22
|
+
process.stdout.write(`[gestalt] Skipping agg setup: ${msg}\n`);
|
|
23
|
+
process.stdout.write('[gestalt] Install manually later: https://github.com/asciinema/agg\n\n');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
main().then(() => process.exit(0)).catch(() => process.exit(0));
|
|
27
|
+
//# sourceMappingURL=postinstall.js.map
|