triflux 10.57.3 → 10.59.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/bin/triflux.mjs +496 -396
- package/hub/team/claude-daemon-control.mjs +5 -5
- package/hud/hud-qos-status.mjs +7 -7
- package/hud/prompt-cache.mjs +22 -0
- package/hud/providers/claude.mjs +12 -3
- package/hud/providers/codex.mjs +144 -14
- package/hud/renderers.mjs +40 -25
- package/hud/terminal.mjs +4 -4
- package/package.json +1 -1
- package/scripts/lib/cli-output.mjs +228 -0
- package/scripts/lib/legacy-mcp-cleanup.mjs +2 -4
- package/scripts/lib/tmr-install.mjs +4 -3
- package/scripts/setup.mjs +279 -222
package/bin/triflux.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { setTimeout as delay } from "node:timers/promises";
|
|
3
|
-
import { execFileSync, execSync } from "child_process";
|
|
3
|
+
import { execFileSync, execSync, spawnSync } from "child_process";
|
|
4
4
|
// triflux CLI — setup, doctor, version
|
|
5
5
|
import {
|
|
6
6
|
chmodSync,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
statSync,
|
|
16
16
|
unlinkSync,
|
|
17
17
|
writeFileSync,
|
|
18
|
+
writeSync,
|
|
18
19
|
} from "fs";
|
|
19
20
|
import { homedir } from "os";
|
|
20
21
|
import { dirname, join, resolve } from "path";
|
|
@@ -31,6 +32,15 @@ import {
|
|
|
31
32
|
listSessions,
|
|
32
33
|
tmuxExec,
|
|
33
34
|
} from "../hub/team/session.mjs";
|
|
35
|
+
import {
|
|
36
|
+
addStep,
|
|
37
|
+
addWarning,
|
|
38
|
+
commandErrorMessage,
|
|
39
|
+
createCliResult,
|
|
40
|
+
outputOptions,
|
|
41
|
+
recordFile,
|
|
42
|
+
renderCliResult,
|
|
43
|
+
} from "../scripts/lib/cli-output.mjs";
|
|
34
44
|
import {
|
|
35
45
|
commandExists,
|
|
36
46
|
inspectMacTimeoutDependency,
|
|
@@ -79,6 +89,7 @@ import {
|
|
|
79
89
|
retireOldInstallFiles,
|
|
80
90
|
runConsentSteps,
|
|
81
91
|
SYNC_MAP,
|
|
92
|
+
setupTargets,
|
|
82
93
|
syncSkills,
|
|
83
94
|
} from "../scripts/setup.mjs";
|
|
84
95
|
import { cleanupTmpFiles } from "../scripts/tmp-cleanup.mjs";
|
|
@@ -91,7 +102,6 @@ const PKG = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
|
|
|
91
102
|
|
|
92
103
|
// ── 색상 체계 (triflux brand: amber/orange accent) ──
|
|
93
104
|
const CYAN = "\x1b[36m";
|
|
94
|
-
const GREEN = "\x1b[32m";
|
|
95
105
|
const RED = "\x1b[31m";
|
|
96
106
|
const YELLOW = "\x1b[33m";
|
|
97
107
|
const DIM = "\x1b[2m";
|
|
@@ -123,9 +133,25 @@ const NORMALIZED_ARGS = RAW_ARGS.filter((arg) => arg !== "--json");
|
|
|
123
133
|
|
|
124
134
|
const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
125
135
|
setup: {
|
|
126
|
-
usage: "tfx setup [--dry-run] [--mods]",
|
|
136
|
+
usage: "tfx setup [--dry-run] [--mods] [--json] [-q|--quiet] [--verbose]",
|
|
127
137
|
description: "파일 동기화 + HUD/MCP 설정",
|
|
128
138
|
options: [
|
|
139
|
+
{
|
|
140
|
+
name: "--json",
|
|
141
|
+
type: "boolean",
|
|
142
|
+
description: "비대화식 JSON 결과 출력",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "-q, --quiet",
|
|
146
|
+
type: "boolean",
|
|
147
|
+
description: "경고와 오류만 출력",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "--verbose",
|
|
151
|
+
type: "boolean",
|
|
152
|
+
description: "최신 파일도 모두 표시",
|
|
153
|
+
},
|
|
154
|
+
|
|
129
155
|
{
|
|
130
156
|
name: "--mods",
|
|
131
157
|
type: "boolean",
|
|
@@ -308,10 +334,31 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
|
308
334
|
},
|
|
309
335
|
},
|
|
310
336
|
update: {
|
|
311
|
-
usage: "tfx update",
|
|
337
|
+
usage: "tfx update [--check] [--json] [-q|--quiet] [--verbose]",
|
|
312
338
|
description:
|
|
313
339
|
"설치 방식(plugin/npm/git)을 감지해 triflux를 업데이트하고 setup/cache를 재동기화",
|
|
314
340
|
options: [
|
|
341
|
+
{
|
|
342
|
+
name: "--json",
|
|
343
|
+
type: "boolean",
|
|
344
|
+
description: "비대화식 JSON 결과 출력",
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
name: "-q, --quiet",
|
|
348
|
+
type: "boolean",
|
|
349
|
+
description: "경고와 오류만 출력",
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
name: "--verbose",
|
|
353
|
+
type: "boolean",
|
|
354
|
+
description: "최신 파일도 모두 표시",
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
name: "--check",
|
|
358
|
+
type: "boolean",
|
|
359
|
+
description:
|
|
360
|
+
"registry 버전만 확인. triflux 관리 파일(~/.claude, ~/.codex 등)은 변경하지 않음 (npm 캐시와 로그는 기록될 수 있음)",
|
|
361
|
+
},
|
|
315
362
|
{
|
|
316
363
|
name: "--help",
|
|
317
364
|
type: "boolean",
|
|
@@ -382,14 +429,19 @@ function printCommandHelp(command) {
|
|
|
382
429
|
const aliases = schema.aliases?.length
|
|
383
430
|
? `\n ${BOLD}Aliases${RESET}\n ${schema.aliases.join(", ")}\n`
|
|
384
431
|
: "";
|
|
385
|
-
|
|
432
|
+
const help = `
|
|
386
433
|
${AMBER}${BOLD}⬡ tfx ${command}${RESET}
|
|
387
434
|
|
|
388
435
|
${GRAY}${schema.description || ""}${RESET}
|
|
389
436
|
|
|
390
437
|
${BOLD}Usage${RESET}
|
|
391
438
|
${WHITE_BRIGHT}${schema.usage}${RESET}
|
|
392
|
-
${aliases}${subcommands ? `\n ${BOLD}Subcommands${RESET}\n${subcommands}\n` : ""}${options ? `\n ${BOLD}Options${RESET}\n${options}\n` : ""}
|
|
439
|
+
${aliases}${subcommands ? `\n ${BOLD}Subcommands${RESET}\n${subcommands}\n` : ""}${options ? `\n ${BOLD}Options${RESET}\n${options}\n` : ""}`;
|
|
440
|
+
console.log(
|
|
441
|
+
["setup", "update"].includes(command) && !outputOptions(RAW_ARGS).color
|
|
442
|
+
? stripAnsi(help)
|
|
443
|
+
: help,
|
|
444
|
+
);
|
|
393
445
|
return true;
|
|
394
446
|
}
|
|
395
447
|
|
|
@@ -1174,7 +1226,7 @@ function buildSetupDryRunPlan() {
|
|
|
1174
1226
|
};
|
|
1175
1227
|
}
|
|
1176
1228
|
|
|
1177
|
-
function refreshSetupCaches() {
|
|
1229
|
+
function refreshSetupCaches(report) {
|
|
1178
1230
|
const cacheDir = join(CLAUDE_DIR, "cache");
|
|
1179
1231
|
for (const name of ["tfx-preflight.json", "mcp-inventory.json"]) {
|
|
1180
1232
|
const file = join(cacheDir, name);
|
|
@@ -1191,250 +1243,314 @@ function refreshSetupCaches() {
|
|
|
1191
1243
|
windowsHide: true,
|
|
1192
1244
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1193
1245
|
});
|
|
1194
|
-
ok(`${name}: 캐시 갱신 완료`);
|
|
1195
1246
|
} catch (error) {
|
|
1196
|
-
|
|
1247
|
+
addWarning(
|
|
1248
|
+
report,
|
|
1249
|
+
"cache-refresh",
|
|
1197
1250
|
`${name}: 캐시 갱신 실패: ${error.message?.split(/\r?\n/)[0] || "unknown"}`,
|
|
1198
1251
|
);
|
|
1199
1252
|
}
|
|
1200
1253
|
}
|
|
1201
1254
|
}
|
|
1202
1255
|
|
|
1203
|
-
export function runUpdatedSetup({
|
|
1256
|
+
export async function runUpdatedSetup({
|
|
1204
1257
|
packageRoot = PKG_ROOT,
|
|
1205
1258
|
execFileSyncFn = execFileSync,
|
|
1259
|
+
options = outputOptions(RAW_ARGS),
|
|
1206
1260
|
} = {}) {
|
|
1207
1261
|
const cli = join(packageRoot, "bin", "triflux.mjs");
|
|
1208
1262
|
for (const file of [cli, join(packageRoot, "scripts", "setup.mjs")]) {
|
|
1209
1263
|
if (!existsSync(file)) throw new Error(`업데이트 핵심 파일 누락: ${file}`);
|
|
1210
1264
|
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1265
|
+
let stdout;
|
|
1266
|
+
try {
|
|
1267
|
+
stdout = execFileSyncFn(
|
|
1268
|
+
process.execPath,
|
|
1269
|
+
[cli, "setup", "--from-update", "--json"],
|
|
1270
|
+
{
|
|
1271
|
+
encoding: "utf8",
|
|
1272
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1273
|
+
windowsHide: true,
|
|
1274
|
+
},
|
|
1275
|
+
);
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
if (!error.stdout?.toString().trim()) {
|
|
1278
|
+
error.message = commandErrorMessage(error);
|
|
1279
|
+
throw error;
|
|
1280
|
+
}
|
|
1281
|
+
stdout = error.stdout.toString();
|
|
1282
|
+
}
|
|
1283
|
+
const result = JSON.parse(stdout.trim().split(/\r?\n/).at(-1));
|
|
1284
|
+
if (result.outcome === "ok") {
|
|
1285
|
+
const interactive = !options.nonInteractive;
|
|
1286
|
+
let consent;
|
|
1287
|
+
try {
|
|
1288
|
+
const child = spawnSync(
|
|
1289
|
+
process.execPath,
|
|
1290
|
+
[
|
|
1291
|
+
cli,
|
|
1292
|
+
"setup",
|
|
1293
|
+
"--from-update",
|
|
1294
|
+
"--consent-only",
|
|
1295
|
+
...(interactive ? [] : ["--json"]),
|
|
1296
|
+
],
|
|
1297
|
+
{
|
|
1298
|
+
encoding: "utf8",
|
|
1299
|
+
env: { ...process.env, TFX_CONSENT_FD: interactive ? "3" : "" },
|
|
1300
|
+
stdio: interactive
|
|
1301
|
+
? ["inherit", "inherit", "inherit", "pipe"]
|
|
1302
|
+
: ["ignore", "pipe", "pipe"],
|
|
1303
|
+
windowsHide: true,
|
|
1304
|
+
},
|
|
1305
|
+
);
|
|
1306
|
+
if (child.error) throw child.error;
|
|
1307
|
+
const consentOutput = interactive ? child.output[3] : child.stdout;
|
|
1308
|
+
if (!consentOutput?.trim())
|
|
1309
|
+
throw new Error(
|
|
1310
|
+
commandErrorMessage({
|
|
1311
|
+
message: child.signal
|
|
1312
|
+
? `시그널 ${child.signal}`
|
|
1313
|
+
: `종료 코드 ${child.status}`,
|
|
1314
|
+
stderr: child.stderr,
|
|
1315
|
+
}),
|
|
1316
|
+
);
|
|
1317
|
+
consent = JSON.parse(consentOutput.trim().split(/\r?\n/).at(-1));
|
|
1318
|
+
if (consent.outcome !== "ok") throw new Error(consent.message);
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
addWarning(
|
|
1321
|
+
result,
|
|
1322
|
+
"consent",
|
|
1323
|
+
`동의 단계 실패: ${commandErrorMessage(error)}. 다시 실행: tfx setup`,
|
|
1324
|
+
);
|
|
1325
|
+
return result;
|
|
1326
|
+
}
|
|
1327
|
+
result.steps.push(...consent.steps);
|
|
1328
|
+
result.notices.push(...consent.notices);
|
|
1329
|
+
for (const warning of consent.warnings)
|
|
1330
|
+
addWarning(result, warning.code, warning.message);
|
|
1331
|
+
result.alreadyInGoalState &&= consent.alreadyInGoalState;
|
|
1332
|
+
const hooks = consent.targets.find(({ name }) => name === "codex");
|
|
1333
|
+
const codex = result.targets.find(({ name }) => name === "codex");
|
|
1334
|
+
if (codex && hooks && hooks.status !== "skipped") {
|
|
1335
|
+
if (hooks.status === "failed") codex.status = "failed";
|
|
1336
|
+
else if (codex.status === "ok") codex.detail += ", 훅 등록";
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
return result;
|
|
1215
1340
|
}
|
|
1216
1341
|
|
|
1217
|
-
function
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1342
|
+
async function cmdConsentOnly(options, report) {
|
|
1343
|
+
try {
|
|
1344
|
+
const consent = await runConsentSteps({
|
|
1345
|
+
interactive: !options.nonInteractive,
|
|
1346
|
+
report,
|
|
1347
|
+
});
|
|
1348
|
+
if (consent.changed) report.alreadyInGoalState = false;
|
|
1349
|
+
report.targets.push({
|
|
1350
|
+
name: "codex",
|
|
1351
|
+
status:
|
|
1352
|
+
consent.codexHooks === "skipped"
|
|
1353
|
+
? "skipped"
|
|
1354
|
+
: consent.codexHooks === "failed"
|
|
1355
|
+
? "failed"
|
|
1356
|
+
: "ok",
|
|
1357
|
+
detail: "훅 등록",
|
|
1358
|
+
});
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
report.outcome = "failed";
|
|
1361
|
+
report.alreadyInGoalState = false;
|
|
1362
|
+
report.message = error.message;
|
|
1363
|
+
report.exitCode = error.exitCode ?? EXIT_ERROR;
|
|
1364
|
+
process.exitCode = report.exitCode;
|
|
1365
|
+
}
|
|
1366
|
+
// 부모가 연 파이프가 있을 때만 질문 출력과 JSON 을 분리한다.
|
|
1367
|
+
writeSync(
|
|
1368
|
+
process.env.TFX_CONSENT_FD === "3" ? 3 : 1,
|
|
1369
|
+
`${JSON.stringify(report)}\n`,
|
|
1370
|
+
);
|
|
1371
|
+
return report;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
export function installNpmGlobal({
|
|
1375
|
+
platform = process.platform,
|
|
1376
|
+
exec = execSync,
|
|
1377
|
+
} = {}) {
|
|
1378
|
+
const command = "npm install -g triflux@latest";
|
|
1379
|
+
const options = {
|
|
1380
|
+
encoding: "utf8",
|
|
1381
|
+
timeout: 90000,
|
|
1382
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1383
|
+
windowsHide: true,
|
|
1384
|
+
};
|
|
1385
|
+
try {
|
|
1386
|
+
exec(command, options);
|
|
1387
|
+
} catch (error) {
|
|
1388
|
+
if (platform !== "win32") throw error;
|
|
1389
|
+
exec(`${command} --force`, options);
|
|
1390
|
+
}
|
|
1223
1391
|
}
|
|
1224
1392
|
|
|
1225
1393
|
async function cmdSetup(options = {}) {
|
|
1226
|
-
const {
|
|
1227
|
-
dryRun = false,
|
|
1228
|
-
fromUpdate = false,
|
|
1229
|
-
overrideVersion,
|
|
1230
|
-
mods = false,
|
|
1231
|
-
} = options;
|
|
1394
|
+
const { dryRun = false, fromUpdate = false, mods = false } = options;
|
|
1232
1395
|
if (dryRun) {
|
|
1233
1396
|
printJson(buildSetupDryRunPlan());
|
|
1234
1397
|
return;
|
|
1235
1398
|
}
|
|
1399
|
+
const report = createCliResult("setup", PKG.version);
|
|
1400
|
+
const warning = (code) => (message) => addWarning(report, code, message);
|
|
1236
1401
|
if (writesRealHomeInTest()) {
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
const cleanup = cleanupLegacyHooks({
|
|
1242
|
-
settingsPath: join(CLAUDE_DIR, "settings.json"),
|
|
1243
|
-
});
|
|
1244
|
-
if (!cleanup.ok) {
|
|
1245
|
-
throw createCliError(`이전 hook 정리 실패: ${cleanup.error}`, {
|
|
1246
|
-
exitCode: EXIT_CONFIG_ERROR,
|
|
1247
|
-
reason: "configError",
|
|
1248
|
-
fix: `${join(CLAUDE_DIR, "settings.json")}의 JSON 문법과 쓰기 권한을 확인하세요.`,
|
|
1249
|
-
});
|
|
1250
|
-
}
|
|
1251
|
-
// agy 는 hooks.json 의 훅을 실행하므로 지워진 훅 스크립트를 가리키는 항목을 남기지 않는다.
|
|
1252
|
-
const agyCleanup = cleanupAgyHooks();
|
|
1253
|
-
if (agyCleanup.changed) ok("agy 옛 triflux-session 훅 정리됨");
|
|
1254
|
-
else if (!agyCleanup.ok) warn(`agy 옛 훅 정리 실패: ${agyCleanup.error}`);
|
|
1255
|
-
// 이주가 막혀도 setup 은 계속한다. 남은 항목은 경고로 알린다.
|
|
1256
|
-
const mcpBackups = new Map();
|
|
1257
|
-
for (const warning of cleanupLegacyMcp({ backups: mcpBackups }).warnings)
|
|
1258
|
-
warn(warning);
|
|
1259
|
-
const hubCleanup = cleanupTfxHub({
|
|
1260
|
-
pluginRoot: existsSync(join(PKG_ROOT, ".git")) ? undefined : PKG_ROOT,
|
|
1261
|
-
log: info,
|
|
1262
|
-
backups: mcpBackups,
|
|
1263
|
-
});
|
|
1264
|
-
for (const warning of hubCleanup.warnings) warn(warning);
|
|
1265
|
-
if (hubCleanup.changed) ok("제거된 허브의 설정과 실행 흔적 정리");
|
|
1266
|
-
const mcpPins = pinRegistryMcpPackages({ backups: mcpBackups });
|
|
1267
|
-
for (const warning of mcpPins.warnings) warn(warning);
|
|
1268
|
-
if (mcpPins.pinned) ok(`MCP 패키지 고정 버전 반영: ${mcpPins.pinned}개 항목`);
|
|
1269
|
-
if (fromUpdate) refreshSetupCaches();
|
|
1270
|
-
|
|
1271
|
-
console.log(`\n${BOLD}triflux setup${RESET}\n`);
|
|
1272
|
-
|
|
1273
|
-
for (const target of SYNC_MAP) {
|
|
1274
|
-
syncFile(target.src, target.dst, target.label);
|
|
1275
|
-
}
|
|
1276
|
-
retireOldInstallFiles(info);
|
|
1277
|
-
reportSkillSync();
|
|
1278
|
-
|
|
1279
|
-
// ── 결과 추적 ──
|
|
1280
|
-
const summary = [];
|
|
1281
|
-
|
|
1282
|
-
const codexProfileResult = ensureCodexProfiles();
|
|
1283
|
-
if (!codexProfileResult.ok) {
|
|
1284
|
-
const reason = renderErrorMessage(codexProfileResult.message);
|
|
1285
|
-
warn(`Codex profiles 설정 실패: ${reason}`);
|
|
1286
|
-
summary.push({ item: "Codex profiles", status: "⚠️", detail: reason });
|
|
1287
|
-
} else if (codexProfileResult.changed > 0) {
|
|
1288
|
-
ok(
|
|
1289
|
-
`Codex profiles: ${codexProfileResult.changed}개 반영됨 (~/.codex/<프로필>.config.toml)`,
|
|
1290
|
-
);
|
|
1291
|
-
summary.push({
|
|
1292
|
-
item: "Codex profiles",
|
|
1293
|
-
status: "✅",
|
|
1294
|
-
detail: `${codexProfileResult.changed}개 반영됨`,
|
|
1295
|
-
});
|
|
1296
|
-
} else {
|
|
1297
|
-
ok("Codex profiles: 이미 준비됨");
|
|
1298
|
-
summary.push({
|
|
1299
|
-
item: "Codex profiles",
|
|
1300
|
-
status: "✅",
|
|
1301
|
-
detail: "이미 준비됨",
|
|
1302
|
-
});
|
|
1402
|
+
report.outcome = "skipped";
|
|
1403
|
+
report.message = "테스트가 홈을 격리하지 않아 건너뜀";
|
|
1404
|
+
process.stdout.write(renderCliResult(report, options));
|
|
1405
|
+
return report;
|
|
1303
1406
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
warn(`Antigravity/Gemini profiles 설정 실패: ${reason}`);
|
|
1310
|
-
summary.push({
|
|
1311
|
-
item: "Antigravity/Gemini profiles",
|
|
1312
|
-
status: "⚠️",
|
|
1313
|
-
detail: reason,
|
|
1314
|
-
});
|
|
1315
|
-
} else if (geminiResult.created) {
|
|
1316
|
-
ok(
|
|
1317
|
-
`Antigravity/Gemini profiles: ${geminiResult.count}개 생성됨 (~/.gemini/triflux-profiles.json)`,
|
|
1318
|
-
);
|
|
1319
|
-
summary.push({
|
|
1320
|
-
item: "Antigravity/Gemini profiles",
|
|
1321
|
-
status: "✅",
|
|
1322
|
-
detail: `${geminiResult.count}개 생성됨`,
|
|
1323
|
-
});
|
|
1324
|
-
} else if (geminiResult.added > 0) {
|
|
1325
|
-
ok(`Antigravity/Gemini profiles: ${geminiResult.added}개 추가됨`);
|
|
1326
|
-
summary.push({
|
|
1327
|
-
item: "Antigravity/Gemini profiles",
|
|
1328
|
-
status: "✅",
|
|
1329
|
-
detail: `${geminiResult.added}개 추가됨 (총 ${geminiResult.count}개)`,
|
|
1330
|
-
});
|
|
1331
|
-
} else {
|
|
1332
|
-
ok(`Antigravity/Gemini profiles: ${geminiResult.count}개 준비됨`);
|
|
1333
|
-
summary.push({
|
|
1334
|
-
item: "Antigravity/Gemini profiles",
|
|
1335
|
-
status: "✅",
|
|
1336
|
-
detail: `${geminiResult.count}개 준비됨`,
|
|
1407
|
+
if (options.fromUpdate && options.consentOnly)
|
|
1408
|
+
return cmdConsentOnly(options, report);
|
|
1409
|
+
try {
|
|
1410
|
+
const cleanup = cleanupLegacyHooks({
|
|
1411
|
+
settingsPath: join(CLAUDE_DIR, "settings.json"),
|
|
1337
1412
|
});
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
// HUD statusLine 설정
|
|
1341
|
-
console.log(`${CYAN}[HUD 설정]${RESET}`);
|
|
1342
|
-
const settingsPath = join(CLAUDE_DIR, "settings.json");
|
|
1343
|
-
const hudPath = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
1344
|
-
|
|
1345
|
-
if (existsSync(hudPath)) {
|
|
1346
|
-
try {
|
|
1347
|
-
let settings = {};
|
|
1348
|
-
if (existsSync(settingsPath)) {
|
|
1349
|
-
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
const changed = applyStatusLine(settings, { hudPath, warn });
|
|
1353
|
-
if (changed) persistSettings(settings, settingsPath);
|
|
1354
|
-
const detail = changed ? "설정 완료" : "기존 설정 유지";
|
|
1355
|
-
ok(`statusLine: ${detail}`);
|
|
1356
|
-
summary.push({ item: "HUD statusLine", status: "✅", detail });
|
|
1357
|
-
} catch (e) {
|
|
1358
|
-
throw createCliError(`settings.json 처리 실패: ${e.message}`, {
|
|
1413
|
+
if (!cleanup.ok)
|
|
1414
|
+
throw createCliError(`settings.json 처리 실패: ${cleanup.error}`, {
|
|
1359
1415
|
exitCode: EXIT_CONFIG_ERROR,
|
|
1360
1416
|
reason: "configError",
|
|
1361
|
-
fix:
|
|
1362
|
-
cause: e,
|
|
1417
|
+
fix: "settings.json의 JSON 문법과 쓰기 권한을 확인하세요.",
|
|
1363
1418
|
});
|
|
1419
|
+
if (cleanup.changed)
|
|
1420
|
+
addStep(report, `옛 Claude 훅 ${cleanup.removed}개 정리`);
|
|
1421
|
+
const agyCleanup = cleanupAgyHooks();
|
|
1422
|
+
if (agyCleanup.changed) addStep(report, "agy 옛 triflux-session 훅 정리");
|
|
1423
|
+
else if (!agyCleanup.ok)
|
|
1424
|
+
addWarning(
|
|
1425
|
+
report,
|
|
1426
|
+
"agy-cleanup",
|
|
1427
|
+
`agy 옛 훅 정리 실패: ${agyCleanup.error}`,
|
|
1428
|
+
);
|
|
1429
|
+
const mcpBackups = new Map();
|
|
1430
|
+
const mcpCleanup = cleanupLegacyMcp({ backups: mcpBackups });
|
|
1431
|
+
if (!mcpCleanup.skipped)
|
|
1432
|
+
mcpCleanup.warnings.forEach(warning("mcp-cleanup"));
|
|
1433
|
+
if (mcpCleanup.changed) addStep(report, "옛 MCP 설정 정리");
|
|
1434
|
+
const hubCleanup = cleanupTfxHub({
|
|
1435
|
+
pluginRoot: existsSync(join(PKG_ROOT, ".git")) ? undefined : PKG_ROOT,
|
|
1436
|
+
backups: mcpBackups,
|
|
1437
|
+
});
|
|
1438
|
+
hubCleanup.warnings.forEach(warning("hub-cleanup"));
|
|
1439
|
+
if (hubCleanup.changed)
|
|
1440
|
+
addStep(report, "제거된 허브의 설정과 실행 흔적 정리");
|
|
1441
|
+
const mcpPins = pinRegistryMcpPackages({ backups: mcpBackups });
|
|
1442
|
+
mcpPins.warnings.forEach(warning("mcp-pins"));
|
|
1443
|
+
if (mcpPins.pinned)
|
|
1444
|
+
addStep(report, `MCP 패키지 고정 버전 반영: ${mcpPins.pinned}개 항목`);
|
|
1445
|
+
if (fromUpdate) refreshSetupCaches(report);
|
|
1446
|
+
|
|
1447
|
+
for (const { src, dst, label } of SYNC_MAP) {
|
|
1448
|
+
const { change } = describeSyncAction(src, dst, label);
|
|
1449
|
+
if (change !== "noop") {
|
|
1450
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
1451
|
+
copyFileSync(src, dst);
|
|
1452
|
+
try {
|
|
1453
|
+
chmodSync(dst, 0o755);
|
|
1454
|
+
} catch {}
|
|
1455
|
+
}
|
|
1456
|
+
recordFile(report, label, change);
|
|
1364
1457
|
}
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1458
|
+
const retired = retireOldInstallFiles();
|
|
1459
|
+
retired.warnings.forEach(warning("retired-files"));
|
|
1460
|
+
if (retired.moved.length || retired.deleted.length)
|
|
1461
|
+
addStep(
|
|
1462
|
+
report,
|
|
1463
|
+
`옛 설치 파일 ${retired.moved.length + retired.deleted.length}개 정리`,
|
|
1464
|
+
);
|
|
1465
|
+
const skills = syncSkills();
|
|
1466
|
+
skills.warnings.forEach(warning("skills"));
|
|
1467
|
+
if (!skills.ok) throw new Error("스킬 동기화 실패");
|
|
1468
|
+
if (skills.changed) report.alreadyInGoalState = false;
|
|
1469
|
+
if (skills.removed) addStep(report, `구형 스킬 ${skills.removed}개 정리`);
|
|
1470
|
+
|
|
1471
|
+
const codex = ensureCodexProfiles();
|
|
1472
|
+
if (!codex.ok)
|
|
1473
|
+
addWarning(
|
|
1474
|
+
report,
|
|
1475
|
+
"codex-profiles",
|
|
1476
|
+
`Codex profiles 설정 실패: ${renderErrorMessage(codex.message)}`,
|
|
1477
|
+
);
|
|
1478
|
+
if (codex.changed) report.alreadyInGoalState = false;
|
|
1479
|
+
const gemini = ensureGeminiProfiles();
|
|
1480
|
+
if (!gemini.ok)
|
|
1481
|
+
addWarning(
|
|
1482
|
+
report,
|
|
1483
|
+
"gemini-profiles",
|
|
1484
|
+
`Antigravity/Gemini profiles 설정 실패: ${renderErrorMessage(gemini.message)}`,
|
|
1485
|
+
);
|
|
1486
|
+
if (gemini.added) report.alreadyInGoalState = false;
|
|
1487
|
+
|
|
1488
|
+
const settingsPath = join(CLAUDE_DIR, "settings.json");
|
|
1489
|
+
const hudPath = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
1490
|
+
if (existsSync(hudPath)) {
|
|
1491
|
+
const settings = existsSync(settingsPath)
|
|
1492
|
+
? JSON.parse(readFileSync(settingsPath, "utf8"))
|
|
1493
|
+
: {};
|
|
1494
|
+
if (applyStatusLine(settings, { hudPath, warn: warning("statusline") })) {
|
|
1495
|
+
persistSettings(settings, settingsPath);
|
|
1496
|
+
report.alreadyInGoalState = false;
|
|
1497
|
+
}
|
|
1498
|
+
} else
|
|
1499
|
+
addWarning(report, "statusline", "HUD 파일 없음: 먼저 파일 동기화 필요");
|
|
1500
|
+
|
|
1501
|
+
const consent = fromUpdate
|
|
1502
|
+
? { changed: false, codexHooks: "skipped" }
|
|
1503
|
+
: await runConsentSteps({
|
|
1504
|
+
modsInstall: mods,
|
|
1505
|
+
interactive: !options.nonInteractive,
|
|
1506
|
+
report,
|
|
1507
|
+
});
|
|
1508
|
+
if (consent.changed) report.alreadyInGoalState = false;
|
|
1509
|
+
setupTargets(report, {
|
|
1510
|
+
skills: skills.total,
|
|
1511
|
+
files: SYNC_MAP.length,
|
|
1512
|
+
codex,
|
|
1513
|
+
gemini,
|
|
1514
|
+
hooks: consent.codexHooks,
|
|
1371
1515
|
});
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
timeout: 3000,
|
|
1393
|
-
stdio: "pipe",
|
|
1394
|
-
});
|
|
1395
|
-
ok(`psmux 기본 셸 → ${pwsh}`);
|
|
1516
|
+
// ── psmux 기본 셸 자동 수정 (cmd.exe → PowerShell) ──
|
|
1517
|
+
if (process.platform === "win32" && which("psmux")) {
|
|
1518
|
+
try {
|
|
1519
|
+
const shellOut = execSync("psmux show-options -g default-shell 2>NUL", {
|
|
1520
|
+
encoding: "utf8",
|
|
1521
|
+
timeout: 3000,
|
|
1522
|
+
}).trim();
|
|
1523
|
+
if (!/powershell|pwsh/i.test(shellOut)) {
|
|
1524
|
+
const pwsh = which("pwsh")
|
|
1525
|
+
? "pwsh"
|
|
1526
|
+
: which("powershell.exe")
|
|
1527
|
+
? "powershell.exe"
|
|
1528
|
+
: "";
|
|
1529
|
+
if (pwsh) {
|
|
1530
|
+
execSync(`psmux set-option -g default-shell "${pwsh}"`, {
|
|
1531
|
+
timeout: 3000,
|
|
1532
|
+
stdio: "pipe",
|
|
1533
|
+
});
|
|
1534
|
+
addStep(report, `psmux 기본 셸: ${pwsh}`);
|
|
1535
|
+
}
|
|
1396
1536
|
}
|
|
1537
|
+
} catch {
|
|
1538
|
+
/* psmux 서버 미실행이면 무시 */
|
|
1397
1539
|
}
|
|
1398
|
-
} catch {
|
|
1399
|
-
/* psmux 서버 미실행이면 무시 */
|
|
1400
1540
|
}
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
// CLI 존재 확인
|
|
1404
|
-
const cliChecks = [
|
|
1405
|
-
{ name: "codex", install: "npm i -g @openai/codex" },
|
|
1406
|
-
{
|
|
1407
|
-
name: "agy",
|
|
1408
|
-
label: "Antigravity",
|
|
1409
|
-
install: "Install Google Antigravity and ensure `agy` is on PATH",
|
|
1410
|
-
},
|
|
1411
|
-
];
|
|
1412
|
-
for (const { name, label, install } of cliChecks) {
|
|
1413
|
-
const displayName = label || name;
|
|
1414
|
-
if (which(name)) {
|
|
1415
|
-
summary.push({
|
|
1416
|
-
item: `${displayName} CLI`,
|
|
1417
|
-
status: "✅",
|
|
1418
|
-
detail: "설치됨",
|
|
1419
|
-
});
|
|
1420
|
-
} else {
|
|
1421
|
-
summary.push({
|
|
1422
|
-
item: `${displayName} CLI`,
|
|
1423
|
-
status: "⏭️",
|
|
1424
|
-
detail: `미설치 (${install})`,
|
|
1425
|
-
});
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
// ── 결과 요약 테이블 ──
|
|
1430
|
-
console.log(`\n${BOLD}── 설정 요약 ──${RESET}`);
|
|
1431
|
-
const maxItem = Math.max(...summary.map((s) => s.item.length));
|
|
1432
|
-
for (const { item, status, detail } of summary) {
|
|
1433
|
-
console.log(` ${status} ${item.padEnd(maxItem)} ${DIM}${detail}${RESET}`);
|
|
1434
|
-
}
|
|
1435
1541
|
|
|
1436
|
-
|
|
1437
|
-
|
|
1542
|
+
report.message = "설정 완료";
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
report.outcome = "failed";
|
|
1545
|
+
report.alreadyInGoalState = false;
|
|
1546
|
+
report.message = error.message;
|
|
1547
|
+
report.exitCode = error.exitCode ?? EXIT_ERROR;
|
|
1548
|
+
process.exitCode = report.exitCode;
|
|
1549
|
+
}
|
|
1550
|
+
process.stdout.write(
|
|
1551
|
+
renderCliResult(report, { ...options, details: fromUpdate }),
|
|
1552
|
+
);
|
|
1553
|
+
return report;
|
|
1438
1554
|
}
|
|
1439
1555
|
|
|
1440
1556
|
function addDoctorCheck(report, entry) {
|
|
@@ -1810,7 +1926,16 @@ async function cmdDoctor(options = {}) {
|
|
|
1810
1926
|
info("Homebrew 설치 후: brew install coreutils");
|
|
1811
1927
|
}
|
|
1812
1928
|
}
|
|
1813
|
-
|
|
1929
|
+
const skillFix = syncSkills();
|
|
1930
|
+
for (const warning of skillFix.warnings) warn(warning);
|
|
1931
|
+
report.actions.push({
|
|
1932
|
+
type: "skill-sync",
|
|
1933
|
+
status: skillFix.ok ? "ok" : "failed",
|
|
1934
|
+
changed: skillFix.changed,
|
|
1935
|
+
});
|
|
1936
|
+
if (!skillFix.ok) warn("스킬 동기화 실패");
|
|
1937
|
+
else if (skillFix.changed) ok(`스킬 ${skillFix.changed}개 반영됨`);
|
|
1938
|
+
else info("스킬: 이미 최신 상태");
|
|
1814
1939
|
const profileFix = ensureCodexProfiles();
|
|
1815
1940
|
if (!profileFix.ok) {
|
|
1816
1941
|
warn(
|
|
@@ -3230,201 +3355,164 @@ async function cmdUpdate(args = []) {
|
|
|
3230
3355
|
printCommandHelp("update");
|
|
3231
3356
|
return;
|
|
3232
3357
|
}
|
|
3358
|
+
const options = outputOptions(RAW_ARGS);
|
|
3359
|
+
const report = createCliResult("update", PKG.version);
|
|
3360
|
+
const started = Date.now();
|
|
3361
|
+
const registryVersion = () =>
|
|
3362
|
+
execSync("npm view triflux version", {
|
|
3363
|
+
encoding: "utf8",
|
|
3364
|
+
timeout: 10000,
|
|
3365
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3366
|
+
windowsHide: true,
|
|
3367
|
+
}).trim();
|
|
3368
|
+
try {
|
|
3369
|
+
if (options.check) {
|
|
3370
|
+
report.to = registryVersion();
|
|
3371
|
+
if (!report.to) throw new Error("레지스트리 버전 응답이 비어 있음");
|
|
3372
|
+
report.alreadyInGoalState = report.from === report.to;
|
|
3373
|
+
} else {
|
|
3374
|
+
// 1. 설치 방식 감지
|
|
3375
|
+
const pluginsFile = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
|
|
3376
|
+
let installMode = "unknown";
|
|
3377
|
+
let pluginPath = null;
|
|
3233
3378
|
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3379
|
+
// 플러그인 모드 감지
|
|
3380
|
+
if (existsSync(pluginsFile)) {
|
|
3381
|
+
try {
|
|
3382
|
+
const entry = findTrifluxPluginEntry(
|
|
3383
|
+
JSON.parse(readFileSync(pluginsFile, "utf8")),
|
|
3384
|
+
);
|
|
3385
|
+
if (entry) {
|
|
3386
|
+
pluginPath = entry.installPath;
|
|
3387
|
+
installMode = "plugin";
|
|
3388
|
+
}
|
|
3389
|
+
} catch {}
|
|
3390
|
+
}
|
|
3240
3391
|
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
);
|
|
3247
|
-
if (entry) {
|
|
3248
|
-
pluginPath = entry.installPath;
|
|
3392
|
+
// PKG_ROOT가 플러그인 캐시 내에 있으면 플러그인 모드
|
|
3393
|
+
if (
|
|
3394
|
+
installMode === "unknown" &&
|
|
3395
|
+
PKG_ROOT.includes(join(".claude", "plugins"))
|
|
3396
|
+
) {
|
|
3249
3397
|
installMode = "plugin";
|
|
3398
|
+
pluginPath = PKG_ROOT;
|
|
3250
3399
|
}
|
|
3251
|
-
} catch {}
|
|
3252
|
-
}
|
|
3253
|
-
|
|
3254
|
-
// PKG_ROOT가 플러그인 캐시 내에 있으면 플러그인 모드
|
|
3255
|
-
if (
|
|
3256
|
-
installMode === "unknown" &&
|
|
3257
|
-
PKG_ROOT.includes(join(".claude", "plugins"))
|
|
3258
|
-
) {
|
|
3259
|
-
installMode = "plugin";
|
|
3260
|
-
pluginPath = PKG_ROOT;
|
|
3261
|
-
}
|
|
3262
|
-
|
|
3263
|
-
// npm global 감지
|
|
3264
|
-
if (installMode === "unknown") {
|
|
3265
|
-
try {
|
|
3266
|
-
const npmList = execSync("npm list -g triflux --depth=0", {
|
|
3267
|
-
encoding: "utf8",
|
|
3268
|
-
timeout: 10000,
|
|
3269
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
3270
|
-
windowsHide: true,
|
|
3271
|
-
});
|
|
3272
|
-
if (npmList.includes("triflux")) installMode = "npm-global";
|
|
3273
|
-
} catch {}
|
|
3274
|
-
}
|
|
3275
3400
|
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
`검색: ${installMode === "plugin" ? "플러그인" : installMode === "npm-global" ? "npm global" : installMode === "npm-local" ? "npm local" : installMode === "git-local" ? "git 로컬 저장소" : "알 수 없음"} 설치 감지`,
|
|
3289
|
-
);
|
|
3290
|
-
|
|
3291
|
-
const networkTargets = resolveUpdateTargets({ installMode, pluginPath });
|
|
3292
|
-
if (networkTargets.length > 0) {
|
|
3293
|
-
const networkStatus = await checkNetworkAvailability(networkTargets);
|
|
3294
|
-
if (!networkStatus.online) {
|
|
3295
|
-
fail(`네트워크 확인 실패: ${networkStatus.unreachable.join(", ")}`);
|
|
3296
|
-
info("네트워크 연결을 확인한 뒤 다시 시도하세요.");
|
|
3297
|
-
return;
|
|
3298
|
-
}
|
|
3299
|
-
ok(`네트워크 확인 완료 (${networkStatus.reachable.join(", ")})`);
|
|
3300
|
-
}
|
|
3401
|
+
// npm global 감지
|
|
3402
|
+
if (installMode === "unknown") {
|
|
3403
|
+
try {
|
|
3404
|
+
const npmList = execSync("npm list -g triflux --depth=0", {
|
|
3405
|
+
encoding: "utf8",
|
|
3406
|
+
timeout: 10000,
|
|
3407
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
3408
|
+
windowsHide: true,
|
|
3409
|
+
});
|
|
3410
|
+
if (npmList.includes("triflux")) installMode = "npm-global";
|
|
3411
|
+
} catch {}
|
|
3412
|
+
}
|
|
3301
3413
|
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
}
|
|
3308
|
-
} else {
|
|
3309
|
-
ok("런타임 캐시 검증 완료");
|
|
3310
|
-
}
|
|
3414
|
+
// npm local 감지
|
|
3415
|
+
if (installMode === "unknown") {
|
|
3416
|
+
const localPkg = join(process.cwd(), "node_modules", "triflux");
|
|
3417
|
+
if (existsSync(localPkg)) installMode = "npm-local";
|
|
3418
|
+
}
|
|
3311
3419
|
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3420
|
+
// git 저장소 직접 사용
|
|
3421
|
+
if (installMode === "unknown" && existsSync(join(PKG_ROOT, ".git"))) {
|
|
3422
|
+
installMode = "git-local";
|
|
3423
|
+
}
|
|
3315
3424
|
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
updated = true;
|
|
3328
|
-
break;
|
|
3425
|
+
report.installMode = installMode;
|
|
3426
|
+
if (["git-local", "plugin", "npm-local"].includes(installMode))
|
|
3427
|
+
report.installPath =
|
|
3428
|
+
installMode === "npm-local" ? process.cwd() : pluginPath || PKG_ROOT;
|
|
3429
|
+
const networkTargets = resolveUpdateTargets({ installMode, pluginPath });
|
|
3430
|
+
if (networkTargets.length) {
|
|
3431
|
+
const network = await checkNetworkAvailability(networkTargets);
|
|
3432
|
+
if (!network.online)
|
|
3433
|
+
throw new Error(
|
|
3434
|
+
`네트워크 확인 실패: ${network.unreachable.join(", ")}`,
|
|
3435
|
+
);
|
|
3329
3436
|
}
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3437
|
+
const cache = validateRuntimeCachePaths(join(CLAUDE_DIR, "cache"));
|
|
3438
|
+
for (const issue of cache.issues)
|
|
3439
|
+
addWarning(report, "cache", `${issue.file}: ${issue.error}`);
|
|
3440
|
+
switch (installMode) {
|
|
3441
|
+
case "plugin":
|
|
3442
|
+
case "git-local":
|
|
3443
|
+
execSync("git pull", {
|
|
3335
3444
|
encoding: "utf8",
|
|
3336
|
-
timeout:
|
|
3337
|
-
|
|
3445
|
+
timeout: 30000,
|
|
3446
|
+
cwd: pluginPath || PKG_ROOT,
|
|
3447
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3338
3448
|
windowsHide: true,
|
|
3339
|
-
})
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3449
|
+
});
|
|
3450
|
+
break;
|
|
3451
|
+
case "npm-global": {
|
|
3452
|
+
let latest;
|
|
3453
|
+
try {
|
|
3454
|
+
latest = registryVersion();
|
|
3455
|
+
} catch {}
|
|
3456
|
+
if (latest === PKG.version) break;
|
|
3457
|
+
installNpmGlobal();
|
|
3458
|
+
break;
|
|
3459
|
+
}
|
|
3460
|
+
case "npm-local":
|
|
3461
|
+
execSync("npm update triflux", {
|
|
3346
3462
|
encoding: "utf8",
|
|
3347
|
-
timeout:
|
|
3463
|
+
timeout: 60000,
|
|
3464
|
+
cwd: process.cwd(),
|
|
3348
3465
|
stdio: ["pipe", "pipe", "pipe"],
|
|
3349
3466
|
windowsHide: true,
|
|
3350
|
-
})
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
ok(`${npmCmd}: ${result || "완료"}`);
|
|
3355
|
-
updated = true;
|
|
3356
|
-
break;
|
|
3357
|
-
}
|
|
3358
|
-
case "npm-local": {
|
|
3359
|
-
const npmLocalCmd = "npm update triflux";
|
|
3360
|
-
const result = execSync(npmLocalCmd, {
|
|
3361
|
-
encoding: "utf8",
|
|
3362
|
-
timeout: 60000,
|
|
3363
|
-
cwd: process.cwd(),
|
|
3364
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
3365
|
-
windowsHide: true,
|
|
3366
|
-
})
|
|
3367
|
-
.trim()
|
|
3368
|
-
.split(/\r?\n/)[0];
|
|
3369
|
-
ok(`${npmLocalCmd}: ${result || "완료"}`);
|
|
3370
|
-
updated = true;
|
|
3371
|
-
break;
|
|
3372
|
-
}
|
|
3373
|
-
case "git-local": {
|
|
3374
|
-
const result = execSync("git pull", {
|
|
3375
|
-
encoding: "utf8",
|
|
3376
|
-
timeout: 30000,
|
|
3377
|
-
cwd: PKG_ROOT,
|
|
3378
|
-
windowsHide: true,
|
|
3379
|
-
}).trim();
|
|
3380
|
-
ok(`git pull — ${result}`);
|
|
3381
|
-
updated = true;
|
|
3382
|
-
break;
|
|
3467
|
+
});
|
|
3468
|
+
break;
|
|
3469
|
+
default:
|
|
3470
|
+
throw new Error("설치 방식을 감지할 수 없음");
|
|
3383
3471
|
}
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
return;
|
|
3388
|
-
}
|
|
3389
|
-
} catch (e) {
|
|
3390
|
-
const stderr = e.stderr?.toString().trim();
|
|
3391
|
-
fail(
|
|
3392
|
-
`업데이트 실패: ${e.message}${stderr ? `\n ${stderr.split(/\r?\n/)[0]}` : ""}`,
|
|
3393
|
-
);
|
|
3394
|
-
return;
|
|
3395
|
-
}
|
|
3396
|
-
|
|
3397
|
-
// 3. setup 재실행 (파일 동기화, 프로파일, HUD, CLI 확인)
|
|
3398
|
-
if (updated) {
|
|
3399
|
-
console.log("");
|
|
3400
|
-
const updatedRoot =
|
|
3401
|
-
installMode === "plugin" ? pluginPath || PKG_ROOT : PKG_ROOT;
|
|
3402
|
-
// 업데이트 후 새 버전 읽기
|
|
3403
|
-
let newVer = oldVer;
|
|
3404
|
-
try {
|
|
3405
|
-
const newPkg = JSON.parse(
|
|
3472
|
+
const updatedRoot =
|
|
3473
|
+
installMode === "plugin" ? pluginPath || PKG_ROOT : PKG_ROOT;
|
|
3474
|
+
report.to = JSON.parse(
|
|
3406
3475
|
readFileSync(join(updatedRoot, "package.json"), "utf8"),
|
|
3407
|
-
);
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3476
|
+
).version;
|
|
3477
|
+
const setup = await runUpdatedSetup({
|
|
3478
|
+
packageRoot: updatedRoot,
|
|
3479
|
+
options,
|
|
3480
|
+
});
|
|
3481
|
+
Object.assign(report, {
|
|
3482
|
+
outcome: setup.outcome,
|
|
3483
|
+
message: setup.message,
|
|
3484
|
+
to: setup.to,
|
|
3485
|
+
changed: setup.changed,
|
|
3486
|
+
unchanged: setup.unchanged,
|
|
3487
|
+
targets: setup.targets,
|
|
3488
|
+
files: setup.files,
|
|
3489
|
+
steps: setup.steps,
|
|
3490
|
+
notices: setup.notices,
|
|
3491
|
+
alreadyInGoalState:
|
|
3492
|
+
setup.alreadyInGoalState && report.from === setup.to,
|
|
3423
3493
|
});
|
|
3494
|
+
for (const warning of setup.warnings)
|
|
3495
|
+
addWarning(report, warning.code, warning.message);
|
|
3496
|
+
if (setup.outcome !== "ok") {
|
|
3497
|
+
process.exitCode =
|
|
3498
|
+
setup.exitCode ?? (setup.outcome === "skipped" ? 0 : EXIT_ERROR);
|
|
3499
|
+
return process.stdout.write(renderCliResult(report, options));
|
|
3500
|
+
}
|
|
3424
3501
|
}
|
|
3502
|
+
report.message =
|
|
3503
|
+
report.from === report.to
|
|
3504
|
+
? "최신"
|
|
3505
|
+
: options.check
|
|
3506
|
+
? "업데이트 있음"
|
|
3507
|
+
: "업데이트 완료";
|
|
3508
|
+
} catch (error) {
|
|
3509
|
+
report.outcome = "failed";
|
|
3510
|
+
report.alreadyInGoalState = false;
|
|
3511
|
+
report.message = commandErrorMessage(error);
|
|
3512
|
+
process.exitCode = error.exitCode ?? EXIT_ERROR;
|
|
3425
3513
|
}
|
|
3426
|
-
|
|
3427
|
-
|
|
3514
|
+
report.elapsed = Date.now() - started;
|
|
3515
|
+
process.stdout.write(renderCliResult(report, options));
|
|
3428
3516
|
}
|
|
3429
3517
|
|
|
3430
3518
|
function cmdList(options = {}) {
|
|
@@ -3815,9 +3903,10 @@ async function main() {
|
|
|
3815
3903
|
const cmd = NORMALIZED_ARGS[0] || "help";
|
|
3816
3904
|
const cmdArgs = NORMALIZED_ARGS.slice(1);
|
|
3817
3905
|
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3906
|
+
if (!(["update", "setup"].includes(cmd) && cmdArgs.includes("--check")))
|
|
3907
|
+
cleanupTmpFiles({
|
|
3908
|
+
protectPaths: [process.env.HOME, process.env.USERPROFILE],
|
|
3909
|
+
}).catch(() => {});
|
|
3821
3910
|
|
|
3822
3911
|
switch (cmd) {
|
|
3823
3912
|
case "setup":
|
|
@@ -3825,9 +3914,20 @@ async function main() {
|
|
|
3825
3914
|
printCommandHelp("setup");
|
|
3826
3915
|
return;
|
|
3827
3916
|
}
|
|
3917
|
+
if (cmdArgs.includes("--check"))
|
|
3918
|
+
throw createCliError(
|
|
3919
|
+
"setup 은 --check 를 지원하지 않습니다. --dry-run 을 쓰세요.",
|
|
3920
|
+
{
|
|
3921
|
+
exitCode: EXIT_ARG_ERROR,
|
|
3922
|
+
reason: "argError",
|
|
3923
|
+
fix: "tfx setup --dry-run",
|
|
3924
|
+
},
|
|
3925
|
+
);
|
|
3828
3926
|
await cmdSetup({
|
|
3927
|
+
...outputOptions(RAW_ARGS),
|
|
3829
3928
|
dryRun: cmdArgs.includes("--dry-run"),
|
|
3830
3929
|
fromUpdate: cmdArgs.includes("--from-update"),
|
|
3930
|
+
consentOnly: cmdArgs.includes("--consent-only"),
|
|
3831
3931
|
mods: cmdArgs.includes("--mods"),
|
|
3832
3932
|
});
|
|
3833
3933
|
return;
|