@zsukim/ctv-run 1.0.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 +277 -0
- package/dist/config.js +29 -0
- package/dist/firetv.js +67 -0
- package/dist/index.js +61 -0
- package/dist/platforms/firetv.js +119 -0
- package/dist/platforms/tizen.js +224 -0
- package/dist/platforms/vizio.js +162 -0
- package/dist/platforms/webos.js +150 -0
- package/dist/tizen.js +94 -0
- package/dist/util.js +35 -0
- package/dist/utils/config.js +20 -0
- package/dist/utils/ip.js +14 -0
- package/dist/utils/platform.js +29 -0
- package/dist/vizio.js +161 -0
- package/dist/webos.js +87 -0
- package/package.json +43 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadConfig = loadConfig;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
function loadConfig() {
|
|
10
|
+
const configPath = path_1.default.resolve(process.cwd(), 'ctv.config.json');
|
|
11
|
+
if (fs_1.default.existsSync(configPath)) {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
console.warn('⚠️ ctv.config.json 형식이 올바르지 않습니다. 기본값을 사용합니다.');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return {};
|
|
20
|
+
}
|
package/dist/utils/ip.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getLocalIp = getLocalIp;
|
|
4
|
+
const os_1 = require("os");
|
|
5
|
+
function getLocalIp() {
|
|
6
|
+
const nets = (0, os_1.networkInterfaces)();
|
|
7
|
+
for (const name of Object.keys(nets)) {
|
|
8
|
+
for (const net of nets[name]) {
|
|
9
|
+
if (net.family === 'IPv4' && !net.internal)
|
|
10
|
+
return net.address;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return 'localhost';
|
|
14
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.makeArgParser = makeArgParser;
|
|
7
|
+
exports.resolveIcon = resolveIcon;
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
function makeArgParser(args) {
|
|
11
|
+
return (name) => { var _a; return (_a = args.find((arg) => arg.startsWith(`--${name}=`))) === null || _a === void 0 ? void 0 : _a.split('=')[1]; };
|
|
12
|
+
}
|
|
13
|
+
function resolveIcon(tempDir) {
|
|
14
|
+
const targetIconPath = path_1.default.join(tempDir, 'icon.png');
|
|
15
|
+
const sourceIconPath = path_1.default.resolve(process.cwd(), 'src', 'assets', 'icon.png');
|
|
16
|
+
if (fs_extra_1.default.existsSync(targetIconPath)) {
|
|
17
|
+
console.log('✅ 기존 프로젝트의 아이콘을 유지합니다: icon.png');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (fs_extra_1.default.existsSync(sourceIconPath)) {
|
|
21
|
+
fs_extra_1.default.copySync(sourceIconPath, targetIconPath);
|
|
22
|
+
console.log('🖼 기본 아이콘을 가져왔습니다.');
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
const dummyPng = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=', 'base64');
|
|
26
|
+
fs_extra_1.default.writeFileSync(targetIconPath, dummyPng);
|
|
27
|
+
console.log('🖼 아이콘 파일이 없어 더미 이미지를 생성했습니다.');
|
|
28
|
+
}
|
|
29
|
+
}
|
package/dist/vizio.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.runVizio = runVizio;
|
|
49
|
+
const playwright_1 = require("playwright");
|
|
50
|
+
const readline = __importStar(require("readline/promises"));
|
|
51
|
+
const path_1 = __importDefault(require("path"));
|
|
52
|
+
const http_1 = __importDefault(require("http"));
|
|
53
|
+
const util_1 = require("./util");
|
|
54
|
+
const serveHandler = require('serve-handler');
|
|
55
|
+
function runVizio() {
|
|
56
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
57
|
+
var _a, _b;
|
|
58
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
59
|
+
const args = process.argv.slice(2);
|
|
60
|
+
const config = (0, util_1.loadConfig)();
|
|
61
|
+
const getArg = (name) => { var _a; return (_a = args.find(arg => arg.startsWith(`--${name}=`))) === null || _a === void 0 ? void 0 : _a.split('=')[1]; };
|
|
62
|
+
const tvIp = getArg('ip') || ((_a = config.vizio) === null || _a === void 0 ? void 0 : _a.ip);
|
|
63
|
+
if (!tvIp) {
|
|
64
|
+
console.error(`\n❌ Vizio TV의 IP 주소가 없습니다!`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const defaultPort = Number(getArg('port')) || ((_b = config.vizio) === null || _b === void 0 ? void 0 : _b.port) || 3300;
|
|
68
|
+
const launcherUrl = 'https://vizio-pm.s3-us-west-1.amazonaws.com/conjure-launcher.html';
|
|
69
|
+
const targetArg = getArg('target');
|
|
70
|
+
const urlArg = getArg('url');
|
|
71
|
+
let appUrl = '';
|
|
72
|
+
if (urlArg) {
|
|
73
|
+
appUrl = urlArg;
|
|
74
|
+
}
|
|
75
|
+
else if (targetArg) {
|
|
76
|
+
const distPath = path_1.default.resolve(process.cwd(), targetArg);
|
|
77
|
+
const server = http_1.default.createServer((req, res) => serveHandler(req, res, { public: distPath }));
|
|
78
|
+
server.listen(3000, '0.0.0.0');
|
|
79
|
+
appUrl = `http://${(0, util_1.getLocalIp)()}:3000`;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
appUrl = `http://${(0, util_1.getLocalIp)()}.nip.io:${defaultPort}`;
|
|
83
|
+
}
|
|
84
|
+
console.log(`-----------------------------------------`);
|
|
85
|
+
console.log(`📡 Vizio TV IP : ${tvIp}`);
|
|
86
|
+
console.log(`🚀 App URL : ${appUrl}`);
|
|
87
|
+
console.log(`-----------------------------------------`);
|
|
88
|
+
// [개선] 로컬 네트워크 권한 팝업을 차단하기 위한 브라우저 실행 인자 설정
|
|
89
|
+
const browser = yield playwright_1.chromium.launch({
|
|
90
|
+
headless: false,
|
|
91
|
+
args: [
|
|
92
|
+
'--disable-web-security',
|
|
93
|
+
'--disable-features=BlockInsecurePrivateNetworkRequests'
|
|
94
|
+
]
|
|
95
|
+
});
|
|
96
|
+
const context = yield browser.newContext({ ignoreHTTPSErrors: true });
|
|
97
|
+
const page = yield context.newPage();
|
|
98
|
+
try {
|
|
99
|
+
console.log(`🌐 런처 접속 중: ${launcherUrl}`);
|
|
100
|
+
yield page.goto(launcherUrl);
|
|
101
|
+
console.log(`⌨️ TV IP 입력 중: ${tvIp}`);
|
|
102
|
+
yield page.waitForSelector('input[placeholder*="IP"]');
|
|
103
|
+
yield page.fill('input[placeholder*="IP"]', tvIp);
|
|
104
|
+
// 스크린샷 10.34.07 반영: Start Pairing 버튼 클릭
|
|
105
|
+
console.log('🔘 Start Pairing 클릭');
|
|
106
|
+
yield page.locator('input[value="Start Pairing"]').click();
|
|
107
|
+
// SSL 인증 자동화 (스크린샷 10.25.20, 10.25.29 대응)
|
|
108
|
+
const sslLink = page.locator('a', { hasText: 'Start SSL process' });
|
|
109
|
+
if (yield sslLink.isVisible({ timeout: 5000 })) {
|
|
110
|
+
console.log('⏳ SSL 인증 페이지 자동 돌파 시도...');
|
|
111
|
+
const [sslPage] = yield Promise.all([
|
|
112
|
+
context.waitForEvent('page'),
|
|
113
|
+
sslLink.click()
|
|
114
|
+
]);
|
|
115
|
+
if (sslPage) {
|
|
116
|
+
yield sslPage.waitForLoadState('domcontentloaded');
|
|
117
|
+
try {
|
|
118
|
+
yield sslPage.locator('#details-button').click({ timeout: 5000 });
|
|
119
|
+
yield sslPage.locator('#proceed-link').click({ timeout: 5000 });
|
|
120
|
+
console.log('✅ SSL 인증 수락 완료.');
|
|
121
|
+
yield sslPage.waitForTimeout(1000);
|
|
122
|
+
yield sslPage.close();
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
console.log('ℹ️ SSL 경고창이 이미 승인되었거나 뜨지 않았습니다.');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Continue 클릭 (이미지 10.25.20의 3단계)
|
|
130
|
+
const continueBtn = page.locator('a', { hasText: 'Continue' });
|
|
131
|
+
if (yield continueBtn.isVisible()) {
|
|
132
|
+
yield continueBtn.click();
|
|
133
|
+
}
|
|
134
|
+
console.log('----------------------------------------------------');
|
|
135
|
+
console.log('📺 TV 화면의 PIN 4자리를 입력하세요.');
|
|
136
|
+
const pin = yield rl.question('PIN: ');
|
|
137
|
+
console.log('----------------------------------------------------');
|
|
138
|
+
// PIN 입력창 대기 및 입력
|
|
139
|
+
yield page.waitForSelector('input[placeholder*="PIN"]');
|
|
140
|
+
yield page.fill('input[placeholder*="PIN"]', pin);
|
|
141
|
+
// [스크린샷 10.39.39 반영] Pair 버튼 클릭 (value="Pair")
|
|
142
|
+
console.log('🔘 Pair 버튼 클릭');
|
|
143
|
+
yield page.locator('input[value="Pair"]').click();
|
|
144
|
+
console.log(`🔗 App URL 주입 중: ${appUrl}`);
|
|
145
|
+
// 페어링 완료 후 App URL 입력 필드가 나타날 때까지 대기
|
|
146
|
+
const urlInput = page.locator('#app-url');
|
|
147
|
+
yield urlInput.waitFor({ state: 'visible', timeout: 30000 });
|
|
148
|
+
// 기존 텍스트가 있을 수 있으므로 fill 대신 clear 후 fill 권장
|
|
149
|
+
yield urlInput.click(); // 스크린샷의 onclick="this.select();" 대응
|
|
150
|
+
yield urlInput.fill(appUrl);
|
|
151
|
+
console.log(`✅ URL 주입 완료: ${appUrl}`);
|
|
152
|
+
// [스크린샷 반영] Launch 버튼 클릭 (id="launch-btn")
|
|
153
|
+
console.log('🚀 Launch 버튼 클릭!');
|
|
154
|
+
yield page.locator('#launch-btn').click();
|
|
155
|
+
console.log('✅ 모든 명령이 성공적으로 전달되었습니다!');
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
console.error('❌ 실행 중 에러 발생:', err);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
package/dist/webos.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.runWebOS = runWebOS;
|
|
16
|
+
const child_process_1 = require("child_process");
|
|
17
|
+
const util_1 = require("./util");
|
|
18
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
19
|
+
const path_1 = __importDefault(require("path"));
|
|
20
|
+
function runWebOS() {
|
|
21
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
22
|
+
var _a, _b;
|
|
23
|
+
const args = process.argv.slice(2);
|
|
24
|
+
const config = (0, util_1.loadConfig)();
|
|
25
|
+
const getArg = (name) => { var _a; return (_a = args.find(arg => arg.startsWith(`--${name}=`))) === null || _a === void 0 ? void 0 : _a.split('=')[1]; };
|
|
26
|
+
// 1. 필수 값 추출 (기본값 제거)
|
|
27
|
+
const deviceName = getArg('device') || ((_a = config.webos) === null || _a === void 0 ? void 0 : _a.deviceName);
|
|
28
|
+
const appId = getArg('appId') || ((_b = config.webos) === null || _b === void 0 ? void 0 : _b.appId);
|
|
29
|
+
const targetArg = getArg('target');
|
|
30
|
+
const appUrl = getArg('url') || `http://${(0, util_1.getLocalIp)()}:5173`;
|
|
31
|
+
// 2. 필수 값 체크 (appId와 deviceName이 없으면 실행 중단)
|
|
32
|
+
if (!deviceName || !appId) {
|
|
33
|
+
console.error(`\n❌ 필수 설정이 누락되었습니다!`);
|
|
34
|
+
console.log(`\n[해결 방법]`);
|
|
35
|
+
console.log(`1. ctv.config.json에 'webos.deviceName'과 'webos.appId'를 설정하세요.`);
|
|
36
|
+
console.log(`2. 또는 실행 시 인자를 추가하세요: ctv-run webos --device=lg-tv --appId=com.my.app`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const tempAppDir = path_1.default.resolve(process.cwd(), '.ctv-temp-webos');
|
|
40
|
+
try {
|
|
41
|
+
if (fs_extra_1.default.existsSync(tempAppDir))
|
|
42
|
+
fs_extra_1.default.removeSync(tempAppDir);
|
|
43
|
+
console.log(`🛠️ Generating webOS app structure...`);
|
|
44
|
+
// appId를 동적으로 받아 생성
|
|
45
|
+
(0, child_process_1.execSync)(`ares-generate -t basic -p "id=${appId},title=CTV-App" ${tempAppDir}`, { stdio: 'inherit' });
|
|
46
|
+
if (targetArg) {
|
|
47
|
+
// [이사 모드] 빌드된 파일 복사
|
|
48
|
+
const distPath = path_1.default.resolve(process.cwd(), targetArg);
|
|
49
|
+
if (!fs_extra_1.default.existsSync(distPath))
|
|
50
|
+
throw new Error(`❌ 폴더를 찾을 수 없습니다: ${distPath}`);
|
|
51
|
+
console.log(`📦 [Static Mode] Copying from ${distPath}...`);
|
|
52
|
+
fs_extra_1.default.copySync(distPath, tempAppDir, { overwrite: true });
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// [창문 모드] 리다이렉트 HTML 생성
|
|
56
|
+
console.log(`🌐 [Live Mode] Redirecting TV to ${appUrl}...`);
|
|
57
|
+
const redirectHtml = `
|
|
58
|
+
<!DOCTYPE html>
|
|
59
|
+
<html>
|
|
60
|
+
<head>
|
|
61
|
+
<meta charset="utf-8">
|
|
62
|
+
<script type="text/javascript">
|
|
63
|
+
window.onload = function() { window.location.href = "${appUrl}"; };
|
|
64
|
+
</script>
|
|
65
|
+
</head>
|
|
66
|
+
<body style="background:#000;color:#fff;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;">
|
|
67
|
+
<h2>🚀 Connecting to Dev Server...</h2>
|
|
68
|
+
</body>
|
|
69
|
+
</html>
|
|
70
|
+
`;
|
|
71
|
+
fs_extra_1.default.writeFileSync(path_1.default.join(tempAppDir, 'index.html'), redirectHtml);
|
|
72
|
+
}
|
|
73
|
+
console.log(`🔨 Packaging & Installing [${appId}]...`);
|
|
74
|
+
(0, child_process_1.execSync)(`ares-package ${tempAppDir}`, { stdio: 'inherit' });
|
|
75
|
+
const ipkFile = fs_extra_1.default.readdirSync(process.cwd()).find(f => f.startsWith(appId) && f.endsWith('.ipk'));
|
|
76
|
+
if (ipkFile) {
|
|
77
|
+
(0, child_process_1.execSync)(`ares-install -d ${deviceName} ${ipkFile}`, { stdio: 'inherit' });
|
|
78
|
+
(0, child_process_1.execSync)(`ares-launch -d ${deviceName} ${appId}`, { stdio: 'inherit' });
|
|
79
|
+
fs_extra_1.default.removeSync(path_1.default.resolve(process.cwd(), ipkFile));
|
|
80
|
+
console.log(`\n✅ webOS Launch Success!`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
console.error(`\n❌ Error: ${err.message}`);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zsukim/ctv-run",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Smart TV deployment CLI for Vizio, LG webOS, Fire TV, and Samsung Tizen",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ctv",
|
|
7
|
+
"smart-tv",
|
|
8
|
+
"tizen",
|
|
9
|
+
"webos",
|
|
10
|
+
"firetv",
|
|
11
|
+
"vizio",
|
|
12
|
+
"cli",
|
|
13
|
+
"deploy"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"main": "dist/index.js",
|
|
17
|
+
"bin": {
|
|
18
|
+
"ctv-run": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc",
|
|
29
|
+
"watch": "tsc -w",
|
|
30
|
+
"prepublishOnly": "tsc && cp src/README.md README.md"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"fs-extra": "^11.3.4",
|
|
34
|
+
"playwright": "^1.58.2"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@biomejs/biome": "2.4.5",
|
|
38
|
+
"@types/fs-extra": "^11.0.4",
|
|
39
|
+
"@types/node": "^25.3.0",
|
|
40
|
+
"tsx": "^4.21.0",
|
|
41
|
+
"typescript": "^5.5.3"
|
|
42
|
+
}
|
|
43
|
+
}
|