triflux 10.41.0 → 10.41.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.
@@ -1,8 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  // tfx-doctor — triflux doctor 바로가기
3
+ import { fileURLToPath } from "node:url";
4
+
3
5
  process.argv = [
4
6
  process.argv[0],
5
- process.argv[1],
7
+ // triflux.mjs's own isMainModule() guard compares this against its real
8
+ // path, not this shim's — point argv[1] at triflux.mjs so it recognizes
9
+ // itself as the entry point instead of silently no-op'ing.
10
+ fileURLToPath(new URL("./triflux.mjs", import.meta.url)),
6
11
  "doctor",
7
12
  ...process.argv.slice(2),
8
13
  ];
package/bin/tfx-live.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFile } from "node:child_process";
3
+ import { realpathSync } from "node:fs";
3
4
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
5
  import { homedir, tmpdir } from "node:os";
5
6
  import { join as pathJoin, resolve as pathResolve } from "node:path";
@@ -2368,11 +2369,19 @@ export {
2368
2369
  resolveAskTransport,
2369
2370
  };
2370
2371
 
2371
- const isDirectRun =
2372
- process.argv[1] &&
2373
- fileURLToPath(import.meta.url) === pathResolve(process.argv[1]);
2372
+ function isMainModule() {
2373
+ if (!process.argv[1]) return false;
2374
+ const modulePath = fileURLToPath(import.meta.url);
2375
+ try {
2376
+ return realpathSync(process.argv[1]) === modulePath;
2377
+ } catch {
2378
+ // process.argv[1] doesn't resolve on disk (e.g. `node -e`) — fall back
2379
+ // to a non-symlink-aware comparison instead of treating it as not-main.
2380
+ return pathResolve(process.argv[1]) === modulePath;
2381
+ }
2382
+ }
2374
2383
 
