triflux 10.9.21 → 10.9.22

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.
Files changed (99) hide show
  1. package/.claude-plugin/marketplace.json +34 -0
  2. package/.claude-plugin/plugin.json +22 -0
  3. package/config/mcp-registry.json +29 -0
  4. package/hub/account-broker.mjs +6 -4
  5. package/hub/cli-adapter-base.mjs +14 -14
  6. package/hub/lib/env-detect.mjs +47 -20
  7. package/hub/server.mjs +17 -15
  8. package/hub/team/headless.mjs +10 -0
  9. package/hub/team/swarm-hypervisor.mjs +2 -2
  10. package/hud/constants.mjs +24 -13
  11. package/hud/renderers.mjs +2 -1
  12. package/package.json +62 -21
  13. package/scripts/__tests__/keyword-detector.test.mjs +4 -4
  14. package/scripts/__tests__/release-governance.test.mjs +148 -0
  15. package/scripts/doctor-diagnose.mjs +6 -7
  16. package/scripts/lib/cross-review-utils.mjs +2 -2
  17. package/scripts/lib/mcp-filter.mjs +9 -5
  18. package/scripts/release/bump-version.mjs +77 -0
  19. package/scripts/release/check-sync.mjs +51 -0
  20. package/scripts/release/lib.mjs +303 -0
  21. package/scripts/release/prepare.mjs +85 -0
  22. package/scripts/release/publish.mjs +87 -0
  23. package/scripts/release/verify.mjs +81 -0
  24. package/scripts/release/version-manifest.json +26 -0
  25. package/scripts/remote-spawn.mjs +3 -3
  26. package/scripts/setup.mjs +18 -15
  27. package/scripts/tfx-route.sh +64 -8
  28. package/tui/codex-profile.mjs +457 -0
  29. package/tui/core.mjs +266 -0
  30. package/tui/doctor.mjs +375 -0
  31. package/tui/gemini-profile.mjs +299 -0
  32. package/tui/monitor-data.mjs +152 -0
  33. package/tui/monitor.mjs +339 -0
  34. package/tui/setup.mjs +598 -0
  35. package/CLAUDE.md +0 -212
  36. package/references/hosts.json +0 -46
  37. package/skills/tfx-workspace/async-tests/run-tests.sh +0 -203
  38. package/skills/tfx-workspace/evals/evals.json +0 -79
  39. package/skills/tfx-workspace/iteration-1/benchmark.json +0 -524
  40. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +0 -11
  41. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +0 -25
  42. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +0 -154
  43. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +0 -5
  44. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +0 -25
  45. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +0 -126
  46. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +0 -5
  47. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +0 -11
  48. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +0 -25
  49. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +0 -119
  50. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +0 -5
  51. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +0 -25
  52. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +0 -115
  53. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +0 -5
  54. package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +0 -10
  55. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +0 -20
  56. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +0 -86
  57. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +0 -5
  58. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +0 -20
  59. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +0 -81
  60. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +0 -5
  61. package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +0 -12
  62. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +0 -30
  63. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +0 -316
  64. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +0 -5
  65. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +0 -30
  66. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +0 -352
  67. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +0 -5
  68. package/skills/tfx-workspace/iteration-1/review.html +0 -1325
  69. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +0 -12
  70. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +0 -30
  71. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +0 -97
  72. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +0 -5
  73. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +0 -30
  74. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +0 -94
  75. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +0 -5
  76. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +0 -12
  77. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +0 -30
  78. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +0 -209
  79. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +0 -5
  80. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +0 -30
  81. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +0 -193
  82. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +0 -5
  83. package/skills/tfx-workspace/iteration-2/benchmark.json +0 -144
  84. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +0 -13
  85. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +0 -35
  86. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +0 -382
  87. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +0 -5
  88. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +0 -35
  89. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +0 -333
  90. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +0 -5
  91. package/skills/tfx-workspace/iteration-2/review.html +0 -1325
  92. package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +0 -217
  93. package/skills/tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md +0 -77
  94. package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +0 -65
  95. package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +0 -94
  96. package/skills/tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md +0 -82
  97. package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +0 -133
  98. package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +0 -426
  99. package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +0 -101
