triflux 9.2.4 → 9.4.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/.claude-plugin/plugin.json +1 -1
- package/bin/triflux.mjs +48 -217
- package/hub/pipe.mjs +1 -8
- package/hub/team/psmux.mjs +21 -1
- package/hub/workers/claude-worker.mjs +1 -24
- package/hub/workers/codex-mcp.mjs +0 -4
- package/hub/workers/gemini-worker.mjs +108 -28
- package/hub/workers/interface.mjs +0 -1
- package/hub/workers/worker-utils.mjs +26 -0
- package/package.json +1 -1
- package/scripts/__tests__/remote-spawn.test.mjs +17 -3
- package/scripts/cross-review-gate.mjs +11 -65
- package/scripts/cross-review-tracker.mjs +10 -51
- package/scripts/headless-guard.mjs +5 -17
- package/scripts/lib/cross-review-utils.mjs +51 -0
- package/scripts/lib/hook-utils.mjs +14 -0
- package/scripts/lib/mcp-filter.mjs +10 -1
- package/scripts/psmux-safety-guard.mjs +64 -0
- package/scripts/remote-spawn.mjs +77 -23
- package/scripts/session-spawn-helper.mjs +2 -1
- package/scripts/setup.mjs +36 -6
- package/scripts/tfx-route.sh +93 -10
- package/skills/tfx-auto/SKILL.md +4 -1
- package/skills/tfx-auto-codex/SKILL.md +3 -1
- package/skills/tfx-autopilot/SKILL.md +1 -1
- package/skills/tfx-autoresearch/SKILL.md +2 -0
- package/skills/tfx-codex/SKILL.md +3 -1
- package/skills/tfx-consensus/SKILL.md +5 -3
- package/skills/tfx-fullcycle/SKILL.md +1 -1
- package/skills/tfx-gemini/SKILL.md +3 -1
- package/skills/tfx-hooks/SKILL.md +216 -0
- package/skills/tfx-multi/SKILL.md +4 -1
- package/skills/tfx-plan/SKILL.md +1 -1
- package/skills/tfx-psmux-rules/SKILL.md +100 -13
- package/skills/tfx-ralph/SKILL.md +11 -66
- package/skills/tfx-research/SKILL.md +1 -1
- package/skills/tfx-review/SKILL.md +1 -1
- package/skills/tfx-setup/SKILL.md +17 -1
package/bin/triflux.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// triflux CLI — setup, doctor, version
|
|
3
|
-
import { copyFileSync, existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, readdirSync, unlinkSync,
|
|
3
|
+
import { copyFileSync, existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, readdirSync, unlinkSync, statSync, openSync, closeSync } from "fs";
|
|
4
4
|
import { join, dirname } from "path";
|
|
5
5
|
import { homedir } from "os";
|
|
6
6
|
import { execSync, execFileSync, spawn } from "child_process";
|
|
@@ -12,6 +12,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
14
|
import { ensureGeminiProfiles } from "../scripts/lib/gemini-profiles.mjs";
|
|
15
|
+
import {
|
|
16
|
+
SYNC_MAP, SKILL_ALIASES, REQUIRED_CODEX_PROFILES,
|
|
17
|
+
syncAliasedSkillDir, hasProfileSection, replaceProfileSection,
|
|
18
|
+
ensureCodexProfiles, getVersion,
|
|
19
|
+
} from "../scripts/setup.mjs";
|
|
15
20
|
|
|
16
21
|
const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
17
22
|
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
@@ -22,72 +27,6 @@ const PKG = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
|
|
|
22
27
|
// 이 배열에 포함된 버전에서만 star prompt를 표시한다 (빈 배열 = 모든 버전에서 표시)
|
|
23
28
|
const STAR_PROMPT_VERSIONS = [];
|
|
24
29
|
|
|
25
|
-
const REQUIRED_CODEX_PROFILES = [
|
|
26
|
-
{
|
|
27
|
-
name: "codex53_high",
|
|
28
|
-
lines: [
|
|
29
|
-
'model = "gpt-5.3-codex"',
|
|
30
|
-
'model_reasoning_effort = "high"',
|
|
31
|
-
],
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
name: "codex53_xhigh",
|
|
35
|
-
lines: [
|
|
36
|
-
'model = "gpt-5.3-codex"',
|
|
37
|
-
'model_reasoning_effort = "xhigh"',
|
|
38
|
-
],
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
name: "spark53_low",
|
|
42
|
-
lines: [
|
|
43
|
-
'model = "gpt-5.3-codex-spark"',
|
|
44
|
-
'model_reasoning_effort = "low"',
|
|
45
|
-
],
|
|
46
|
-
},
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
const SKILL_ALIASES = [
|
|
50
|
-
{
|
|
51
|
-
alias: "tfx-ralph",
|
|
52
|
-
source: "tfx-persist",
|
|
53
|
-
},
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
function buildAliasedSkillContent(srcContent, { alias, source }) {
|
|
57
|
-
return srcContent
|
|
58
|
-
.replace(/^name:\s*.+$/m, `name: ${alias}`)
|
|
59
|
-
.replaceAll(source, alias)
|
|
60
|
-
.replace(/^#\s+.+$/m, `# ${alias} — Compatibility Alias for ${source}`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function syncAliasedSkillDir(srcDir, dstDir, { alias, source }) {
|
|
64
|
-
if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
|
|
65
|
-
|
|
66
|
-
let count = 0;
|
|
67
|
-
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
|
|
68
|
-
const srcPath = join(srcDir, entry.name);
|
|
69
|
-
const dstPath = join(dstDir, entry.name);
|
|
70
|
-
|
|
71
|
-
if (entry.isDirectory()) {
|
|
72
|
-
count += syncAliasedSkillDir(srcPath, dstPath, { alias, source });
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (!entry.name.endsWith(".md")) continue;
|
|
77
|
-
|
|
78
|
-
const rawContent = readFileSync(srcPath, "utf8");
|
|
79
|
-
const nextContent = entry.name === "SKILL.md"
|
|
80
|
-
? buildAliasedSkillContent(rawContent, { alias, source })
|
|
81
|
-
: rawContent;
|
|
82
|
-
|
|
83
|
-
if (!existsSync(dstPath) || readFileSync(dstPath, "utf8") !== nextContent) {
|
|
84
|
-
writeFileSync(dstPath, nextContent, "utf8");
|
|
85
|
-
count++;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return count;
|
|
90
|
-
}
|
|
91
30
|
|
|
92
31
|
// ── 색상 체계 (triflux brand: amber/orange accent) ──
|
|
93
32
|
const CYAN = "\x1b[36m";
|
|
@@ -328,13 +267,6 @@ function checkShellAvailable(shell) {
|
|
|
328
267
|
} catch { return false; }
|
|
329
268
|
}
|
|
330
269
|
|
|
331
|
-
function getVersion(filePath) {
|
|
332
|
-
try {
|
|
333
|
-
const content = readFileSync(filePath, "utf8");
|
|
334
|
-
const match = content.match(/VERSION\s*=\s*"([^"]+)"/);
|
|
335
|
-
return match ? match[1] : null;
|
|
336
|
-
} catch { return null; }
|
|
337
|
-
}
|
|
338
270
|
|
|
339
271
|
function parseSessionCreated(rawValue) {
|
|
340
272
|
const value = String(rawValue || "").trim();
|
|
@@ -459,57 +391,35 @@ async function cleanupStaleTeamSessions(staleSessions) {
|
|
|
459
391
|
return { cleaned, failed };
|
|
460
392
|
}
|
|
461
393
|
|
|
462
|
-
function escapeRegExp(value) {
|
|
463
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
function hasProfileSection(tomlContent, profileName) {
|
|
467
|
-
const section = `^\\[profiles\\.${escapeRegExp(profileName)}\\]\\s*$`;
|
|
468
|
-
return new RegExp(section, "m").test(tomlContent);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
function ensureCodexProfiles() {
|
|
472
|
-
try {
|
|
473
|
-
if (!existsSync(CODEX_DIR)) mkdirSync(CODEX_DIR, { recursive: true });
|
|
474
|
-
|
|
475
|
-
const original = existsSync(CODEX_CONFIG_PATH)
|
|
476
|
-
? readFileSync(CODEX_CONFIG_PATH, "utf8")
|
|
477
|
-
: "";
|
|
478
394
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
395
|
+
function previewCodexProfiles() {
|
|
396
|
+
const original = existsSync(CODEX_CONFIG_PATH)
|
|
397
|
+
? readFileSync(CODEX_CONFIG_PATH, "utf8")
|
|
398
|
+
: "";
|
|
399
|
+
let updated = original;
|
|
400
|
+
const profiles = [];
|
|
484
401
|
|
|
402
|
+
for (const profile of REQUIRED_CODEX_PROFILES) {
|
|
403
|
+
const before = updated;
|
|
404
|
+
if (hasProfileSection(updated, profile.name)) {
|
|
405
|
+
updated = replaceProfileSection(updated, profile.name, profile.lines);
|
|
406
|
+
} else {
|
|
485
407
|
if (updated.length > 0 && !updated.endsWith("\n")) updated += "\n";
|
|
486
408
|
if (updated.trim().length > 0) updated += "\n";
|
|
487
409
|
updated += `[profiles.${profile.name}]\n${profile.lines.join("\n")}\n`;
|
|
488
|
-
added++;
|
|
489
410
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
writeFileSync(CODEX_CONFIG_PATH, updated, "utf8");
|
|
411
|
+
if (updated !== before) {
|
|
412
|
+
profiles.push(profile.name);
|
|
493
413
|
}
|
|
494
|
-
|
|
495
|
-
return { ok: true, added };
|
|
496
|
-
} catch (e) {
|
|
497
|
-
return { ok: false, added: 0, message: e.message };
|
|
498
414
|
}
|
|
499
|
-
}
|
|
500
415
|
|
|
501
|
-
|
|
502
|
-
const original = existsSync(CODEX_CONFIG_PATH)
|
|
503
|
-
? readFileSync(CODEX_CONFIG_PATH, "utf8")
|
|
504
|
-
: "";
|
|
505
|
-
const missingProfiles = REQUIRED_CODEX_PROFILES
|
|
506
|
-
.filter((profile) => !hasProfileSection(original, profile.name))
|
|
507
|
-
.map((profile) => profile.name);
|
|
416
|
+
const windowsSandbox = process.platform === "win32" && !updated.includes("[windows]");
|
|
508
417
|
|
|
509
418
|
return {
|
|
510
419
|
path: CODEX_CONFIG_PATH,
|
|
511
|
-
|
|
512
|
-
|
|
420
|
+
profiles,
|
|
421
|
+
windowsSandbox,
|
|
422
|
+
change: profiles.length > 0 || windowsSandbox ? (original ? "update" : "create") : "noop",
|
|
513
423
|
};
|
|
514
424
|
}
|
|
515
425
|
|
|
@@ -637,101 +547,6 @@ function checkCliCrossShell(cmd, installHint) {
|
|
|
637
547
|
|
|
638
548
|
// ── 명령어 ──
|
|
639
549
|
|
|
640
|
-
function getSetupSyncTargets() {
|
|
641
|
-
return [
|
|
642
|
-
{
|
|
643
|
-
src: join(PKG_ROOT, "scripts", "tfx-route.sh"),
|
|
644
|
-
dst: join(CLAUDE_DIR, "scripts", "tfx-route.sh"),
|
|
645
|
-
label: "tfx-route.sh",
|
|
646
|
-
},
|
|
647
|
-
{
|
|
648
|
-
src: join(PKG_ROOT, "hud", "hud-qos-status.mjs"),
|
|
649
|
-
dst: join(CLAUDE_DIR, "hud", "hud-qos-status.mjs"),
|
|
650
|
-
label: "hud-qos-status.mjs",
|
|
651
|
-
},
|
|
652
|
-
{
|
|
653
|
-
src: join(PKG_ROOT, "scripts", "notion-read.mjs"),
|
|
654
|
-
dst: join(CLAUDE_DIR, "scripts", "notion-read.mjs"),
|
|
655
|
-
label: "notion-read.mjs",
|
|
656
|
-
},
|
|
657
|
-
{
|
|
658
|
-
src: join(PKG_ROOT, "scripts", "tfx-route-post.mjs"),
|
|
659
|
-
dst: join(CLAUDE_DIR, "scripts", "tfx-route-post.mjs"),
|
|
660
|
-
label: "tfx-route-post.mjs",
|
|
661
|
-
},
|
|
662
|
-
{
|
|
663
|
-
src: join(PKG_ROOT, "scripts", "tfx-batch-stats.mjs"),
|
|
664
|
-
dst: join(CLAUDE_DIR, "scripts", "tfx-batch-stats.mjs"),
|
|
665
|
-
label: "tfx-batch-stats.mjs",
|
|
666
|
-
},
|
|
667
|
-
{
|
|
668
|
-
src: join(PKG_ROOT, "scripts", "lib", "mcp-filter.mjs"),
|
|
669
|
-
dst: join(CLAUDE_DIR, "scripts", "lib", "mcp-filter.mjs"),
|
|
670
|
-
label: "lib/mcp-filter.mjs",
|
|
671
|
-
},
|
|
672
|
-
{
|
|
673
|
-
src: join(PKG_ROOT, "scripts", "lib", "mcp-server-catalog.mjs"),
|
|
674
|
-
dst: join(CLAUDE_DIR, "scripts", "lib", "mcp-server-catalog.mjs"),
|
|
675
|
-
label: "lib/mcp-server-catalog.mjs",
|
|
676
|
-
},
|
|
677
|
-
{
|
|
678
|
-
src: join(PKG_ROOT, "scripts", "lib", "keyword-rules.mjs"),
|
|
679
|
-
dst: join(CLAUDE_DIR, "scripts", "lib", "keyword-rules.mjs"),
|
|
680
|
-
label: "lib/keyword-rules.mjs",
|
|
681
|
-
},
|
|
682
|
-
{
|
|
683
|
-
src: join(PKG_ROOT, "scripts", "lib", "gemini-profiles.mjs"),
|
|
684
|
-
dst: join(CLAUDE_DIR, "scripts", "lib", "gemini-profiles.mjs"),
|
|
685
|
-
label: "lib/gemini-profiles.mjs",
|
|
686
|
-
},
|
|
687
|
-
{
|
|
688
|
-
src: join(PKG_ROOT, "scripts", "tfx-route-worker.mjs"),
|
|
689
|
-
dst: join(CLAUDE_DIR, "scripts", "tfx-route-worker.mjs"),
|
|
690
|
-
label: "tfx-route-worker.mjs",
|
|
691
|
-
},
|
|
692
|
-
{
|
|
693
|
-
src: join(PKG_ROOT, "hub", "workers", "codex-mcp.mjs"),
|
|
694
|
-
dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "codex-mcp.mjs"),
|
|
695
|
-
label: "hub/workers/codex-mcp.mjs",
|
|
696
|
-
},
|
|
697
|
-
{
|
|
698
|
-
src: join(PKG_ROOT, "hub", "workers", "delegator-mcp.mjs"),
|
|
699
|
-
dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "delegator-mcp.mjs"),
|
|
700
|
-
label: "hub/workers/delegator-mcp.mjs",
|
|
701
|
-
},
|
|
702
|
-
{
|
|
703
|
-
src: join(PKG_ROOT, "hub", "workers", "interface.mjs"),
|
|
704
|
-
dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "interface.mjs"),
|
|
705
|
-
label: "hub/workers/interface.mjs",
|
|
706
|
-
},
|
|
707
|
-
{
|
|
708
|
-
src: join(PKG_ROOT, "hub", "workers", "gemini-worker.mjs"),
|
|
709
|
-
dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "gemini-worker.mjs"),
|
|
710
|
-
label: "hub/workers/gemini-worker.mjs",
|
|
711
|
-
},
|
|
712
|
-
{
|
|
713
|
-
src: join(PKG_ROOT, "hub", "workers", "claude-worker.mjs"),
|
|
714
|
-
dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "claude-worker.mjs"),
|
|
715
|
-
label: "hub/workers/claude-worker.mjs",
|
|
716
|
-
},
|
|
717
|
-
{
|
|
718
|
-
src: join(PKG_ROOT, "hub", "workers", "factory.mjs"),
|
|
719
|
-
dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "factory.mjs"),
|
|
720
|
-
label: "hub/workers/factory.mjs",
|
|
721
|
-
},
|
|
722
|
-
{
|
|
723
|
-
src: join(PKG_ROOT, "scripts", "remote-spawn.mjs"),
|
|
724
|
-
dst: join(CLAUDE_DIR, "scripts", "remote-spawn.mjs"),
|
|
725
|
-
label: "remote-spawn.mjs",
|
|
726
|
-
},
|
|
727
|
-
{
|
|
728
|
-
src: join(PKG_ROOT, "hub", "team", "psmux.mjs"),
|
|
729
|
-
dst: join(CLAUDE_DIR, "hub", "team", "psmux.mjs"),
|
|
730
|
-
label: "hub/team/psmux.mjs",
|
|
731
|
-
},
|
|
732
|
-
];
|
|
733
|
-
}
|
|
734
|
-
|
|
735
550
|
function listSkillSyncActions() {
|
|
736
551
|
const skillsSrc = join(PKG_ROOT, "skills");
|
|
737
552
|
if (!existsSync(skillsSrc)) return [];
|
|
@@ -816,7 +631,7 @@ function previewMcpRegistrationActions(mcpUrl) {
|
|
|
816
631
|
|
|
817
632
|
function buildSetupDryRunPlan() {
|
|
818
633
|
const actions = [
|
|
819
|
-
...
|
|
634
|
+
...SYNC_MAP.map(({ src, dst, label }) => describeSyncAction(src, dst, label)),
|
|
820
635
|
...listSkillSyncActions(),
|
|
821
636
|
];
|
|
822
637
|
const codexProfiles = previewCodexProfiles();
|
|
@@ -824,7 +639,8 @@ function buildSetupDryRunPlan() {
|
|
|
824
639
|
type: "codex-profiles",
|
|
825
640
|
path: codexProfiles.path,
|
|
826
641
|
change: codexProfiles.change,
|
|
827
|
-
profiles: codexProfiles.
|
|
642
|
+
profiles: codexProfiles.profiles,
|
|
643
|
+
windowsSandbox: codexProfiles.windowsSandbox,
|
|
828
644
|
});
|
|
829
645
|
|
|
830
646
|
const defaultHubUrl = `http://127.0.0.1:${process.env.TFX_HUB_PORT || "27888"}/mcp`;
|
|
@@ -846,7 +662,7 @@ function cmdSetup(options = {}) {
|
|
|
846
662
|
|
|
847
663
|
console.log(`\n${BOLD}triflux setup${RESET}\n`);
|
|
848
664
|
|
|
849
|
-
for (const target of
|
|
665
|
+
for (const target of SYNC_MAP) {
|
|
850
666
|
syncFile(target.src, target.dst, target.label);
|
|
851
667
|
}
|
|
852
668
|
|
|
@@ -876,6 +692,21 @@ function cmdSetup(options = {}) {
|
|
|
876
692
|
skillCount++;
|
|
877
693
|
}
|
|
878
694
|
}
|
|
695
|
+
// references/ 디렉토리 동기화 (존재하면)
|
|
696
|
+
const refSrc = join(skillsSrc, name, "references");
|
|
697
|
+
const refDst = join(skillsDst, name, "references");
|
|
698
|
+
if (existsSync(refSrc)) {
|
|
699
|
+
mkdirSync(refDst, { recursive: true });
|
|
700
|
+
for (const refFile of readdirSync(refSrc)) {
|
|
701
|
+
const rSrc = join(refSrc, refFile);
|
|
702
|
+
const rDst = join(refDst, refFile);
|
|
703
|
+
if (statSync(rSrc).isFile()) {
|
|
704
|
+
if (!existsSync(rDst) || readFileSync(rSrc, "utf8") !== readFileSync(rDst, "utf8")) {
|
|
705
|
+
copyFileSync(rSrc, rDst);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
879
710
|
}
|
|
880
711
|
for (const { alias, source } of SKILL_ALIASES) {
|
|
881
712
|
const srcDir = join(skillsSrc, source);
|
|
@@ -898,9 +729,9 @@ function cmdSetup(options = {}) {
|
|
|
898
729
|
if (!codexProfileResult.ok) {
|
|
899
730
|
warn(`Codex profiles 설정 실패: ${codexProfileResult.message}`);
|
|
900
731
|
summary.push({ item: "Codex profiles", status: "⚠️", detail: codexProfileResult.message });
|
|
901
|
-
} else if (codexProfileResult.
|
|
902
|
-
ok(`Codex profiles: ${codexProfileResult.
|
|
903
|
-
summary.push({ item: "Codex profiles", status: "✅", detail: `${codexProfileResult.
|
|
732
|
+
} else if (codexProfileResult.changed > 0) {
|
|
733
|
+
ok(`Codex profiles: ${codexProfileResult.changed}개 반영됨 (~/.codex/config.toml)`);
|
|
734
|
+
summary.push({ item: "Codex profiles", status: "✅", detail: `${codexProfileResult.changed}개 반영됨` });
|
|
904
735
|
} else {
|
|
905
736
|
ok("Codex profiles: 이미 준비됨");
|
|
906
737
|
summary.push({ item: "Codex profiles", status: "✅", detail: "이미 준비됨" });
|
|
@@ -1143,7 +974,7 @@ async function cmdDoctor(options = {}) {
|
|
|
1143
974
|
// ── fix 모드: 파일 동기화 + 캐시 정리 후 진단 ──
|
|
1144
975
|
if (fix) {
|
|
1145
976
|
section("Auto Fix");
|
|
1146
|
-
for (const target of
|
|
977
|
+
for (const target of SYNC_MAP) {
|
|
1147
978
|
syncFile(target.src, target.dst, target.label);
|
|
1148
979
|
}
|
|
1149
980
|
// 스킬 동기화
|
|
@@ -1167,8 +998,8 @@ async function cmdDoctor(options = {}) {
|
|
|
1167
998
|
const profileFix = ensureCodexProfiles();
|
|
1168
999
|
if (!profileFix.ok) {
|
|
1169
1000
|
warn(`Codex Profiles 자동 복구 실패: ${profileFix.message}`);
|
|
1170
|
-
} else if (profileFix.
|
|
1171
|
-
ok(`Codex Profiles: ${profileFix.
|
|
1001
|
+
} else if (profileFix.changed > 0) {
|
|
1002
|
+
ok(`Codex Profiles: ${profileFix.changed}개 반영됨`);
|
|
1172
1003
|
} else {
|
|
1173
1004
|
info("Codex Profiles: 이미 최신 상태");
|
|
1174
1005
|
}
|
package/hub/pipe.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
listPipelineStates,
|
|
19
19
|
readPipelineState,
|
|
20
20
|
} from './pipeline/state.mjs';
|
|
21
|
+
import { safeJsonParse } from './workers/worker-utils.mjs';
|
|
21
22
|
|
|
22
23
|
const DEFAULT_HEARTBEAT_TTL_MS = 60000;
|
|
23
24
|
|
|
@@ -29,14 +30,6 @@ export function getPipePath(sessionId = process.pid) {
|
|
|
29
30
|
return join('/tmp', `triflux-${sessionId}.sock`);
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
function safeJsonParse(line) {
|
|
33
|
-
try {
|
|
34
|
-
return JSON.parse(line);
|
|
35
|
-
} catch {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
33
|
function normalizeTopics(topics) {
|
|
41
34
|
if (!Array.isArray(topics)) return [];
|
|
42
35
|
return topics
|
package/hub/team/psmux.mjs
CHANGED
|
@@ -5,7 +5,27 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
5
5
|
import { tmpdir, homedir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
|
|
8
|
-
const PSMUX_BIN =
|
|
8
|
+
const PSMUX_BIN = (() => {
|
|
9
|
+
if (process.env.PSMUX_BIN) return process.env.PSMUX_BIN;
|
|
10
|
+
// PATH에서 찾기
|
|
11
|
+
try {
|
|
12
|
+
childProcess.execFileSync("psmux", ["-V"], { stdio: "ignore", timeout: 2000, windowsHide: true });
|
|
13
|
+
return "psmux";
|
|
14
|
+
} catch { /* not in PATH */ }
|
|
15
|
+
// Windows 기본 설치 경로 탐색
|
|
16
|
+
if (process.platform === "win32") {
|
|
17
|
+
const candidates = [
|
|
18
|
+
join(process.env.LOCALAPPDATA || "", "psmux", "psmux.exe"),
|
|
19
|
+
join(process.env.APPDATA || "", "npm", "psmux.cmd"),
|
|
20
|
+
join(homedir(), "AppData", "Local", "psmux", "psmux.exe"),
|
|
21
|
+
join(homedir(), "scoop", "shims", "psmux.exe"),
|
|
22
|
+
];
|
|
23
|
+
for (const p of candidates) {
|
|
24
|
+
if (existsSync(p)) return p;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return "psmux"; // 최종 fallback — 원래대로
|
|
28
|
+
})();
|
|
9
29
|
const GIT_BASH = process.env.GIT_BASH_PATH || "C:\\Program Files\\Git\\bin\\bash.exe";
|
|
10
30
|
const IS_WINDOWS = process.platform === "win32";
|
|
11
31
|
const PSMUX_TIMEOUT_MS = 10000;
|
|
@@ -3,24 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import readline from 'node:readline';
|
|
6
|
-
|
|
7
|
-
const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
|
|
8
|
-
const DEFAULT_KILL_GRACE_MS = 1000;
|
|
9
|
-
|
|
10
|
-
function toStringList(value) {
|
|
11
|
-
if (!Array.isArray(value)) return [];
|
|
12
|
-
return value
|
|
13
|
-
.map((item) => String(item ?? '').trim())
|
|
14
|
-
.filter(Boolean);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function safeJsonParse(line) {
|
|
18
|
-
try {
|
|
19
|
-
return JSON.parse(line);
|
|
20
|
-
} catch {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
6
|
+
import { toStringList, safeJsonParse, createWorkerError, DEFAULT_TIMEOUT_MS, DEFAULT_KILL_GRACE_MS } from './worker-utils.mjs';
|
|
24
7
|
|
|
25
8
|
function appendTextFragments(value, parts) {
|
|
26
9
|
if (value == null) return;
|
|
@@ -59,12 +42,6 @@ function findSessionId(event) {
|
|
|
59
42
|
|| null;
|
|
60
43
|
}
|
|
61
44
|
|
|
62
|
-
function createWorkerError(message, details = {}) {
|
|
63
|
-
const error = new Error(message);
|
|
64
|
-
Object.assign(error, details);
|
|
65
|
-
return error;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
45
|
function buildClaudeArgs(worker, options) {
|
|
69
46
|
const args = [...worker.commandArgs];
|
|
70
47
|
|
|
@@ -292,10 +292,6 @@ export class CodexMcpWorker {
|
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
export function createCodexMcpWorker(options = {}) {
|
|
296
|
-
return new CodexMcpWorker(options);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
295
|
function parseCliArgs(argv) {
|
|
300
296
|
const options = {
|
|
301
297
|
command: process.env.CODEX_BIN || 'codex',
|
|
@@ -2,25 +2,10 @@
|
|
|
2
2
|
// ADR-006: --output-format stream-json 기반 단발 실행 워커.
|
|
3
3
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { delimiter, extname, join } from 'node:path';
|
|
5
7
|
import readline from 'node:readline';
|
|
6
|
-
|
|
7
|
-
const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
|
|
8
|
-
const DEFAULT_KILL_GRACE_MS = 1000;
|
|
9
|
-
|
|
10
|
-
function toStringList(value) {
|
|
11
|
-
if (!Array.isArray(value)) return [];
|
|
12
|
-
return value
|
|
13
|
-
.map((item) => String(item ?? '').trim())
|
|
14
|
-
.filter(Boolean);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function safeJsonParse(line) {
|
|
18
|
-
try {
|
|
19
|
-
return JSON.parse(line);
|
|
20
|
-
} catch {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
8
|
+
import { toStringList, safeJsonParse, createWorkerError, DEFAULT_TIMEOUT_MS, DEFAULT_KILL_GRACE_MS } from './worker-utils.mjs';
|
|
24
9
|
|
|
25
10
|
function appendTextFragments(value, parts) {
|
|
26
11
|
if (value == null) return;
|
|
@@ -84,10 +69,100 @@ function buildGeminiArgs(options) {
|
|
|
84
69
|
return args;
|
|
85
70
|
}
|
|
86
71
|
|
|
87
|
-
function
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
72
|
+
function resolveSpawnCommand(command, env = process.env) {
|
|
73
|
+
const raw = String(command ?? '').trim();
|
|
74
|
+
if (!raw || process.platform !== 'win32') return raw;
|
|
75
|
+
|
|
76
|
+
const pathExts = (env.PATHEXT || process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
|
|
77
|
+
.split(';')
|
|
78
|
+
.map((ext) => ext.trim().toLowerCase())
|
|
79
|
+
.filter(Boolean);
|
|
80
|
+
const extensions = extname(raw)
|
|
81
|
+
? ['']
|
|
82
|
+
: [...new Set(['.cmd', '.exe', '.bat', ...pathExts, ''])];
|
|
83
|
+
|
|
84
|
+
const tryResolve = (base) => {
|
|
85
|
+
for (const ext of extensions) {
|
|
86
|
+
const candidate = `${base}${ext}`;
|
|
87
|
+
if (existsSync(candidate)) return candidate;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (raw.includes('\\') || raw.includes('/')) {
|
|
93
|
+
return tryResolve(raw.replaceAll('/', '\\')) || raw;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const pathEntries = String(env.PATH || process.env.PATH || '')
|
|
97
|
+
.split(delimiter)
|
|
98
|
+
.map((entry) => entry.trim())
|
|
99
|
+
.filter(Boolean);
|
|
100
|
+
|
|
101
|
+
for (const entry of pathEntries) {
|
|
102
|
+
const resolved = tryResolve(join(entry, raw));
|
|
103
|
+
if (resolved) return resolved;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return raw;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function quoteWindowsCmdArg(value) {
|
|
110
|
+
const raw = String(value ?? '');
|
|
111
|
+
if (raw.length === 0) return '""';
|
|
112
|
+
|
|
113
|
+
const escaped = raw
|
|
114
|
+
.replace(/(\\*)"/g, '$1$1\\"')
|
|
115
|
+
.replace(/(\\+)$/g, '$1$1');
|
|
116
|
+
|
|
117
|
+
return /[\s"&()<>^|]/.test(raw)
|
|
118
|
+
? `"${escaped}"`
|
|
119
|
+
: escaped;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function quotePosixShellArg(value) {
|
|
123
|
+
const raw = String(value ?? '');
|
|
124
|
+
return `'${raw.replaceAll("'", `'\"'\"'`)}'`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function toBashPath(value) {
|
|
128
|
+
return String(value ?? '')
|
|
129
|
+
.replace(/^([A-Za-z]):/, (_, drive) => `/${drive.toLowerCase()}`)
|
|
130
|
+
.replaceAll('\\', '/');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function buildSpawnSpec(command, args, env = process.env) {
|
|
134
|
+
const resolvedCommand = resolveSpawnCommand(command, env);
|
|
135
|
+
|
|
136
|
+
if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(resolvedCommand)) {
|
|
137
|
+
const commandLine = [resolvedCommand, ...args]
|
|
138
|
+
.map((part) => quoteWindowsCmdArg(part))
|
|
139
|
+
.join(' ');
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
command: 'cmd.exe',
|
|
143
|
+
args: ['/d', '/s', '/c', commandLine],
|
|
144
|
+
resolvedCommand,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (process.platform === 'win32' && !extname(resolvedCommand) && existsSync(resolvedCommand)) {
|
|
149
|
+
const bashCommand = env.TFX_BASH_BIN || env.BASH || 'bash';
|
|
150
|
+
const commandLine = [toBashPath(resolvedCommand), ...args]
|
|
151
|
+
.map((part) => quotePosixShellArg(part))
|
|
152
|
+
.join(' ');
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
command: bashCommand,
|
|
156
|
+
args: ['-lc', commandLine],
|
|
157
|
+
resolvedCommand,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
command: resolvedCommand,
|
|
163
|
+
args,
|
|
164
|
+
resolvedCommand,
|
|
165
|
+
};
|
|
91
166
|
}
|
|
92
167
|
|
|
93
168
|
/**
|
|
@@ -187,10 +262,12 @@ export class GeminiWorker {
|
|
|
187
262
|
promptArgument: options.promptArgument ?? '',
|
|
188
263
|
}),
|
|
189
264
|
];
|
|
265
|
+
const env = { ...this.env, ...(options.env || {}) };
|
|
266
|
+
const spawnSpec = buildSpawnSpec(this.command, args, env);
|
|
190
267
|
|
|
191
|
-
const child = spawn(
|
|
268
|
+
const child = spawn(spawnSpec.command, spawnSpec.args, {
|
|
192
269
|
cwd: options.cwd || this.cwd,
|
|
193
|
-
env
|
|
270
|
+
env,
|
|
194
271
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
195
272
|
windowsHide: true,
|
|
196
273
|
});
|
|
@@ -269,10 +346,13 @@ export class GeminiWorker {
|
|
|
269
346
|
const response = [
|
|
270
347
|
extractText(resultEvent),
|
|
271
348
|
...events
|
|
272
|
-
.filter((event) =>
|
|
349
|
+
.filter((event) => (
|
|
350
|
+
event?.type === 'assistant'
|
|
351
|
+
|| (event?.type === 'message' && event?.role === 'assistant')
|
|
352
|
+
))
|
|
273
353
|
.map((event) => extractText(event))
|
|
274
354
|
.filter(Boolean),
|
|
275
|
-
...stdoutLines,
|
|
355
|
+
...stdoutLines.filter((line) => line.trim() !== '""'),
|
|
276
356
|
]
|
|
277
357
|
.filter(Boolean)
|
|
278
358
|
.join('\n')
|
|
@@ -280,8 +360,8 @@ export class GeminiWorker {
|
|
|
280
360
|
|
|
281
361
|
const result = {
|
|
282
362
|
type: 'gemini',
|
|
283
|
-
command:
|
|
284
|
-
args,
|
|
363
|
+
command: spawnSpec.resolvedCommand,
|
|
364
|
+
args: spawnSpec.args,
|
|
285
365
|
response,
|
|
286
366
|
events,
|
|
287
367
|
resultEvent,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// hub/workers/worker-utils.mjs — 워커 공통 유틸리티
|
|
2
|
+
// claude-worker, gemini-worker, pipe 등에서 공유하는 순수 유틸 함수 모음.
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
|
|
5
|
+
export const DEFAULT_KILL_GRACE_MS = 1000;
|
|
6
|
+
|
|
7
|
+
export function toStringList(value) {
|
|
8
|
+
if (!Array.isArray(value)) return [];
|
|
9
|
+
return value
|
|
10
|
+
.map((item) => String(item ?? '').trim())
|
|
11
|
+
.filter(Boolean);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function safeJsonParse(line) {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(line);
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createWorkerError(message, details = {}) {
|
|
23
|
+
const error = new Error(message);
|
|
24
|
+
Object.assign(error, details);
|
|
25
|
+
return error;
|
|
26
|
+
}
|