2375
- if (isDirectRun) {
2384
+ if (isMainModule()) {
2376
2385
  main().catch((error) => {
2377
2386
  printJson({
2378
2387
  ok: false,
package/bin/tfx-setup.mjs CHANGED
@@ -1,8 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  // tfx-setup — triflux setup 바로가기
3
+ import { fileURLToPath } from "node:url";
4
+
3
5
  process.argv = [
4
6
  process.argv[0],
5
- process.argv[1],
7
+ // triflux.mjs's own isMainModule() guard compares this against its real
8
+ // path, not this shim's — point argv[1] at triflux.mjs so it recognizes
9
+ // itself as the entry point instead of silently no-op'ing.
10
+ fileURLToPath(new URL("./triflux.mjs", import.meta.url)),
6
11
  "setup",
7
12
  ...process.argv.slice(2),
8
13
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "10.41.0",
3
+ "version": "10.41.1",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Antigravity, and Claude",
5
5
  "type": "module",
6
6
  "bin": {
@@ -165,7 +165,13 @@ describe("release governance scripts", () => {
165
165
  );
166
166
  assert.deepEqual(
167
167
  tagOnlyPublish.steps.map((step) => step.label),
168
- ["git tag", "git push", "gh release create"],
168
+ [
169
+ "git tag",
170
+ "git push branch",
171
+ "git push tag",
172
+ "wait for tag",
173
+ "gh release create",
174
+ ],
169
175
  );
170
176
 
171
177
  const executed = [];
@@ -178,6 +184,9 @@ describe("release governance scripts", () => {
178
184
  execFileSyncFn: (command, args) => {
179
185
  executed.push([command, ...args].join(" "));
180
186
  if (command === "git" && args[0] === "rev-parse") return "tag\n";
187
+ if (command === "git" && args[0] === "ls-remote") {
188
+ return "abc123\trefs/tags/v1.2.3\n";
189
+ }
181
190
  if (command === "gh" && args[0] === "release") {
182
191
  return '{"tagName":"v1.2.3"}\n';
183
192
  }
@@ -187,7 +196,9 @@ describe("release governance scripts", () => {
187
196
  assert.equal(resumedPublish.allowExistingArtifacts, true);
188
197
  assert.deepEqual(executed, [
189
198
  "git rev-parse --verify v1.2.3",
190
- "git push origin HEAD --tags",
199
+ "git push origin HEAD",
200
+ "git push origin v1.2.3",
201
+ "git ls-remote --tags origin v1.2.3",
191
202
  "gh release view v1.2.3 --json tagName",
192
203
  ]);
193
204
 
@@ -222,6 +233,41 @@ describe("release governance scripts", () => {
222
233
  }
223
234
  });
224
235
 
236
+ it("tag-only(CI) 모드는 preflight로 HEAD==origin/main을 검증하고 branch push를 생략한다", async () => {
237
+ const root = makeRepo();
238
+ const prevRef = process.env.GITHUB_REF_NAME;
239
+ process.env.GITHUB_REF_NAME = "main";
240
+ try {
241
+ assertVersionSync({ rootDir: root, fix: true });
242
+ const executed = [];
243
+ await publishRelease({
244
+ rootDir: root,
245
+ version: "1.2.3",
246
+ dryRun: false,
247
+ publishNpm: false,
248
+ pushBranch: false,
249
+ createGithubRelease: false,
250
+ tagPoll: { attempts: 1, sleepFn: async () => {} },
251
+ execFileSyncFn: (command, args) => {
252
+ executed.push([command, ...args].join(" "));
253
+ if (command === "git" && args[0] === "rev-parse") {
254
+ return "deadbeef\n";
255
+ }
256
+ return "";
257
+ },
258
+ });
259
+ assert.ok(executed.includes("git fetch origin main"));
260
+ assert.ok(executed.includes("git rev-parse HEAD"));
261
+ assert.ok(executed.includes("git rev-parse origin/main"));
262
+ assert.ok(executed.includes("git push origin v1.2.3"));
263
+ assert.ok(!executed.includes("git push origin HEAD"));
264
+ } finally {
265
+ if (prevRef === undefined) delete process.env.GITHUB_REF_NAME;
266
+ else process.env.GITHUB_REF_NAME = prevRef;
267
+ rmSync(root, { recursive: true, force: true });
268
+ }
269
+ });
270
+
225
271
  it("release workflows use trusted publishing and skip duplicate tag publishes", () => {
226
272
  const releaseWorkflow = readFileSync(
227
273
  new URL("../../.github/workflows/release.yml", import.meta.url),
@@ -230,6 +276,7 @@ describe("release governance scripts", () => {
230
276
  assert.match(releaseWorkflow, /actions:\s*write/);
231
277
  assert.match(releaseWorkflow, /node-version:\s*24/);
232
278
  assert.match(releaseWorkflow, /publish\.mjs.*--skip-npm.*--allow-existing/);
279
+ assert.match(releaseWorkflow, /publish\.mjs.*--tag-only/);
233
280
  assert.match(
234
281
  releaseWorkflow,
235
282
  /gh workflow run npm-publish\.yml --ref \$\{\{ github\.ref_name \}\}/,
@@ -3,6 +3,40 @@ import { join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { assertVersionSync, parseArgs, ROOT, runCommand } from "./lib.mjs";
5
5
 
6
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
7
+
8
+ export async function waitForRemoteTag(
9
+ tagName,
10
+ {
11
+ rootDir = ROOT,
12
+ execFileSyncFn,
13
+ attempts = 30,
14
+ intervalMs = 2000,
15
+ sleepFn = defaultSleep,
16
+ } = {},
17
+ ) {
18
+ for (let attempt = 0; attempt < attempts; attempt++) {
19
+ try {
20
+ const out = runCommand(
21
+ "git",
22
+ ["ls-remote", "--tags", "origin", tagName],
23
+ {
24
+ cwd: rootDir,
25
+ execFileSyncFn,
26
+ stdio: "pipe",
27
+ },
28
+ );
29
+ if (String(out).includes(`refs/tags/${tagName}`)) return true;
30
+ } catch {
31
+ // Transient network/auth blips are retried below.
32
+ }
33
+ if (attempt < attempts - 1) await sleepFn(intervalMs);
34
+ }
35
+ throw new Error(
36
+ `Tag ${tagName} not visible on origin after ${attempts} attempts`,
37
+ );
38
+ }
39
+
6
40
  export async function publishRelease({
7
41
  version,
8
42
  rootDir = ROOT,
@@ -12,6 +46,8 @@ export async function publishRelease({
12
46
  publishNpm = true,
13
47
  provenance = false,
14
48
  allowExistingArtifacts = false,
49
+ pushBranch = true,
50
+ tagPoll = {},
15
51
  execFileSyncFn,
16
52
  } = {}) {
17
53
  const sync = assertVersionSync({ rootDir });
@@ -73,14 +109,27 @@ export async function publishRelease({
73
109
  }))
74
110
  : []),
75
111
  { label: "git tag", command: "git", args: ["tag", `v${releaseVersion}`] },
76
- {
77
- label: "git push",
78
- command: "git",
79
- args: ["push", "origin", "HEAD", "--tags"],
80
- },
81
112
  ];
113
+ if (pushBranch) {
114
+ steps.push({
115
+ label: "git push branch",
116
+ command: "git",
117
+ args: ["push", "origin", "HEAD"],
118
+ });
119
+ }
120
+ steps.push({
121
+ label: "git push tag",
122
+ command: "git",
123
+ args: ["push", "origin", `v${releaseVersion}`],
124
+ });
82
125
 
83
126
  if (createGithubRelease) {
127
+ steps.push({
128
+ label: "wait for tag",
129
+ kind: "wait-tag",
130
+ command: "git",
131
+ args: ["ls-remote", "--tags", "origin", `v${releaseVersion}`],
132
+ });
84
133
  steps.push({
85
134
  label: "gh release create",
86
135
  command: "gh",
@@ -125,11 +174,45 @@ export async function publishRelease({
125
174
  };
126
175
 
127
176
  if (!dryRun) {
177
+ if (!pushBranch) {
178
+ const branch = process.env.GITHUB_REF_NAME || "main";
179
+ runCommand("git", ["fetch", "origin", branch], {
180
+ cwd: rootDir,
181
+ execFileSyncFn,
182
+ });
183
+ const head = String(
184
+ runCommand("git", ["rev-parse", "HEAD"], {
185
+ cwd: rootDir,
186
+ execFileSyncFn,
187
+ stdio: "pipe",
188
+ }),
189
+ ).trim();
190
+ const remote = String(
191
+ runCommand("git", ["rev-parse", `origin/${branch}`], {
192
+ cwd: rootDir,
193
+ execFileSyncFn,
194
+ stdio: "pipe",
195
+ }),
196
+ ).trim();
197
+ if (head !== remote) {
198
+ throw new Error(
199
+ `Refusing tag-only publish: HEAD ${head} != origin/${branch} ${remote} (stale checkout or unpushed ${branch})`,
200
+ );
201
+ }
202
+ }
128
203
  for (const step of steps) {
129
204
  const tagName = `v${releaseVersion}`;
130
205
  if (step.label === "git tag" && shouldSkipExistingTag(tagName)) {
131
206
  continue;
132
207
  }
208
+ if (step.kind === "wait-tag") {
209
+ await waitForRemoteTag(tagName, {
210
+ rootDir,
211
+ execFileSyncFn,
212
+ ...tagPoll,
213
+ });
214
+ continue;
215
+ }
133
216
  if (
134
217
  step.label === "gh release create" &&
135
218
  shouldSkipExistingGithubRelease(tagName)
@@ -151,6 +234,7 @@ export async function publishRelease({
151
234
  publishNpm,
152
235
  provenance,
153
236
  allowExistingArtifacts,
237
+ pushBranch,
154
238
  dryRun,
155
239
  notesPath,
156
240
  steps: steps.map((step) => ({
@@ -175,6 +259,7 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
175
259
  publishNpm: !args["skip-npm"],
176
260
  provenance: Boolean(args.provenance || envProvenance),
177
261
  allowExistingArtifacts: Boolean(args["allow-existing"]),
262
+ pushBranch: !args["tag-only"],
178
263
  });
179
264
  console.log(JSON.stringify(result, null, 2));
180
265
  }