@triflux/remote 10.49.0 → 10.50.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/hub/team/claude-daemon-control.mjs +54 -6
- package/hub/team/claude-transcript.mjs +240 -0
- package/hub/team/cli/commands/start/index.mjs +17 -3
- package/hub/team/cli/commands/start/parse-args.mjs +1 -0
- package/hub/team/session-context.mjs +203 -0
- package/hub/team/uds-orchestrator.mjs +63 -1
- package/hub/team/worktree-lifecycle.mjs +0 -1
- package/package.json +1 -1
- package/scripts/lib/doctor-env-checks.mjs +0 -85
- package/scripts/lib/legacy-hook-cleanup.mjs +230 -0
- package/scripts/lib/legacy-mcp-cleanup.mjs +772 -0
- package/scripts/lib/mcp-guard-engine.mjs +44 -271
- package/scripts/lib/mcp-manifest.mjs +2 -2
- package/scripts/lib/cross-review-utils.mjs +0 -59
- package/scripts/lib/hook-utils.mjs +0 -16
- package/scripts/lib/keyword-rules.mjs +0 -296
- package/scripts/lib/mcp-gateway-health-check.mjs +0 -224
- package/scripts/lib/mcp-gateway-servers.mjs +0 -70
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +0 -35
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
dispatchClaudeDaemonJob,
|
|
19
19
|
teardownClaudeDaemonJob,
|
|
20
20
|
} from "./claude-daemon-control.mjs";
|
|
21
|
+
import { contextGuard, readCodexContext } from "./session-context.mjs";
|
|
21
22
|
|
|
22
23
|
const DEFAULT_TIMEOUT_MS = 90_000;
|
|
23
24
|
const DEFAULT_CODEX_APP_SERVER_UDS_TIMEOUT_MS = 120_000;
|
|
@@ -44,6 +45,7 @@ async function runCodexAppServerTurn({
|
|
|
44
45
|
timeoutMs,
|
|
45
46
|
maxTurnMs,
|
|
46
47
|
expectedTurnId,
|
|
48
|
+
noWait = false,
|
|
47
49
|
}) {
|
|
48
50
|
const messages = new Map();
|
|
49
51
|
const pending = [];
|
|
@@ -159,6 +161,16 @@ async function runCodexAppServerTurn({
|
|
|
159
161
|
turnId = started?.turn?.id || started?.turnId;
|
|
160
162
|
if (!turnId)
|
|
161
163
|
throw new Error("codex app-server turn/start returned no turn id");
|
|
164
|
+
if (noWait) {
|
|
165
|
+
return {
|
|
166
|
+
status: "submitted",
|
|
167
|
+
inputSent: true,
|
|
168
|
+
done: false,
|
|
169
|
+
turnId,
|
|
170
|
+
submittedAt: new Date().toISOString(),
|
|
171
|
+
target: threadId,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
162
174
|
for (const [kind, params] of pending) receive(kind, params);
|
|
163
175
|
const result = await completion;
|
|
164
176
|
const entries = [...messages.values()].map((item) => ({
|
|
@@ -264,6 +276,30 @@ export async function listCodexAppServerThreads({
|
|
|
264
276
|
}
|
|
265
277
|
}
|
|
266
278
|
|
|
279
|
+
export async function renameCodexAppServerThread({
|
|
280
|
+
socketPath,
|
|
281
|
+
threadId,
|
|
282
|
+
name,
|
|
283
|
+
clientFactory = (opts) => new JsonRpcWsUdsClient(opts),
|
|
284
|
+
} = {}) {
|
|
285
|
+
const timeoutMs = DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS;
|
|
286
|
+
const client = codexClient(socketPath, timeoutMs, clientFactory);
|
|
287
|
+
try {
|
|
288
|
+
await initializeCodexClient(client, timeoutMs);
|
|
289
|
+
await client.request("thread/name/set", { threadId, name }, timeoutMs);
|
|
290
|
+
return {
|
|
291
|
+
ok: true,
|
|
292
|
+
cli: "codex",
|
|
293
|
+
transport: "uds",
|
|
294
|
+
threadId,
|
|
295
|
+
name,
|
|
296
|
+
nameApplied: true,
|
|
297
|
+
};
|
|
298
|
+
} finally {
|
|
299
|
+
client.close();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
267
303
|
export async function askCodexAppServerThread({
|
|
268
304
|
socketPath,
|
|
269
305
|
threadId,
|
|
@@ -274,6 +310,9 @@ export async function askCodexAppServerThread({
|
|
|
274
310
|
ifBusy = "wait",
|
|
275
311
|
busyTimeoutMs = timeoutMs,
|
|
276
312
|
pollIntervalMs = 200,
|
|
313
|
+
noWait = false,
|
|
314
|
+
warnContextPct,
|
|
315
|
+
maxContextPct,
|
|
277
316
|
clientFactory = (opts) => new JsonRpcWsUdsClient(opts),
|
|
278
317
|
} = {}) {
|
|
279
318
|
if (typeof prompt !== "string" || !prompt.trim())
|
|
@@ -353,6 +392,24 @@ export async function askCodexAppServerThread({
|
|
|
353
392
|
{ threadId: selectedId, excludeTurns: true },
|
|
354
393
|
DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
|
|
355
394
|
);
|
|
395
|
+
const context = await readCodexContext(
|
|
396
|
+
resumed?.thread?.path || thread.path,
|
|
397
|
+
selectedId,
|
|
398
|
+
);
|
|
399
|
+
const guard = contextGuard("codex", context, {
|
|
400
|
+
warnContextPct,
|
|
401
|
+
maxContextPct,
|
|
402
|
+
});
|
|
403
|
+
if (guard.ok === false) {
|
|
404
|
+
return {
|
|
405
|
+
...guard,
|
|
406
|
+
status: "failed",
|
|
407
|
+
done: false,
|
|
408
|
+
cli: "codex",
|
|
409
|
+
transport: "uds",
|
|
410
|
+
threadId: selectedId,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
356
413
|
let activeTurns = null;
|
|
357
414
|
if (resumed?.thread?.status?.type === "active")
|
|
358
415
|
activeTurns = resumed.thread.turns?.length
|
|
@@ -371,9 +428,13 @@ export async function askCodexAppServerThread({
|
|
|
371
428
|
timeoutMs,
|
|
372
429
|
maxTurnMs,
|
|
373
430
|
expectedTurnId,
|
|
431
|
+
noWait,
|
|
374
432
|
});
|
|
375
433
|
if (turn.message) throw new Error(turn.message);
|
|
376
434
|
return {
|
|
435
|
+
...turn,
|
|
436
|
+
...context,
|
|
437
|
+
...guard,
|
|
377
438
|
cli: "codex",
|
|
378
439
|
transport: "uds",
|
|
379
440
|
threadId: selectedId,
|
|
@@ -381,7 +442,8 @@ export async function askCodexAppServerThread({
|
|
|
381
442
|
response: turn.response,
|
|
382
443
|
done: turn.status === "completed",
|
|
383
444
|
matchedCompletion: turn.matchedCompletion,
|
|
384
|
-
timedOut: turn.timedOut,
|
|
445
|
+
...(noWait ? {} : { timedOut: turn.timedOut }),
|
|
446
|
+
...(noWait ? { ok: true } : {}),
|
|
385
447
|
status: turn.status,
|
|
386
448
|
commentary: turn.commentary,
|
|
387
449
|
meta: { retries: turn.retries },
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// hub/team/worktree-lifecycle.mjs — Git worktree lifecycle management
|
|
2
|
-
// Replaces shell commands in tfx-codex-swarm Step 5 + merge-worktree Phase 6.
|
|
3
2
|
// Convention: .codex-swarm/wt-{slug} paths, swarm/{runId}/{slug} branches.
|
|
4
3
|
// Remote support: host option → SSH-based git operations via remote-session.mjs.
|
|
5
4
|
|
package/package.json
CHANGED
|
@@ -34,88 +34,3 @@ export function inspectMacTimeoutDependency({
|
|
|
34
34
|
fix: "brew install coreutils",
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
|
|
38
|
-
export function addPluginRootFallbackToCommand(command, pluginRoot) {
|
|
39
|
-
if (typeof command !== "string") return command;
|
|
40
|
-
const fallbackRoot = String(pluginRoot || "").replace(/\\/g, "/");
|
|
41
|
-
if (!fallbackRoot) return command;
|
|
42
|
-
return command
|
|
43
|
-
.replaceAll("${PLUGIN_ROOT}", `\${PLUGIN_ROOT:-${fallbackRoot}}`)
|
|
44
|
-
.replaceAll(
|
|
45
|
-
"${CLAUDE_PLUGIN_ROOT}",
|
|
46
|
-
`\${CLAUDE_PLUGIN_ROOT:-${fallbackRoot}}`,
|
|
47
|
-
)
|
|
48
|
-
.replace(
|
|
49
|
-
/(^|[\s"'])\/(hooks|scripts)\//g,
|
|
50
|
-
`$1\${PLUGIN_ROOT:-${fallbackRoot}}/$2/`,
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function hasBarePluginRootReference(command) {
|
|
55
|
-
if (typeof command !== "string") return false;
|
|
56
|
-
return (
|
|
57
|
-
command.includes("${PLUGIN_ROOT}/") ||
|
|
58
|
-
command.includes("${CLAUDE_PLUGIN_ROOT}/")
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function hasCollapsedRootHookPath(command) {
|
|
63
|
-
if (typeof command !== "string") return false;
|
|
64
|
-
return /(^|[\s"'])\/(?:hooks|scripts)\//.test(command);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function findPluginRootHookIssues(settings) {
|
|
68
|
-
const hooksByEvent =
|
|
69
|
-
settings?.hooks && typeof settings.hooks === "object" ? settings.hooks : {};
|
|
70
|
-
const issues = [];
|
|
71
|
-
|
|
72
|
-
for (const [event, entries] of Object.entries(hooksByEvent)) {
|
|
73
|
-
if (!Array.isArray(entries)) continue;
|
|
74
|
-
for (const entry of entries) {
|
|
75
|
-
const hooks = Array.isArray(entry?.hooks) ? entry.hooks : [];
|
|
76
|
-
for (const hook of hooks) {
|
|
77
|
-
const command = hook?.command;
|
|
78
|
-
if (
|
|
79
|
-
!hasBarePluginRootReference(command) &&
|
|
80
|
-
!hasCollapsedRootHookPath(command)
|
|
81
|
-
) {
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
issues.push({
|
|
85
|
-
event,
|
|
86
|
-
matcher: entry?.matcher || "*",
|
|
87
|
-
command,
|
|
88
|
-
reason: hasCollapsedRootHookPath(command)
|
|
89
|
-
? "collapsed-root-path"
|
|
90
|
-
: "missing-plugin-root-fallback",
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return issues;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function applyPluginRootHookFallbacks(settings, { pluginRoot } = {}) {
|
|
100
|
-
if (!settings?.hooks || typeof settings.hooks !== "object") {
|
|
101
|
-
return { changed: false, count: 0, settings };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
let count = 0;
|
|
105
|
-
for (const entries of Object.values(settings.hooks)) {
|
|
106
|
-
if (!Array.isArray(entries)) continue;
|
|
107
|
-
for (const entry of entries) {
|
|
108
|
-
const hooks = Array.isArray(entry?.hooks) ? entry.hooks : [];
|
|
109
|
-
for (const hook of hooks) {
|
|
110
|
-
if (typeof hook?.command !== "string") continue;
|
|
111
|
-
const next = addPluginRootFallbackToCommand(hook.command, pluginRoot);
|
|
112
|
-
if (next !== hook.command) {
|
|
113
|
-
hook.command = next;
|
|
114
|
-
count++;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return { changed: count > 0, count, settings };
|
|
121
|
-
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import {
|
|
2
|
+
constants,
|
|
3
|
+
copyFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
realpathSync,
|
|
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
|
+
|
|
16
|
+
// 설치된 Triflux 실행 대상만 식별한다.
|
|
17
|
+
const MANAGED_FILENAMES = [
|
|
18
|
+
"hook-orchestrator.mjs",
|
|
19
|
+
"claude-cwd-projection-refresh.mjs",
|
|
20
|
+
"permission-safe-allow.mjs",
|
|
21
|
+
"safety-guard.mjs",
|
|
22
|
+
"pipeline-stop.mjs",
|
|
23
|
+
"agent-route-guard.mjs",
|
|
24
|
+
"cross-review-tracker.mjs",
|
|
25
|
+
"cross-review-gate.mjs",
|
|
26
|
+
"tfx-gate-activate.mjs",
|
|
27
|
+
"subagent-tracker.mjs",
|
|
28
|
+
"subagent-verifier.mjs",
|
|
29
|
+
"error-context.mjs",
|
|
30
|
+
"hook-adaptive-collector.mjs",
|
|
31
|
+
"post-tool-tips.mjs",
|
|
32
|
+
"pre-compact-snapshot.mjs",
|
|
33
|
+
"cto-north-star-brief.mjs",
|
|
34
|
+
"session-start-lake.mjs",
|
|
35
|
+
"session-start-fast.mjs",
|
|
36
|
+
"session-end-cleanup.mjs",
|
|
37
|
+
"mcp-config-watcher.mjs",
|
|
38
|
+
"keyword-detector.mjs",
|
|
39
|
+
"setup.mjs",
|
|
40
|
+
"hub-ensure.mjs",
|
|
41
|
+
"preflight-cache.mjs",
|
|
42
|
+
"mcp-gateway-ensure.mjs",
|
|
43
|
+
"mcp-safety-guard.mjs",
|
|
44
|
+
"session-stale-cleanup.mjs",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const targetNames = MANAGED_FILENAMES.join("|").replaceAll(".", "\\.");
|
|
48
|
+
const uniqueTargetNames = MANAGED_FILENAMES.filter(
|
|
49
|
+
(name) =>
|
|
50
|
+
![
|
|
51
|
+
"setup.mjs",
|
|
52
|
+
"hub-ensure.mjs",
|
|
53
|
+
"preflight-cache.mjs",
|
|
54
|
+
"mcp-gateway-ensure.mjs",
|
|
55
|
+
"mcp-safety-guard.mjs",
|
|
56
|
+
"session-stale-cleanup.mjs",
|
|
57
|
+
].includes(name),
|
|
58
|
+
)
|
|
59
|
+
.join("|")
|
|
60
|
+
.replaceAll(".", "\\.");
|
|
61
|
+
const packageTarget = new RegExp(
|
|
62
|
+
String.raw`[/\\]triflux(?:[/\\](?:\.worktrees[/\\][^/\\]+|v?\d+\.\d+\.\d+[^/\\]*))?[/\\](?:hooks|scripts)[/\\](?:${targetNames})$`,
|
|
63
|
+
);
|
|
64
|
+
const pluginRootTarget = new RegExp(
|
|
65
|
+
String.raw`^\$\{(?:CLAUDE_)?PLUGIN_ROOT\}[/\\](?:hooks|scripts)[/\\](?:${uniqueTargetNames})$`,
|
|
66
|
+
);
|
|
67
|
+
const gateSuffix = ".claude/scripts/tfx-gate-activate.mjs";
|
|
68
|
+
|
|
69
|
+
function isInstalledGateTarget(target) {
|
|
70
|
+
const normalized = target.replaceAll("\\", "/");
|
|
71
|
+
if (
|
|
72
|
+
["${HOME}", "$HOME", "~"].some(
|
|
73
|
+
(root) => normalized === `${root}/${gateSuffix}`,
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
return true;
|
|
77
|
+
const installedTargets = [join(homedir(), gateSuffix)];
|
|
78
|
+
if (process.env.CLAUDE_CONFIG_DIR)
|
|
79
|
+
installedTargets.push(
|
|
80
|
+
join(process.env.CLAUDE_CONFIG_DIR, "scripts/tfx-gate-activate.mjs"),
|
|
81
|
+
);
|
|
82
|
+
return installedTargets.some(
|
|
83
|
+
(installed) => normalized === installed.replaceAll("\\", "/"),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readCommandToken(command) {
|
|
88
|
+
const match = command.match(/^\s*(?:"([^"]*)"|'([^']*)'|(\S+))(.*)$/s);
|
|
89
|
+
return match
|
|
90
|
+
? { value: match[1] ?? match[2] ?? match[3], rest: match[4] }
|
|
91
|
+
: null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function matchesTarget(target) {
|
|
95
|
+
const resolvedFallback = target.replace(
|
|
96
|
+
/^\$\{(?:CLAUDE_)?PLUGIN_ROOT:-([^}]+)\}/,
|
|
97
|
+
"$1",
|
|
98
|
+
);
|
|
99
|
+
return (
|
|
100
|
+
packageTarget.test(resolvedFallback) ||
|
|
101
|
+
pluginRootTarget.test(target) ||
|
|
102
|
+
isInstalledGateTarget(target)
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function isLegacyTrifluxHook(hook) {
|
|
107
|
+
if (hook?.type !== "command" || typeof hook.command !== "string")
|
|
108
|
+
return false;
|
|
109
|
+
const command = hook.command;
|
|
110
|
+
const executable = readCommandToken(command);
|
|
111
|
+
if (!executable || !/(?:^|[/\\])node(?:\.exe)?$/.test(executable.value))
|
|
112
|
+
return false;
|
|
113
|
+
const target = readCommandToken(executable.rest);
|
|
114
|
+
if (!target) return false;
|
|
115
|
+
// 옛 inline bootstrap만 제거한다.
|
|
116
|
+
if (target.value === "-e" || target.value === "--eval")
|
|
117
|
+
return (
|
|
118
|
+
command.includes(".tfx-pkg-root") &&
|
|
119
|
+
command.includes("hook-orchestrator.mjs") &&
|
|
120
|
+
command.includes("spawnSync") &&
|
|
121
|
+
command.includes("path.join(root")
|
|
122
|
+
);
|
|
123
|
+
if (matchesTarget(target.value)) return true;
|
|
124
|
+
// run.cjs의 keyword-detector 인자를 확인한다.
|
|
125
|
+
if (/[/\\]scripts[/\\]run\.cjs$/.test(target.value)) {
|
|
126
|
+
const script = readCommandToken(target.rest);
|
|
127
|
+
return (
|
|
128
|
+
script?.value.endsWith("/scripts/keyword-detector.mjs") &&
|
|
129
|
+
(matchesTarget(script.value) || pluginRootTarget.test(script.value))
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function removeManagedHooks(settings) {
|
|
136
|
+
let removed = 0;
|
|
137
|
+
const events = settings?.hooks;
|
|
138
|
+
if (!events || typeof events !== "object" || Array.isArray(events))
|
|
139
|
+
return removed;
|
|
140
|
+
for (const [event, entries] of Object.entries(events)) {
|
|
141
|
+
if (!Array.isArray(entries)) continue;
|
|
142
|
+
const kept = [];
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
if (!Array.isArray(entry?.hooks)) {
|
|
145
|
+
kept.push(entry);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const hooks = entry.hooks.filter((hook) => {
|
|
149
|
+
if (!isLegacyTrifluxHook(hook)) return true;
|
|
150
|
+
removed++;
|
|
151
|
+
return false;
|
|
152
|
+
});
|
|
153
|
+
if (hooks.length)
|
|
154
|
+
kept.push(
|
|
155
|
+
hooks.length === entry.hooks.length ? entry : { ...entry, hooks },
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (kept.length) events[event] = kept;
|
|
159
|
+
else delete events[event];
|
|
160
|
+
}
|
|
161
|
+
if (Object.keys(events).length === 0) delete settings.hooks;
|
|
162
|
+
return removed;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Claude 설정의 이전 Triflux command hook을 정리한다. */
|
|
166
|
+
export function cleanupLegacyHooks({
|
|
167
|
+
settingsPath = join(
|
|
168
|
+
process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"),
|
|
169
|
+
"settings.json",
|
|
170
|
+
),
|
|
171
|
+
dryRun = false,
|
|
172
|
+
} = {}) {
|
|
173
|
+
const result = {
|
|
174
|
+
ok: true,
|
|
175
|
+
changed: false,
|
|
176
|
+
wouldChange: false,
|
|
177
|
+
removed: 0,
|
|
178
|
+
backupPath: null,
|
|
179
|
+
error: null,
|
|
180
|
+
};
|
|
181
|
+
let original;
|
|
182
|
+
let settings;
|
|
183
|
+
try {
|
|
184
|
+
const pathInfo = lstatSync(settingsPath, { throwIfNoEntry: false });
|
|
185
|
+
if (!pathInfo) return result;
|
|
186
|
+
// symlink는 유지하고 실제 파일을 교체한다.
|
|
187
|
+
const targetPath = pathInfo.isSymbolicLink()
|
|
188
|
+
? realpathSync(settingsPath)
|
|
189
|
+
: settingsPath;
|
|
190
|
+
original = readFileSync(targetPath, "utf8");
|
|
191
|
+
settings = JSON.parse(original);
|
|
192
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings))
|
|
193
|
+
throw new Error("settings.json must contain an object");
|
|
194
|
+
result.removed = removeManagedHooks(settings);
|
|
195
|
+
result.wouldChange = result.removed > 0;
|
|
196
|
+
if (!result.wouldChange || dryRun) return result;
|
|
197
|
+
|
|
198
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
199
|
+
let backupPath = `${settingsPath}.tfx-bak-${stamp}`;
|
|
200
|
+
for (let suffix = 1; existsSync(backupPath); suffix++)
|
|
201
|
+
backupPath = `${settingsPath}.tfx-bak-${stamp}-${suffix}`;
|
|
202
|
+
copyFileSync(settingsPath, backupPath, constants.COPYFILE_EXCL);
|
|
203
|
+
result.backupPath = backupPath;
|
|
204
|
+
|
|
205
|
+
const temporary = join(
|
|
206
|
+
dirname(targetPath),
|
|
207
|
+
`.settings.json.tfx-${process.pid}-${Date.now()}.tmp`,
|
|
208
|
+
);
|
|
209
|
+
try {
|
|
210
|
+
writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}\n`, {
|
|
211
|
+
encoding: "utf8",
|
|
212
|
+
flag: "wx",
|
|
213
|
+
mode: statSync(targetPath).mode,
|
|
214
|
+
});
|
|
215
|
+
renameSync(temporary, targetPath);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
try {
|
|
218
|
+
unlinkSync(temporary);
|
|
219
|
+
} catch {
|
|
220
|
+
/* 임시 파일 없음 */
|
|
221
|
+
}
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
result.changed = true;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
result.ok = false;
|
|
227
|
+
result.error = error.message;
|
|
228
|
+
}
|
|
229
|
+
return result;
|
|
230
|
+
}
|