triflux 10.0.0-alpha.1 → 10.0.0-alpha.2
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 +290 -25
- package/package.json +1 -1
- package/skills/_templates/base.md +9 -0
- package/skills/_templates/deep.md +6 -0
- package/skills/tfx-auto/SKILL.md +8 -0
- package/skills/tfx-auto/SKILL.md.tmpl +286 -0
- package/skills/tfx-codex/SKILL.md +8 -0
- package/skills/tfx-codex/SKILL.md.tmpl +72 -0
- package/skills/tfx-codex-swarm/SKILL.md +55 -11
- package/skills/tfx-find/SKILL.md +12 -1
- package/skills/tfx-find/SKILL.md.tmpl +129 -0
- package/skills/tfx-index/SKILL.md +12 -1
- package/skills/tfx-index/SKILL.md.tmpl +148 -0
- package/skills/tfx-plan/SKILL.md +8 -0
- package/skills/tfx-plan/SKILL.md.tmpl +67 -0
- package/skills/tfx-qa/SKILL.md +8 -0
- package/skills/tfx-qa/SKILL.md.tmpl +121 -0
- package/skills/tfx-research/SKILL.md +8 -0
- package/skills/tfx-research/SKILL.md.tmpl +148 -0
- package/skills/tfx-review/SKILL.md +8 -0
- package/skills/tfx-review/SKILL.md.tmpl +56 -0
- package/skills/tfx-swarm/SKILL.md +122 -0
package/bin/triflux.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// triflux CLI — setup, doctor, version
|
|
3
3
|
import { copyFileSync, existsSync, readFileSync, readSync, writeFileSync, mkdirSync, chmodSync, readdirSync, unlinkSync, statSync, openSync, closeSync } from "fs";
|
|
4
|
-
import { join, dirname, basename } from "path";
|
|
4
|
+
import { join, dirname, basename, resolve } from "path";
|
|
5
5
|
import { homedir, tmpdir } from "os";
|
|
6
6
|
import { execSync, execFileSync, spawn } from "child_process";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
@@ -11,14 +11,17 @@ import { detectMultiplexer, getSessionAttachedCount, killSession, listSessions,
|
|
|
11
11
|
import { forceCleanupTeam } from "../hub/team/nativeProxy.mjs";
|
|
12
12
|
import { cleanupStaleOmcTeams, inspectStaleOmcTeams } from "../hub/team/staleState.mjs";
|
|
13
13
|
import { getPipelineStateDbPath } from "../hub/pipeline/state.mjs";
|
|
14
|
+
import { serializeHandoff } from "../scripts/lib/handoff.mjs";
|
|
14
15
|
import { ensureGeminiProfiles } from "../scripts/lib/gemini-profiles.mjs";
|
|
15
16
|
import { probePsmuxSupport, formatPsmuxInstallGuidance, formatPsmuxUpdateGuidance } from "../scripts/lib/psmux-info.mjs";
|
|
16
17
|
import {
|
|
17
18
|
addRegistryServer,
|
|
19
|
+
createDefaultRegistry,
|
|
18
20
|
inspectRegistry,
|
|
19
21
|
inspectRegistryStatus,
|
|
20
22
|
removeRegistryServer,
|
|
21
23
|
removeServerFromTargets,
|
|
24
|
+
saveRegistry,
|
|
22
25
|
syncRegistryTargets,
|
|
23
26
|
} from "../scripts/lib/mcp-guard-engine.mjs";
|
|
24
27
|
import {
|
|
@@ -28,7 +31,13 @@ import {
|
|
|
28
31
|
extractManagedHookFilename, getManagedRegistryHooks, ensureHooksInSettings,
|
|
29
32
|
ensureCodexHubServerConfig,
|
|
30
33
|
} from "../scripts/setup.mjs";
|
|
34
|
+
import {
|
|
35
|
+
ensureGlobalClaudeRoutingSection,
|
|
36
|
+
ensureTfxSection,
|
|
37
|
+
getLatestRoutingTable,
|
|
38
|
+
} from "../scripts/claudemd-sync.mjs";
|
|
31
39
|
import { cleanupTmpFiles } from "../scripts/tmp-cleanup.mjs";
|
|
40
|
+
import { checkNetworkAvailability, validateRuntimeCachePaths } from "../hub/lib/cache-guard.mjs";
|
|
32
41
|
|
|
33
42
|
const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
34
43
|
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
@@ -98,6 +107,17 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
|
98
107
|
{ name: "--json", type: "boolean", description: "버전 정보를 JSON으로 출력" },
|
|
99
108
|
],
|
|
100
109
|
},
|
|
110
|
+
handoff: {
|
|
111
|
+
usage: "tfx handoff [--target local|remote] [--decision <text>] [--decision-file <path>] [--output <path>] [--json]",
|
|
112
|
+
description: "현재 작업 컨텍스트를 세션 핸드오프 프롬프트로 직렬화",
|
|
113
|
+
options: [
|
|
114
|
+
{ name: "--target", type: "string", description: "주입 대상 (local|remote, 기본값 remote)" },
|
|
115
|
+
{ name: "--decision", type: "string", description: "핸드오프 결정사항 (반복 지정 가능)" },
|
|
116
|
+
{ name: "--decision-file", type: "string", description: "결정사항 파일 (라인/불릿 단위)" },
|
|
117
|
+
{ name: "--output", type: "string", description: "생성한 핸드오프 프롬프트 저장 경로" },
|
|
118
|
+
{ name: "--json", type: "boolean", description: "핸드오프 결과를 JSON으로 출력" },
|
|
119
|
+
],
|
|
120
|
+
},
|
|
101
121
|
list: {
|
|
102
122
|
usage: "tfx list [--json]",
|
|
103
123
|
description: "패키지 스킬과 사용자 스킬 목록 표시",
|
|
@@ -522,6 +542,26 @@ function describeSyncAction(src, dst, label) {
|
|
|
522
542
|
};
|
|
523
543
|
}
|
|
524
544
|
|
|
545
|
+
function syncClaudeRoutingSectionsForCli() {
|
|
546
|
+
try {
|
|
547
|
+
const routingTable = getLatestRoutingTable();
|
|
548
|
+
return [
|
|
549
|
+
ensureTfxSection(join(PKG_ROOT, "CLAUDE.md"), routingTable),
|
|
550
|
+
ensureGlobalClaudeRoutingSection(CLAUDE_DIR),
|
|
551
|
+
];
|
|
552
|
+
} catch (error) {
|
|
553
|
+
const reason = error instanceof Error ? error.message : "routing_sync_failed";
|
|
554
|
+
return [{ action: "unchanged", path: join(PKG_ROOT, "CLAUDE.md"), skipped: true, reason }];
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function getClaudeRoutingSyncSummary(results) {
|
|
559
|
+
return results.reduce((summary, result) => ({
|
|
560
|
+
changed: summary.changed + (result.action === "created" || result.action === "updated" ? 1 : 0),
|
|
561
|
+
skipped: summary.skipped + (result.skipped ? 1 : 0),
|
|
562
|
+
}), { changed: 0, skipped: 0 });
|
|
563
|
+
}
|
|
564
|
+
|
|
525
565
|
// ── 크로스 셸 진단 ──
|
|
526
566
|
|
|
527
567
|
function checkCliCrossShell(cmd, installHint) {
|
|
@@ -691,7 +731,7 @@ function buildSetupDryRunPlan() {
|
|
|
691
731
|
}
|
|
692
732
|
|
|
693
733
|
function cmdSetup(options = {}) {
|
|
694
|
-
const { dryRun = false, overrideVersion } = options;
|
|
734
|
+
const { dryRun = false, overrideVersion, skipClaudeMdSync = false } = options;
|
|
695
735
|
if (dryRun) {
|
|
696
736
|
printJson(buildSetupDryRunPlan());
|
|
697
737
|
return;
|
|
@@ -781,6 +821,21 @@ function cmdSetup(options = {}) {
|
|
|
781
821
|
// ── 결과 추적 ──
|
|
782
822
|
const summary = [];
|
|
783
823
|
|
|
824
|
+
if (!skipClaudeMdSync) {
|
|
825
|
+
const claudeRoutingResults = syncClaudeRoutingSectionsForCli();
|
|
826
|
+
const claudeRoutingSummary = getClaudeRoutingSyncSummary(claudeRoutingResults);
|
|
827
|
+
if (claudeRoutingSummary.changed > 0) {
|
|
828
|
+
ok(`CLAUDE.md 라우팅: ${claudeRoutingSummary.changed}개 파일 반영`);
|
|
829
|
+
summary.push({ item: "CLAUDE.md 라우팅", status: "✅", detail: `${claudeRoutingSummary.changed}개 파일 반영` });
|
|
830
|
+
} else if (claudeRoutingSummary.skipped > 0) {
|
|
831
|
+
ok("CLAUDE.md 라우팅: 대상 파일 없음 (건너뜀)");
|
|
832
|
+
summary.push({ item: "CLAUDE.md 라우팅", status: "⏭️", detail: "대상 파일 없음" });
|
|
833
|
+
} else {
|
|
834
|
+
ok("CLAUDE.md 라우팅: 최신 상태");
|
|
835
|
+
summary.push({ item: "CLAUDE.md 라우팅", status: "✅", detail: "최신 상태" });
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
784
839
|
const codexProfileResult = ensureCodexProfiles();
|
|
785
840
|
if (!codexProfileResult.ok) {
|
|
786
841
|
warn(`Codex profiles 설정 실패: ${codexProfileResult.message}`);
|
|
@@ -1128,13 +1183,10 @@ function buildMcpStatusRows(statusInfo) {
|
|
|
1128
1183
|
}
|
|
1129
1184
|
|
|
1130
1185
|
function ensureValidRegistryState() {
|
|
1131
|
-
|
|
1186
|
+
let registryState = inspectRegistry();
|
|
1132
1187
|
if (!registryState.exists) {
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
reason: "configError",
|
|
1136
|
-
fix: "config/mcp-registry.json을 복원하거나 `tfx mcp add <name> --url <url>`로 다시 생성하세요.",
|
|
1137
|
-
});
|
|
1188
|
+
saveRegistry(createDefaultRegistry());
|
|
1189
|
+
registryState = inspectRegistry();
|
|
1138
1190
|
}
|
|
1139
1191
|
if (!registryState.valid) {
|
|
1140
1192
|
throw createCliError(`MCP registry invalid: ${registryState.errors.join("; ")}`, {
|
|
@@ -1335,9 +1387,13 @@ async function cmdDoctor(options = {}) {
|
|
|
1335
1387
|
warn(`MCP registry 자동 동기화 실패: ${error.message}`);
|
|
1336
1388
|
}
|
|
1337
1389
|
} else if (registryStateForFix.exists) {
|
|
1338
|
-
|
|
1390
|
+
saveRegistry(createDefaultRegistry());
|
|
1391
|
+
report.actions.push({ type: "mcp-registry-reset", status: "ok" });
|
|
1392
|
+
ok("MCP registry 손상 → 기본값으로 재생성됨");
|
|
1339
1393
|
} else {
|
|
1340
|
-
|
|
1394
|
+
saveRegistry(createDefaultRegistry());
|
|
1395
|
+
report.actions.push({ type: "mcp-registry-create", status: "ok" });
|
|
1396
|
+
ok("MCP registry 없음 → 기본값으로 자동 생성됨");
|
|
1341
1397
|
}
|
|
1342
1398
|
console.log(`\n ${LINE}`);
|
|
1343
1399
|
info("수정 완료 — 아래 진단 결과를 확인하세요");
|
|
@@ -2157,28 +2213,27 @@ async function cmdDoctor(options = {}) {
|
|
|
2157
2213
|
// ── MCP 중앙 레지스트리 ──
|
|
2158
2214
|
section("MCP Registry");
|
|
2159
2215
|
{
|
|
2160
|
-
|
|
2216
|
+
let registryState = inspectRegistry();
|
|
2161
2217
|
if (!registryState.exists) {
|
|
2218
|
+
saveRegistry(createDefaultRegistry());
|
|
2219
|
+
registryState = inspectRegistry();
|
|
2162
2220
|
addDoctorCheck(report, {
|
|
2163
2221
|
name: "mcp-registry",
|
|
2164
|
-
status: "
|
|
2222
|
+
status: "fixed",
|
|
2165
2223
|
path: registryState.path,
|
|
2166
|
-
|
|
2224
|
+
action: "기본값으로 자동 생성됨",
|
|
2167
2225
|
});
|
|
2168
|
-
|
|
2169
|
-
info(`path: ${registryState.path}`);
|
|
2170
|
-
issues++;
|
|
2226
|
+
ok("mcp-registry.json 없음 → 기본값으로 자동 생성됨");
|
|
2171
2227
|
} else if (!registryState.valid) {
|
|
2228
|
+
saveRegistry(createDefaultRegistry());
|
|
2229
|
+
registryState = inspectRegistry();
|
|
2172
2230
|
addDoctorCheck(report, {
|
|
2173
2231
|
name: "mcp-registry",
|
|
2174
|
-
status: "
|
|
2232
|
+
status: "fixed",
|
|
2175
2233
|
path: registryState.path,
|
|
2176
|
-
|
|
2177
|
-
fix: "config/mcp-registry.json 구조를 수정하세요.",
|
|
2234
|
+
action: "손상 감지 → 기본값으로 재생성됨",
|
|
2178
2235
|
});
|
|
2179
|
-
|
|
2180
|
-
for (const entry of registryState.errors) info(entry);
|
|
2181
|
-
issues++;
|
|
2236
|
+
warn("mcp-registry.json 손상 → 기본값으로 재생성됨");
|
|
2182
2237
|
} else {
|
|
2183
2238
|
const statusInfo = inspectRegistryStatus(registryState.registry);
|
|
2184
2239
|
const invalidConfigs = statusInfo.configs.filter((config) => config.parseError);
|
|
@@ -2434,7 +2489,56 @@ async function cmdDoctor(options = {}) {
|
|
|
2434
2489
|
});
|
|
2435
2490
|
}
|
|
2436
2491
|
|
|
2437
|
-
function
|
|
2492
|
+
function normalizeRemoteReachabilityUrl(remoteUrl) {
|
|
2493
|
+
if (!remoteUrl) return null;
|
|
2494
|
+
if (/^https?:\/\//iu.test(remoteUrl)) {
|
|
2495
|
+
try {
|
|
2496
|
+
return new URL(remoteUrl).origin;
|
|
2497
|
+
} catch {
|
|
2498
|
+
return null;
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
const scpMatch = /^git@([^:]+):/iu.exec(remoteUrl);
|
|
2502
|
+
if (scpMatch) return `https://${scpMatch[1]}`;
|
|
2503
|
+
if (/^ssh:\/\//iu.test(remoteUrl)) {
|
|
2504
|
+
try {
|
|
2505
|
+
return `https://${new URL(remoteUrl).hostname}`;
|
|
2506
|
+
} catch {
|
|
2507
|
+
return null;
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
return null;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
function resolveGitUpdateUrl(repoDir) {
|
|
2514
|
+
try {
|
|
2515
|
+
const remoteUrl = execSync("git remote get-url origin", {
|
|
2516
|
+
encoding: "utf8",
|
|
2517
|
+
timeout: 10_000,
|
|
2518
|
+
cwd: repoDir,
|
|
2519
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
2520
|
+
windowsHide: true,
|
|
2521
|
+
}).trim();
|
|
2522
|
+
return normalizeRemoteReachabilityUrl(remoteUrl);
|
|
2523
|
+
} catch {
|
|
2524
|
+
return null;
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
function resolveUpdateTargets({ installMode, pluginPath }) {
|
|
2529
|
+
const repoDir = installMode === "plugin" ? (pluginPath || PKG_ROOT) : PKG_ROOT;
|
|
2530
|
+
const gitUrl = resolveGitUpdateUrl(repoDir);
|
|
2531
|
+
|
|
2532
|
+
if (installMode === "npm-global" || installMode === "npm-local") {
|
|
2533
|
+
return ["https://registry.npmjs.org/triflux"];
|
|
2534
|
+
}
|
|
2535
|
+
if (installMode === "plugin" || installMode === "git-local") {
|
|
2536
|
+
return gitUrl ? [gitUrl] : ["https://github.com"];
|
|
2537
|
+
}
|
|
2538
|
+
return [];
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
async function cmdUpdate() {
|
|
2438
2542
|
const isDev = isDevUpdateRequested(NORMALIZED_ARGS);
|
|
2439
2543
|
const tagLabel = isDev ? ` ${YELLOW}--dev${RESET}` : "";
|
|
2440
2544
|
console.log(`\n${BOLD}triflux update${RESET}${tagLabel}\n`);
|
|
@@ -2490,6 +2594,27 @@ function cmdUpdate() {
|
|
|
2490
2594
|
|
|
2491
2595
|
info(`검색: ${installMode === "plugin" ? "플러그인" : installMode === "npm-global" ? "npm global" : installMode === "npm-local" ? "npm local" : installMode === "git-local" ? "git 로컬 저장소" : "알 수 없음"} 설치 감지`);
|
|
2492
2596
|
|
|
2597
|
+
const networkTargets = resolveUpdateTargets({ installMode, pluginPath });
|
|
2598
|
+
if (networkTargets.length > 0) {
|
|
2599
|
+
const networkStatus = await checkNetworkAvailability(networkTargets);
|
|
2600
|
+
if (!networkStatus.online) {
|
|
2601
|
+
fail(`네트워크 확인 실패: ${networkStatus.unreachable.join(", ")}`);
|
|
2602
|
+
info("네트워크 연결을 확인한 뒤 다시 시도하세요.");
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
ok(`네트워크 확인 완료 (${networkStatus.reachable.join(", ")})`);
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
const cacheValidation = validateRuntimeCachePaths(join(CLAUDE_DIR, "cache"));
|
|
2609
|
+
if (!cacheValidation.ok) {
|
|
2610
|
+
warn(`런타임 캐시 검증 이슈 ${cacheValidation.issues.length}건 발견`);
|
|
2611
|
+
for (const issue of cacheValidation.issues) {
|
|
2612
|
+
info(`${issue.file}: ${issue.error}`);
|
|
2613
|
+
}
|
|
2614
|
+
} else {
|
|
2615
|
+
ok("런타임 캐시 검증 완료");
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2493
2618
|
// 2. 설치 방식에 따라 업데이트
|
|
2494
2619
|
const oldVer = PKG.version;
|
|
2495
2620
|
let updated = false;
|
|
@@ -2654,9 +2779,23 @@ function cmdUpdate() {
|
|
|
2654
2779
|
}
|
|
2655
2780
|
}
|
|
2656
2781
|
|
|
2782
|
+
// ── Post-update: CLAUDE.md 라우팅 동기화 ──
|
|
2783
|
+
console.log(`\n${CYAN}── CLAUDE.md 라우팅 동기화 ──${RESET}`);
|
|
2784
|
+
{
|
|
2785
|
+
const claudeRoutingResults = syncClaudeRoutingSectionsForCli();
|
|
2786
|
+
const claudeRoutingSummary = getClaudeRoutingSyncSummary(claudeRoutingResults);
|
|
2787
|
+
if (claudeRoutingSummary.changed > 0) {
|
|
2788
|
+
ok(`CLAUDE.md 라우팅 ${claudeRoutingSummary.changed}개 파일 반영`);
|
|
2789
|
+
} else if (claudeRoutingSummary.skipped > 0) {
|
|
2790
|
+
ok("CLAUDE.md 라우팅 대상 파일 없음 (건너뜀)");
|
|
2791
|
+
} else {
|
|
2792
|
+
ok("CLAUDE.md 라우팅 최신 상태");
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2657
2796
|
// ── Post-update: 설정 동기화 ──
|
|
2658
2797
|
console.log(`\n${CYAN}── 설정 동기화 ──${RESET}`);
|
|
2659
|
-
cmdSetup({ fromUpdate: true, overrideVersion: newVer });
|
|
2798
|
+
cmdSetup({ fromUpdate: true, overrideVersion: newVer, skipClaudeMdSync: true });
|
|
2660
2799
|
|
|
2661
2800
|
// ── Post-update: 훅 오케스트레이터 적용 ──
|
|
2662
2801
|
{
|
|
@@ -2783,6 +2922,128 @@ function cmdVersion(options = {}) {
|
|
|
2783
2922
|
console.log("");
|
|
2784
2923
|
}
|
|
2785
2924
|
|
|
2925
|
+
function cmdHandoff(args = [], options = {}) {
|
|
2926
|
+
const { json = false } = options;
|
|
2927
|
+
const parsed = {
|
|
2928
|
+
target: "remote",
|
|
2929
|
+
decisions: [],
|
|
2930
|
+
decisionFile: null,
|
|
2931
|
+
output: null,
|
|
2932
|
+
cwd: process.cwd(),
|
|
2933
|
+
};
|
|
2934
|
+
|
|
2935
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2936
|
+
const arg = args[index];
|
|
2937
|
+
const next = args[index + 1];
|
|
2938
|
+
|
|
2939
|
+
if (arg === "--target") {
|
|
2940
|
+
if (!next || next.startsWith("-")) {
|
|
2941
|
+
throw createCliError("--target 값이 필요합니다 (local|remote)", {
|
|
2942
|
+
exitCode: EXIT_ARG_ERROR,
|
|
2943
|
+
reason: "argError",
|
|
2944
|
+
fix: "tfx handoff --target remote",
|
|
2945
|
+
});
|
|
2946
|
+
}
|
|
2947
|
+
if (!["local", "remote"].includes(next)) {
|
|
2948
|
+
throw createCliError(`지원하지 않는 --target 값: ${next}`, {
|
|
2949
|
+
exitCode: EXIT_ARG_ERROR,
|
|
2950
|
+
reason: "argError",
|
|
2951
|
+
fix: "tfx handoff --target local|remote",
|
|
2952
|
+
});
|
|
2953
|
+
}
|
|
2954
|
+
parsed.target = next;
|
|
2955
|
+
index += 1;
|
|
2956
|
+
continue;
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
if (arg === "--decision") {
|
|
2960
|
+
if (!next || next.startsWith("-")) {
|
|
2961
|
+
throw createCliError("--decision 값이 필요합니다", {
|
|
2962
|
+
exitCode: EXIT_ARG_ERROR,
|
|
2963
|
+
reason: "argError",
|
|
2964
|
+
fix: "tfx handoff --decision \"결정사항\"",
|
|
2965
|
+
});
|
|
2966
|
+
}
|
|
2967
|
+
parsed.decisions.push(next);
|
|
2968
|
+
index += 1;
|
|
2969
|
+
continue;
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
if (arg === "--decision-file") {
|
|
2973
|
+
if (!next || next.startsWith("-")) {
|
|
2974
|
+
throw createCliError("--decision-file 경로가 필요합니다", {
|
|
2975
|
+
exitCode: EXIT_ARG_ERROR,
|
|
2976
|
+
reason: "argError",
|
|
2977
|
+
fix: "tfx handoff --decision-file .omx/notepad.md",
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
parsed.decisionFile = resolve(next);
|
|
2981
|
+
index += 1;
|
|
2982
|
+
continue;
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
if (arg === "--output" || arg === "--out") {
|
|
2986
|
+
if (!next || next.startsWith("-")) {
|
|
2987
|
+
throw createCliError(`${arg} 경로가 필요합니다`, {
|
|
2988
|
+
exitCode: EXIT_ARG_ERROR,
|
|
2989
|
+
reason: "argError",
|
|
2990
|
+
fix: "tfx handoff --output .omx/handoff.md",
|
|
2991
|
+
});
|
|
2992
|
+
}
|
|
2993
|
+
parsed.output = resolve(next);
|
|
2994
|
+
index += 1;
|
|
2995
|
+
continue;
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
if (arg === "--cwd") {
|
|
2999
|
+
if (!next || next.startsWith("-")) {
|
|
3000
|
+
throw createCliError("--cwd 경로가 필요합니다", {
|
|
3001
|
+
exitCode: EXIT_ARG_ERROR,
|
|
3002
|
+
reason: "argError",
|
|
3003
|
+
fix: "tfx handoff --cwd <project-path>",
|
|
3004
|
+
});
|
|
3005
|
+
}
|
|
3006
|
+
parsed.cwd = resolve(next);
|
|
3007
|
+
index += 1;
|
|
3008
|
+
continue;
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
throw createCliError(`알 수 없는 handoff 옵션: ${arg}`, {
|
|
3012
|
+
exitCode: EXIT_ARG_ERROR,
|
|
3013
|
+
reason: "argError",
|
|
3014
|
+
fix: "tfx handoff --target remote --output .omx/handoff.md",
|
|
3015
|
+
});
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
const result = serializeHandoff({
|
|
3019
|
+
target: parsed.target,
|
|
3020
|
+
decisions: parsed.decisions,
|
|
3021
|
+
decisionFile: parsed.decisionFile,
|
|
3022
|
+
cwd: parsed.cwd,
|
|
3023
|
+
});
|
|
3024
|
+
|
|
3025
|
+
if (parsed.output) {
|
|
3026
|
+
const outputDir = dirname(parsed.output);
|
|
3027
|
+
if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true });
|
|
3028
|
+
writeFileSync(parsed.output, `${result.prompt}\n`, "utf8");
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
if (json) {
|
|
3032
|
+
printJson({
|
|
3033
|
+
handoff: {
|
|
3034
|
+
...result,
|
|
3035
|
+
...(parsed.output ? { output: parsed.output } : {}),
|
|
3036
|
+
},
|
|
3037
|
+
});
|
|
3038
|
+
return;
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
console.log(result.prompt);
|
|
3042
|
+
if (parsed.output) {
|
|
3043
|
+
console.log(`\n${DIM}saved:${RESET} ${parsed.output}`);
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
|
|
2786
3047
|
function cmdSchema(args = []) {
|
|
2787
3048
|
const bundle = loadDelegatorSchemaBundle();
|
|
2788
3049
|
const selector = String(args[0] || "").trim();
|
|
@@ -3054,6 +3315,7 @@ ${updateNotice}
|
|
|
3054
3315
|
${WHITE_BRIGHT}tfx update${RESET} ${GRAY}최신 안정 버전으로 업데이트${RESET}
|
|
3055
3316
|
${DIM} --dev / dev${RESET} ${GRAY}dev 태그로 업데이트${RESET}
|
|
3056
3317
|
${WHITE_BRIGHT}tfx list${RESET} ${GRAY}설치된 스킬 목록${RESET}
|
|
3318
|
+
${WHITE_BRIGHT}tfx handoff${RESET} ${GRAY}현재 컨텍스트를 원격/로컬 핸드오프 프롬프트로 생성${RESET}
|
|
3057
3319
|
${WHITE_BRIGHT}tfx schema${RESET} ${GRAY}CLI/Hub schema JSON 출력${RESET}
|
|
3058
3320
|
${WHITE_BRIGHT}tfx hub${RESET} ${GRAY}MCP 메시지 버스 관리 (start/stop/status)${RESET}
|
|
3059
3321
|
${WHITE_BRIGHT}tfx tray${RESET} ${GRAY}Windows 시스템 트레이 실행${RESET}
|
|
@@ -3553,12 +3815,15 @@ async function main() {
|
|
|
3553
3815
|
cmdSchema(cmdArgs);
|
|
3554
3816
|
return;
|
|
3555
3817
|
case "update":
|
|
3556
|
-
cmdUpdate();
|
|
3818
|
+
await cmdUpdate();
|
|
3557
3819
|
return;
|
|
3558
3820
|
case "list":
|
|
3559
3821
|
case "ls":
|
|
3560
3822
|
cmdList({ json: JSON_OUTPUT });
|
|
3561
3823
|
return;
|
|
3824
|
+
case "handoff":
|
|
3825
|
+
cmdHandoff(cmdArgs, { json: JSON_OUTPUT });
|
|
3826
|
+
return;
|
|
3562
3827
|
case "hub":
|
|
3563
3828
|
await cmdHub(cmdArgs, { json: JSON_OUTPUT && (cmdArgs[0] || "status") === "status" });
|
|
3564
3829
|
return;
|
package/package.json
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
> **ARGUMENTS 처리**: 이 스킬이 `ARGUMENTS: <값>`과 함께 호출되면, 해당 값을 사용자 입력으로 취급하여
|
|
2
|
+
> 워크플로우의 첫 단계 입력으로 사용한다. ARGUMENTS가 비어있거나 없으면 기존 절차대로 사용자에게 입력을 요청한다.
|
|
3
|
+
|
|
4
|
+
> **Telemetry**
|
|
5
|
+
>
|
|
6
|
+
> - Skill: `{{SKILL_NAME}}`
|
|
7
|
+
> - Description: `{{SKILL_DESCRIPTION}}`
|
|
8
|
+
> - Session: 요청별 식별자를 유지해 단계별 실행 로그를 추적한다.
|
|
9
|
+
> - Errors: 실패 시 원인/복구/재시도 여부를 구조화해 기록한다.
|
package/skills/tfx-auto/SKILL.md
CHANGED
|
@@ -32,6 +32,14 @@ argument-hint: "<command|task> [args...]"
|
|
|
32
32
|
> **ARGUMENTS 처리**: 이 스킬이 `ARGUMENTS: <값>`과 함께 호출되면, 해당 값을 사용자 입력으로 취급하여
|
|
33
33
|
> 워크플로우의 첫 단계 입력으로 사용한다. ARGUMENTS가 비어있거나 없으면 기존 절차대로 사용자에게 입력을 요청한다.
|
|
34
34
|
|
|
35
|
+
> **Telemetry**
|
|
36
|
+
>
|
|
37
|
+
> - Skill: `tfx-auto`
|
|
38
|
+
> - Description: `통합 CLI 오케스트레이터. 커맨드 숏컷(단일) + 자동 분류/분해(병렬) + 수동 병렬. tfx-route.sh 기반. '코드 짜줘', '구현해줘', '만들어줘', '수정해줘', '고쳐줘', 'implement', 'build', 'fix' 같은 구현/수정 요청에 사용. CLI 라우팅이 필요한 모든 작업에 적극 활용.`
|
|
39
|
+
> - Session: 요청별 식별자를 유지해 단계별 실행 로그를 추적한다.
|
|
40
|
+
> - Errors: 실패 시 원인/복구/재시도 여부를 구조화해 기록한다.
|
|
41
|
+
|
|
42
|
+
|
|
35
43
|
|
|
36
44
|
> **MANDATORY RULES**
|
|
37
45
|
>
|