triflux 10.18.0 → 10.18.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/CLAUDE.md +1 -0
- package/LICENSE +21 -21
- package/bin/triflux.mjs +137 -0
- package/hub/codex-adapter.mjs +4 -1
- package/hud/providers/claude.mjs +105 -9
- package/package.json +1 -1
- package/scripts/__tests__/release-governance.test.mjs +6 -1
- package/scripts/completions/tfx.bash +47 -47
- package/scripts/completions/tfx.fish +44 -44
- package/scripts/completions/tfx.zsh +83 -83
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/release/prepare.mjs +21 -12
- package/scripts/setup.mjs +10 -3
- package/scripts/tfx-route.sh +6 -2
- package/scripts/token-snapshot.mjs +4 -2
- package/references/hosts.json +0 -46
- package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +0 -1
- package/skills/.omc/state/idle-notif-cooldown.json +0 -3
- package/skills/.omc/state/last-tool-error.json +0 -7
- package/skills/.omc/state/subagent-tracking.json +0 -7
- package/skills/tfx-remote-spawn/references/hosts.json +0 -41
- package/skills/tfx-remote-spawn/references/hosts.json.bak.20260425_040814 +0 -16
package/CLAUDE.md
CHANGED
|
@@ -157,6 +157,7 @@ background로 실행한 headless 결과는 **반드시 task-notification 완료
|
|
|
157
157
|
| `.claude/rules/tfx-autoplan-principles.md` | gstack autoplan의 6 decision principles, phase 우선순위, 충돌 해소 규칙 추출본 |
|
|
158
158
|
| `.claude/rules/tfx-update-logic.md` | triflux / OMC / gstack / Codex / Gemini 업데이트 로직 |
|
|
159
159
|
| `.claude/rules/tfx-stack-coexistence.md` | gstack / superpowers / triflux 공존 원칙, 레이어 분리, 의존 방향, 충돌 해소 |
|
|
160
|
+
| `.claude/rules/tfx-mirror-policy.md` | packages/ 3-layer mirror 정책 (core 단순 cp / remote import 변환 / triflux byte-identical), tests 제외 룰, drift 차단 |
|
|
160
161
|
|
|
161
162
|
Claude Code는 `.claude/rules/*.md` 를 자동 로드한다. Codex CLI는 `@import` 미지원이므로 필요 시 `AGENTS.md` 를 독립 유지한다.
|
|
162
163
|
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 tellang
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 tellang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/bin/triflux.mjs
CHANGED
|
@@ -43,6 +43,12 @@ import {
|
|
|
43
43
|
ensureTfxSection,
|
|
44
44
|
getLatestRoutingTable,
|
|
45
45
|
} from "../scripts/claudemd-sync.mjs";
|
|
46
|
+
import {
|
|
47
|
+
applyPluginRootHookFallbacks,
|
|
48
|
+
commandExists,
|
|
49
|
+
findPluginRootHookIssues,
|
|
50
|
+
inspectMacTimeoutDependency,
|
|
51
|
+
} from "../scripts/lib/doctor-env-checks.mjs";
|
|
46
52
|
import { ensureGeminiProfiles } from "../scripts/lib/gemini-profiles.mjs";
|
|
47
53
|
import { serializeHandoff } from "../scripts/lib/handoff.mjs";
|
|
48
54
|
import {
|
|
@@ -2041,6 +2047,60 @@ async function cmdDoctor(options = {}) {
|
|
|
2041
2047
|
for (const target of SYNC_MAP) {
|
|
2042
2048
|
syncFile(target.src, target.dst, target.label);
|
|
2043
2049
|
}
|
|
2050
|
+
const macTimeoutForFix = inspectMacTimeoutDependency();
|
|
2051
|
+
if (!macTimeoutForFix.ok) {
|
|
2052
|
+
if (commandExists("brew")) {
|
|
2053
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
2054
|
+
info("macOS GNU timeout 부재: coreutils 설치 가능");
|
|
2055
|
+
process.stdout.write(
|
|
2056
|
+
" brew install coreutils 실행할까요? [y/N] ",
|
|
2057
|
+
);
|
|
2058
|
+
let answer = "";
|
|
2059
|
+
try {
|
|
2060
|
+
const buf = Buffer.alloc(128);
|
|
2061
|
+
const n = readSync(0, buf, 0, 128);
|
|
2062
|
+
answer = buf.toString("utf8", 0, n).trim().toLowerCase();
|
|
2063
|
+
} catch {
|
|
2064
|
+
answer = "";
|
|
2065
|
+
}
|
|
2066
|
+
if (answer.startsWith("y")) {
|
|
2067
|
+
try {
|
|
2068
|
+
execFileSync("brew", ["install", "coreutils"], {
|
|
2069
|
+
stdio: "inherit",
|
|
2070
|
+
timeout: 600000,
|
|
2071
|
+
windowsHide: true,
|
|
2072
|
+
});
|
|
2073
|
+
report.actions.push({
|
|
2074
|
+
type: "install",
|
|
2075
|
+
name: "coreutils",
|
|
2076
|
+
status: "ok",
|
|
2077
|
+
});
|
|
2078
|
+
ok("coreutils 설치 완료");
|
|
2079
|
+
} catch (error) {
|
|
2080
|
+
report.actions.push({
|
|
2081
|
+
type: "install",
|
|
2082
|
+
name: "coreutils",
|
|
2083
|
+
status: "failed",
|
|
2084
|
+
message: error.message,
|
|
2085
|
+
});
|
|
2086
|
+
warn(
|
|
2087
|
+
`coreutils 설치 실패: ${renderErrorMessage(error.message)}`,
|
|
2088
|
+
);
|
|
2089
|
+
}
|
|
2090
|
+
} else {
|
|
2091
|
+
info("건너뜀: brew install coreutils");
|
|
2092
|
+
}
|
|
2093
|
+
} else {
|
|
2094
|
+
warn(
|
|
2095
|
+
"macOS GNU timeout 부재 — 비대화형 모드에서는 자동 설치하지 않습니다.",
|
|
2096
|
+
);
|
|
2097
|
+
info("수동 설치: brew install coreutils");
|
|
2098
|
+
}
|
|
2099
|
+
} else {
|
|
2100
|
+
warn("macOS GNU timeout 부재 — Homebrew를 찾지 못했습니다.");
|
|
2101
|
+
info("Homebrew 설치 후: brew install coreutils");
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2044
2104
|
{
|
|
2045
2105
|
const claudeGuide = ensureGlobalClaudeRoutingSection(CLAUDE_DIR);
|
|
2046
2106
|
if (
|
|
@@ -2197,6 +2257,34 @@ async function cmdDoctor(options = {}) {
|
|
|
2197
2257
|
issues++;
|
|
2198
2258
|
}
|
|
2199
2259
|
|
|
2260
|
+
// macOS GNU timeout/coreutils
|
|
2261
|
+
section("macOS timeout");
|
|
2262
|
+
const macTimeout = inspectMacTimeoutDependency();
|
|
2263
|
+
if (macTimeout.status === "skipped") {
|
|
2264
|
+
addDoctorCheck(report, {
|
|
2265
|
+
name: "macos-timeout",
|
|
2266
|
+
status: "skipped",
|
|
2267
|
+
platform: process.platform,
|
|
2268
|
+
});
|
|
2269
|
+
info("macOS 아님 — 건너뜀");
|
|
2270
|
+
} else if (macTimeout.ok) {
|
|
2271
|
+
addDoctorCheck(report, {
|
|
2272
|
+
name: "macos-timeout",
|
|
2273
|
+
status: "ok",
|
|
2274
|
+
provider: macTimeout.provider,
|
|
2275
|
+
});
|
|
2276
|
+
ok(`timeout provider: ${macTimeout.provider}`);
|
|
2277
|
+
} else {
|
|
2278
|
+
addDoctorCheck(report, {
|
|
2279
|
+
name: "macos-timeout",
|
|
2280
|
+
status: "missing",
|
|
2281
|
+
fix: macTimeout.fix,
|
|
2282
|
+
});
|
|
2283
|
+
warn("GNU timeout/gtimeout 미설치");
|
|
2284
|
+
info("수정: brew install coreutils");
|
|
2285
|
+
issues++;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2200
2288
|
// 2. HUD
|
|
2201
2289
|
section("HUD");
|
|
2202
2290
|
const hud = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
@@ -3813,6 +3901,55 @@ async function cmdDoctor(options = {}) {
|
|
|
3813
3901
|
}
|
|
3814
3902
|
|
|
3815
3903
|
if (settings) {
|
|
3904
|
+
let pluginRootHookIssues = findPluginRootHookIssues(settings);
|
|
3905
|
+
if (pluginRootHookIssues.length > 0 && fix) {
|
|
3906
|
+
const fallbackResult = applyPluginRootHookFallbacks(settings, {
|
|
3907
|
+
pluginRoot: PKG_ROOT,
|
|
3908
|
+
});
|
|
3909
|
+
if (fallbackResult.changed) {
|
|
3910
|
+
writeFileSync(
|
|
3911
|
+
settingsPath,
|
|
3912
|
+
JSON.stringify(settings, null, 2) + "\n",
|
|
3913
|
+
"utf8",
|
|
3914
|
+
);
|
|
3915
|
+
ok(
|
|
3916
|
+
`PLUGIN_ROOT fallback ${fallbackResult.count}개 hook command에 적용됨`,
|
|
3917
|
+
);
|
|
3918
|
+
try {
|
|
3919
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
3920
|
+
pluginRootHookIssues = findPluginRootHookIssues(settings);
|
|
3921
|
+
} catch (error) {
|
|
3922
|
+
warn(`PLUGIN_ROOT fallback 재검증 실패: ${error.message}`);
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3927
|
+
addDoctorCheck(report, {
|
|
3928
|
+
name: "hook-plugin-root",
|
|
3929
|
+
status:
|
|
3930
|
+
pluginRootHookIssues.length === 0 ? "ok" : "missing-fallback",
|
|
3931
|
+
count: pluginRootHookIssues.length,
|
|
3932
|
+
examples: pluginRootHookIssues.slice(0, 3).map((issue) => ({
|
|
3933
|
+
event: issue.event,
|
|
3934
|
+
reason: issue.reason,
|
|
3935
|
+
command: issue.command,
|
|
3936
|
+
})),
|
|
3937
|
+
...(pluginRootHookIssues.length > 0
|
|
3938
|
+
? { fix: "tfx doctor --fix 또는 tfx setup" }
|
|
3939
|
+
: {}),
|
|
3940
|
+
});
|
|
3941
|
+
if (pluginRootHookIssues.length === 0) {
|
|
3942
|
+
ok("PLUGIN_ROOT hook fallback 확인됨");
|
|
3943
|
+
} else {
|
|
3944
|
+
warn(
|
|
3945
|
+
`PLUGIN_ROOT fallback 없는 hook ${pluginRootHookIssues.length}개 감지`,
|
|
3946
|
+
);
|
|
3947
|
+
info(
|
|
3948
|
+
"영향: npm 단독 설치에서 /hooks/... 경로로 붕괴할 수 있습니다.",
|
|
3949
|
+
);
|
|
3950
|
+
issues += pluginRootHookIssues.length;
|
|
3951
|
+
}
|
|
3952
|
+
|
|
3816
3953
|
let coverage = computeHookCoverage(settings, managedHooks);
|
|
3817
3954
|
|
|
3818
3955
|
if (coverage.missing.length > 0 && fix) {
|
package/hub/codex-adapter.mjs
CHANGED
|
@@ -184,7 +184,10 @@ function buildLeaseSpawnEnv(lease) {
|
|
|
184
184
|
|
|
185
185
|
function dirnameOf(filePath) {
|
|
186
186
|
if (typeof filePath !== "string" || !filePath) return null;
|
|
187
|
-
const lastSep = Math.max(
|
|
187
|
+
const lastSep = Math.max(
|
|
188
|
+
filePath.lastIndexOf("/"),
|
|
189
|
+
filePath.lastIndexOf("\\"),
|
|
190
|
+
);
|
|
188
191
|
if (lastSep < 0) return null;
|
|
189
192
|
return filePath.slice(0, lastSep);
|
|
190
193
|
}
|
package/hud/providers/claude.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Claude Usage API (api.anthropic.com/api/oauth/usage)
|
|
3
3
|
// ============================================================================
|
|
4
4
|
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
5
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
6
6
|
import { existsSync, writeFileSync } from "node:fs";
|
|
7
7
|
import https from "node:https";
|
|
8
8
|
import {
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
|
|
32
32
|
export const CLAUDE_USAGE_POLL_BASE_MS = 5_000;
|
|
33
33
|
export const CLAUDE_USAGE_POLL_JITTER_RATIO = 0.2;
|
|
34
|
+
const CLAUDE_KEYCHAIN_SERVICE = "Claude Code-credentials";
|
|
34
35
|
export const CLAUDE_USAGE_RATE_LIMIT_BACKOFF_MS = [
|
|
35
36
|
CLAUDE_USAGE_POLL_BASE_MS,
|
|
36
37
|
10_000,
|
|
@@ -98,8 +99,7 @@ function getSnapshotSchedule(cache) {
|
|
|
98
99
|
};
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
const data = readJson(CLAUDE_CREDENTIALS_PATH, null);
|
|
102
|
+
function normalizeClaudeCredentials(data, source, supportsUsageApi = true) {
|
|
103
103
|
if (!data) return null;
|
|
104
104
|
const creds = data.claudeAiOauth || data;
|
|
105
105
|
if (!creds.accessToken) return null;
|
|
@@ -107,9 +107,51 @@ export function readClaudeCredentials() {
|
|
|
107
107
|
accessToken: creds.accessToken,
|
|
108
108
|
refreshToken: creds.refreshToken,
|
|
109
109
|
expiresAt: creds.expiresAt,
|
|
110
|
+
source,
|
|
111
|
+
supportsUsageApi,
|
|
110
112
|
};
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
function readClaudeKeychainCredentials(execFileSyncFn = execFileSync) {
|
|
116
|
+
try {
|
|
117
|
+
const raw = execFileSyncFn(
|
|
118
|
+
"security",
|
|
119
|
+
["find-generic-password", "-s", CLAUDE_KEYCHAIN_SERVICE, "-w"],
|
|
120
|
+
{ encoding: "utf8" },
|
|
121
|
+
);
|
|
122
|
+
return normalizeClaudeCredentials(JSON.parse(raw.trim()), "keychain");
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function readClaudeCredentials({
|
|
129
|
+
readCredentialFile = () => readJson(CLAUDE_CREDENTIALS_PATH, null),
|
|
130
|
+
platform = process.platform,
|
|
131
|
+
execFileSyncFn = execFileSync,
|
|
132
|
+
env = process.env,
|
|
133
|
+
} = {}) {
|
|
134
|
+
const fileCreds = normalizeClaudeCredentials(readCredentialFile(), "file");
|
|
135
|
+
if (fileCreds) return fileCreds;
|
|
136
|
+
|
|
137
|
+
if (platform === "darwin") {
|
|
138
|
+
const keychainCreds = readClaudeKeychainCredentials(execFileSyncFn);
|
|
139
|
+
if (keychainCreds) return keychainCreds;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (env.ANTHROPIC_API_KEY) {
|
|
143
|
+
return {
|
|
144
|
+
accessToken: env.ANTHROPIC_API_KEY,
|
|
145
|
+
refreshToken: null,
|
|
146
|
+
expiresAt: null,
|
|
147
|
+
source: "env",
|
|
148
|
+
supportsUsageApi: false,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
113
155
|
export function refreshClaudeAccessToken(refreshToken) {
|
|
114
156
|
return new Promise((resolve) => {
|
|
115
157
|
const clientId =
|
|
@@ -166,17 +208,67 @@ export function refreshClaudeAccessToken(refreshToken) {
|
|
|
166
208
|
});
|
|
167
209
|
}
|
|
168
210
|
|
|
169
|
-
|
|
211
|
+
function serializeClaudeCredentials(creds) {
|
|
212
|
+
const oauth = {
|
|
213
|
+
accessToken: creds.accessToken,
|
|
214
|
+
};
|
|
215
|
+
if (creds.expiresAt != null) oauth.expiresAt = creds.expiresAt;
|
|
216
|
+
if (creds.refreshToken) oauth.refreshToken = creds.refreshToken;
|
|
217
|
+
return { claudeAiOauth: oauth };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function writeClaudeKeychainCredentials(creds, execFileSyncFn = execFileSync) {
|
|
221
|
+
execFileSyncFn(
|
|
222
|
+
"security",
|
|
223
|
+
[
|
|
224
|
+
"add-generic-password",
|
|
225
|
+
"-s",
|
|
226
|
+
CLAUDE_KEYCHAIN_SERVICE,
|
|
227
|
+
"-w",
|
|
228
|
+
JSON.stringify(serializeClaudeCredentials(creds)),
|
|
229
|
+
"-U",
|
|
230
|
+
],
|
|
231
|
+
{ stdio: "ignore" },
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function writeBackClaudeCredentials(
|
|
236
|
+
creds,
|
|
237
|
+
{
|
|
238
|
+
readCredentialFile = () => readJson(CLAUDE_CREDENTIALS_PATH, null),
|
|
239
|
+
writeCredentialFile = (data) =>
|
|
240
|
+
writeFileSync(CLAUDE_CREDENTIALS_PATH, JSON.stringify(data, null, 2)),
|
|
241
|
+
platform = process.platform,
|
|
242
|
+
execFileSyncFn = execFileSync,
|
|
243
|
+
} = {},
|
|
244
|
+
) {
|
|
245
|
+
if (creds?.source === "env") return;
|
|
246
|
+
|
|
247
|
+
let data = null;
|
|
170
248
|
try {
|
|
171
|
-
|
|
172
|
-
|
|
249
|
+
data = readCredentialFile();
|
|
250
|
+
} catch {
|
|
251
|
+
data = null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (data) {
|
|
173
255
|
const target = data.claudeAiOauth || data;
|
|
174
256
|
target.accessToken = creds.accessToken;
|
|
175
257
|
if (creds.expiresAt != null) target.expiresAt = creds.expiresAt;
|
|
176
258
|
if (creds.refreshToken) target.refreshToken = creds.refreshToken;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
259
|
+
try {
|
|
260
|
+
writeCredentialFile(data);
|
|
261
|
+
} catch {
|
|
262
|
+
/* 파일 쓰기 실패 무시 */
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (platform === "darwin") {
|
|
267
|
+
try {
|
|
268
|
+
writeClaudeKeychainCredentials(creds, execFileSyncFn);
|
|
269
|
+
} catch {
|
|
270
|
+
/* Keychain 쓰기 실패 무시 */
|
|
271
|
+
}
|
|
180
272
|
}
|
|
181
273
|
}
|
|
182
274
|
|
|
@@ -399,6 +491,10 @@ export async function fetchClaudeUsage(forceRefresh = false) {
|
|
|
399
491
|
writeClaudeUsageCache(null, { type: "auth", status: 0 });
|
|
400
492
|
return existingSnapshot.data || null;
|
|
401
493
|
}
|
|
494
|
+
if (creds.supportsUsageApi === false) {
|
|
495
|
+
writeClaudeUsageCache(null, { type: "auth", status: 0 });
|
|
496
|
+
return existingSnapshot.data || null;
|
|
497
|
+
}
|
|
402
498
|
|
|
403
499
|
// 토큰 만료 시 리프레시
|
|
404
500
|
if (creds.expiresAt && creds.expiresAt <= Date.now() && creds.refreshToken) {
|
package/package.json
CHANGED
|
@@ -191,8 +191,13 @@ describe("release governance scripts", () => {
|
|
|
191
191
|
});
|
|
192
192
|
assert.equal(executed.skipTests, false);
|
|
193
193
|
|
|
194
|
+
// F3 (issue #192): test step 의 command 가 "npm test" 에서 직접 node
|
|
195
|
+
// test-lock.mjs 호출로 바뀜. legacy npm test 와 F3 후 node test-lock.mjs
|
|
196
|
+
// 둘 다 인식.
|
|
194
197
|
const testCall = calls.find(
|
|
195
|
-
(call) =>
|
|
198
|
+
(call) =>
|
|
199
|
+
(call.command === "npm" && call.args.join(" ") === "test") ||
|
|
200
|
+
call.args[0]?.endsWith("test-lock.mjs"),
|
|
196
201
|
);
|
|
197
202
|
assert.ok(testCall);
|
|
198
203
|
assert.deepEqual(testCall.options.stdio, ["ignore", "pipe", "pipe"]);
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Installation: source /path/to/tfx.bash 또는 ~/.bashrc에 추가
|
|
3
|
-
|
|
4
|
-
_tfx_completion() {
|
|
5
|
-
local cur prev words cword
|
|
6
|
-
COMPREPLY=()
|
|
7
|
-
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
8
|
-
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
|
9
|
-
words=("${COMP_WORDS[@]}")
|
|
10
|
-
cword=$COMP_CWORD
|
|
11
|
-
|
|
12
|
-
local commands="setup doctor multi hub auto codex gemini"
|
|
13
|
-
local multi_cmds="status stop kill attach list"
|
|
14
|
-
local hub_cmds="start stop status restart"
|
|
15
|
-
local flags="--thorough --quick --tmux --psmux --agents --no-attach --timeout"
|
|
16
|
-
|
|
17
|
-
if [[ $cword -eq 1 ]]; then
|
|
18
|
-
COMPREPLY=( $(compgen -W "${commands}" -- "$cur") )
|
|
19
|
-
return 0
|
|
20
|
-
fi
|
|
21
|
-
|
|
22
|
-
local cmd="${words[1]}"
|
|
23
|
-
case "${cmd}" in
|
|
24
|
-
multi)
|
|
25
|
-
if [[ $cword -eq 2 && ! "$cur" == -* ]]; then
|
|
26
|
-
COMPREPLY=( $(compgen -W "${multi_cmds}" -- "$cur") )
|
|
27
|
-
else
|
|
28
|
-
COMPREPLY=( $(compgen -W "${flags}" -- "$cur") )
|
|
29
|
-
fi
|
|
30
|
-
;;
|
|
31
|
-
hub)
|
|
32
|
-
if [[ $cword -eq 2 ]]; then
|
|
33
|
-
COMPREPLY=( $(compgen -W "${hub_cmds}" -- "$cur") )
|
|
34
|
-
fi
|
|
35
|
-
;;
|
|
36
|
-
doctor)
|
|
37
|
-
COMPREPLY=( $(compgen -W "--fix --reset" -- "$cur") )
|
|
38
|
-
;;
|
|
39
|
-
setup|auto|codex|gemini)
|
|
40
|
-
if [[ "$cur" == -* ]]; then
|
|
41
|
-
COMPREPLY=( $(compgen -W "${flags}" -- "$cur") )
|
|
42
|
-
fi
|
|
43
|
-
;;
|
|
44
|
-
esac
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
complete -F _tfx_completion tfx
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Installation: source /path/to/tfx.bash 또는 ~/.bashrc에 추가
|
|
3
|
+
|
|
4
|
+
_tfx_completion() {
|
|
5
|
+
local cur prev words cword
|
|
6
|
+
COMPREPLY=()
|
|
7
|
+
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
8
|
+
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
|
9
|
+
words=("${COMP_WORDS[@]}")
|
|
10
|
+
cword=$COMP_CWORD
|
|
11
|
+
|
|
12
|
+
local commands="setup doctor multi hub auto codex gemini"
|
|
13
|
+
local multi_cmds="status stop kill attach list"
|
|
14
|
+
local hub_cmds="start stop status restart"
|
|
15
|
+
local flags="--thorough --quick --tmux --psmux --agents --no-attach --timeout"
|
|
16
|
+
|
|
17
|
+
if [[ $cword -eq 1 ]]; then
|
|
18
|
+
COMPREPLY=( $(compgen -W "${commands}" -- "$cur") )
|
|
19
|
+
return 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
local cmd="${words[1]}"
|
|
23
|
+
case "${cmd}" in
|
|
24
|
+
multi)
|
|
25
|
+
if [[ $cword -eq 2 && ! "$cur" == -* ]]; then
|
|
26
|
+
COMPREPLY=( $(compgen -W "${multi_cmds}" -- "$cur") )
|
|
27
|
+
else
|
|
28
|
+
COMPREPLY=( $(compgen -W "${flags}" -- "$cur") )
|
|
29
|
+
fi
|
|
30
|
+
;;
|
|
31
|
+
hub)
|
|
32
|
+
if [[ $cword -eq 2 ]]; then
|
|
33
|
+
COMPREPLY=( $(compgen -W "${hub_cmds}" -- "$cur") )
|
|
34
|
+
fi
|
|
35
|
+
;;
|
|
36
|
+
doctor)
|
|
37
|
+
COMPREPLY=( $(compgen -W "--fix --reset" -- "$cur") )
|
|
38
|
+
;;
|
|
39
|
+
setup|auto|codex|gemini)
|
|
40
|
+
if [[ "$cur" == -* ]]; then
|
|
41
|
+
COMPREPLY=( $(compgen -W "${flags}" -- "$cur") )
|
|
42
|
+
fi
|
|
43
|
+
;;
|
|
44
|
+
esac
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
complete -F _tfx_completion tfx
|
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
# Installation: ~/.config/fish/completions/에 복사
|
|
2
|
-
# e.g., cp /path/to/tfx.fish ~/.config/fish/completions/tfx.fish
|
|
3
|
-
|
|
4
|
-
set -l commands setup doctor multi hub auto codex gemini
|
|
5
|
-
set -l multi_cmds status stop kill attach list
|
|
6
|
-
set -l hub_cmds start stop status restart
|
|
7
|
-
|
|
8
|
-
complete -c tfx -f
|
|
9
|
-
|
|
10
|
-
# Subcommands
|
|
11
|
-
complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "setup" -d "Setup and sync files"
|
|
12
|
-
complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "doctor" -d "Diagnose CLI and issues"
|
|
13
|
-
complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "multi" -d "Multi-CLI team mode"
|
|
14
|
-
complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "hub" -d "MCP message bus management"
|
|
15
|
-
complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "auto" -d "Auto mode"
|
|
16
|
-
complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "codex" -d "Codex mode"
|
|
17
|
-
complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "gemini" -d "Gemini mode"
|
|
18
|
-
|
|
19
|
-
# Doctor flags
|
|
20
|
-
complete -c tfx -n "__fish_seen_subcommand_from doctor" -l fix -d "Auto fix issues"
|
|
21
|
-
complete -c tfx -n "__fish_seen_subcommand_from doctor" -l reset -d "Reset all caches"
|
|
22
|
-
|
|
23
|
-
# Multi subcommands
|
|
24
|
-
complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "status"
|
|
25
|
-
complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "stop"
|
|
26
|
-
complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "kill"
|
|
27
|
-
complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "attach"
|
|
28
|
-
complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "list"
|
|
29
|
-
|
|
30
|
-
# Hub subcommands
|
|
31
|
-
complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "start"
|
|
32
|
-
complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "stop"
|
|
33
|
-
complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "status"
|
|
34
|
-
complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "restart"
|
|
35
|
-
|
|
36
|
-
# Global or multi flags
|
|
37
|
-
set -l flags_cond "__fish_seen_subcommand_from setup multi auto codex gemini"
|
|
38
|
-
complete -c tfx -n "$flags_cond" -l thorough -d "Thorough execution"
|
|
39
|
-
complete -c tfx -n "$flags_cond" -l quick -d "Quick execution"
|
|
40
|
-
complete -c tfx -n "$flags_cond" -l tmux -d "Use tmux"
|
|
41
|
-
complete -c tfx -n "$flags_cond" -l psmux -d "Use psmux"
|
|
42
|
-
complete -c tfx -n "$flags_cond" -l agents -d "Specify agents"
|
|
43
|
-
complete -c tfx -n "$flags_cond" -l no-attach -d "Do not attach"
|
|
44
|
-
complete -c tfx -n "$flags_cond" -l timeout -d "Set timeout"
|
|
1
|
+
# Installation: ~/.config/fish/completions/에 복사
|
|
2
|
+
# e.g., cp /path/to/tfx.fish ~/.config/fish/completions/tfx.fish
|
|
3
|
+
|
|
4
|
+
set -l commands setup doctor multi hub auto codex gemini
|
|
5
|
+
set -l multi_cmds status stop kill attach list
|
|
6
|
+
set -l hub_cmds start stop status restart
|
|
7
|
+
|
|
8
|
+
complete -c tfx -f
|
|
9
|
+
|
|
10
|
+
# Subcommands
|
|
11
|
+
complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "setup" -d "Setup and sync files"
|
|
12
|
+
complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "doctor" -d "Diagnose CLI and issues"
|
|
13
|
+
complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "multi" -d "Multi-CLI team mode"
|
|
14
|
+
complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "hub" -d "MCP message bus management"
|
|
15
|
+
complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "auto" -d "Auto mode"
|
|
16
|
+
complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "codex" -d "Codex mode"
|
|
17
|
+
complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "gemini" -d "Gemini mode"
|
|
18
|
+
|
|
19
|
+
# Doctor flags
|
|
20
|
+
complete -c tfx -n "__fish_seen_subcommand_from doctor" -l fix -d "Auto fix issues"
|
|
21
|
+
complete -c tfx -n "__fish_seen_subcommand_from doctor" -l reset -d "Reset all caches"
|
|
22
|
+
|
|
23
|
+
# Multi subcommands
|
|
24
|
+
complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "status"
|
|
25
|
+
complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "stop"
|
|
26
|
+
complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "kill"
|
|
27
|
+
complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "attach"
|
|
28
|
+
complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "list"
|
|
29
|
+
|
|
30
|
+
# Hub subcommands
|
|
31
|
+
complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "start"
|
|
32
|
+
complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "stop"
|
|
33
|
+
complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "status"
|
|
34
|
+
complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "restart"
|
|
35
|
+
|
|
36
|
+
# Global or multi flags
|
|
37
|
+
set -l flags_cond "__fish_seen_subcommand_from setup multi auto codex gemini"
|
|
38
|
+
complete -c tfx -n "$flags_cond" -l thorough -d "Thorough execution"
|
|
39
|
+
complete -c tfx -n "$flags_cond" -l quick -d "Quick execution"
|
|
40
|
+
complete -c tfx -n "$flags_cond" -l tmux -d "Use tmux"
|
|
41
|
+
complete -c tfx -n "$flags_cond" -l psmux -d "Use psmux"
|
|
42
|
+
complete -c tfx -n "$flags_cond" -l agents -d "Specify agents"
|
|
43
|
+
complete -c tfx -n "$flags_cond" -l no-attach -d "Do not attach"
|
|
44
|
+
complete -c tfx -n "$flags_cond" -l timeout -d "Set timeout"
|
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
#compdef tfx
|
|
2
|
-
# Installation: fpath에 추가 후 compinit
|
|
3
|
-
# e.g., fpath=(/path/to/dir $fpath) && compinit
|
|
4
|
-
|
|
5
|
-
_tfx() {
|
|
6
|
-
local line state
|
|
7
|
-
local -a commands multi_cmds hub_cmds flags
|
|
8
|
-
|
|
9
|
-
commands=(
|
|
10
|
-
'setup:Setup and sync files'
|
|
11
|
-
'doctor:Diagnose CLI and issues'
|
|
12
|
-
'multi:Multi-CLI team mode'
|
|
13
|
-
'hub:MCP message bus management'
|
|
14
|
-
'auto:Auto mode'
|
|
15
|
-
'codex:Codex mode'
|
|
16
|
-
'gemini:Gemini mode'
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
multi_cmds=(
|
|
20
|
-
'status:Show status'
|
|
21
|
-
'stop:Stop multi'
|
|
22
|
-
'kill:Kill multi'
|
|
23
|
-
'attach:Attach to multi'
|
|
24
|
-
'list:List multi sessions'
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
hub_cmds=(
|
|
28
|
-
'start:Start hub'
|
|
29
|
-
'stop:Stop hub'
|
|
30
|
-
'status:Show hub status'
|
|
31
|
-
'restart:Restart hub'
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
_arguments -C \
|
|
35
|
-
'1: :->cmds' \
|
|
36
|
-
'*: :->args'
|
|
37
|
-
|
|
38
|
-
case $state in
|
|
39
|
-
cmds)
|
|
40
|
-
_describe -t commands 'tfx commands' commands
|
|
41
|
-
;;
|
|
42
|
-
args)
|
|
43
|
-
case $words[2] in
|
|
44
|
-
multi)
|
|
45
|
-
if (( CURRENT == 3 )) && [[ $words[CURRENT] != -* ]]; then
|
|
46
|
-
_describe -t multi_cmds 'multi commands' multi_cmds
|
|
47
|
-
else
|
|
48
|
-
_arguments \
|
|
49
|
-
'--thorough[Thorough execution]' \
|
|
50
|
-
'--quick[Quick execution]' \
|
|
51
|
-
'--tmux[Use tmux]' \
|
|
52
|
-
'--psmux[Use psmux]' \
|
|
53
|
-
'--agents[Specify agents]' \
|
|
54
|
-
'--no-attach[Do not attach]' \
|
|
55
|
-
'--timeout[Set timeout]'
|
|
56
|
-
fi
|
|
57
|
-
;;
|
|
58
|
-
hub)
|
|
59
|
-
if (( CURRENT == 3 )); then
|
|
60
|
-
_describe -t hub_cmds 'hub commands' hub_cmds
|
|
61
|
-
fi
|
|
62
|
-
;;
|
|
63
|
-
doctor)
|
|
64
|
-
_arguments \
|
|
65
|
-
'--fix[Auto fix issues]' \
|
|
66
|
-
'--reset[Reset all caches]'
|
|
67
|
-
;;
|
|
68
|
-
*)
|
|
69
|
-
_arguments \
|
|
70
|
-
'--thorough[Thorough execution]' \
|
|
71
|
-
'--quick[Quick execution]' \
|
|
72
|
-
'--tmux[Use tmux]' \
|
|
73
|
-
'--psmux[Use psmux]' \
|
|
74
|
-
'--agents[Specify agents]' \
|
|
75
|
-
'--no-attach[Do not attach]' \
|
|
76
|
-
'--timeout[Set timeout]'
|
|
77
|
-
;;
|
|
78
|
-
esac
|
|
79
|
-
;;
|
|
80
|
-
esac
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
_tfx "$@"
|
|
1
|
+
#compdef tfx
|
|
2
|
+
# Installation: fpath에 추가 후 compinit
|
|
3
|
+
# e.g., fpath=(/path/to/dir $fpath) && compinit
|
|
4
|
+
|
|
5
|
+
_tfx() {
|
|
6
|
+
local line state
|
|
7
|
+
local -a commands multi_cmds hub_cmds flags
|
|
8
|
+
|
|
9
|
+
commands=(
|
|
10
|
+
'setup:Setup and sync files'
|
|
11
|
+
'doctor:Diagnose CLI and issues'
|
|
12
|
+
'multi:Multi-CLI team mode'
|
|
13
|
+
'hub:MCP message bus management'
|
|
14
|
+
'auto:Auto mode'
|
|
15
|
+
'codex:Codex mode'
|
|
16
|
+
'gemini:Gemini mode'
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
multi_cmds=(
|
|
20
|
+
'status:Show status'
|
|
21
|
+
'stop:Stop multi'
|
|
22
|
+
'kill:Kill multi'
|
|
23
|
+
'attach:Attach to multi'
|
|
24
|
+
'list:List multi sessions'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
hub_cmds=(
|
|
28
|
+
'start:Start hub'
|
|
29
|
+
'stop:Stop hub'
|
|
30
|
+
'status:Show hub status'
|
|
31
|
+
'restart:Restart hub'
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_arguments -C \
|
|
35
|
+
'1: :->cmds' \
|
|
36
|
+
'*: :->args'
|
|
37
|
+
|
|
38
|
+
case $state in
|
|
39
|
+
cmds)
|
|
40
|
+
_describe -t commands 'tfx commands' commands
|
|
41
|
+
;;
|
|
42
|
+
args)
|
|
43
|
+
case $words[2] in
|
|
44
|
+
multi)
|
|
45
|
+
if (( CURRENT == 3 )) && [[ $words[CURRENT] != -* ]]; then
|
|
46
|
+
_describe -t multi_cmds 'multi commands' multi_cmds
|
|
47
|
+
else
|
|
48
|
+
_arguments \
|
|
49
|
+
'--thorough[Thorough execution]' \
|
|
50
|
+
'--quick[Quick execution]' \
|
|
51
|
+
'--tmux[Use tmux]' \
|
|
52
|
+
'--psmux[Use psmux]' \
|
|
53
|
+
'--agents[Specify agents]' \
|
|
54
|
+
'--no-attach[Do not attach]' \
|
|
55
|
+
'--timeout[Set timeout]'
|
|
56
|
+
fi
|
|
57
|
+
;;
|
|
58
|
+
hub)
|
|
59
|
+
if (( CURRENT == 3 )); then
|
|
60
|
+
_describe -t hub_cmds 'hub commands' hub_cmds
|
|
61
|
+
fi
|
|
62
|
+
;;
|
|
63
|
+
doctor)
|
|
64
|
+
_arguments \
|
|
65
|
+
'--fix[Auto fix issues]' \
|
|
66
|
+
'--reset[Reset all caches]'
|
|
67
|
+
;;
|
|
68
|
+
*)
|
|
69
|
+
_arguments \
|
|
70
|
+
'--thorough[Thorough execution]' \
|
|
71
|
+
'--quick[Quick execution]' \
|
|
72
|
+
'--tmux[Use tmux]' \
|
|
73
|
+
'--psmux[Use psmux]' \
|
|
74
|
+
'--agents[Specify agents]' \
|
|
75
|
+
'--no-attach[Do not attach]' \
|
|
76
|
+
'--timeout[Set timeout]'
|
|
77
|
+
;;
|
|
78
|
+
esac
|
|
79
|
+
;;
|
|
80
|
+
esac
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_tfx "$@"
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export function commandExists(name) {
|
|
4
|
+
try {
|
|
5
|
+
execFileSync("sh", ["-c", 'command -v "$1" >/dev/null 2>&1', "sh", name], {
|
|
6
|
+
stdio: "ignore",
|
|
7
|
+
timeout: 2000,
|
|
8
|
+
});
|
|
9
|
+
return true;
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function inspectMacTimeoutDependency({
|
|
16
|
+
platform = process.platform,
|
|
17
|
+
commandExists: exists = commandExists,
|
|
18
|
+
} = {}) {
|
|
19
|
+
if (platform !== "darwin") {
|
|
20
|
+
return { ok: true, status: "skipped", platform };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (exists("gtimeout")) {
|
|
24
|
+
return { ok: true, status: "ok", platform, provider: "gtimeout" };
|
|
25
|
+
}
|
|
26
|
+
if (exists("timeout")) {
|
|
27
|
+
return { ok: true, status: "ok", platform, provider: "timeout" };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
status: "missing",
|
|
33
|
+
platform,
|
|
34
|
+
fix: "brew install coreutils",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function addPluginRootFallbackToCommand(command, pluginRoot) {
|
|
39
|
+
if (typeof command !== "string") return command;
|
|
40
|
+
const fallbackRoot = String(pluginRoot || "").replace(/\\/g, "/");
|
|
41
|
+
if (!fallbackRoot) return command;
|
|
42
|
+
return command
|
|
43
|
+
.replaceAll("${PLUGIN_ROOT}", `\${PLUGIN_ROOT:-${fallbackRoot}}`)
|
|
44
|
+
.replaceAll(
|
|
45
|
+
"${CLAUDE_PLUGIN_ROOT}",
|
|
46
|
+
`\${CLAUDE_PLUGIN_ROOT:-${fallbackRoot}}`,
|
|
47
|
+
)
|
|
48
|
+
.replace(
|
|
49
|
+
/(^|[\s"'])\/(hooks|scripts)\//g,
|
|
50
|
+
`$1\${PLUGIN_ROOT:-${fallbackRoot}}/$2/`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function hasBarePluginRootReference(command) {
|
|
55
|
+
if (typeof command !== "string") return false;
|
|
56
|
+
return (
|
|
57
|
+
command.includes("${PLUGIN_ROOT}/") ||
|
|
58
|
+
command.includes("${CLAUDE_PLUGIN_ROOT}/")
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hasCollapsedRootHookPath(command) {
|
|
63
|
+
if (typeof command !== "string") return false;
|
|
64
|
+
return /(^|[\s"'])\/(?:hooks|scripts)\//.test(command);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function findPluginRootHookIssues(settings) {
|
|
68
|
+
const hooksByEvent =
|
|
69
|
+
settings?.hooks && typeof settings.hooks === "object" ? settings.hooks : {};
|
|
70
|
+
const issues = [];
|
|
71
|
+
|
|
72
|
+
for (const [event, entries] of Object.entries(hooksByEvent)) {
|
|
73
|
+
if (!Array.isArray(entries)) continue;
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
const hooks = Array.isArray(entry?.hooks) ? entry.hooks : [];
|
|
76
|
+
for (const hook of hooks) {
|
|
77
|
+
const command = hook?.command;
|
|
78
|
+
if (
|
|
79
|
+
!hasBarePluginRootReference(command) &&
|
|
80
|
+
!hasCollapsedRootHookPath(command)
|
|
81
|
+
) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
issues.push({
|
|
85
|
+
event,
|
|
86
|
+
matcher: entry?.matcher || "*",
|
|
87
|
+
command,
|
|
88
|
+
reason: hasCollapsedRootHookPath(command)
|
|
89
|
+
? "collapsed-root-path"
|
|
90
|
+
: "missing-plugin-root-fallback",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return issues;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function applyPluginRootHookFallbacks(settings, { pluginRoot } = {}) {
|
|
100
|
+
if (!settings?.hooks || typeof settings.hooks !== "object") {
|
|
101
|
+
return { changed: false, count: 0, settings };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let count = 0;
|
|
105
|
+
for (const entries of Object.values(settings.hooks)) {
|
|
106
|
+
if (!Array.isArray(entries)) continue;
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
const hooks = Array.isArray(entry?.hooks) ? entry.hooks : [];
|
|
109
|
+
for (const hook of hooks) {
|
|
110
|
+
if (typeof hook?.command !== "string") continue;
|
|
111
|
+
const next = addPluginRootFallbackToCommand(hook.command, pluginRoot);
|
|
112
|
+
if (next !== hook.command) {
|
|
113
|
+
hook.command = next;
|
|
114
|
+
count++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { changed: count > 0, count, settings };
|
|
121
|
+
}
|
|
@@ -64,23 +64,32 @@ export async function prepareRelease({
|
|
|
64
64
|
const steps = [
|
|
65
65
|
{
|
|
66
66
|
name: "npm-test",
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
// F3 fix (issue #192 — silent EXIT 0 회귀 우회):
|
|
68
|
+
// npm wrapper 경유 시 prepare.mjs process 가 native level 에서 hard kill
|
|
69
|
+
// 됨 (process.on("exit") 핸들러 호출 안 됨, EXIT 0 silent, 후속 lint/pack
|
|
70
|
+
// step 누락). 2026-04-30 진단 세션에서 5 시나리오 reproduce 결과:
|
|
71
|
+
// - npm wrapper 경유 + npm test 실행: silent EXIT 0 재현 (12-24s 비결정)
|
|
72
|
+
// - 직접 node 실행 또는 npm test 단독: 정상 진행
|
|
73
|
+
// - --skip-tests 우회: 정상 unhandled rejection (lint fail visible)
|
|
74
|
+
// 따라서 npm wrapper 한 단계 줄여 trigger 약화 (cmd.exe shim → npm-cli.js
|
|
75
|
+
// → spawn 3 layers 제거). package.json `scripts.test` 와 sync 유지 필수.
|
|
76
|
+
command: process.execPath,
|
|
77
|
+
args: [
|
|
78
|
+
"scripts/test-lock.mjs",
|
|
79
|
+
"--test",
|
|
80
|
+
"--test-force-exit",
|
|
81
|
+
"--test-concurrency=8",
|
|
82
|
+
"tests/**/*.test.mjs",
|
|
83
|
+
"scripts/__tests__/**/*.test.mjs",
|
|
84
|
+
],
|
|
69
85
|
skip: skipTests,
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
// heavy test step non-interactively and fail fast if it never returns.
|
|
73
|
-
// maxBuffer raised explicitly: 1 MiB default is too small for piped
|
|
74
|
-
// npm test --test-concurrency=8 verbose output. This is generic
|
|
75
|
-
// robustness, NOT a fix for the prepare-only EXIT=1 mismatch — that
|
|
76
|
-
// root cause is the test-lock.mjs spawn `stdio: "inherit"` cascading
|
|
77
|
-
// the parent's ignore/pipe/pipe down to grand-child `node --test`,
|
|
78
|
-
// breaking ConPTY assumptions on Windows. See issue #192 for the
|
|
79
|
-
// diagnosis and fix candidates (F1 = test-lock stdio split).
|
|
86
|
+
// maxBuffer: 1 MiB default is too small for piped node --test verbose
|
|
87
|
+
// output. 128 MiB matches the prior npm-test ceiling.
|
|
80
88
|
options: {
|
|
81
89
|
stdio: ["ignore", "pipe", "pipe"],
|
|
82
90
|
timeoutMs: TEST_TIMEOUT_MS,
|
|
83
91
|
maxBuffer: 128 * 1024 * 1024,
|
|
92
|
+
shell: false,
|
|
84
93
|
},
|
|
85
94
|
},
|
|
86
95
|
{
|
package/scripts/setup.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
ensureTfxSection,
|
|
26
26
|
getLatestRoutingTable,
|
|
27
27
|
} from "./claudemd-sync.mjs";
|
|
28
|
+
import { addPluginRootFallbackToCommand } from "./lib/doctor-env-checks.mjs";
|
|
28
29
|
import { cleanupTmpFiles } from "./tmp-cleanup.mjs";
|
|
29
30
|
|
|
30
31
|
const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -141,7 +142,7 @@ function isSetupUserStateFile(fileName) {
|
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
/**
|
|
144
|
-
* scripts/lib/*.mjs 자동 스캔.
|
|
145
|
+
* scripts/lib/*.mjs 및 *.sh 자동 스캔.
|
|
145
146
|
* 수동 리스트 대신 glob으로 탐색하여 lib 파일 추가 시 sync 누락 방지.
|
|
146
147
|
*/
|
|
147
148
|
function scanLibFiles(pluginRoot, claudeDir) {
|
|
@@ -149,7 +150,7 @@ function scanLibFiles(pluginRoot, claudeDir) {
|
|
|
149
150
|
if (!existsSync(libDir)) return [];
|
|
150
151
|
return readdirSync(libDir)
|
|
151
152
|
.sort()
|
|
152
|
-
.filter((f) => f.endsWith(".mjs"))
|
|
153
|
+
.filter((f) => f.endsWith(".mjs") || f.endsWith(".sh"))
|
|
153
154
|
.map((f) => ({
|
|
154
155
|
src: join(libDir, f),
|
|
155
156
|
dst: join(claudeDir, "scripts", "lib", f),
|
|
@@ -563,7 +564,13 @@ function ensureHooksInSettings({ settingsPath, registryPath }) {
|
|
|
563
564
|
|
|
564
565
|
entries.push({
|
|
565
566
|
matcher: spec.matcher,
|
|
566
|
-
hooks: [
|
|
567
|
+
hooks: [
|
|
568
|
+
{
|
|
569
|
+
type: "command",
|
|
570
|
+
command: addPluginRootFallbackToCommand(spec.command, PLUGIN_ROOT),
|
|
571
|
+
timeout: 5,
|
|
572
|
+
},
|
|
573
|
+
],
|
|
567
574
|
});
|
|
568
575
|
added.push(spec.id || spec.fileName);
|
|
569
576
|
}
|
package/scripts/tfx-route.sh
CHANGED
|
@@ -1956,8 +1956,12 @@ _codex_config_swap() {
|
|
|
1956
1956
|
|
|
1957
1957
|
# codex-recovery.sh 의 recover_codex_stdout 헬퍼 사용. STDOUT_LOG/STDERR_LOG env.
|
|
1958
1958
|
_TFX_ROUTE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
1959
|
-
|
|
1960
|
-
source
|
|
1959
|
+
if [[ -f "$_TFX_ROUTE_DIR/lib/codex-recovery.sh" ]]; then
|
|
1960
|
+
# shellcheck source=lib/codex-recovery.sh
|
|
1961
|
+
source "$_TFX_ROUTE_DIR/lib/codex-recovery.sh"
|
|
1962
|
+
else
|
|
1963
|
+
echo "[tfx-route] WARNING: optional helper missing: $_TFX_ROUTE_DIR/lib/codex-recovery.sh (run: tfx doctor --fix)" >&2
|
|
1964
|
+
fi
|
|
1961
1965
|
|
|
1962
1966
|
run_codex_exec() {
|
|
1963
1967
|
local prompt="$1"
|
|
@@ -30,7 +30,7 @@ const REPORTS_DIR = join(STATE_DIR, "reports");
|
|
|
30
30
|
// ── 가격 모델 ($/MTok, 비캐시 기준, 보수적 추정) ──
|
|
31
31
|
const PRICING = {
|
|
32
32
|
claude_sonnet: { input: 3, output: 15 },
|
|
33
|
-
claude_opus: { input:
|
|
33
|
+
claude_opus: { input: 5, output: 25 },
|
|
34
34
|
codex: { input: 0, output: 0 },
|
|
35
35
|
gemini_flash: { input: 0.1, output: 0.4 },
|
|
36
36
|
};
|
|
@@ -38,7 +38,7 @@ const PRICING = {
|
|
|
38
38
|
// Claude 캐시 가격 ($/MTok) — 오케스트레이션 비용 정밀 계산용
|
|
39
39
|
const CLAUDE_CACHE_PRICING = {
|
|
40
40
|
claude_sonnet: { cache_write: 3.75, cache_read: 0.3 },
|
|
41
|
-
claude_opus: { cache_write:
|
|
41
|
+
claude_opus: { cache_write: 6.25, cache_read: 0.5 },
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
// 에이전트 → Claude 대체 모델
|
|
@@ -628,11 +628,13 @@ function generateReport(sessionId) {
|
|
|
628
628
|
|
|
629
629
|
// ── Named exports (파이프라인 벤치마크 훅용) ──
|
|
630
630
|
export {
|
|
631
|
+
CLAUDE_CACHE_PRICING,
|
|
631
632
|
computeDiff,
|
|
632
633
|
DIFFS_DIR,
|
|
633
634
|
estimateSavings,
|
|
634
635
|
formatCost,
|
|
635
636
|
formatTokenCount,
|
|
637
|
+
PRICING,
|
|
636
638
|
STATE_DIR,
|
|
637
639
|
takeSnapshot,
|
|
638
640
|
};
|
package/references/hosts.json
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"hosts": {
|
|
3
|
-
"ultra4": {
|
|
4
|
-
"description": "Windows 데스크탑 (22코어/64GB RAM)",
|
|
5
|
-
"aliases": ["울트라", "울트라4", "데스크탑"],
|
|
6
|
-
"default_dir": "~/Desktop/Projects/cli/triflux",
|
|
7
|
-
"tailscale": {
|
|
8
|
-
"ip": "100.110.136.64",
|
|
9
|
-
"dns": "ultra-book-4-1",
|
|
10
|
-
"ssh_mode": "ssh-over-vpn"
|
|
11
|
-
},
|
|
12
|
-
"ssh_user": "SSAFY",
|
|
13
|
-
"os": "windows",
|
|
14
|
-
"specs": {
|
|
15
|
-
"cores": 22,
|
|
16
|
-
"ram_gb": 64,
|
|
17
|
-
"codex": "0.118.0",
|
|
18
|
-
"node": "24.14.0"
|
|
19
|
-
},
|
|
20
|
-
"capabilities": ["codex", "claude", "high-memory"]
|
|
21
|
-
},
|
|
22
|
-
"m2": {
|
|
23
|
-
"description": "MacBook Air M2 (8코어/16GB RAM)",
|
|
24
|
-
"aliases": ["맥", "맥북", "m2"],
|
|
25
|
-
"default_dir": "~/Desktop/Projects/triflux",
|
|
26
|
-
"tailscale": {
|
|
27
|
-
"ip": "100.104.61.126",
|
|
28
|
-
"dns": "tellang의-macbook-air",
|
|
29
|
-
"ssh_mode": "ssh-over-vpn"
|
|
30
|
-
},
|
|
31
|
-
"ssh_user": "tellang",
|
|
32
|
-
"os": "darwin",
|
|
33
|
-
"specs": {
|
|
34
|
-
"cores": 8,
|
|
35
|
-
"ram_gb": 16,
|
|
36
|
-
"claude": "2.1.89",
|
|
37
|
-
"codex": "0.114.0",
|
|
38
|
-
"gemini": "0.33.1",
|
|
39
|
-
"node": "25.9.0"
|
|
40
|
-
},
|
|
41
|
-
"capabilities": ["codex", "claude", "gemini"]
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
"default_host": "m2",
|
|
45
|
-
"triggers": ["원격에서", "다른 머신에서", "다른 컴퓨터에서"]
|
|
46
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"t":0,"agent":"adb34b0","agent_type":"general-purpose","event":"agent_stop","success":true}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"tool_name": "Bash",
|
|
3
|
-
"tool_input_preview": "{\"command\":\"ls -la \\\"C:/Users/tellang/Desktop/Projects/triflux/skills/tfx-deslop/\\\" 2>/dev/null; echo \\\"===\\\"; ls -la \\\"C:/Users/tellang/Desktop/Projects/triflux/skills/tfx-codebase-search/\\\" 2>/dev/n...",
|
|
4
|
-
"error": "Exit code 2\n===",
|
|
5
|
-
"timestamp": "2026-03-29T03:40:35.913Z",
|
|
6
|
-
"retry_count": 1
|
|
7
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"hosts": {
|
|
3
|
-
"m2": {
|
|
4
|
-
"description": "MacBook Air (tellang M2)",
|
|
5
|
-
"aliases": ["맥북", "맥"],
|
|
6
|
-
"default_dir": "~/projects",
|
|
7
|
-
"os": "darwin",
|
|
8
|
-
"ssh_user": "tellang",
|
|
9
|
-
"tailscale": {
|
|
10
|
-
"ip": "100.104.61.126",
|
|
11
|
-
"dns": "m2.sole-hexatonic.ts.net",
|
|
12
|
-
"ssh_mode": "ssh-over-vpn"
|
|
13
|
-
},
|
|
14
|
-
"capabilities": {
|
|
15
|
-
"ssh_active": true,
|
|
16
|
-
"claude": true,
|
|
17
|
-
"node": "v25.9.0"
|
|
18
|
-
},
|
|
19
|
-
"last_probe": "2026-04-24T19:04:48Z"
|
|
20
|
-
},
|
|
21
|
-
"fold7": {
|
|
22
|
-
"description": "Samsung Z Fold 7 (Termux)",
|
|
23
|
-
"aliases": ["폴드", "폰", "fold"],
|
|
24
|
-
"default_dir": "~",
|
|
25
|
-
"os": "android",
|
|
26
|
-
"ssh_user": "",
|
|
27
|
-
"tailscale": {
|
|
28
|
-
"ip": "100.107.139.115",
|
|
29
|
-
"dns": "fold7.sole-hexatonic.ts.net",
|
|
30
|
-
"ssh_mode": "ssh-over-vpn"
|
|
31
|
-
},
|
|
32
|
-
"capabilities": {
|
|
33
|
-
"ssh_active": false,
|
|
34
|
-
"note": "Termux sshd 미실행 — Fold7에서 'pkg install openssh && sshd -p 8022' 후 probe 재실행"
|
|
35
|
-
},
|
|
36
|
-
"last_probe": "2026-04-24T19:04:48Z"
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
"default_host": "m2",
|
|
40
|
-
"triggers": ["원격에서", "다른 머신에서", "다른 컴퓨터에서"]
|
|
41
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"hosts": {
|
|
3
|
-
"ultra4": {
|
|
4
|
-
"description": "Windows 데스크탑 (SSAFY)",
|
|
5
|
-
"aliases": ["울트라", "데스크탑"],
|
|
6
|
-
"default_dir": "~/Desktop/Projects"
|
|
7
|
-
},
|
|
8
|
-
"m2": {
|
|
9
|
-
"description": "MacBook Pro",
|
|
10
|
-
"aliases": ["맥북", "맥"],
|
|
11
|
-
"default_dir": "~/projects"
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
"default_host": "ultra4",
|
|
15
|
-
"triggers": ["원격에서", "다른 머신에서", "다른 컴퓨터에서"]
|
|
16
|
-
}
|