@@ -0,0 +1,148 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { describe, it } from "node:test";
6
+
7
+ import { bumpVersion } from "../release/bump-version.mjs";
8
+ import { assertVersionSync, writeJson } from "../release/lib.mjs";
9
+ import { prepareRelease } from "../release/prepare.mjs";
10
+ import { publishRelease } from "../release/publish.mjs";
11
+ import { verifyRelease } from "../release/verify.mjs";
12
+
13
+ function makeRepo() {
14
+ const root = mkdtempSync(join(tmpdir(), "tfx-release-"));
15
+ mkdirSync(join(root, "scripts", "release"), { recursive: true });
16
+ mkdirSync(join(root, ".claude-plugin"), { recursive: true });
17
+ mkdirSync(join(root, "packages", "triflux"), { recursive: true });
18
+ mkdirSync(join(root, ".omx", "plans"), { recursive: true });
19
+
20
+ writeJson(join(root, "package.json"), { name: "triflux", version: "1.2.3" });
21
+ writeJson(join(root, "packages", "triflux", "package.json"), {
22
+ name: "triflux",
23
+ version: "1.2.0",
24
+ });
25
+ writeJson(join(root, ".claude-plugin", "plugin.json"), {
26
+ name: "triflux",
27
+ version: "1.1.0",
28
+ });
29
+ writeJson(join(root, ".claude-plugin", "marketplace.json"), {
30
+ version: "1.2.0",
31
+ plugins: [{ name: "triflux", version: "1.0.0" }],
32
+ });
33
+ writeJson(join(root, "package-lock.json"), {
34
+ name: "triflux",
35
+ version: "1.2.0",
36
+ packages: {
37
+ "": {
38
+ version: "1.0.0",
39
+ },
40
+ },
41
+ });
42
+ writeJson(join(root, "scripts", "release", "version-manifest.json"), {
43
+ canonicalFile: "package.json",
44
+ canonicalPath: ["version"],
45
+ targets: [
46
+ { file: "package.json", paths: [["version"]] },
47
+ { file: "packages/triflux/package.json", paths: [["version"]] },
48
+ { file: ".claude-plugin/plugin.json", paths: [["version"]] },
49
+ {
50
+ file: ".claude-plugin/marketplace.json",
51
+ paths: [["version"], ["plugins", 0, "version"]],
52
+ },
53
+ {
54
+ file: "package-lock.json",
55
+ paths: [["version"], ["packages", "", "version"]],
56
+ },
57
+ ],
58
+ });
59
+ return root;
60
+ }
61
+
62
+ describe("release governance scripts", () => {
63
+ it("assertVersionSync detects mismatches and fixes them", () => {
64
+ const root = makeRepo();
65
+ try {
66
+ const before = assertVersionSync({ rootDir: root });
67
+ assert.equal(before.ok, false);
68
+ assert.ok(before.mismatches.length >= 4);
69
+
70
+ const after = assertVersionSync({ rootDir: root, fix: true });
71
+ assert.equal(after.ok, true);
72
+ assert.deepEqual(
73
+ JSON.parse(readFileSync(join(root, ".claude-plugin", "plugin.json"))),
74
+ { name: "triflux", version: "1.2.3" },
75
+ );
76
+ } finally {
77
+ rmSync(root, { recursive: true, force: true });
78
+ }
79
+ });
80
+
81
+ it("bumpVersion writes canonical version and syncs targets", async () => {
82
+ const root = makeRepo();
83
+ try {
84
+ const result = await bumpVersion({
85
+ rootDir: root,
86
+ nextVersion: "2.0.0",
87
+ write: true,
88
+ });
89
+ assert.equal(result.ok, true);
90
+ assert.equal(
91
+ JSON.parse(readFileSync(join(root, "package.json"), "utf8")).version,
92
+ "2.0.0",
93
+ );
94
+ assert.equal(
95
+ JSON.parse(
96
+ readFileSync(
97
+ join(root, "packages", "triflux", "package.json"),
98
+ "utf8",
99
+ ),
100
+ ).version,
101
+ "2.0.0",
102
+ );
103
+ } finally {
104
+ rmSync(root, { recursive: true, force: true });
105
+ }
106
+ });
107
+
108
+ it("prepare/publish/verify support dry-run planning", async () => {
109
+ const root = makeRepo();
110
+ try {
111
+ assertVersionSync({ rootDir: root, fix: true });
112
+ const execStub = (command, args) => {
113
+ if (command === "git" && args[0] === "status") return "";
114
+ if (command === "git" && args[0] === "describe") return "v1.2.2";
115
+ if (command === "git" && args[0] === "log")
116
+ return "abc1234 feat: sample\n";
117
+ return "";
118
+ };
119
+
120
+ const prepare = await prepareRelease({
121
+ rootDir: root,
122
+ version: "1.2.3",
123
+ allowDirty: true,
124
+ dryRun: true,
125
+ execFileSyncFn: execStub,
126
+ });
127
+ assert.equal(prepare.ok, true);
128
+ assert.equal(prepare.commands.length, 3);
129
+
130
+ const publish = await publishRelease({
131
+ rootDir: root,
132
+ version: "1.2.3",
133
+ dryRun: true,
134
+ });
135
+ assert.equal(publish.steps.length >= 3, true);
136
+
137
+ const verify = await verifyRelease({
138
+ rootDir: root,
139
+ version: "1.2.3",
140
+ dryRun: true,
141
+ });
142
+ assert.equal(verify.ok, true);
143
+ assert.equal(verify.checks[1].name, "npm-view");
144
+ } finally {
145
+ rmSync(root, { recursive: true, force: true });
146
+ }
147
+ });
148
+ });
@@ -139,14 +139,13 @@ function collectSystemInfo() {
139
139
  freeMemMB: Math.round(freemem() / 1024 / 1024),
140
140
  };
