triflux 10.3.4 → 10.7.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/LICENSE +21 -21
- package/bin/tfx-doctor-tui.mjs +1 -1
- package/bin/tfx-doctor.mjs +6 -1
- package/bin/tfx-profile.mjs +1 -1
- package/bin/tfx-setup-tui.mjs +1 -1
- package/bin/tfx-setup.mjs +6 -1
- package/bin/triflux.mjs +2396 -1140
- package/hooks/agent-route-guard.mjs +12 -8
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +19 -7
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +108 -24
- package/hooks/hook-registry.json +11 -0
- package/hooks/keyword-rules.json +6 -10
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hooks/mcp-config-watcher.mjs +6 -2
- package/hooks/pipeline-stop.mjs +3 -6
- package/hooks/safety-guard.mjs +99 -28
- package/hooks/session-start-fast.mjs +143 -0
- package/hooks/subagent-verifier.mjs +5 -4
- package/hub/account-broker.mjs +256 -60
- package/hub/adaptive-diagnostic.mjs +75 -48
- package/hub/adaptive-inject.mjs +95 -57
- package/hub/adaptive-memory.mjs +156 -42
- package/hub/adaptive.mjs +60 -31
- package/hub/assign-callbacks.mjs +67 -30
- package/hub/bridge.mjs +0 -1
- package/hub/cli-adapter-base.mjs +200 -48
- package/hub/codex-adapter.mjs +76 -96
- package/hub/codex-compat.mjs +3 -3
- package/hub/codex-preflight.mjs +63 -37
- package/hub/delegator/contracts.mjs +19 -23
- package/hub/delegator/index.mjs +3 -3
- package/hub/delegator/service.mjs +88 -64
- package/hub/delegator/tool-definitions.mjs +5 -5
- package/hub/fullcycle.mjs +33 -17
- package/hub/gemini-adapter.mjs +69 -94
- package/hub/hitl.mjs +89 -30
- package/hub/intent.mjs +161 -38
- package/hub/lib/cache-guard.mjs +43 -17
- package/hub/lib/mcp-response-cache.mjs +66 -32
- package/hub/lib/memory-store.mjs +285 -111
- package/hub/lib/path-utils.mjs +35 -37
- package/hub/lib/process-utils.mjs +106 -37
- package/hub/lib/spawn-trace.mjs +527 -0
- package/hub/lib/ssh-command.mjs +34 -4
- package/hub/lib/ssh-retry.mjs +5 -1
- package/hub/lib/uuidv7.mjs +4 -3
- package/hub/memory-doctor.mjs +266 -106
- package/hub/middleware/request-logger.mjs +61 -34
- package/hub/paths.mjs +9 -9
- package/hub/pipeline/gates/confidence.mjs +34 -15
- package/hub/pipeline/gates/consensus.mjs +27 -15
- package/hub/pipeline/gates/index.mjs +7 -3
- package/hub/pipeline/gates/selfcheck.mjs +57 -19
- package/hub/pipeline/index.mjs +77 -42
- package/hub/pipeline/state.mjs +10 -10
- package/hub/pipeline/transitions.mjs +40 -23
- package/hub/platform.mjs +57 -48
- package/hub/promote-penalties.mjs +25 -7
- package/hub/quality/deslop.mjs +70 -49
- package/hub/research.mjs +32 -25
- package/hub/router.mjs +240 -107
- package/hub/routing/complexity.mjs +132 -29
- package/hub/routing/index.mjs +17 -12
- package/hub/routing/q-learning.mjs +76 -28
- package/hub/server.mjs +4 -4
- package/hub/session-fingerprint.mjs +126 -60
- package/hub/state.mjs +84 -43
- package/hub/store-adapter.mjs +59 -26
- package/hub/store.mjs +356 -153
- package/hub/team/agent-map.json +22 -7
- package/hub/team/ansi.mjs +186 -122
- package/hub/team/backend.mjs +28 -10
- package/hub/team/cli/commands/attach.mjs +29 -9
- package/hub/team/cli/commands/control.mjs +29 -8
- package/hub/team/cli/commands/debug.mjs +32 -11
- package/hub/team/cli/commands/focus.mjs +38 -11
- package/hub/team/cli/commands/interrupt.mjs +18 -6
- package/hub/team/cli/commands/kill.mjs +16 -5
- package/hub/team/cli/commands/list.mjs +11 -4
- package/hub/team/cli/commands/send.mjs +19 -6
- package/hub/team/cli/commands/start/index.mjs +154 -31
- package/hub/team/cli/commands/start/parse-args.mjs +38 -11
- package/hub/team/cli/commands/start/start-headless.mjs +112 -36
- package/hub/team/cli/commands/start/start-in-process.mjs +12 -2
- package/hub/team/cli/commands/start/start-mux.mjs +70 -21
- package/hub/team/cli/commands/start/start-wt.mjs +29 -12
- package/hub/team/cli/commands/status.mjs +43 -14
- package/hub/team/cli/commands/stop.mjs +11 -4
- package/hub/team/cli/commands/task.mjs +8 -3
- package/hub/team/cli/commands/tasks.mjs +1 -1
- package/hub/team/cli/index.mjs +2 -2
- package/hub/team/cli/manifest.mjs +38 -8
- package/hub/team/cli/render.mjs +30 -8
- package/hub/team/cli/services/attach-fallback.mjs +31 -11
- package/hub/team/cli/services/hub-client.mjs +42 -14
- package/hub/team/cli/services/member-selector.mjs +11 -4
- package/hub/team/cli/services/native-control.mjs +48 -21
- package/hub/team/cli/services/runtime-mode.mjs +2 -1
- package/hub/team/cli/services/state-store.mjs +25 -8
- package/hub/team/cli/services/task-model.mjs +16 -6
- package/hub/team/conductor-mesh-bridge.mjs +24 -23
- package/hub/team/conductor.mjs +8 -4
- package/hub/team/dashboard-anchor.mjs +4 -5
- package/hub/team/dashboard-layout.mjs +3 -1
- package/hub/team/dashboard-open.mjs +41 -21
- package/hub/team/dashboard.mjs +76 -28
- package/hub/team/event-log.mjs +18 -10
- package/hub/team/handoff.mjs +31 -15
- package/hub/team/headless.mjs +2 -1
- package/hub/team/health-probe.mjs +69 -54
- package/hub/team/launcher-template.mjs +16 -13
- package/hub/team/native-supervisor.mjs +65 -21
- package/hub/team/native.mjs +74 -35
- package/hub/team/nativeProxy.mjs +184 -113
- package/hub/team/notify.mjs +119 -76
- package/hub/team/orchestrator.mjs +9 -4
- package/hub/team/pane.mjs +12 -7
- package/hub/team/process-cleanup.mjs +25 -16
- package/hub/team/psmux.mjs +491 -201
- package/hub/team/remote-probe.mjs +68 -52
- package/hub/team/remote-session.mjs +117 -59
- package/hub/team/remote-watcher.mjs +61 -33
- package/hub/team/routing.mjs +51 -25
- package/hub/team/runtime-strategy.mjs +3 -1
- package/hub/team/session.mjs +98 -34
- package/hub/team/staleState.mjs +72 -30
- package/hub/team/swarm-locks.mjs +15 -13
- package/hub/team/swarm-planner.mjs +32 -21
- package/hub/team/swarm-reconciler.mjs +48 -23
- package/hub/team/tui-lite.mjs +266 -68
- package/hub/team/tui-remote-adapter.mjs +14 -10
- package/hub/team/tui-viewer.mjs +99 -43
- package/hub/team/tui.mjs +708 -271
- package/hub/team/worktree-lifecycle.mjs +152 -58
- package/hub/team/wt-manager.mjs +24 -14
- package/hub/token-mode.mjs +71 -71
- package/hub/tray.mjs +66 -23
- package/hub/workers/claude-worker.mjs +162 -118
- package/hub/workers/codex-mcp.mjs +192 -141
- package/hub/workers/delegator-mcp.mjs +507 -333
- package/hub/workers/factory.mjs +8 -8
- package/hub/workers/gemini-worker.mjs +115 -84
- package/hub/workers/interface.mjs +6 -1
- package/hub/workers/worker-utils.mjs +21 -14
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +162 -26
- package/hud/context-monitor.mjs +82 -41
- package/hud/hud-qos-status.mjs +129 -49
- package/hud/mission-board.mjs +6 -3
- package/hud/providers/claude.mjs +226 -115
- package/hud/providers/codex.mjs +62 -22
- package/hud/providers/gemini.mjs +168 -56
- package/hud/renderers.mjs +384 -119
- package/hud/terminal.mjs +101 -31
- package/hud/utils.mjs +78 -38
- package/mesh/index.mjs +11 -5
- package/mesh/mesh-budget.mjs +18 -9
- package/mesh/mesh-heartbeat.mjs +1 -1
- package/mesh/mesh-queue.mjs +3 -5
- package/mesh/mesh-router.mjs +5 -4
- package/package.json +2 -1
- package/scripts/__tests__/gen-skill-docs.test.mjs +36 -7
- package/scripts/__tests__/keyword-detector.test.mjs +77 -28
- package/scripts/__tests__/mcp-guard-engine.test.mjs +58 -20
- package/scripts/__tests__/remote-spawn-transfer.test.mjs +30 -19
- package/scripts/__tests__/remote-spawn.test.mjs +10 -4
- package/scripts/__tests__/session-start-fast.test.mjs +36 -0
- package/scripts/__tests__/skill-template.test.mjs +98 -50
- package/scripts/__tests__/smoke.test.mjs +1 -1
- package/scripts/__tests__/spawn-trace.test.mjs +102 -0
- package/scripts/__tests__/tfx-doctor-diagnose.test.mjs +48 -0
- package/scripts/cache-doctor.mjs +11 -4
- package/scripts/cache-warmup.mjs +96 -37
- package/scripts/claudemd-sync.mjs +27 -17
- package/scripts/codex-gateway-preflight.mjs +52 -37
- package/scripts/codex-mcp-gateway-sync.mjs +59 -39
- package/scripts/completions/tfx.bash +47 -47
- package/scripts/completions/tfx.fish +44 -44
- package/scripts/completions/tfx.zsh +83 -83
- package/scripts/config-audit.mjs +232 -0
- package/scripts/convert-to-tmpl.mjs +54 -0
- package/scripts/cross-review-gate.mjs +35 -12
- package/scripts/cross-review-tracker.mjs +21 -8
- package/scripts/demo.mjs +35 -17
- package/scripts/doctor-diagnose.mjs +284 -0
- package/scripts/gen-skill-docs.mjs +7 -2
- package/scripts/gen-skill-manifest.mjs +2 -1
- package/scripts/headless-guard.mjs +86 -48
- package/scripts/hub-ensure.mjs +45 -26
- package/scripts/keyword-detector.mjs +41 -20
- package/scripts/keyword-rules-expander.mjs +47 -30
- package/scripts/lib/claudemd-scanner.mjs +6 -1
- package/scripts/lib/context.mjs +3 -3
- package/scripts/lib/cross-review-utils.mjs +6 -3
- package/scripts/lib/env-probe.mjs +47 -28
- package/scripts/lib/gemini-profiles.mjs +44 -10
- package/scripts/lib/handoff.mjs +33 -17
- package/scripts/lib/hook-utils.mjs +8 -6
- package/scripts/lib/keyword-rules.mjs +43 -19
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +377 -239
- package/scripts/lib/mcp-guard-engine.mjs +194 -79
- package/scripts/lib/mcp-manifest.mjs +23 -13
- package/scripts/lib/mcp-server-catalog.mjs +300 -63
- package/scripts/lib/psmux-info.mjs +11 -6
- package/scripts/lib/remote-spawn-transfer.mjs +44 -14
- package/scripts/lib/skill-template.mjs +30 -7
- package/scripts/mcp-check.mjs +58 -39
- package/scripts/mcp-gateway-config.mjs +83 -39
- package/scripts/mcp-gateway-ensure.mjs +43 -35
- package/scripts/mcp-gateway-integration-test.mjs +70 -58
- package/scripts/mcp-gateway-start.mjs +126 -60
- package/scripts/mcp-gateway-verify.mjs +24 -22
- package/scripts/mcp-safety-guard.mjs +44 -11
- package/scripts/notion-read.mjs +199 -84
- package/scripts/pack.mjs +94 -89
- package/scripts/preflight-cache.mjs +27 -10
- package/scripts/preinstall.mjs +42 -13
- package/scripts/remote-spawn.mjs +309 -94
- package/scripts/run.cjs +8 -5
- package/scripts/session-spawn-helper.mjs +130 -39
- package/scripts/session-stale-cleanup.mjs +123 -0
- package/scripts/setup.mjs +941 -492
- package/scripts/test-lock.mjs +20 -7
- package/scripts/test-tfx-route-no-claude-native.mjs +16 -12
- package/scripts/tfx-batch-stats.mjs +32 -11
- package/scripts/tfx-gate-activate.mjs +11 -4
- package/scripts/tfx-route-post.mjs +87 -20
- package/scripts/tfx-route-worker.mjs +57 -51
- package/scripts/tfx-route.sh +41 -124
- package/scripts/tmp-cleanup.mjs +21 -7
- package/scripts/token-snapshot.mjs +204 -85
- package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +1 -0
- package/skills/.omc/state/idle-notif-cooldown.json +3 -0
- package/skills/.omc/state/last-tool-error.json +7 -0
- package/skills/.omc/state/subagent-tracking.json +7 -0
- package/skills/_templates/base.md +1 -6
- package/skills/merge-worktree/SKILL.md.tmpl +144 -0
- package/skills/shared/telemetry-segment.md +6 -0
- package/skills/star-prompt/SKILL.md.tmpl +222 -0
- package/skills/tfx-analysis/SKILL.md.tmpl +107 -0
- package/skills/tfx-analysis/skill.json +1 -6
- package/skills/tfx-auto/SKILL.md +1 -0
- package/skills/tfx-auto-codex/SKILL.md.tmpl +106 -0
- package/skills/tfx-auto-codex/skill.json +1 -3
- package/skills/tfx-autopilot/SKILL.md.tmpl +116 -0
- package/skills/tfx-autopilot/skill.json +1 -5
- package/skills/tfx-autoresearch/SKILL.md.tmpl +136 -0
- package/skills/tfx-autoroute/SKILL.md.tmpl +189 -0
- package/skills/tfx-autoroute/skill.json +1 -7
- package/skills/tfx-codex/SKILL.md +1 -0
- package/skills/tfx-codex/skill.json +1 -3
- package/skills/tfx-codex-swarm/SKILL.md.tmpl +16 -0
- package/skills/tfx-codex-swarm/evals/evals.json +1 -1
- package/skills/tfx-codex-swarm/skill.json +1 -4
- package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +54 -12
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +35 -7
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +35 -7
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +25 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +25 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +20 -4
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +16 -4
- package/skills/tfx-consensus/SKILL.md.tmpl +146 -0
- package/skills/tfx-debate/SKILL.md.tmpl +192 -0
- package/skills/tfx-debate/skill.json +1 -7
- package/skills/tfx-deep-analysis/SKILL.md.tmpl +228 -0
- package/skills/tfx-deep-analysis/skill.json +1 -5
- package/skills/tfx-deep-interview/SKILL.md.tmpl +203 -0
- package/skills/tfx-deep-plan/SKILL.md.tmpl +282 -0
- package/skills/tfx-deep-qa/SKILL.md.tmpl +165 -0
- package/skills/tfx-deep-qa/skill.json +1 -6
- package/skills/tfx-deep-research/SKILL.md.tmpl +217 -0
- package/skills/tfx-deep-review/SKILL.md.tmpl +179 -0
- package/skills/tfx-doctor/SKILL.md +21 -0
- package/skills/tfx-doctor/SKILL.md.tmpl +172 -0
- package/skills/tfx-doctor/skill.json +1 -3
- package/skills/tfx-find/SKILL.md +1 -0
- package/skills/tfx-forge/SKILL.md.tmpl +187 -0
- package/skills/tfx-fullcycle/SKILL.md.tmpl +286 -0
- package/skills/tfx-fullcycle/skill.json +1 -6
- package/skills/tfx-gemini/SKILL.md.tmpl +91 -0
- package/skills/tfx-gemini/skill.json +1 -3
- package/skills/tfx-hooks/SKILL.md.tmpl +216 -0
- package/skills/tfx-hooks/skill.json +1 -3
- package/skills/tfx-hub/SKILL.md.tmpl +212 -0
- package/skills/tfx-hub/skill.json +1 -3
- package/skills/tfx-index/SKILL.md +1 -0
- package/skills/tfx-index/skill.json +1 -6
- package/skills/tfx-interview/SKILL.md.tmpl +285 -0
- package/skills/tfx-multi/SKILL.md.tmpl +183 -0
- package/skills/tfx-multi/skill.json +1 -3
- package/skills/tfx-panel/SKILL.md.tmpl +189 -0
- package/skills/tfx-panel/skill.json +1 -7
- package/skills/tfx-persist/SKILL.md.tmpl +270 -0
- package/skills/tfx-persist/skill.json +1 -7
- package/skills/tfx-plan/SKILL.md +1 -0
- package/skills/tfx-plan/skill.json +1 -6
- package/skills/tfx-profile/SKILL.md.tmpl +239 -0
- package/skills/tfx-profile/skill.json +1 -3
- package/skills/tfx-prune/SKILL.md.tmpl +200 -0
- package/skills/tfx-prune/skill.json +1 -7
- package/skills/tfx-psmux-rules/SKILL.md.tmpl +326 -0
- package/skills/tfx-psmux-rules/skill.json +1 -4
- package/skills/tfx-qa/SKILL.md +1 -0
- package/skills/tfx-qa/skill.json +1 -6
- package/skills/tfx-ralph/SKILL.md.tmpl +28 -0
- package/skills/tfx-ralph/skill.json +1 -4
- package/skills/tfx-remote-setup/SKILL.md.tmpl +576 -0
- package/skills/tfx-remote-setup/skill.json +1 -3
- package/skills/tfx-remote-spawn/SKILL.md.tmpl +263 -0
- package/skills/tfx-remote-spawn/references/hosts.json +16 -0
- package/skills/tfx-remote-spawn/skill.json +1 -4
- package/skills/tfx-research/SKILL.md +1 -0
- package/skills/tfx-review/SKILL.md +1 -0
- package/skills/tfx-review/skill.json +1 -6
- package/skills/tfx-setup/SKILL.md.tmpl +504 -0
- package/skills/tfx-setup/skill.json +1 -3
- package/skills/tfx-swarm/SKILL.md +22 -0
- package/skills/tfx-swarm/SKILL.md.tmpl +218 -0
- package/tui/codex-profile.mjs +88 -33
- package/tui/core.mjs +45 -15
- package/tui/doctor.mjs +75 -28
- package/tui/gemini-profile.mjs +74 -29
- package/tui/monitor-data.mjs +8 -4
- package/tui/monitor.mjs +71 -27
- package/tui/setup.mjs +133 -42
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { createHash } from
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
2
|
|
|
3
|
-
import { normalizePath } from
|
|
4
|
-
import { withRetry } from
|
|
3
|
+
import { normalizePath } from "./platform.mjs";
|
|
4
|
+
import { withRetry } from "./workers/worker-utils.mjs";
|
|
5
5
|
|
|
6
6
|
const ADAPTIVE_FINGERPRINT_VERSION = 1;
|
|
7
|
-
const DEFAULT_SCOPE =
|
|
8
|
-
const META_PREFIX =
|
|
7
|
+
const DEFAULT_SCOPE = "default";
|
|
8
|
+
const META_PREFIX = "adaptive_fingerprint:";
|
|
9
9
|
const DEFAULT_RETRY_OPTIONS = Object.freeze({
|
|
10
10
|
maxAttempts: 3,
|
|
11
11
|
baseDelayMs: 50,
|
|
12
12
|
maxDelayMs: 250,
|
|
13
13
|
});
|
|
14
14
|
const TIME_WINDOWS = Object.freeze([
|
|
15
|
-
{ name:
|
|
16
|
-
{ name:
|
|
17
|
-
{ name:
|
|
18
|
-
{ name:
|
|
15
|
+
{ name: "overnight", start: 0, end: 5 },
|
|
16
|
+
{ name: "morning", start: 6, end: 11 },
|
|
17
|
+
{ name: "afternoon", start: 12, end: 17 },
|
|
18
|
+
{ name: "evening", start: 18, end: 23 },
|
|
19
19
|
]);
|
|
20
20
|
const MEMORY_FINGERPRINT_CACHE = new WeakMap();
|
|
21
21
|
|
|
@@ -30,8 +30,8 @@ function toIsoTimestamp(value = Date.now()) {
|
|
|
30
30
|
|
|
31
31
|
function toTimestamp(value) {
|
|
32
32
|
if (value instanceof Date) return value.getTime();
|
|
33
|
-
if (typeof value ===
|
|
34
|
-
if (typeof value ===
|
|
33
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
34
|
+
if (typeof value === "string") {
|
|
35
35
|
const parsed = Date.parse(value);
|
|
36
36
|
return Number.isFinite(parsed) ? parsed : null;
|
|
37
37
|
}
|
|
@@ -39,9 +39,21 @@ function toTimestamp(value) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function normalizeRetryOptions(options = {}) {
|
|
42
|
-
const maxAttempts = Math.max(
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
const maxAttempts = Math.max(
|
|
43
|
+
1,
|
|
44
|
+
Number(options.maxAttempts ?? DEFAULT_RETRY_OPTIONS.maxAttempts) ||
|
|
45
|
+
DEFAULT_RETRY_OPTIONS.maxAttempts,
|
|
46
|
+
);
|
|
47
|
+
const baseDelayMs = Math.max(
|
|
48
|
+
0,
|
|
49
|
+
Number(options.baseDelayMs ?? DEFAULT_RETRY_OPTIONS.baseDelayMs) ||
|
|
50
|
+
DEFAULT_RETRY_OPTIONS.baseDelayMs,
|
|
51
|
+
);
|
|
52
|
+
const maxDelayMs = Math.max(
|
|
53
|
+
baseDelayMs,
|
|
54
|
+
Number(options.maxDelayMs ?? DEFAULT_RETRY_OPTIONS.maxDelayMs) ||
|
|
55
|
+
DEFAULT_RETRY_OPTIONS.maxDelayMs,
|
|
56
|
+
);
|
|
45
57
|
return { maxAttempts, baseDelayMs, maxDelayMs };
|
|
46
58
|
}
|
|
47
59
|
|
|
@@ -55,21 +67,23 @@ function metaKey(scope) {
|
|
|
55
67
|
}
|
|
56
68
|
|
|
57
69
|
function hashValue(value) {
|
|
58
|
-
const hash = createHash(
|
|
70
|
+
const hash = createHash("sha256");
|
|
59
71
|
hash.update(JSON.stringify(value));
|
|
60
|
-
return `sha256:${hash.digest(
|
|
72
|
+
return `sha256:${hash.digest("hex")}`;
|
|
61
73
|
}
|
|
62
74
|
|
|
63
75
|
function normalizeContextPath(value) {
|
|
64
|
-
const normalized = normalizePath(String(value ??
|
|
65
|
-
return normalized.replace(/\/+/gu,
|
|
76
|
+
const normalized = normalizePath(String(value ?? ""));
|
|
77
|
+
return normalized.replace(/\/+/gu, "/").replace(/\/+$/u, "") || "/";
|
|
66
78
|
}
|
|
67
79
|
|
|
68
80
|
function toRelativePath(targetPath, cwd) {
|
|
69
81
|
if (!cwd) return targetPath;
|
|
70
82
|
const base = normalizeContextPath(cwd);
|
|
71
|
-
if (targetPath === base) return
|
|
72
|
-
return targetPath.startsWith(`${base}/`)
|
|
83
|
+
if (targetPath === base) return ".";
|
|
84
|
+
return targetPath.startsWith(`${base}/`)
|
|
85
|
+
? targetPath.slice(base.length + 1)
|
|
86
|
+
: targetPath;
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
function toUniqueList(values) {
|
|
@@ -84,29 +98,49 @@ function toUniqueList(values) {
|
|
|
84
98
|
}
|
|
85
99
|
|
|
86
100
|
function collectRawPathCandidates(context = {}) {
|
|
87
|
-
const direct = [
|
|
101
|
+
const direct = [
|
|
102
|
+
context.file_path,
|
|
103
|
+
context.filePath,
|
|
104
|
+
context.path,
|
|
105
|
+
context.target_path,
|
|
106
|
+
context.targetPath,
|
|
107
|
+
];
|
|
88
108
|
const fromArrays = [context.files, context.paths, context.targets]
|
|
89
109
|
.filter(Array.isArray)
|
|
90
110
|
.flat()
|
|
91
|
-
.map((entry) => (typeof entry ===
|
|
92
|
-
return [...direct, ...fromArrays].filter(
|
|
111
|
+
.map((entry) => (typeof entry === "string" ? entry : entry?.path));
|
|
112
|
+
return [...direct, ...fromArrays].filter(
|
|
113
|
+
(entry) => typeof entry === "string" && entry.trim().length > 0,
|
|
114
|
+
);
|
|
93
115
|
}
|
|
94
116
|
|
|
95
117
|
function collectPathPattern(context = {}) {
|
|
96
118
|
const normalized = toUniqueList(
|
|
97
119
|
collectRawPathCandidates(context)
|
|
98
120
|
.map((entry) => normalizeContextPath(entry))
|
|
99
|
-
.map((entry) =>
|
|
121
|
+
.map((entry) =>
|
|
122
|
+
toRelativePath(
|
|
123
|
+
entry,
|
|
124
|
+
context.cwd || context.project_root || context.projectRoot,
|
|
125
|
+
),
|
|
126
|
+
),
|
|
100
127
|
).sort();
|
|
101
128
|
|
|
102
129
|
const primaryPath = normalized[0] ?? null;
|
|
103
130
|
const extensions = normalized
|
|
104
|
-
.map((entry) => entry.split(
|
|
105
|
-
.map((entry) =>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
131
|
+
.map((entry) => entry.split("/").pop() || "")
|
|
132
|
+
.map((entry) =>
|
|
133
|
+
entry.includes(".")
|
|
134
|
+
? entry.slice(entry.lastIndexOf(".")).toLowerCase()
|
|
135
|
+
: "none",
|
|
136
|
+
);
|
|
137
|
+
const extensionCounts = extensions.reduce(
|
|
138
|
+
(acc, ext) => ({
|
|
139
|
+
...acc,
|
|
140
|
+
[ext]: (acc[ext] || 0) + 1,
|
|
141
|
+
}),
|
|
142
|
+
{},
|
|
143
|
+
);
|
|
110
144
|
|
|
111
145
|
return {
|
|
112
146
|
count: normalized.length,
|
|
@@ -118,17 +152,24 @@ function collectPathPattern(context = {}) {
|
|
|
118
152
|
}
|
|
119
153
|
|
|
120
154
|
function normalizeWorkType(value) {
|
|
121
|
-
const text = String(value ??
|
|
155
|
+
const text = String(value ?? "")
|
|
156
|
+
.trim()
|
|
157
|
+
.toLowerCase();
|
|
122
158
|
if (!text) {
|
|
123
|
-
return { raw: null, normalized:
|
|
159
|
+
return { raw: null, normalized: "general" };
|
|
124
160
|
}
|
|
125
|
-
const normalized =
|
|
161
|
+
const normalized =
|
|
162
|
+
text.replace(/\s+/gu, "-").replace(/[^a-z0-9-]/gu, "") || "general";
|
|
126
163
|
return { raw: value, normalized };
|
|
127
164
|
}
|
|
128
165
|
|
|
129
166
|
function collectActivityTimestamps(context = {}, now = Date.now) {
|
|
130
|
-
const nowValue = typeof now ===
|
|
131
|
-
const fromList = [
|
|
167
|
+
const nowValue = typeof now === "function" ? now() : now;
|
|
168
|
+
const fromList = [
|
|
169
|
+
context.activity_timestamps,
|
|
170
|
+
context.activityTimestamps,
|
|
171
|
+
context.timestamps,
|
|
172
|
+
]
|
|
132
173
|
.filter(Array.isArray)
|
|
133
174
|
.flat()
|
|
134
175
|
.map(toTimestamp)
|
|
@@ -136,17 +177,24 @@ function collectActivityTimestamps(context = {}, now = Date.now) {
|
|
|
136
177
|
const singles = [context.timestamp, context.started_at, context.startedAt]
|
|
137
178
|
.map(toTimestamp)
|
|
138
179
|
.filter((entry) => entry != null);
|
|
139
|
-
return fromList.length || singles.length
|
|
180
|
+
return fromList.length || singles.length
|
|
181
|
+
? [...fromList, ...singles]
|
|
182
|
+
: [Number(nowValue)];
|
|
140
183
|
}
|
|
141
184
|
|
|
142
185
|
function classifyHour(hour) {
|
|
143
186
|
const safeHour = Number.isFinite(Number(hour)) ? Number(hour) : 0;
|
|
144
|
-
const matched = TIME_WINDOWS.find(
|
|
145
|
-
|
|
187
|
+
const matched = TIME_WINDOWS.find(
|
|
188
|
+
(window) => safeHour >= window.start && safeHour <= window.end,
|
|
189
|
+
);
|
|
190
|
+
return matched?.name || "overnight";
|
|
146
191
|
}
|
|
147
192
|
|
|
148
193
|
function buildWindowHistogram(timestamps = []) {
|
|
149
|
-
const histogram = TIME_WINDOWS.reduce(
|
|
194
|
+
const histogram = TIME_WINDOWS.reduce(
|
|
195
|
+
(acc, window) => ({ ...acc, [window.name]: 0 }),
|
|
196
|
+
{},
|
|
197
|
+
);
|
|
150
198
|
for (const timestamp of timestamps) {
|
|
151
199
|
const bucket = classifyHour(new Date(timestamp).getHours());
|
|
152
200
|
histogram[bucket] = (histogram[bucket] || 0) + 1;
|
|
@@ -155,14 +203,16 @@ function buildWindowHistogram(timestamps = []) {
|
|
|
155
203
|
}
|
|
156
204
|
|
|
157
205
|
function dominantWindow(histogram) {
|
|
158
|
-
const sorted = Object.entries(histogram)
|
|
159
|
-
|
|
160
|
-
|
|
206
|
+
const sorted = Object.entries(histogram).sort(
|
|
207
|
+
(left, right) => right[1] - left[1] || left[0].localeCompare(right[0]),
|
|
208
|
+
);
|
|
209
|
+
return sorted[0]?.[0] || "overnight";
|
|
161
210
|
}
|
|
162
211
|
|
|
163
212
|
function resolveTimezoneName(context = {}) {
|
|
164
|
-
const fromContext =
|
|
165
|
-
|
|
213
|
+
const fromContext =
|
|
214
|
+
typeof context.timezone === "string" ? context.timezone.trim() : "";
|
|
215
|
+
const fromIntl = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
166
216
|
return fromContext || fromIntl;
|
|
167
217
|
}
|
|
168
218
|
|
|
@@ -199,11 +249,13 @@ function getMemoryStoreMap(store) {
|
|
|
199
249
|
|
|
200
250
|
function readFingerprintFromStore(store, scope) {
|
|
201
251
|
if (!store) return null;
|
|
202
|
-
if (typeof store.loadAdaptiveFingerprint ===
|
|
252
|
+
if (typeof store.loadAdaptiveFingerprint === "function") {
|
|
203
253
|
return store.loadAdaptiveFingerprint(scope);
|
|
204
254
|
}
|
|
205
255
|
if (store.db?.prepare) {
|
|
206
|
-
const row = store.db
|
|
256
|
+
const row = store.db
|
|
257
|
+
.prepare("SELECT value FROM _meta WHERE key = ?")
|
|
258
|
+
.get(metaKey(scope));
|
|
207
259
|
return row?.value ? JSON.parse(row.value) : null;
|
|
208
260
|
}
|
|
209
261
|
return clone(getMemoryStoreMap(store).get(normalizeScope(scope)) || null);
|
|
@@ -211,11 +263,12 @@ function readFingerprintFromStore(store, scope) {
|
|
|
211
263
|
|
|
212
264
|
function writeFingerprintToStore(store, scope, record) {
|
|
213
265
|
if (!store) return clone(record);
|
|
214
|
-
if (typeof store.saveAdaptiveFingerprint ===
|
|
266
|
+
if (typeof store.saveAdaptiveFingerprint === "function") {
|
|
215
267
|
return store.saveAdaptiveFingerprint(scope, clone(record));
|
|
216
268
|
}
|
|
217
269
|
if (store.db?.prepare) {
|
|
218
|
-
store.db
|
|
270
|
+
store.db
|
|
271
|
+
.prepare("INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)")
|
|
219
272
|
.run(metaKey(scope), JSON.stringify(record));
|
|
220
273
|
return clone(record);
|
|
221
274
|
}
|
|
@@ -225,7 +278,7 @@ function writeFingerprintToStore(store, scope, record) {
|
|
|
225
278
|
|
|
226
279
|
function buildHealthSnapshot(base, patch = {}) {
|
|
227
280
|
return {
|
|
228
|
-
state: patch.state || base.state ||
|
|
281
|
+
state: patch.state || base.state || "healthy",
|
|
229
282
|
retry: { ...base.retry },
|
|
230
283
|
last_success_at: patch.last_success_at ?? base.last_success_at ?? null,
|
|
231
284
|
last_failure_at: patch.last_failure_at ?? base.last_failure_at ?? null,
|
|
@@ -235,7 +288,7 @@ function buildHealthSnapshot(base, patch = {}) {
|
|
|
235
288
|
|
|
236
289
|
function createInitialHealth(retryOptions) {
|
|
237
290
|
return {
|
|
238
|
-
state:
|
|
291
|
+
state: "healthy",
|
|
239
292
|
retry: { ...retryOptions },
|
|
240
293
|
last_success_at: null,
|
|
241
294
|
last_failure_at: null,
|
|
@@ -244,7 +297,7 @@ function createInitialHealth(retryOptions) {
|
|
|
244
297
|
}
|
|
245
298
|
|
|
246
299
|
function resolveNowValue(now) {
|
|
247
|
-
return typeof now ===
|
|
300
|
+
return typeof now === "function" ? now() : now;
|
|
248
301
|
}
|
|
249
302
|
|
|
250
303
|
function mergeFingerprintSnapshot(previous, computed, scope) {
|
|
@@ -258,7 +311,7 @@ function mergeFingerprintSnapshot(previous, computed, scope) {
|
|
|
258
311
|
|
|
259
312
|
function markHealthyHealth(base, now) {
|
|
260
313
|
return buildHealthSnapshot(base, {
|
|
261
|
-
state:
|
|
314
|
+
state: "healthy",
|
|
262
315
|
last_success_at: toIsoTimestamp(resolveNowValue(now)),
|
|
263
316
|
last_error: null,
|
|
264
317
|
});
|
|
@@ -266,11 +319,11 @@ function markHealthyHealth(base, now) {
|
|
|
266
319
|
|
|
267
320
|
function markDegradedHealth(base, now, error) {
|
|
268
321
|
return buildHealthSnapshot(base, {
|
|
269
|
-
state:
|
|
322
|
+
state: "degraded",
|
|
270
323
|
last_failure_at: toIsoTimestamp(resolveNowValue(now)),
|
|
271
324
|
last_error: {
|
|
272
|
-
name: error?.name ||
|
|
273
|
-
message: error?.message ||
|
|
325
|
+
name: error?.name || "Error",
|
|
326
|
+
message: error?.message || "unknown adaptive fingerprint error",
|
|
274
327
|
},
|
|
275
328
|
});
|
|
276
329
|
}
|
|
@@ -279,7 +332,9 @@ export function buildAdaptiveFingerprint(sessionContext = {}, options = {}) {
|
|
|
279
332
|
const now = options.now ?? Date.now;
|
|
280
333
|
const capturedAt = toIsoTimestamp(resolveNowValue(now));
|
|
281
334
|
const pathPattern = collectPathPattern(sessionContext);
|
|
282
|
-
const workType = normalizeWorkType(
|
|
335
|
+
const workType = normalizeWorkType(
|
|
336
|
+
sessionContext.work_type ?? sessionContext.workType,
|
|
337
|
+
);
|
|
283
338
|
const timezonePattern = collectTimezonePattern(sessionContext, now);
|
|
284
339
|
const fingerprintId = computeFingerprintSignature({
|
|
285
340
|
path_pattern: pathPattern,
|
|
@@ -303,10 +358,18 @@ export async function loadAdaptiveFingerprint(store, scope = DEFAULT_SCOPE) {
|
|
|
303
358
|
return clone(loaded);
|
|
304
359
|
}
|
|
305
360
|
|
|
306
|
-
export async function saveAdaptiveFingerprint(
|
|
361
|
+
export async function saveAdaptiveFingerprint(
|
|
362
|
+
store,
|
|
363
|
+
scope,
|
|
364
|
+
fingerprint,
|
|
365
|
+
options = {},
|
|
366
|
+
) {
|
|
307
367
|
const retryOptions = normalizeRetryOptions(options.retryOptions);
|
|
308
368
|
const normalizedScope = normalizeScope(scope);
|
|
309
|
-
const write = async () =>
|
|
369
|
+
const write = async () =>
|
|
370
|
+
Promise.resolve(
|
|
371
|
+
writeFingerprintToStore(store, normalizedScope, fingerprint),
|
|
372
|
+
);
|
|
310
373
|
const saved = await withRetry(write, { ...retryOptions });
|
|
311
374
|
return clone(saved);
|
|
312
375
|
}
|
|
@@ -324,7 +387,9 @@ export function createAdaptiveFingerprintService(options = {}) {
|
|
|
324
387
|
const merged = mergeFingerprintSnapshot(previous, computed, scope);
|
|
325
388
|
|
|
326
389
|
try {
|
|
327
|
-
const saved = await saveAdaptiveFingerprint(store, scope, merged, {
|
|
390
|
+
const saved = await saveAdaptiveFingerprint(store, scope, merged, {
|
|
391
|
+
retryOptions,
|
|
392
|
+
});
|
|
328
393
|
health = markHealthyHealth(health, now);
|
|
329
394
|
return saved;
|
|
330
395
|
} catch (error) {
|
|
@@ -343,7 +408,8 @@ export function createAdaptiveFingerprintService(options = {}) {
|
|
|
343
408
|
|
|
344
409
|
return Object.freeze({
|
|
345
410
|
captureFingerprint: capture,
|
|
346
|
-
computeFingerprint: (sessionContext = {}) =>
|
|
411
|
+
computeFingerprint: (sessionContext = {}) =>
|
|
412
|
+
buildAdaptiveFingerprint(sessionContext, { now }),
|
|
347
413
|
loadFingerprint: read,
|
|
348
414
|
getHealth,
|
|
349
415
|
});
|
package/hub/state.mjs
CHANGED
|
@@ -1,20 +1,34 @@
|
|
|
1
|
-
import { execSync } from
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
openSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
renameSync,
|
|
9
|
+
statSync,
|
|
10
|
+
unlinkSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
const PROJECT_ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
18
|
+
const PID_FILE_NAME = "hub.pid";
|
|
19
|
+
const LEGACY_STATE_FILE_NAME = "hub-state.json";
|
|
20
|
+
const LOCK_FILE_NAME = "hub-start.lock";
|
|
11
21
|
|
|
12
22
|
let heldLockPath = null;
|
|
13
23
|
let heldLockFd = null;
|
|
14
24
|
let cachedVersionHash = null;
|
|
15
25
|
|
|
16
26
|
function getStateDir(options = {}) {
|
|
17
|
-
return
|
|
27
|
+
return (
|
|
28
|
+
options.stateDir ||
|
|
29
|
+
process.env.TFX_HUB_STATE_DIR?.trim() ||
|
|
30
|
+
join(homedir(), ".claude", "cache", "tfx-hub")
|
|
31
|
+
);
|
|
18
32
|
}
|
|
19
33
|
|
|
20
34
|
function getStatePath(options = {}) {
|
|
@@ -55,25 +69,32 @@ function safeReplaceFile(tempPath, targetPath) {
|
|
|
55
69
|
try {
|
|
56
70
|
renameSync(tempPath, targetPath);
|
|
57
71
|
} catch (error) {
|
|
58
|
-
if (![
|
|
59
|
-
try {
|
|
72
|
+
if (!["EEXIST", "EPERM", "EACCES"].includes(error?.code)) {
|
|
73
|
+
try {
|
|
74
|
+
unlinkSync(tempPath);
|
|
75
|
+
} catch {}
|
|
60
76
|
throw error;
|
|
61
77
|
}
|
|
62
|
-
try {
|
|
78
|
+
try {
|
|
79
|
+
unlinkSync(targetPath);
|
|
80
|
+
} catch {}
|
|
63
81
|
renameSync(tempPath, targetPath);
|
|
64
82
|
}
|
|
65
83
|
}
|
|
66
84
|
|
|
67
85
|
function writeJsonFile(targetPath, payload) {
|
|
68
86
|
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
|
|
69
|
-
writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, {
|
|
87
|
+
writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, {
|
|
88
|
+
encoding: "utf8",
|
|
89
|
+
mode: 0o600,
|
|
90
|
+
});
|
|
70
91
|
safeReplaceFile(tempPath, targetPath);
|
|
71
92
|
}
|
|
72
93
|
|
|
73
94
|
function readJsonFile(filePath) {
|
|
74
95
|
try {
|
|
75
96
|
if (!existsSync(filePath)) return null;
|
|
76
|
-
return parseJson(readFileSync(filePath,
|
|
97
|
+
return parseJson(readFileSync(filePath, "utf8"), null);
|
|
77
98
|
} catch {
|
|
78
99
|
return null;
|
|
79
100
|
}
|
|
@@ -82,7 +103,7 @@ function readJsonFile(filePath) {
|
|
|
82
103
|
/**
|
|
83
104
|
* 허브의 현재 상태(PID, 포트, 버전 등)를 파일에 기록합니다.
|
|
84
105
|
* 원자적(atomic) 쓰기를 위해 임시 파일을 생성한 후 교체하는 방식을 사용합니다.
|
|
85
|
-
*
|
|
106
|
+
*
|
|
86
107
|
* @param {object} payload - 상태 데이터
|
|
87
108
|
* @param {number} payload.pid - 허브 프로세스 ID
|
|
88
109
|
* @param {number} payload.port - 허브 서버 포트
|
|
@@ -93,31 +114,39 @@ function readJsonFile(filePath) {
|
|
|
93
114
|
* @param {string} [options.stateDir] - 상태 파일이 저장될 디렉토리
|
|
94
115
|
* @returns {object} 기록된 상태 데이터
|
|
95
116
|
*/
|
|
96
|
-
export function writeState(
|
|
117
|
+
export function writeState(
|
|
118
|
+
{ pid, port, version, sessionId, startedAt, ...rest },
|
|
119
|
+
options = {},
|
|
120
|
+
) {
|
|
97
121
|
const stateDir = getStateDir(options);
|
|
98
122
|
const statePath = getStatePath(options);
|
|
99
123
|
const payload = { pid, port, version, sessionId, startedAt, ...rest };
|
|
100
124
|
|
|
101
125
|
mkdirSync(stateDir, { recursive: true });
|
|
102
126
|
writeJsonFile(statePath, payload);
|
|
103
|
-
try {
|
|
127
|
+
try {
|
|
128
|
+
unlinkSync(getLegacyStatePath(options));
|
|
129
|
+
} catch {}
|
|
104
130
|
return payload;
|
|
105
131
|
}
|
|
106
132
|
|
|
107
133
|
/**
|
|
108
134
|
* 파일로부터 허브의 현재 상태를 읽어옵니다.
|
|
109
|
-
*
|
|
135
|
+
*
|
|
110
136
|
* @param {object} [options] - 옵션
|
|
111
137
|
* @param {string} [options.stateDir] - 상태 파일이 저장된 디렉토리
|
|
112
138
|
* @returns {object|null} 읽어온 상태 데이터 또는 실패 시 null
|
|
113
139
|
*/
|
|
114
140
|
export function readState(options = {}) {
|
|
115
|
-
return
|
|
141
|
+
return (
|
|
142
|
+
readJsonFile(getStatePath(options)) ??
|
|
143
|
+
readJsonFile(getLegacyStatePath(options))
|
|
144
|
+
);
|
|
116
145
|
}
|
|
117
146
|
|
|
118
147
|
/**
|
|
119
148
|
* 지정된 포트에서 실행 중인 허브 서버의 헬스 체크를 수행합니다.
|
|
120
|
-
*
|
|
149
|
+
*
|
|
121
150
|
* @param {number|string} port - 서버 포트
|
|
122
151
|
* @param {object} [options] - 옵션
|
|
123
152
|
* @param {number} [options.timeoutMs=1000] - 요청 타임아웃
|
|
@@ -133,7 +162,7 @@ export async function isServerHealthy(port, options = {}) {
|
|
|
133
162
|
|
|
134
163
|
try {
|
|
135
164
|
const response = await fetch(`${baseUrl}/health`, {
|
|
136
|
-
method:
|
|
165
|
+
method: "GET",
|
|
137
166
|
signal: AbortSignal.timeout(timeoutMs),
|
|
138
167
|
});
|
|
139
168
|
if (!response.ok) return false;
|
|
@@ -147,7 +176,7 @@ export async function isServerHealthy(port, options = {}) {
|
|
|
147
176
|
/**
|
|
148
177
|
* 현재 프로젝트의 버전 해시를 생성합니다.
|
|
149
178
|
* package.json의 버전과 Git commit SHA를 조합합니다.
|
|
150
|
-
*
|
|
179
|
+
*
|
|
151
180
|
* @param {object} [options] - 옵션
|
|
152
181
|
* @param {boolean} [options.force=false] - 캐시를 무시하고 새로 생성할지 여부
|
|
153
182
|
* @returns {string} 버전 해시 문자열
|
|
@@ -155,21 +184,21 @@ export async function isServerHealthy(port, options = {}) {
|
|
|
155
184
|
export function getVersionHash(options = {}) {
|
|
156
185
|
if (cachedVersionHash && !options.force) return cachedVersionHash;
|
|
157
186
|
|
|
158
|
-
const packageJsonPath = join(PROJECT_ROOT,
|
|
159
|
-
const pkg = parseJson(readFileSync(packageJsonPath,
|
|
160
|
-
const version = String(pkg?.version ||
|
|
187
|
+
const packageJsonPath = join(PROJECT_ROOT, "package.json");
|
|
188
|
+
const pkg = parseJson(readFileSync(packageJsonPath, "utf8"), {});
|
|
189
|
+
const version = String(pkg?.version || "0.0.0").trim();
|
|
161
190
|
|
|
162
|
-
let sha = String(process.env.TFX_HUB_GIT_SHA ||
|
|
191
|
+
let sha = String(process.env.TFX_HUB_GIT_SHA || "").trim();
|
|
163
192
|
if (!sha) {
|
|
164
193
|
try {
|
|
165
|
-
sha = execSync(
|
|
194
|
+
sha = execSync("git rev-parse --short HEAD", {
|
|
166
195
|
cwd: PROJECT_ROOT,
|
|
167
|
-
encoding:
|
|
168
|
-
stdio: [
|
|
196
|
+
encoding: "utf8",
|
|
197
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
169
198
|
windowsHide: true,
|
|
170
199
|
}).trim();
|
|
171
200
|
} catch {
|
|
172
|
-
sha =
|
|
201
|
+
sha = "";
|
|
173
202
|
}
|
|
174
203
|
}
|
|
175
204
|
|
|
@@ -180,7 +209,7 @@ export function getVersionHash(options = {}) {
|
|
|
180
209
|
/**
|
|
181
210
|
* 허브 시작 시 중복 실행을 방지하기 위한 잠금(lock)을 획득합니다.
|
|
182
211
|
* 이미 실행 중인 다른 프로세스가 있는지 확인하고 유효한 잠금을 획득할 때까지 재시도합니다.
|
|
183
|
-
*
|
|
212
|
+
*
|
|
184
213
|
* @param {object} [options] - 옵션
|
|
185
214
|
* @param {number} [options.timeoutMs=3000] - 최대 대기 시간
|
|
186
215
|
* @param {number} [options.pollMs=50] - 재시도 간격
|
|
@@ -202,27 +231,37 @@ export async function acquireLock(options = {}) {
|
|
|
202
231
|
|
|
203
232
|
while (Date.now() <= deadline) {
|
|
204
233
|
try {
|
|
205
|
-
const fd = openSync(lockPath,
|
|
206
|
-
writeFileSync(
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
234
|
+
const fd = openSync(lockPath, "wx", 0o600);
|
|
235
|
+
writeFileSync(
|
|
236
|
+
fd,
|
|
237
|
+
`${JSON.stringify(
|
|
238
|
+
{
|
|
239
|
+
pid: process.pid,
|
|
240
|
+
createdAt: new Date().toISOString(),
|
|
241
|
+
},
|
|
242
|
+
null,
|
|
243
|
+
2,
|
|
244
|
+
)}\n`,
|
|
245
|
+
"utf8",
|
|
246
|
+
);
|
|
210
247
|
heldLockFd = fd;
|
|
211
248
|
heldLockPath = lockPath;
|
|
212
249
|
return { path: lockPath };
|
|
213
250
|
} catch (error) {
|
|
214
|
-
if (error?.code !==
|
|
251
|
+
if (error?.code !== "EEXIST") {
|
|
215
252
|
throw error;
|
|
216
253
|
}
|
|
217
254
|
|
|
218
255
|
try {
|
|
219
|
-
const raw = readFileSync(lockPath,
|
|
256
|
+
const raw = readFileSync(lockPath, "utf8");
|
|
220
257
|
const data = parseJson(raw, {});
|
|
221
258
|
const stats = statSync(lockPath);
|
|
222
259
|
const staleByPid = !isPidAlive(data?.pid);
|
|
223
260
|
const staleByAge = Date.now() - stats.mtimeMs > timeoutMs;
|
|
224
261
|
if (staleByPid || staleByAge) {
|
|
225
|
-
try {
|
|
262
|
+
try {
|
|
263
|
+
unlinkSync(lockPath);
|
|
264
|
+
} catch {}
|
|
226
265
|
continue;
|
|
227
266
|
}
|
|
228
267
|
} catch {}
|
|
@@ -236,7 +275,7 @@ export async function acquireLock(options = {}) {
|
|
|
236
275
|
|
|
237
276
|
/**
|
|
238
277
|
* 획득했던 잠금을 해제합니다. 잠금 파일을 삭제하고 관련 리소스를 정리합니다.
|
|
239
|
-
*
|
|
278
|
+
*
|
|
240
279
|
* @param {object} [options] - 옵션
|
|
241
280
|
* @param {string} [options.lockPath] - 명시적인 잠금 파일 경로
|
|
242
281
|
*/
|
|
@@ -244,7 +283,9 @@ export function releaseLock(options = {}) {
|
|
|
244
283
|
const lockPath = options.lockPath || heldLockPath || getLockPath(options);
|
|
245
284
|
|
|
246
285
|
if (heldLockFd !== null) {
|
|
247
|
-
try {
|
|
286
|
+
try {
|
|
287
|
+
closeSync(heldLockFd);
|
|
288
|
+
} catch {}
|
|
248
289
|
heldLockFd = null;
|
|
249
290
|
}
|
|
250
291
|
|