@triflux/remote 10.58.0 → 10.59.1
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/package.json
CHANGED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
const AGENT_ENV = [
|
|
2
|
+
["claude-code", ["CLAUDECODE", "CLAUDE_CODE"]],
|
|
3
|
+
["codex", ["CODEX_SANDBOX", "CODEX_CI", "CODEX_THREAD_ID"]],
|
|
4
|
+
["gemini-cli", ["GEMINI_CLI"]],
|
|
5
|
+
["antigravity", ["ANTIGRAVITY_AGENT"]],
|
|
6
|
+
["opencode", ["OPENCODE_CLIENT"]],
|
|
7
|
+
["cursor", ["CURSOR_AGENT"]],
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
const enabled = (value) =>
|
|
11
|
+
Boolean(value) && !/^(0|false|no|off)$/iu.test(value);
|
|
12
|
+
|
|
13
|
+
export function outputOptions(
|
|
14
|
+
args = [],
|
|
15
|
+
{ env = process.env, input = process.stdin, output = process.stdout } = {},
|
|
16
|
+
) {
|
|
17
|
+
const agent =
|
|
18
|
+
AGENT_ENV.find(([, keys]) => keys.some((key) => enabled(env[key])))?.[0] ||
|
|
19
|
+
(enabled(env.AI_AGENT)
|
|
20
|
+
? env.AI_AGENT === "1"
|
|
21
|
+
? "ai-agent"
|
|
22
|
+
: env.AI_AGENT
|
|
23
|
+
: null);
|
|
24
|
+
const json = args.includes("--json");
|
|
25
|
+
return {
|
|
26
|
+
json,
|
|
27
|
+
quiet: args.includes("--quiet") || args.includes("-q"),
|
|
28
|
+
verbose: args.includes("--verbose"),
|
|
29
|
+
check: args.includes("--check"),
|
|
30
|
+
agent,
|
|
31
|
+
nonInteractive:
|
|
32
|
+
json ||
|
|
33
|
+
Boolean(agent) ||
|
|
34
|
+
!input?.isTTY ||
|
|
35
|
+
!output?.isTTY ||
|
|
36
|
+
enabled(env.CI) ||
|
|
37
|
+
enabled(env.DOCKER) ||
|
|
38
|
+
env.npm_lifecycle_event === "postinstall",
|
|
39
|
+
color:
|
|
40
|
+
Boolean(output?.isTTY) &&
|
|
41
|
+
env.NO_COLOR === undefined &&
|
|
42
|
+
env.TERM !== "dumb" &&
|
|
43
|
+
(env.FORCE_COLOR === undefined ||
|
|
44
|
+
["", "true", "1", "2", "3"].includes(env.FORCE_COLOR)),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// setup/update JSON 계약: command, outcome, message, alreadyInGoalState,
|
|
49
|
+
// from, to, changed, unchanged, targets, warnings, notices.
|
|
50
|
+
// 해당 결과에만 포함: installMode, installPath, exitCode.
|
|
51
|
+
// 내부 자식 결과에만 포함: files, steps, elapsed.
|
|
52
|
+
export function createCliResult(command, version) {
|
|
53
|
+
return {
|
|
54
|
+
command,
|
|
55
|
+
outcome: "ok",
|
|
56
|
+
message: "",
|
|
57
|
+
alreadyInGoalState: true,
|
|
58
|
+
from: version,
|
|
59
|
+
to: version,
|
|
60
|
+
changed: [],
|
|
61
|
+
unchanged: 0,
|
|
62
|
+
targets: [],
|
|
63
|
+
warnings: [],
|
|
64
|
+
notices: [],
|
|
65
|
+
files: [],
|
|
66
|
+
steps: [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function recordFile(result, path, change) {
|
|
71
|
+
result.files.push({ path, change });
|
|
72
|
+
if (change === "noop") result.unchanged++;
|
|
73
|
+
else {
|
|
74
|
+
result.changed.push(path);
|
|
75
|
+
result.alreadyInGoalState = false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function addWarning(result, code, message) {
|
|
80
|
+
if (!result.warnings.some((warning) => warning.message === message))
|
|
81
|
+
result.warnings.push({ code, message });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function addStep(result, message) {
|
|
85
|
+
result.steps.push(message);
|
|
86
|
+
result.alreadyInGoalState = false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function addNotice(result, message) {
|
|
90
|
+
if (!result.notices.includes(message)) result.notices.push(message);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function commandErrorMessage(error) {
|
|
94
|
+
const message = error.message.split(/\r?\n/)[0];
|
|
95
|
+
const cause = String(error.stderr || "")
|
|
96
|
+
.split(/\r?\n/)
|
|
97
|
+
.find((line) => line.trim())
|
|
98
|
+
?.trim();
|
|
99
|
+
return cause && !message.includes(cause) ? `${message}: ${cause}` : message;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function updateCommand(result, rollback = false) {
|
|
103
|
+
if (["git-local", "plugin"].includes(result.installMode))
|
|
104
|
+
return rollback ? `git checkout v${result.from}` : "git pull";
|
|
105
|
+
const version = rollback ? result.from : "latest";
|
|
106
|
+
return result.installMode === "npm-local"
|
|
107
|
+
? rollback
|
|
108
|
+
? `npm i triflux@${version}`
|
|
109
|
+
: "npm update triflux"
|
|
110
|
+
: `npm i -g triflux@${version}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function renderCliResult(result, options = {}) {
|
|
114
|
+
const { json, quiet, verbose, check, agent, nonInteractive, color, details } =
|
|
115
|
+
options;
|
|
116
|
+
if (json) {
|
|
117
|
+
const { files, steps, elapsed, ...publicResult } = result;
|
|
118
|
+
return `${JSON.stringify(details ? result : publicResult)}\n`;
|
|
119
|
+
}
|
|
120
|
+
const mark = (symbol) =>
|
|
121
|
+
color
|
|
122
|
+
? `\x1b[${symbol === "✗" ? 31 : symbol === "!" ? 33 : 32}m${symbol}\x1b[0m`
|
|
123
|
+
: symbol;
|
|
124
|
+
const lines = [];
|
|
125
|
+
if (result.outcome === "failed") {
|
|
126
|
+
lines.push(
|
|
127
|
+
`${mark("✗")} ${result.command === "update" ? "업데이트" : "설정"} 실패: ${result.message}`,
|
|
128
|
+
);
|
|
129
|
+
if (result.command === "update") {
|
|
130
|
+
if (result.installPath) lines.push(`실행 경로: ${result.installPath}`);
|
|
131
|
+
lines.push(`직접 실행: ${updateCommand(result)}`);
|
|
132
|
+
if (result.from !== result.to)
|
|
133
|
+
lines.push(`되돌리기: ${updateCommand(result, true)}`);
|
|
134
|
+
}
|
|
135
|
+
} else if (result.outcome === "skipped") {
|
|
136
|
+
if (!quiet) lines.push(`skipped: ${result.message}`);
|
|
137
|
+
} else if (!quiet) {
|
|
138
|
+
if (result.command === "update") {
|
|
139
|
+
const versionChanged = result.from !== result.to;
|
|
140
|
+
const version = versionChanged
|
|
141
|
+
? `${result.from} -> ${result.to}`
|
|
142
|
+
: `${result.to} 최신`;
|
|
143
|
+
if (check)
|
|
144
|
+
lines.push(
|
|
145
|
+
versionChanged
|
|
146
|
+
? `업데이트 있음: ${result.from} -> ${result.to}`
|
|
147
|
+
: `${mark("✓")} triflux ${version}`,
|
|
148
|
+
);
|
|
149
|
+
else {
|
|
150
|
+
const time = `${(result.elapsed / 1000).toFixed(1)}s`;
|
|
151
|
+
const detail =
|
|
152
|
+
!versionChanged && !result.changed.length
|
|
153
|
+
? `파일 ${result.unchanged}개 일치, ${time}`
|
|
154
|
+
: time;
|
|
155
|
+
lines.push(`${mark("✓")} triflux ${version} (${detail})`);
|
|
156
|
+
if (versionChanged)
|
|
157
|
+
lines.push(
|
|
158
|
+
` https://github.com/tellang/triflux/releases/tag/v${result.to}`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (result.changed.length)
|
|
162
|
+
lines.push(
|
|
163
|
+
`파일 ${result.changed.length}개 반영 (${result.unchanged}개 그대로)`,
|
|
164
|
+
);
|
|
165
|
+
} else {
|
|
166
|
+
const context = agent
|
|
167
|
+
? ` (에이전트: ${agent}, 비대화식)`
|
|
168
|
+
: nonInteractive
|
|
169
|
+
? " (비대화식)"
|
|
170
|
+
: "";
|
|
171
|
+
lines.push(`triflux ${result.to} setup${context}`);
|
|
172
|
+
}
|
|
173
|
+
if (!check) {
|
|
174
|
+
if (result.command === "update" || verbose) {
|
|
175
|
+
for (const { path, change } of result.files) {
|
|
176
|
+
if (change === "noop" && !verbose) continue;
|
|
177
|
+
lines.push(
|
|
178
|
+
` ${mark(change === "create" ? "+" : change === "noop" ? "✓" : "~")} ${path}`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (
|
|
183
|
+
result.command === "setup" ||
|
|
184
|
+
!result.alreadyInGoalState ||
|
|
185
|
+
result.warnings.length
|
|
186
|
+
) {
|
|
187
|
+
for (const { name, status, detail } of result.targets)
|
|
188
|
+
lines.push(
|
|
189
|
+
` ${mark(status === "ok" ? "✓" : status === "skipped" ? "-" : "✗")} ${name.padEnd(8)} ${detail}`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
for (const step of result.steps) lines.push(` ${mark("✓")} ${step}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
for (const warning of result.warnings)
|
|
196
|
+
lines.push(`${mark("!")} ${warning.message}`);
|
|
197
|
+
if (!quiet)
|
|
198
|
+
for (const notice of result.notices) lines.push(`안내: ${notice}`);
|
|
199
|
+
if (!quiet && result.outcome === "ok" && !check) {
|
|
200
|
+
if (result.command === "setup") {
|
|
201
|
+
lines.push(
|
|
202
|
+
`설정 완료${result.warnings.length ? `, 경고 ${result.warnings.length}건` : ""}`,
|
|
203
|
+
"다음: tfx doctor",
|
|
204
|
+
);
|
|
205
|
+
} else if (result.warnings.length || result.from !== result.to) {
|
|
206
|
+
if (result.from !== result.to && result.installPath)
|
|
207
|
+
lines.push(`되돌리기 실행 경로: ${result.installPath}`);
|
|
208
|
+
lines.push(
|
|
209
|
+
`${result.warnings.length ? `경고 ${result.warnings.length}건. ` : ""}${result.from !== result.to ? `되돌리기: ${updateCommand(result, true)}` : ""}`.trim(),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return lines.length ? `${lines.join("\n")}\n` : "";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function postinstallMessage(version, report) {
|
|
217
|
+
if (!report) return "저장소 체크아웃이라 setup 을 건너뜀. 설정: tfx setup\n";
|
|
218
|
+
const status =
|
|
219
|
+
report.outcome === "failed"
|
|
220
|
+
? `실패: ${report.message}`
|
|
221
|
+
: report.outcome === "skipped"
|
|
222
|
+
? `건너뜀: ${report.message}`
|
|
223
|
+
: "완료";
|
|
224
|
+
const warnings = report.warnings
|
|
225
|
+
.map(({ message }) => `! ${message}\n`)
|
|
226
|
+
.join("");
|
|
227
|
+
return `triflux ${version} 설치됨. 설정 ${status}${report.warnings.length ? `, 경고 ${report.warnings.length}건` : ""}. 확인: tfx setup\n${warnings}`;
|
|
228
|
+
}
|
|
@@ -1053,7 +1053,7 @@ function listHubProcesses(platform, run) {
|
|
|
1053
1053
|
|
|
1054
1054
|
// 허브는 제거됐으므로 triflux 패키지의 hub/server.mjs 로 확인된 프로세스는 설치본,
|
|
1055
1055
|
// 체크아웃, worktree 를 가리지 않고 종료한다(2026-10-08 결정).
|
|
1056
|
-
function stopHubs(home, platform, run, result
|
|
1056
|
+
function stopHubs(home, platform, run, result) {
|
|
1057
1057
|
const processes = listHubProcesses(platform, run);
|
|
1058
1058
|
if (!processes) {
|
|
1059
1059
|
result.warnings.push("허브 프로세스 조회 실패, 종료하지 않음");
|
|
@@ -1064,7 +1064,6 @@ function stopHubs(home, platform, run, result, log) {
|
|
|
1064
1064
|
if (Number(pid) === process.pid || typeof command !== "string") continue;
|
|
1065
1065
|
const root = trifluxHubRoot(processArgv(pid, command, platform));
|
|
1066
1066
|
if (!root) continue;
|
|
1067
|
-
log(`허브 종료: pid ${pid} (${root})`);
|
|
1068
1067
|
const stopped =
|
|
1069
1068
|
platform === "win32"
|
|
1070
1069
|
? tryRun(run, "taskkill", ["/F", "/T", "/PID", String(pid)])
|
|
@@ -1157,7 +1156,6 @@ export function cleanupTfxHub({
|
|
|
1157
1156
|
platform = osPlatform(),
|
|
1158
1157
|
run = defaultRun,
|
|
1159
1158
|
pluginRoot,
|
|
1160
|
-
log = () => {},
|
|
1161
1159
|
backups,
|
|
1162
1160
|
} = {}) {
|
|
1163
1161
|
const result = {
|
|
@@ -1210,7 +1208,7 @@ export function cleanupTfxHub({
|
|
|
1210
1208
|
(resolve(home) === resolve(homedir()) && platform === osPlatform());
|
|
1211
1209
|
if (realSystem) {
|
|
1212
1210
|
try {
|
|
1213
|
-
stopHubs(home, platform, run, result
|
|
1211
|
+
stopHubs(home, platform, run, result);
|
|
1214
1212
|
if (platform === "win32") removeHubTasks(run, result);
|
|
1215
1213
|
} catch (error) {
|
|
1216
1214
|
result.ok = false;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
chmodSync,
|
|
5
|
+
copyFileSync,
|
|
6
|
+
linkSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
mkdtempSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
symlinkSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { delimiter, join } from "node:path";
|
|
17
|
+
|
|
18
|
+
// 버전과 체크섬은 이 상수 한 곳에서만 올린다. 받은 파일은 이 sha256 으로만 믿는다.
|
|
19
|
+
export const TMR_RELEASE = Object.freeze({
|
|
20
|
+
version: "0.1.0",
|
|
21
|
+
asset: "tmux-rooms-cli-0.1.0-macos-arm64.zip",
|
|
22
|
+
sha256: "342b34768a6f054d6bda6d3a63de37aeae08c5304d4531eecedf9631805a282f",
|
|
23
|
+
});
|
|
24
|
+
const releaseBase = ({ version }) =>
|
|
25
|
+
`https://github.com/tellang/tmux-rooms/releases/download/v${version}`;
|
|
26
|
+
|
|
27
|
+
export function tmrSupported(platform = process.platform, arch = process.arch) {
|
|
28
|
+
return platform === "darwin" && arch === "arm64";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function tmrBinDir(home) {
|
|
32
|
+
return join(home, ".local", "bin");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function findTmr({ home, env = process.env }) {
|
|
36
|
+
const dirs = [
|
|
37
|
+
tmrBinDir(home),
|
|
38
|
+
...String(env.PATH || "").split(delimiter),
|
|
39
|
+
].filter(Boolean);
|
|
40
|
+
// 깨진 링크도 사용자 것으로 보고 건드리지 않는다. 읽을 수 없는 PATH 항목은 건너뛴다.
|
|
41
|
+
for (const dir of dirs)
|
|
42
|
+
for (const name of ["tmuxrooms", "tmr"]) {
|
|
43
|
+
try {
|
|
44
|
+
if (lstatSync(join(dir, name), { throwIfNoEntry: false }))
|
|
45
|
+
return join(dir, name);
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function download(url, fetchFn) {
|
|
52
|
+
const response = await fetchFn(url, { signal: AbortSignal.timeout(60_000) });
|
|
53
|
+
if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`);
|
|
54
|
+
return Buffer.from(await response.arrayBuffer());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sha256(buffer) {
|
|
58
|
+
return createHash("sha256").update(buffer).digest("hex");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function extractWithDitto(zipPath, directory) {
|
|
62
|
+
execFileSync("ditto", ["-x", "-k", zipPath, directory], {
|
|
63
|
+
stdio: "ignore",
|
|
64
|
+
timeout: 30_000,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 체크섬이 맞는 tmuxrooms 를 ~/.local/bin 에 넣고 상대 링크 tmr 을 만든다. */
|
|
69
|
+
export async function installTmr({
|
|
70
|
+
home,
|
|
71
|
+
release = TMR_RELEASE,
|
|
72
|
+
fetchFn = fetch,
|
|
73
|
+
extract = extractWithDitto,
|
|
74
|
+
run = execFileSync,
|
|
75
|
+
symlink = symlinkSync,
|
|
76
|
+
}) {
|
|
77
|
+
const base = releaseBase(release);
|
|
78
|
+
const zip = await download(`${base}/${release.asset}`, fetchFn);
|
|
79
|
+
if (sha256(zip) !== release.sha256)
|
|
80
|
+
throw new Error("zip 체크섬이 고정값과 다르다");
|
|
81
|
+
const sums = (await download(`${base}/SHA256SUMS`, fetchFn))
|
|
82
|
+
.toString("utf8")
|
|
83
|
+
.split(/\r?\n/)
|
|
84
|
+
.map((line) => line.trim().split(/\s+/));
|
|
85
|
+
const listed = sums.find(([, name]) => name === release.asset)?.[0];
|
|
86
|
+
if (listed !== release.sha256)
|
|
87
|
+
throw new Error("SHA256SUMS 의 값이 고정값과 다르다");
|
|
88
|
+
|
|
89
|
+
const work = mkdtempSync(join(tmpdir(), "tfx-tmr-"));
|
|
90
|
+
try {
|
|
91
|
+
const zipPath = join(work, release.asset);
|
|
92
|
+
const out = join(work, "out");
|
|
93
|
+
writeFileSync(zipPath, zip);
|
|
94
|
+
mkdirSync(out);
|
|
95
|
+
extract(zipPath, out);
|
|
96
|
+
const extracted = join(out, "tmuxrooms");
|
|
97
|
+
// zip 에는 실행 파일 하나만 있어야 한다. 다른 구성이면 받은 것을 쓰지 않는다.
|
|
98
|
+
const entries = readdirSync(out);
|
|
99
|
+
if (
|
|
100
|
+
entries.length !== 1 ||
|
|
101
|
+
entries[0] !== "tmuxrooms" ||
|
|
102
|
+
!lstatSync(extracted).isFile()
|
|
103
|
+
)
|
|
104
|
+
throw new Error("zip 구성이 예상과 다르다");
|
|
105
|
+
|
|
106
|
+
const binDir = tmrBinDir(home);
|
|
107
|
+
mkdirSync(binDir, { recursive: true, mode: 0o755 });
|
|
108
|
+
const target = join(binDir, "tmuxrooms");
|
|
109
|
+
// 임시 파일은 이번 시도만 쓰는 디렉터리에 둔다. 정리할 때 남의 파일을 지우지 않는다.
|
|
110
|
+
const staging = mkdtempSync(join(binDir, ".tfx-tmr-"));
|
|
111
|
+
try {
|
|
112
|
+
const temporary = join(staging, "tmuxrooms");
|
|
113
|
+
copyFileSync(extracted, temporary);
|
|
114
|
+
chmodSync(temporary, 0o755);
|
|
115
|
+
// 묻는 사이 다른 쪽이 tmuxrooms 를 만들었으면 덮지 않고 실패한다(EEXIST).
|
|
116
|
+
linkSync(temporary, target);
|
|
117
|
+
} finally {
|
|
118
|
+
rmSync(staging, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
const link = join(binDir, "tmr");
|
|
121
|
+
// 이미 있는 tmr 은 사용자 것일 수 있어 건드리지 않는다. 링크를 못 만들어도
|
|
122
|
+
// 체크섬이 맞는 tmuxrooms 는 완전한 설치본이라 지우지 않고 안내만 남긴다.
|
|
123
|
+
let linked = false;
|
|
124
|
+
try {
|
|
125
|
+
if (!lstatSync(link, { throwIfNoEntry: false })) {
|
|
126
|
+
symlink("tmuxrooms", link);
|
|
127
|
+
linked = true;
|
|
128
|
+
}
|
|
129
|
+
} catch {}
|
|
130
|
+
|
|
131
|
+
let runs = true;
|
|
132
|
+
try {
|
|
133
|
+
run(target, ["--help"], { stdio: "ignore", timeout: 10_000 });
|
|
134
|
+
} catch {
|
|
135
|
+
runs = false;
|
|
136
|
+
}
|
|
137
|
+
return { target, link, linked, runs };
|
|
138
|
+
} finally {
|
|
139
|
+
rmSync(work, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** setup 동의 단계. macOS arm64 에서 tmr 이 없을 때만 묻고, 물을 수 없으면 안내만 남긴다. */
|
|
144
|
+
export async function offerTmrInstall({
|
|
145
|
+
interactive,
|
|
146
|
+
ask,
|
|
147
|
+
home,
|
|
148
|
+
env = process.env,
|
|
149
|
+
platform = process.platform,
|
|
150
|
+
arch = process.arch,
|
|
151
|
+
install = installTmr,
|
|
152
|
+
log = console.log,
|
|
153
|
+
warn = console.warn,
|
|
154
|
+
notice = console.log,
|
|
155
|
+
}) {
|
|
156
|
+
if (!tmrSupported(platform, arch)) return "not-needed";
|
|
157
|
+
if (findTmr({ home, env })) return "present";
|
|
158
|
+
if (!interactive) {
|
|
159
|
+
notice("tmr 이 없다. tfx setup 에서 설치를 물을 수 있다.");
|
|
160
|
+
return "deferred";
|
|
161
|
+
}
|
|
162
|
+
if (!(await ask("tmr 을 설치할까요? (tmux 세션 목록 도구, ~/.local/bin)"))) {
|
|
163
|
+
notice("tmr 설치를 건너뜀.");
|
|
164
|
+
return "declined";
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const { target, link, linked, runs } = await install({ home });
|
|
168
|
+
log(`tmr ${TMR_RELEASE.version} 설치: ${target}`);
|
|
169
|
+
if (!linked && !lstatSync(link, { throwIfNoEntry: false }))
|
|
170
|
+
warn(`tmr 링크를 만들지 못했다. 직접 만든다: ln -s tmuxrooms "${link}"`);
|
|
171
|
+
if (!runs) warn(`설치한 ${target} 가 실행되지 않는다.`);
|
|
172
|
+
const binDir = tmrBinDir(home);
|
|
173
|
+
if (
|
|
174
|
+
!String(env.PATH || "")
|
|
175
|
+
.split(delimiter)
|
|
176
|
+
.includes(binDir)
|
|
177
|
+
)
|
|
178
|
+
warn(`${binDir} 가 PATH 에 없다. 셸 설정에 추가한다.`);
|
|
179
|
+
return "installed";
|
|
180
|
+
} catch (error) {
|
|
181
|
+
warn(`tmr 설치 실패: ${error?.message || error}`);
|
|
182
|
+
return "failed";
|
|
183
|
+
}
|
|
184
|
+
}
|