141
141
 
142
- // Windows Terminal version
142
+ // Windows Terminal version (wt.exe --version은 GUI 다이얼로그를 띄우므로 AppxPackage로 조회)
143
143
  try {
144
- const wtVer = execSync("wt.exe --version 2>&1", {
145
- encoding: "utf8",
146
- timeout: 5000,
147
- windowsHide: true,
148
- }).trim();
149
- info.wtVersion = wtVer;
144
+ const wtVer = execSync(
145
+ 'powershell.exe -NoProfile -NoLogo -Command "(Get-AppxPackage Microsoft.WindowsTerminal).Version"',
146
+ { encoding: "utf8", timeout: 5000, windowsHide: true },
147
+ ).trim();
148
+ info.wtVersion = wtVer || "not found";
150
149
  } catch {
151
150
  info.wtVersion = "not found";
152
151
  }
@@ -1,7 +1,7 @@
1
1
  import { join } from "node:path";
2
2
 
3
3
  export const SESSION_TTL_SEC = 30 * 60;
4
- export const STATE_REL_PATH = join(".omc", "state", "cross-review.json");
4
+ export const STATE_REL_PATH = join(".triflux", "state", "cross-review.json");
5
5
 
6
6
  export function readStdin() {
7
7
  return new Promise((resolve) => {
@@ -39,7 +39,7 @@ export function shouldTrackPath(filePath) {
39
39
  if (typeof filePath !== "string" || !filePath.trim()) return false;
40
40
 
41
41
  const lower = filePath.toLowerCase();
42
- if (lower.startsWith(".omc/") || lower.startsWith(".claude/")) return false;
42
+ if (lower.startsWith(".triflux/") || lower.startsWith(".omc/") || lower.startsWith(".claude/")) return false;
43
43
  if (lower === "package-lock.json" || lower.endsWith("/package-lock.json"))
44
44
  return false;
45
45
  if (/\.(md|lock|yml|yaml)$/i.test(lower)) return false;
@@ -672,18 +672,22 @@ export function getCodexMcpConfig(options = {}) {
672
672
  const targetServers = registeredServers;
673
673
 
674
674
  if (resolvedProfile === "none") {
675
- // Codex 0.115+: transport 없는 서버에 enabled=false 보내면 "invalid transport" 에러.
676
- // 비허용 서버는 override에서 제외하고, 허용 서버만 명시적으로 설정한다.
677
- return { mcp_servers: {} };
675
+ // 등록된 서버를 전부 enabled=false 비활성화.
676
+ // 미등록 서버에 보내면 "invalid transport" 에러지만, registeredServers는 등록 확인 완료.
677
+ const config = { mcp_servers: {} };
678
+ for (const server of targetServers) {
679
+ config.mcp_servers[server] = { enabled: false };
680
+ }
681
+ return config;
678
682
  }
679
683
 
680
684
  const config = { mcp_servers: {} };
681
685
  const allowedToolsByServer =
682
686
  getProfileDefinition(resolvedProfile).allowedToolsByServer;
683
687
  for (const server of targetServers) {
684
- // Codex 0.115+: transport 없는 서버에 enabled=false를 보내면 "invalid transport" 에러.
685
- // 비허용 서버는 override에서 제외한다 (Codex 기본 설정이 유지됨).
686
688
  if (!allowedServers.has(server)) {
689
+ // 비허용 서버는 명시적으로 비활성화하여 MCP 시작 자체를 방지.
690
+ config.mcp_servers[server] = { enabled: false };
687
691
  continue;
688
692
  }
689
693
 
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ import { join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import {
5
+ assertVersionSync,
6
+ isSemver,
7
+ parseArgs,
8
+ ROOT,
9
+ readJson,
10
+ syncVersionTargets,
11
+ writeJson,
12
+ } from "./lib.mjs";
13
+
14
+ export async function bumpVersion({
15
+ nextVersion,
16
+ rootDir = ROOT,
17
+ write = false,
18
+ } = {}) {
19
+ if (!isSemver(nextVersion)) {
20
+ throw new Error(`Invalid semver version: ${nextVersion}`);
21
+ }
22
+
23
+ const packagePath = join(rootDir, "package.json");
24
+ const packageJson = readJson(packagePath);
25
+ const previousVersion = packageJson.version;
26
+ packageJson.version = nextVersion;
27
+
28
+ if (write) {
29
+ writeJson(packagePath, packageJson);
30
+ const syncedFiles = syncVersionTargets({
31
+ rootDir,
32
+ expectedVersion: nextVersion,
33
+ });
34
+ const post = assertVersionSync({ rootDir, expectedVersion: nextVersion });
35
+ return {
36
+ ok: post.ok,
37
+ previousVersion,
38
+ nextVersion,
39
+ updatedFiles: ["package.json", ...syncedFiles],
40
+ targets: post.targets,
41
+ };
42
+ }
43
+
44
+ const preview = assertVersionSync({
45
+ rootDir,
46
+ expectedVersion: nextVersion,
47
+ });
48
+ return {
49
+ ok: true,
50
+ previousVersion,
51
+ nextVersion,
52
+ updatedFiles: ["package.json"],
53
+ targets: preview.targets.map((target) => ({
54
+ ...target,
55
+ expected: nextVersion,
56
+ inSync: target.file === "package.json",
57
+ })),
58
+ };
59
+ }
60
+
61
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
62
+ const args = parseArgs(process.argv.slice(2));
63
+ const nextVersion = args.next || args.version;
64
+ const result = await bumpVersion({
65
+ nextVersion,
66
+ rootDir: args.root,
67
+ write: Boolean(args.write),
68
+ });
69
+ if (args.json) {
70
+ console.log(JSON.stringify(result, null, 2));
71
+ } else {
72
+ console.log(
73
+ `${args.write ? "Bumped" : "Planned"} version ${result.previousVersion} -> ${result.nextVersion}`,
74
+ );
75
+ console.log(`Updated files: ${result.updatedFiles.join(", ")}`);
76
+ }
77
+ }
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import { assertVersionSync, formatPathSegments, parseArgs } from "./lib.mjs";
5
+
6
+ export { assertVersionSync } from "./lib.mjs";
7
+
8
+ function toJson(result) {
9
+ return {
10
+ ok: result.ok,
11
+ rootVersion: result.rootVersion,
12
+ fixedFiles: result.fixedFiles,
13
+ mismatches: result.mismatches.map((target) => ({
14
+ file: target.file,
15
+ path: formatPathSegments(target.path),
16
+ found: target.found ?? null,
17
+ expected: target.expected,
18
+ missing: target.missing,
19
+ })),
20
+ };
21
+ }
22
+
23
+ function printHuman(result) {
24
+ if (result.ok) {
25
+ console.log(`Version sync OK (${result.rootVersion})`);
26
+ return;
27
+ }
28
+ console.log(`Version sync mismatch (${result.rootVersion})`);
29
+ for (const target of result.mismatches) {
30
+ console.log(
31
+ `- ${target.file} :: ${formatPathSegments(target.path)} => found=${target.found ?? "missing"}, expected=${target.expected}`,
32
+ );
33
+ }
34
+ if (result.fixedFiles.length) {
35
+ console.log(`Fixed files: ${result.fixedFiles.join(", ")}`);
36
+ }
37
+ }
38
+
39
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
40
+ const args = parseArgs(process.argv.slice(2));
41
+ const result = assertVersionSync({
42
+ rootDir: args.root,
43
+ fix: Boolean(args.fix),
44
+ });
45
+ if (args.json) {
46
+ console.log(JSON.stringify(toJson(result), null, 2));
47
+ } else {
48
+ printHuman(result);
49
+ }
50
+ process.exitCode = result.ok ? 0 : 1;
51
+ }
@@ -0,0 +1,303 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ export const ROOT = resolve(
7
+ dirname(fileURLToPath(import.meta.url)),
8
+ "..",
9
+ "..",
10
+ );
11
+ export const DEFAULT_MANIFEST_PATH = join(
12
+ ROOT,
13
+ "scripts",
14
+ "release",
15
+ "version-manifest.json",
16
+ );
17
+
18
+ export function parseArgs(argv) {
19
+ const args = {};
20
+ for (let i = 0; i < argv.length; i++) {
21
+ const token = argv[i];
22
+ if (!token.startsWith("--")) continue;
23
+ const key = token.slice(2);
24
+ const next = argv[i + 1];
25
+ if (!next || next.startsWith("--")) {
26
+ args[key] = true;
27
+ continue;
28
+ }
29
+ args[key] = next;
30
+ i++;
31
+ }
32
+ return args;
33
+ }
34
+
35
+ export function readJson(filePath) {
36
+ return JSON.parse(readFileSync(filePath, "utf8"));
37
+ }
38
+
39
+ export function writeJson(filePath, value) {
40
+ mkdirSync(dirname(filePath), { recursive: true });
41
+ writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
42
+ }
43
+
44
+ export function getValueAtPath(obj, pathSegments) {
45
+ return pathSegments.reduce((acc, segment) => {
46
+ if (acc === undefined || acc === null) return undefined;
47
+ return acc[segment];
48
+ }, obj);
49
+ }
50
+
51
+ export function setValueAtPath(obj, pathSegments, value) {
52
+ if (!pathSegments.length) {
53
+ throw new Error("pathSegments must not be empty");
54
+ }
55
+ let cursor = obj;
56
+ for (let i = 0; i < pathSegments.length - 1; i++) {
57
+ const segment = pathSegments[i];
58
+ const nextSegment = pathSegments[i + 1];
59
+ if (
60
+ cursor[segment] === undefined ||
61
+ cursor[segment] === null ||
62
+ typeof cursor[segment] !== "object"
63
+ ) {
64
+ cursor[segment] = typeof nextSegment === "number" ? [] : {};
65
+ }
66
+ cursor = cursor[segment];
67
+ }
68
+ cursor[pathSegments.at(-1)] = value;
69
+ }
70
+
71
+ export function formatPathSegments(pathSegments) {
72
+ return pathSegments
73
+ .map((segment) =>
74
+ typeof segment === "number"
75
+ ? `[${segment}]`
76
+ : segment === ""
77
+ ? '[""]'
78
+ : `.${segment}`,
79
+ )
80
+ .join("")
81
+ .replace(/^\./, "");
82
+ }
83
+
84
+ export function isSemver(value) {
85
+ return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(String(value || "").trim());
86
+ }
87
+
88
+ export function loadVersionManifest({
89
+ rootDir = ROOT,
90
+ manifestPath = join(rootDir, "scripts", "release", "version-manifest.json"),
91
+ } = {}) {
92
+ const manifest = readJson(manifestPath);
93
+ if (!manifest.canonicalFile || !Array.isArray(manifest.targets)) {
94
+ throw new Error(`Invalid version manifest: ${manifestPath}`);
95
+ }
96
+ return manifest;
97
+ }
98
+
99
+ export function getCanonicalVersion({
100
+ rootDir = ROOT,
101
+ manifest = loadVersionManifest({ rootDir }),
102
+ } = {}) {
103
+ const canonicalPath = join(rootDir, manifest.canonicalFile);
104
+ const canonicalJson = readJson(canonicalPath);
105
+ const value = getValueAtPath(
106
+ canonicalJson,
107
+ manifest.canonicalPath || ["version"],
108
+ );
109
+ if (!isSemver(value)) {
110
+ throw new Error(
111
+ `Canonical version is missing or invalid at ${manifest.canonicalFile}`,
112
+ );
113
+ }
114
+ return value;
115
+ }
116
+
117
+ export function collectVersionTargets({
118
+ rootDir = ROOT,
119
+ manifest = loadVersionManifest({ rootDir }),
120
+ expectedVersion = getCanonicalVersion({ rootDir, manifest }),
121
+ } = {}) {
122
+ return manifest.targets.flatMap((target) => {
123
+ const absolutePath = join(rootDir, target.file);
124
+ if (!existsSync(absolutePath)) {
125
+ return target.paths.map((pathSegments) => ({
126
+ file: target.file,
127
+ absolutePath,
128
+ path: pathSegments,
129
+ found: undefined,
130
+ expected: expectedVersion,
131
+ inSync: false,
132
+ missing: true,
133
+ }));
134
+ }
135
+ const json = readJson(absolutePath);
136
+ return target.paths.map((pathSegments) => {
137
+ const found = getValueAtPath(json, pathSegments);
138
+ return {
139
+ file: target.file,
140
+ absolutePath,
141
+ path: pathSegments,
142
+ found,
143
+ expected: expectedVersion,
144
+ inSync: found === expectedVersion,
145
+ missing: false,
146
+ };
147
+ });
148
+ });
149
+ }
150
+
151
+ export function syncVersionTargets({
152
+ rootDir = ROOT,
153
+ manifest = loadVersionManifest({ rootDir }),
154
+ expectedVersion = getCanonicalVersion({ rootDir, manifest }),
155
+ } = {}) {
156
+ const touched = new Set();
157
+ for (const target of manifest.targets) {
158
+ const absolutePath = join(rootDir, target.file);
159
+ if (!existsSync(absolutePath)) {
160
+ throw new Error(`Cannot sync missing target: ${target.file}`);
161
+ }
162
+ const json = readJson(absolutePath);
163
+ let changed = false;
164
+ for (const pathSegments of target.paths) {
165
+ if (getValueAtPath(json, pathSegments) !== expectedVersion) {
166
+ setValueAtPath(json, pathSegments, expectedVersion);
167
+ changed = true;
168
+ }
169
+ }
170
+ if (changed) {
171
+ writeJson(absolutePath, json);
172
+ touched.add(target.file);
173
+ }
174
+ }
175
+ return [...touched];
176
+ }
177
+
178
+ export function assertVersionSync({
179
+ rootDir = ROOT,
180
+ manifestPath = join(rootDir, "scripts", "release", "version-manifest.json"),
181
+ expectedVersion,
182
+ fix = false,
183
+ } = {}) {
184
+ const manifest = loadVersionManifest({ rootDir, manifestPath });
185
+ const rootVersion =
186
+ expectedVersion || getCanonicalVersion({ rootDir, manifest });
187
+ let targets = collectVersionTargets({
188
+ rootDir,
189
+ manifest,
190
+ expectedVersion: rootVersion,
191
+ });
192
+ const mismatches = targets.filter((target) => !target.inSync);
193
+ let fixedFiles = [];
194
+
195
+ if (fix && mismatches.length) {
196
+ fixedFiles = syncVersionTargets({
197
+ rootDir,
198
+ manifest,
199
+ expectedVersion: rootVersion,
200
+ });
201
+ targets = collectVersionTargets({
202
+ rootDir,
203
+ manifest,
204
+ expectedVersion: rootVersion,
205
+ });
206
+ }
207
+
208
+ return {
209
+ ok: targets.every((target) => target.inSync),
210
+ rootVersion,
211
+ targets,
212
+ mismatches: targets.filter((target) => !target.inSync),
213
+ fixedFiles,
214
+ };
215
+ }
216
+
217
+ export function ensureGitClean({
218
+ rootDir = ROOT,
219
+ execFileSyncFn = execFileSync,
220
+ } = {}) {
221
+ const output = execFileSyncFn("git", ["status", "--porcelain"], {
222
+ cwd: rootDir,
223
+ encoding: "utf8",
224
+ }).trim();
225
+ return { clean: output.length === 0, output };
226
+ }
227
+
228
+ export function getPreviousTag({
229
+ rootDir = ROOT,
230
+ execFileSyncFn = execFileSync,
231
+ } = {}) {
232
+ try {
233
+ return execFileSyncFn("git", ["describe", "--tags", "--abbrev=0"], {
234
+ cwd: rootDir,
235
+ encoding: "utf8",
236
+ }).trim();
237
+ } catch {
238
+ return null;
239
+ }
240
+ }
241
+
242
+ export function getCommitSummaries({
243
+ rootDir = ROOT,
244
+ previousTag,
245
+ execFileSyncFn = execFileSync,
246
+ } = {}) {
247
+ const range = previousTag ? `${previousTag}..HEAD` : "HEAD~10..HEAD";
248
+ try {
249
+ return execFileSyncFn("git", ["log", "--oneline", range], {
250
+ cwd: rootDir,
251
+ encoding: "utf8",
252
+ })
253
+ .trim()
254
+ .split(/\r?\n/)
255
+ .filter(Boolean);
256
+ } catch {
257
+ return [];
258
+ }
259
+ }
260
+
261
+ export function buildReleaseNotes({
262
+ version,
263
+ rootDir = ROOT,
264
+ execFileSyncFn = execFileSync,
265
+ } = {}) {
266
+ const previousTag = getPreviousTag({ rootDir, execFileSyncFn });
267
+ const commits = getCommitSummaries({
268
+ rootDir,
269
+ previousTag,
270
+ execFileSyncFn,
271
+ });
272
+ const heading = previousTag
273
+ ? `Changes since ${previousTag}`
274
+ : "Recent changes (no prior tag found)";
275
+ const lines = commits.length
276
+ ? commits.map((commit) => `- ${commit}`)
277
+ : ["- No commit summary available"];
278
+
279
+ return [
280
+ `# Release v${version}`,
281
+ "",
282
+ `## ${heading}`,
283
+ ...lines,
284
+ "",
285
+ "## Install",
286
+ `- npm: \`npm install -g triflux@${version}\``,
287
+ "- Claude Code:",
288
+ " - `/plugin marketplace add tellang/triflux`",
289
+ " - `/plugin install triflux@tellang`",
290
+ "",
291
+ ].join("\n");
292
+ }
293
+
294
+ export function runCommand(
295
+ command,
296
+ args,
297
+ { cwd = ROOT, execFileSyncFn = execFileSync } = {},
298
+ ) {
299
+ execFileSyncFn(command, args, {
300
+ cwd,
301
+ stdio: "inherit",
302
+ });
303
+ }