charge-connect 0.1.9 → 0.2.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/cli.js +220 -15
- package/collect.js +884 -108
- package/package.json +4 -3
- package/updater.js +967 -0
package/cli.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// 사용법:
|
|
5
5
|
// npx charge-connect <페어링코드> [--label 이름] # 페어링 + 첫 수집 + 스케줄 등록 (원라이너)
|
|
6
6
|
// # --label(또는 CHARGE_LABEL)로 앱에 표시될 기기명 지정, 기본은 호스트명
|
|
7
|
-
// charge-connect run # 수집 1회 (
|
|
7
|
+
// charge-connect run # 수집 1회 (설치된 ~/.charge/app의 collect.js로, 스케줄 실행과 상태와 잠금을 같이 쓴다)
|
|
8
|
+
// charge-connect update # 기존 설치 갱신: npx charge-connect@latest update
|
|
8
9
|
// charge-connect unpair # 페어링 해제 (서버 디바이스 토큰도 폐기)
|
|
9
10
|
//
|
|
10
11
|
// 설정 파일: ~/.charge/config.json (CHARGE_HOME으로 위치 변경 가능)
|
|
@@ -14,6 +15,10 @@ const path = require("node:path");
|
|
|
14
15
|
const { execFileSync } = require("node:child_process");
|
|
15
16
|
const { CCUSAGE_PKG } = require("./collect.js"); // 버전 고정을 한 곳에서 관리
|
|
16
17
|
const { resolveInstallationID } = require("./identity.js");
|
|
18
|
+
const {
|
|
19
|
+
UPDATE_MARKER, acquireUpdateLock, checkSyntax, installStaged, nodeSelfTest, recoverInterruptedUpdate, releaseUpdateLock,
|
|
20
|
+
sweepUpdateLeftovers,
|
|
21
|
+
} = require("./updater.js");
|
|
17
22
|
|
|
18
23
|
const PACKAGE_NAME = "charge-connect";
|
|
19
24
|
const CURRENT_VERSION = (() => {
|
|
@@ -221,19 +226,105 @@ async function unpair() {
|
|
|
221
226
|
console.log("✓ 페어링 해제 (스케줄 해제는 install.sh/install.ps1 안내 참고)");
|
|
222
227
|
}
|
|
223
228
|
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
if (
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
229
|
+
// 런타임 파일 목록은 package.json의 files를 따른다, 새 파일(updater.js 등)을 여기서 따로 챙기지 않게.
|
|
230
|
+
// package.json을 못 읽는 경우에만 아래 고정 목록으로 대신한다.
|
|
231
|
+
function runtimeFiles(dir = __dirname) {
|
|
232
|
+
const fallback = ["cli.js", "collect.js", "identity.js", "updater.js", "install.sh", "install.linux.sh", "install.ps1", "cloud.json"];
|
|
233
|
+
let listed = null;
|
|
234
|
+
try {
|
|
235
|
+
const files = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8")).files;
|
|
236
|
+
if (Array.isArray(files)) listed = files.filter((f) => typeof f === "string" && /^[A-Za-z0-9._-]+$/.test(f));
|
|
237
|
+
} catch {}
|
|
238
|
+
return [...new Set([...(listed ?? fallback), "package.json"])];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 페어링 직후 자동 업데이트가 마침 앱 폴더를 바꾸는 중이면 이만큼 기다렸다가 한 번 더 잠금을 잡아 본다
|
|
242
|
+
const PAIRING_LOCK_WAIT_MS = 5_000;
|
|
243
|
+
const LOCKED_MESSAGE = "자동 업데이트가 수집기 파일을 교체하는 중이라 지금은 설치하지 않습니다. 잠시 뒤 `npx charge-connect@latest update`로 다시 시도하세요.";
|
|
244
|
+
|
|
245
|
+
function packageVersion(dir) {
|
|
246
|
+
try {
|
|
247
|
+
const version = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8")).version;
|
|
248
|
+
return typeof version === "string" && version ? version : null;
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// npx 캐시는 언제든 청소될 수 있으므로, 스케줄 등록 전에 런타임을 ~/.charge/app으로 복사한다.
|
|
255
|
+
// 자동 업데이트와 같은 순서로 바꾼다 (updater.js의 installStaged): ~/.charge/update.lock을 잡아 스케줄 실행의 자동 업데이트와
|
|
256
|
+
// 겹치지 않게 하고, 모든 .js를 node --check로 검사한 뒤, 지금 파일을 app.prev에 백업하고 진행 표시(UPDATE_MARKER)를 남긴 채
|
|
257
|
+
// 파일마다 <파일>.new로 쓰고 이름을 바꿔 덮는다. collect.js는 그다음이 package.json인 맨 뒤에 바꾼다: 도중에 끊기면 옛 collect.js가
|
|
258
|
+
// 그대로 돌고, 표시를 본 다음 수집이 app.prev로 되돌린다 (전원 차단, Ctrl+C로 반쪽짜리 collect.js가 남아 수집이 매번 죽고 자동
|
|
259
|
+
// 업데이트에도 닿지 못하는 일이 없게). 설치 뒤 자체 점검에 실패하면 app.prev로 되돌리고 던진다. 원본 폴더(npx 캐시, 저장소)는
|
|
260
|
+
// 옮기거나 지우지 않는다. 앱 폴더가 아직 없는 첫 설치는 백업할 것이 없으니 app.prev를 남기지 않는다. 잠금을 살아 있는 다른
|
|
261
|
+
// 프로세스가 잡고 있으면 waitMs만큼 기다렸다가 한 번 더 시도하고, 그래도 잡혀 있으면 code "LOCKED" 오류를 던진다 (아무것도
|
|
262
|
+
// 바꾸지 않는다). source는 테스트 주입용이다.
|
|
263
|
+
async function installRuntime(confDir = CONF_DIR, { waitMs = 0, log = console.log, source = __dirname } = {}) {
|
|
264
|
+
const dest = path.join(confDir, "app");
|
|
265
|
+
const backupDir = path.join(confDir, "app.prev");
|
|
266
|
+
const lockFile = path.join(confDir, "update.lock");
|
|
267
|
+
fs.mkdirSync(confDir, { recursive: true, mode: 0o700 });
|
|
268
|
+
let locked = acquireUpdateLock(lockFile);
|
|
269
|
+
if (!locked && waitMs > 0) {
|
|
270
|
+
log(`자동 업데이트가 수집기 파일을 교체하는 중입니다. ${Math.round(waitMs / 1000)}초 뒤 다시 시도합니다…`);
|
|
271
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
272
|
+
locked = acquireUpdateLock(lockFile);
|
|
273
|
+
}
|
|
274
|
+
if (!locked) throw Object.assign(new Error(LOCKED_MESSAGE), { code: "LOCKED" });
|
|
275
|
+
try {
|
|
276
|
+
// 잠금을 잡았으니 남아 있는 점검, 스테이징 폴더는 도중에 죽은 설치의 것이다
|
|
277
|
+
sweepUpdateLeftovers(confDir);
|
|
278
|
+
const names = runtimeFiles(source);
|
|
279
|
+
// 교체 전에 검사한다: 빠진 파일이나 문법이 깨진 파일이 하나라도 있으면 아무것도 바꾸지 않는다. 자동 업데이트(extractRelease)처럼
|
|
280
|
+
// .json은 모두 해석돼야 하고, package.json이 나열한 파일은 모두 있어야 한다. 없는 파일을 건너뛰고 설치하면 옛 파일 하나가
|
|
281
|
+
// 새 파일들 사이에 남는데, 설치 후 자체 점검은 package.json의 버전만 보므로 그런 설치를 걸러 주지 않는다.
|
|
282
|
+
const missing = names.filter((name) => !fs.existsSync(path.join(source, name)));
|
|
283
|
+
if (missing.length) throw new Error(`설치할 패키지에 없는 파일: ${missing.join(", ")} (${source})`);
|
|
284
|
+
for (const name of names.filter((name) => name.endsWith(".json"))) {
|
|
285
|
+
try {
|
|
286
|
+
JSON.parse(fs.readFileSync(path.join(source, name), "utf8"));
|
|
287
|
+
} catch {
|
|
288
|
+
throw new Error(`설치할 패키지의 ${name} 파일을 해석할 수 없습니다 (${source})`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
await checkSyntax(source, names);
|
|
292
|
+
const fresh = !fs.existsSync(dest);
|
|
293
|
+
fs.mkdirSync(dest, { recursive: true, mode: 0o700 });
|
|
294
|
+
// 교체 도중 끊긴 뒤 되돌리기까지 실패한 업데이트의 진행 표시가 남아 있으면 installStaged는 백업을 덮지 않으려고 거부한다.
|
|
295
|
+
// 잠금을 잡았으니 그 업데이트는 살아 있지 않다. 먼저 app.prev로 되돌려 보고(백업이 섞인 폴더가 아니라 온전한 옛 런타임이 되게),
|
|
296
|
+
// 되돌리지 못하면 표시만 지우고 계속한다 (어차피 런타임 전체를 이 패키지의 파일로 바꾼다).
|
|
297
|
+
if (fs.existsSync(path.join(dest, UPDATE_MARKER))) {
|
|
298
|
+
try {
|
|
299
|
+
recoverInterruptedUpdate({ appDir: dest });
|
|
300
|
+
log("교체 도중 끊긴 업데이트를 app.prev 백업으로 되돌린 뒤 설치합니다.");
|
|
301
|
+
} catch (e) {
|
|
302
|
+
log(`교체 도중 끊긴 업데이트를 되돌리지 못했습니다 (${e?.message ?? e}). 런타임 전체를 새로 설치합니다.`);
|
|
303
|
+
fs.rmSync(path.join(dest, UPDATE_MARKER), { force: true });
|
|
304
|
+
}
|
|
236
305
|
}
|
|
306
|
+
const last = ["collect.js", "package.json"];
|
|
307
|
+
const ordered = [...names.filter((name) => !last.includes(name)), ...last.filter((name) => names.includes(name))];
|
|
308
|
+
const toVersion = packageVersion(source);
|
|
309
|
+
await installStaged({
|
|
310
|
+
stagingDir: source,
|
|
311
|
+
names: ordered,
|
|
312
|
+
appDir: dest,
|
|
313
|
+
backupDir,
|
|
314
|
+
fromVersion: packageVersion(dest),
|
|
315
|
+
toVersion,
|
|
316
|
+
// 자동 업데이트와 같은 설치 후 자체 점검 (버전을 모르는 패키지는 결과 줄을 맞출 수 없어 건너뛴다)
|
|
317
|
+
verify: toVersion ? () => nodeSelfTest(dest, toVersion) : null,
|
|
318
|
+
});
|
|
319
|
+
// 첫 설치의 빈 백업은 지운다 (교체가 끝나 진행 표시도 지워졌으니 되돌릴 일이 없다)
|
|
320
|
+
if (fresh) fs.rmSync(backupDir, { recursive: true, force: true });
|
|
321
|
+
if (!WIN) {
|
|
322
|
+
for (const f of ["install.sh", "install.linux.sh"]) {
|
|
323
|
+
try { fs.chmodSync(path.join(dest, f), 0o755); } catch {}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} finally {
|
|
327
|
+
releaseUpdateLock(lockFile);
|
|
237
328
|
}
|
|
238
329
|
return dest;
|
|
239
330
|
}
|
|
@@ -242,6 +333,14 @@ function collect(dir = __dirname) {
|
|
|
242
333
|
execFileSync(process.execPath, [path.join(dir, "collect.js")], { stdio: "inherit" });
|
|
243
334
|
}
|
|
244
335
|
|
|
336
|
+
// `charge-connect run`이 돌릴 수집기 폴더. 상태 파일(429 게이트, 요청 간격과 그 잠금, 수집 건강)은 collect.js 옆에 있으므로,
|
|
337
|
+
// 설치된 런타임(~/.charge/app, 스케줄이 돌리는 것)이 있으면 그것을 돌려야 수동 실행이 스케줄 실행과 같은 기록을 보고 같은 잠금에
|
|
338
|
+
// 걸린다. npx 캐시의 이 패키지를 돌리면 기록이 따로라 둘이 같은 순간에 usage를 두 번 묻는다. 설치 전(페어링 전)에는 이 패키지다.
|
|
339
|
+
function installedRuntimeDir(confDir = CONF_DIR) {
|
|
340
|
+
const dir = path.join(confDir, "app");
|
|
341
|
+
return fs.existsSync(path.join(dir, "collect.js")) ? dir : __dirname;
|
|
342
|
+
}
|
|
343
|
+
|
|
245
344
|
function installSchedule(dir) {
|
|
246
345
|
const p = path.join(dir, INSTALLER);
|
|
247
346
|
if (WIN) {
|
|
@@ -287,6 +386,95 @@ async function offerCcusageInstall() {
|
|
|
287
386
|
}
|
|
288
387
|
}
|
|
289
388
|
|
|
389
|
+
function launchAgentRegistered(dir, home = HOME) {
|
|
390
|
+
try {
|
|
391
|
+
const plist = fs.readFileSync(path.join(home, "Library", "LaunchAgents", "com.charge.connect.plist"), "utf8");
|
|
392
|
+
return plist.includes(path.join(dir, "collect.js"));
|
|
393
|
+
} catch {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// 기존 설치 갱신: npx charge-connect@latest update
|
|
399
|
+
// 0.1.x 수집기는 스스로 업데이트하지 못한다. 새 런타임을 ~/.charge/app에 복사하고, 설치 스크립트가
|
|
400
|
+
// 조용히 다시 돌려도 되는 플랫폼에서만 스케줄을 다시 등록한 뒤, 한 번 수집해 확인한다.
|
|
401
|
+
// 반환: { installed, collected, version }. 설정 파일은 건드리지 않는다.
|
|
402
|
+
async function update({
|
|
403
|
+
confDir = CONF_DIR,
|
|
404
|
+
platform = process.platform,
|
|
405
|
+
home = HOME,
|
|
406
|
+
currentVersion = CURRENT_VERSION,
|
|
407
|
+
install = installRuntime,
|
|
408
|
+
schedule = installSchedule,
|
|
409
|
+
runCollect = collect,
|
|
410
|
+
log = console.log,
|
|
411
|
+
logError = console.error,
|
|
412
|
+
} = {}) {
|
|
413
|
+
if (!fs.existsSync(path.join(confDir, "config.json"))) {
|
|
414
|
+
logError("이 컴퓨터는 아직 연동되지 않았습니다. 앱에서 코드를 발급받아 `npx charge-connect <페어링코드>`를 먼저 실행하세요.");
|
|
415
|
+
return { installed: false, collected: false, version: null };
|
|
416
|
+
}
|
|
417
|
+
const installedVersion = (dir) => {
|
|
418
|
+
try {
|
|
419
|
+
return JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8")).version ?? null;
|
|
420
|
+
} catch {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
// 자동 업데이트로 이미 더 새 버전이 깔려 있으면 덮지 않는다. `@latest` 없이 부른 npx 캐시나
|
|
425
|
+
// 전역 설치의 옛 패키지가 새 런타임을 되돌리면, 다음 확인 창(최대 13시간)까지 옛 버전으로 돈다.
|
|
426
|
+
// 교체 도중 끊긴 업데이트의 진행 표시가 남아 있으면 package.json의 버전을 믿지 않는다 (맨 마지막에 바꾸는 파일이라 새 버전이
|
|
427
|
+
// 적혀 있어도 다음 수집이 app.prev로 되돌린다). 그때는 installRuntime이 잠금 아래에서 되돌린 뒤 이 패키지를 설치한다.
|
|
428
|
+
const appDir = path.join(confDir, "app");
|
|
429
|
+
let dir;
|
|
430
|
+
if (!fs.existsSync(path.join(appDir, UPDATE_MARKER)) && isNewerVersion(currentVersion, installedVersion(appDir))) {
|
|
431
|
+
dir = appDir;
|
|
432
|
+
log(`${dir}에 더 새 버전(${installedVersion(dir)})이 이미 설치돼 있어 파일은 그대로 둡니다 (실행한 패키지는 ${currentVersion}).`);
|
|
433
|
+
} else {
|
|
434
|
+
try {
|
|
435
|
+
dir = await install(confDir);
|
|
436
|
+
} catch (e) {
|
|
437
|
+
// 잠금(자동 업데이트가 교체 중)이면 아무것도 바꾸지 않았고, 그 밖의 실패는 installStaged가 app.prev로 되돌렸다
|
|
438
|
+
logError(e?.code === "LOCKED" ? e.message : `수집기 파일 설치 실패: ${e?.message ?? e}`);
|
|
439
|
+
return { installed: false, collected: false, version: null };
|
|
440
|
+
}
|
|
441
|
+
log(`수집기 파일을 ${dir}에 설치했습니다.`);
|
|
442
|
+
}
|
|
443
|
+
const version = installedVersion(dir);
|
|
444
|
+
|
|
445
|
+
if (platform === "win32") {
|
|
446
|
+
// install.ps1은 비관리자 창에서 돌면 숨김(S4U) 작업을 콘솔 창이 뜨는 작업으로 되돌리고, 작업을 직접
|
|
447
|
+
// 실행해 보기도 한다. 관리자 창에서 만든 등록을 망칠 수 있어 자동으로 부르지 않는다.
|
|
448
|
+
// 작업은 이미 같은 경로의 collect.js를 가리키므로 다음 실행부터 새 파일이 쓰인다.
|
|
449
|
+
log("작업 스케줄러 등록은 그대로 둡니다 (다음 5분 수집부터 새 버전이 실행됩니다).");
|
|
450
|
+
log(`등록을 다시 하려면: powershell -ExecutionPolicy Bypass -File "${path.join(dir, "install.ps1")}"`);
|
|
451
|
+
} else if (platform === "darwin" && launchAgentRegistered(dir, home)) {
|
|
452
|
+
// install.sh를 다시 돌리면 launchd가 RunAtLoad로 수집을 한 번 더 띄워 Claude 요청이 겹친다.
|
|
453
|
+
// 등록이 이미 이 경로를 가리키면 그대로 둔다 (사용자가 unload해 둔 상태도 존중한다).
|
|
454
|
+
log("launchd 자동 수집 등록은 그대로 유지합니다.");
|
|
455
|
+
} else {
|
|
456
|
+
log("5분 간격 자동 수집을 다시 등록합니다…");
|
|
457
|
+
try {
|
|
458
|
+
schedule(dir);
|
|
459
|
+
} catch (e) {
|
|
460
|
+
// 종료 코드 3 = 설치 스크립트가 수동 등록 방법(cron 등)을 이미 출력했다
|
|
461
|
+
if (e?.status === 3) log("이미 위 방법으로 등록해 두었다면 그대로 두면 됩니다.");
|
|
462
|
+
else logError(`자동 수집 등록 실패: ${e?.message ?? e}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
log("수집을 한 번 실행합니다…");
|
|
467
|
+
let collected = true;
|
|
468
|
+
try {
|
|
469
|
+
runCollect(dir);
|
|
470
|
+
} catch {
|
|
471
|
+
collected = false;
|
|
472
|
+
logError("수집에 실패했습니다 (네트워크 문제일 수 있습니다). 5분 뒤 자동 수집이 다시 시도합니다.");
|
|
473
|
+
}
|
|
474
|
+
log(`✓ charge-connect ${version ?? "(버전 미상)"} 설치 완료`);
|
|
475
|
+
return { installed: true, collected, version };
|
|
476
|
+
}
|
|
477
|
+
|
|
290
478
|
async function main() {
|
|
291
479
|
const arg = process.argv[2];
|
|
292
480
|
if (!arg) {
|
|
@@ -294,7 +482,13 @@ async function main() {
|
|
|
294
482
|
process.exit(1);
|
|
295
483
|
}
|
|
296
484
|
if (arg === "run") {
|
|
297
|
-
collect();
|
|
485
|
+
collect(installedRuntimeDir());
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (arg === "update") {
|
|
489
|
+
// 페어링 코드가 아니므로 maybeUpdateBeforePairing을 거치지 않는다 (@latest로 부르는 명령이다)
|
|
490
|
+
const result = await update();
|
|
491
|
+
if (!result.installed) process.exit(1);
|
|
298
492
|
return;
|
|
299
493
|
}
|
|
300
494
|
if (arg === "unpair") {
|
|
@@ -308,7 +502,15 @@ async function main() {
|
|
|
308
502
|
// 스케줄을 먼저 등록한다 — 첫 수집이 실패해도(네트워크가 잠깐 끊기는 등) 자동 수집은
|
|
309
503
|
// 살아 있어야 한다. 페어링 코드는 이미 소모돼서 같은 코드로 재시도할 수 없기 때문이다.
|
|
310
504
|
await pair(arg);
|
|
311
|
-
|
|
505
|
+
let dir;
|
|
506
|
+
try {
|
|
507
|
+
// 자동 업데이트가 마침 교체 중이면 잠깐 기다렸다가 한 번 더 시도한다 (페어링 코드는 이미 소모돼 다시 페어링할 수 없다)
|
|
508
|
+
dir = await installRuntime(CONF_DIR, { waitMs: PAIRING_LOCK_WAIT_MS });
|
|
509
|
+
} catch (e) {
|
|
510
|
+
console.error(e?.code === "LOCKED" ? e.message : `수집기 파일 설치 실패: ${e?.message ?? e}`);
|
|
511
|
+
console.error("페어링은 끝났습니다. 잠시 뒤 `npx charge-connect@latest update`로 설치와 자동 수집 등록을 마무리하세요.");
|
|
512
|
+
process.exit(1);
|
|
513
|
+
}
|
|
312
514
|
|
|
313
515
|
console.log("5분 간격 자동 수집을 등록합니다…");
|
|
314
516
|
let scheduled = true;
|
|
@@ -360,10 +562,13 @@ if (require.main === module) {
|
|
|
360
562
|
|
|
361
563
|
module.exports = {
|
|
362
564
|
installRuntime,
|
|
565
|
+
installedRuntimeDir,
|
|
363
566
|
isNewerVersion,
|
|
364
567
|
main,
|
|
365
568
|
maybeUpdateBeforePairing,
|
|
366
569
|
pair,
|
|
367
570
|
resolveLabel,
|
|
368
571
|
resolveNpmCLI,
|
|
572
|
+
runtimeFiles,
|
|
573
|
+
update,
|
|
369
574
|
};
|