@triflux/remote 10.53.0 → 10.55.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/backend.mjs +10 -3
- package/hub/team/cli/commands/start/index.mjs +0 -2
- package/hub/team/handoff.mjs +1 -1
- package/hub/team/headless.mjs +41 -57
- package/hub/team/intervention.mjs +6 -4
- package/hub/team/nativeProxy.mjs +2 -48
- package/hub/team/pane.mjs +31 -18
- package/hub/team/psmux.mjs +36 -20
- package/hub/team/remote-session.mjs +66 -56
- package/hub/team/retry-state-machine.mjs +1 -1
- package/hub/team/session.mjs +80 -59
- package/hub/team/terminal-opener.mjs +5 -19
- package/hub/team/tui-viewer.mjs +2 -2
- package/hub/team/tui.mjs +0 -6
- package/hub/workers/factory.mjs +1 -10
- package/hub/workers/interface.mjs +0 -7
- package/package.json +2 -6
- package/scripts/lib/backup-rotation.mjs +60 -0
- package/scripts/lib/codex-profile-config.mjs +20 -7
- package/scripts/lib/codex-recovery.sh +2 -3
- package/scripts/lib/install-blob-history.json +3196 -0
- package/scripts/lib/install-retire.mjs +203 -0
- package/scripts/lib/legacy-hook-cleanup.mjs +148 -13
- package/scripts/lib/legacy-mcp-cleanup.mjs +405 -104
- package/scripts/lib/machine-profile.mjs +2 -2
- package/scripts/lib/mcp-filter.mjs +1 -42
- package/scripts/lib/mcp-guard-engine.mjs +198 -19
- package/scripts/lib/mcp-manifest.mjs +0 -40
- package/scripts/lib/process-utils.mjs +19 -0
- package/scripts/lib/stable-node.mjs +35 -0
- package/scripts/lib/test-env.mjs +20 -0
- package/cto/brief.mjs +0 -73
- package/cto/collect.mjs +0 -490
- package/cto/current.schema.json +0 -148
- package/cto/events.mjs +0 -208
- package/cto/hygiene.mjs +0 -320
- package/cto/lake-root.mjs +0 -127
- package/cto/status.mjs +0 -124
- package/hub/lib/cto-env.mjs +0 -1
- package/hub/team/notify.mjs +0 -385
- package/hub/team/process-cleanup.mjs +0 -386
- package/hub/team/routing.mjs +0 -249
- package/hub/team/staleState.mjs +0 -403
- package/hub/team/worker-signal.mjs +0 -263
- package/hub/team/worker-signal.types.d.ts +0 -52
- package/hub/workers/codex-mcp.mjs +0 -853
- package/scripts/lib/remote-spawn-transfer.mjs +0 -226
package/cto/status.mjs
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
|
|
4
|
-
import { compactHygieneCounts, projectCtoHygiene } from "./hygiene.mjs";
|
|
5
|
-
import { resolveLakeRootDir } from "./lake-root.mjs";
|
|
6
|
-
|
|
7
|
-
const SCHEMA_VERSION = "cto-lake.v1";
|
|
8
|
-
|
|
9
|
-
function hasFlag(args, flag) {
|
|
10
|
-
return Array.isArray(args) && args.includes(flag);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function writeJson(stdout, payload) {
|
|
14
|
-
stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function readJson(filePath) {
|
|
18
|
-
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function projectStatus(current) {
|
|
22
|
-
return {
|
|
23
|
-
schema_version: current?.schema_version || SCHEMA_VERSION,
|
|
24
|
-
generated_at: current?.generated_at || null,
|
|
25
|
-
repo: current?.repo || {},
|
|
26
|
-
sources: current?.sources || {},
|
|
27
|
-
summary: current?.summary || {},
|
|
28
|
-
ledger_tail: Array.isArray(current?.ledger_tail) ? current.ledger_tail : [],
|
|
29
|
-
hygiene: compactHygieneCounts(projectCtoHygiene({ current })),
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function countAvailableSources(sources) {
|
|
34
|
-
const values = Object.values(sources || {});
|
|
35
|
-
return {
|
|
36
|
-
available: values.filter((source) => source?.available === true).length,
|
|
37
|
-
total: values.length,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function countActiveGoals(current) {
|
|
42
|
-
const sources = current?.sources || {};
|
|
43
|
-
return Object.values(sources).reduce((count, source) => {
|
|
44
|
-
const goals = source?.detail?.active_goals;
|
|
45
|
-
return count + (Array.isArray(goals) ? goals.length : 0);
|
|
46
|
-
}, 0);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function formatRepo(repo) {
|
|
50
|
-
const branch = repo?.branch || "unknown";
|
|
51
|
-
const head = repo?.head ? String(repo.head).slice(0, 12) : "unknown";
|
|
52
|
-
const dirty = repo?.dirty ? "dirty" : "clean";
|
|
53
|
-
return `${repo?.root || "unknown"} (${branch}@${head}, ${dirty})`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function formatLedgerEntry(entry) {
|
|
57
|
-
const ts = entry?.ts || "?";
|
|
58
|
-
const event = entry?.event || "event";
|
|
59
|
-
const summary = entry?.summary ? ` - ${entry.summary}` : "";
|
|
60
|
-
return `${ts} ${event}${summary}`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function formatSnapshotAge(generatedAt, now) {
|
|
64
|
-
const ageMs = new Date(now).getTime() - Date.parse(generatedAt);
|
|
65
|
-
if (!Number.isFinite(ageMs)) return "unknown age";
|
|
66
|
-
if (ageMs < 0) return "in the future";
|
|
67
|
-
for (const [unit, ms] of [
|
|
68
|
-
["d", 86400000],
|
|
69
|
-
["h", 3600000],
|
|
70
|
-
["m", 60000],
|
|
71
|
-
["s", 1000],
|
|
72
|
-
]) {
|
|
73
|
-
if (ageMs >= ms || unit === "s")
|
|
74
|
-
return `${Math.floor(ageMs / ms)}${unit} ago`;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function renderHumanStatus(status, now) {
|
|
79
|
-
const { available, total } = countAvailableSources(status.sources);
|
|
80
|
-
const activeGoals = countActiveGoals(status);
|
|
81
|
-
const recent = Array.isArray(status.ledger_tail)
|
|
82
|
-
? status.ledger_tail.slice(-2)
|
|
83
|
-
: [];
|
|
84
|
-
const lines = [
|
|
85
|
-
`generated_at: ${status.generated_at || "unknown"} (${formatSnapshotAge(status.generated_at, now)})`,
|
|
86
|
-
`repo: ${formatRepo(status.repo)}`,
|
|
87
|
-
`sources: ${available}/${total} available`,
|
|
88
|
-
`active: ${activeGoals} goals`,
|
|
89
|
-
];
|
|
90
|
-
if (recent.length) {
|
|
91
|
-
lines.push("ledger:");
|
|
92
|
-
for (const entry of recent) lines.push(`- ${formatLedgerEntry(entry)}`);
|
|
93
|
-
} else {
|
|
94
|
-
lines.push("ledger: no recent events");
|
|
95
|
-
}
|
|
96
|
-
return `${lines.join("\n")}\n`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export async function runStatus(args = [], opts = {}) {
|
|
100
|
-
const rootDir = opts.rootDir || resolveLakeRootDir(process.cwd());
|
|
101
|
-
const lakeRoot = opts.lakeRoot || join(rootDir, ".triflux", "lake");
|
|
102
|
-
const stdout = opts.stdout || process.stdout;
|
|
103
|
-
const jsonOut = opts.json === true || hasFlag(args, "--json");
|
|
104
|
-
const currentPath = join(lakeRoot, "current.json");
|
|
105
|
-
|
|
106
|
-
if (!existsSync(currentPath)) {
|
|
107
|
-
const missing = {
|
|
108
|
-
schema_version: SCHEMA_VERSION,
|
|
109
|
-
available: false,
|
|
110
|
-
hint: "run tfx cto collect",
|
|
111
|
-
};
|
|
112
|
-
if (jsonOut) writeJson(stdout, missing);
|
|
113
|
-
else stdout.write("cto status unavailable: run tfx cto collect\n");
|
|
114
|
-
return missing;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const current = readJson(currentPath);
|
|
118
|
-
const status = projectStatus(current);
|
|
119
|
-
|
|
120
|
-
if (jsonOut) writeJson(stdout, status);
|
|
121
|
-
else stdout.write(renderHumanStatus(status, opts.now ?? Date.now()));
|
|
122
|
-
|
|
123
|
-
return status;
|
|
124
|
-
}
|
package/hub/lib/cto-env.mjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "@triflux/core/hub/lib/cto-env.mjs";
|
package/hub/team/notify.mjs
DELETED
|
@@ -1,385 +0,0 @@
|
|
|
1
|
-
// hub/team/notify.mjs — team notifier (bell / Windows toast / webhook)
|
|
2
|
-
|
|
3
|
-
import { execFile } from "node:child_process";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
|
|
6
|
-
export const NOTIFY_EVENT_TYPES = Object.freeze([
|
|
7
|
-
"completed",
|
|
8
|
-
"failed",
|
|
9
|
-
"inputWait",
|
|
10
|
-
"ctoHygiene",
|
|
11
|
-
]);
|
|
12
|
-
export const NOTIFY_CHANNELS = Object.freeze(["bell", "toast", "webhook"]);
|
|
13
|
-
|
|
14
|
-
function freezeRecord(record) {
|
|
15
|
-
return Object.freeze({ ...record });
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function escapePowerShellSingleQuoted(value) {
|
|
19
|
-
return String(value ?? "").replaceAll("'", "''");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function escapeAppleScriptString(value) {
|
|
23
|
-
return String(value ?? "")
|
|
24
|
-
.replaceAll("\\", "\\\\")
|
|
25
|
-
.replaceAll('"', '\\"')
|
|
26
|
-
.replace(/\r?\n/g, " ");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function normalizeTimestamp(value) {
|
|
30
|
-
if (value instanceof Date) return value.toISOString();
|
|
31
|
-
if (value == null || value === "") return new Date().toISOString();
|
|
32
|
-
return String(value);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function normalizeEvent(event, defaults = {}) {
|
|
36
|
-
if (!event || typeof event !== "object") {
|
|
37
|
-
throw new TypeError("notify(event) requires an event object");
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const type = String(event.type || "").trim();
|
|
41
|
-
if (!NOTIFY_EVENT_TYPES.includes(type)) {
|
|
42
|
-
throw new TypeError(`Unsupported notify event type: ${type || "<empty>"}`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return freezeRecord({
|
|
46
|
-
type,
|
|
47
|
-
sessionId: event.sessionId == null ? "" : String(event.sessionId),
|
|
48
|
-
host:
|
|
49
|
-
event.host == null || event.host === ""
|
|
50
|
-
? String(defaults.host || os.hostname())
|
|
51
|
-
: String(event.host),
|
|
52
|
-
summary: event.summary == null ? "" : String(event.summary),
|
|
53
|
-
timestamp: normalizeTimestamp(event.timestamp),
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function defaultChannelConfig(name, env, platform = process.platform) {
|
|
58
|
-
switch (name) {
|
|
59
|
-
case "bell":
|
|
60
|
-
return { enabled: envFlag(env?.TRIFLUX_NOTIFY_BELL, true) };
|
|
61
|
-
case "toast":
|
|
62
|
-
return {
|
|
63
|
-
// macOS `osascript display notification` can surface Script Editor
|
|
64
|
-
// authorization/UI prompts. Keep it opt-in there; Windows keeps the
|
|
65
|
-
// historical toast default, and other platforms skip as unsupported.
|
|
66
|
-
enabled: envFlag(env?.TRIFLUX_NOTIFY_TOAST, platform !== "darwin"),
|
|
67
|
-
};
|
|
68
|
-
case "webhook": {
|
|
69
|
-
const url = String(env?.TRIFLUX_NOTIFY_WEBHOOK || "");
|
|
70
|
-
return { enabled: Boolean(url), url };
|
|
71
|
-
}
|
|
72
|
-
default:
|
|
73
|
-
throw new TypeError(`Unknown notify channel: ${name}`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function envFlag(value, fallback) {
|
|
78
|
-
if (value === undefined || value === null || value === "") return fallback;
|
|
79
|
-
return !["0", "false", "no", "off"].includes(String(value).toLowerCase());
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function normalizeChannelConfig(name, value, env, platform) {
|
|
83
|
-
const base = defaultChannelConfig(name, env, platform);
|
|
84
|
-
const patch =
|
|
85
|
-
typeof value === "boolean"
|
|
86
|
-
? { enabled: value }
|
|
87
|
-
: value && typeof value === "object"
|
|
88
|
-
? value
|
|
89
|
-
: {};
|
|
90
|
-
|
|
91
|
-
const next = {
|
|
92
|
-
...base,
|
|
93
|
-
...patch,
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
if ("enabled" in next) {
|
|
97
|
-
next.enabled = Boolean(next.enabled);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (name === "webhook") {
|
|
101
|
-
next.url = String(next.url || env?.TRIFLUX_NOTIFY_WEBHOOK || "");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (name === "toast") {
|
|
105
|
-
if (next.command != null) next.command = String(next.command);
|
|
106
|
-
if (next.timeoutMs != null)
|
|
107
|
-
next.timeoutMs = Math.max(
|
|
108
|
-
1,
|
|
109
|
-
Number.parseInt(String(next.timeoutMs), 10) || 5000,
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return freezeRecord(next);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function normalizeChannels(channels, env, platform) {
|
|
117
|
-
const source = channels && typeof channels === "object" ? channels : {};
|
|
118
|
-
const normalized = {};
|
|
119
|
-
for (const name of NOTIFY_CHANNELS) {
|
|
120
|
-
normalized[name] = normalizeChannelConfig(
|
|
121
|
-
name,
|
|
122
|
-
source[name],
|
|
123
|
-
env,
|
|
124
|
-
platform,
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
return Object.freeze(normalized);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function updateChannelConfig(channels, channel, config, env, platform) {
|
|
131
|
-
if (!NOTIFY_CHANNELS.includes(channel)) {
|
|
132
|
-
throw new TypeError(`Unknown notify channel: ${channel}`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return Object.freeze({
|
|
136
|
-
...channels,
|
|
137
|
-
[channel]: normalizeChannelConfig(
|
|
138
|
-
channel,
|
|
139
|
-
{
|
|
140
|
-
...channels[channel],
|
|
141
|
-
...(typeof config === "boolean" ? { enabled: config } : config || {}),
|
|
142
|
-
},
|
|
143
|
-
env,
|
|
144
|
-
platform,
|
|
145
|
-
),
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function formatEventTitle(event) {
|
|
150
|
-
switch (event.type) {
|
|
151
|
-
case "completed":
|
|
152
|
-
return "Triflux completed";
|
|
153
|
-
case "failed":
|
|
154
|
-
return "Triflux failed";
|
|
155
|
-
case "inputWait":
|
|
156
|
-
return "Triflux waiting for input";
|
|
157
|
-
case "ctoHygiene":
|
|
158
|
-
return "Triflux CTO hygiene needs action";
|
|
159
|
-
default:
|
|
160
|
-
return "Triflux notification";
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function formatEventBody(event) {
|
|
165
|
-
const parts = [];
|
|
166
|
-
if (event.summary) parts.push(event.summary);
|
|
167
|
-
|
|
168
|
-
const meta = [
|
|
169
|
-
event.sessionId ? `session ${event.sessionId}` : "",
|
|
170
|
-
event.host ? `host ${event.host}` : "",
|
|
171
|
-
]
|
|
172
|
-
.filter(Boolean)
|
|
173
|
-
.join(" · ");
|
|
174
|
-
if (meta) parts.push(meta);
|
|
175
|
-
|
|
176
|
-
return parts.join("\n") || event.timestamp;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function createResult(channel, status, extra = {}) {
|
|
180
|
-
return freezeRecord({ channel, status, ...extra });
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function execFileAsync(command, args, options, execFileFn) {
|
|
184
|
-
return new Promise((resolve, reject) => {
|
|
185
|
-
execFileFn(command, args, options, (error, stdout, stderr) => {
|
|
186
|
-
if (error) {
|
|
187
|
-
error.stdout = stdout;
|
|
188
|
-
error.stderr = stderr;
|
|
189
|
-
reject(error);
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
resolve({ stdout, stderr });
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async function sendBell(config, deps) {
|
|
198
|
-
if (!config.enabled)
|
|
199
|
-
return createResult("bell", "skipped", { reason: "disabled" });
|
|
200
|
-
const stream = deps.stdout;
|
|
201
|
-
if (!stream || typeof stream.write !== "function") {
|
|
202
|
-
return createResult("bell", "skipped", { reason: "stdout-unavailable" });
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
try {
|
|
206
|
-
stream.write("\u0007");
|
|
207
|
-
return createResult("bell", "sent");
|
|
208
|
-
} catch (error) {
|
|
209
|
-
return createResult("bell", "failed", { error: error.message });
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function buildToastScript(title, body) {
|
|
214
|
-
const safeTitle = escapePowerShellSingleQuoted(title);
|
|
215
|
-
const safeBody = escapePowerShellSingleQuoted(body);
|
|
216
|
-
return [
|
|
217
|
-
"$ErrorActionPreference = 'Stop'",
|
|
218
|
-
`$Title = '${safeTitle}'`,
|
|
219
|
-
`$Body = '${safeBody}'`,
|
|
220
|
-
"if (Get-Command New-BurntToastNotification -ErrorAction SilentlyContinue) {",
|
|
221
|
-
" New-BurntToastNotification -Text @($Title, $Body) | Out-Null",
|
|
222
|
-
" return",
|
|
223
|
-
"}",
|
|
224
|
-
"[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null",
|
|
225
|
-
"[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] > $null",
|
|
226
|
-
"$escapedTitle = [System.Security.SecurityElement]::Escape($Title)",
|
|
227
|
-
"$escapedBody = [System.Security.SecurityElement]::Escape($Body)",
|
|
228
|
-
"$xml = New-Object Windows.Data.Xml.Dom.XmlDocument",
|
|
229
|
-
"$xml.LoadXml(\"<toast><visual><binding template='ToastGeneric'><text>$escapedTitle</text><text>$escapedBody</text></binding></visual></toast>\")",
|
|
230
|
-
"$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)",
|
|
231
|
-
"[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Triflux').Show($toast)",
|
|
232
|
-
].join("; ");
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
async function sendToast(event, config, deps) {
|
|
236
|
-
if (!config.enabled)
|
|
237
|
-
return createResult("toast", "skipped", { reason: "disabled" });
|
|
238
|
-
const execFileFn = deps.execFile || execFile;
|
|
239
|
-
|
|
240
|
-
// macOS: osascript 네이티브 알림
|
|
241
|
-
if ((deps.platform || process.platform) === "darwin") {
|
|
242
|
-
const title = formatEventTitle(event);
|
|
243
|
-
const body = formatEventBody(event);
|
|
244
|
-
const safeTitle = escapeAppleScriptString(title);
|
|
245
|
-
const safeBody = escapeAppleScriptString(body);
|
|
246
|
-
try {
|
|
247
|
-
await execFileAsync(
|
|
248
|
-
"osascript",
|
|
249
|
-
["-e", `display notification "${safeBody}" with title "${safeTitle}"`],
|
|
250
|
-
{ timeout: config.timeoutMs || 5000 },
|
|
251
|
-
execFileFn,
|
|
252
|
-
);
|
|
253
|
-
return createResult("toast", "sent", { command: "osascript" });
|
|
254
|
-
} catch (error) {
|
|
255
|
-
return createResult("toast", "failed", { error: error.message });
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if ((deps.platform || process.platform) !== "win32") {
|
|
260
|
-
return createResult("toast", "skipped", { reason: "unsupported-platform" });
|
|
261
|
-
}
|
|
262
|
-
const candidates = config.command
|
|
263
|
-
? [config.command]
|
|
264
|
-
: Array.isArray(deps.powerShellCandidates) &&
|
|
265
|
-
deps.powerShellCandidates.length > 0
|
|
266
|
-
? deps.powerShellCandidates
|
|
267
|
-
: ["pwsh", "powershell.exe"];
|
|
268
|
-
|
|
269
|
-
const title = formatEventTitle(event);
|
|
270
|
-
const body = formatEventBody(event);
|
|
271
|
-
const script = buildToastScript(title, body);
|
|
272
|
-
const failures = [];
|
|
273
|
-
|
|
274
|
-
for (const command of candidates) {
|
|
275
|
-
try {
|
|
276
|
-
await execFileAsync(
|
|
277
|
-
command,
|
|
278
|
-
["-NoLogo", "-NoProfile", "-Command", script],
|
|
279
|
-
{
|
|
280
|
-
windowsHide: true,
|
|
281
|
-
timeout: config.timeoutMs || 5000,
|
|
282
|
-
},
|
|
283
|
-
execFileFn,
|
|
284
|
-
);
|
|
285
|
-
return createResult("toast", "sent", { command });
|
|
286
|
-
} catch (error) {
|
|
287
|
-
failures.push(`${command}: ${error.message}`);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return createResult("toast", "failed", {
|
|
292
|
-
error: failures.join(" | ") || "toast-send-failed",
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
async function sendWebhook(event, config, deps) {
|
|
297
|
-
if (!config.enabled)
|
|
298
|
-
return createResult("webhook", "skipped", { reason: "disabled" });
|
|
299
|
-
if (!config.url)
|
|
300
|
-
return createResult("webhook", "skipped", { reason: "missing-url" });
|
|
301
|
-
if (typeof deps.fetch !== "function") {
|
|
302
|
-
return createResult("webhook", "failed", { error: "fetch-unavailable" });
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
try {
|
|
306
|
-
const response = await deps.fetch(config.url, {
|
|
307
|
-
method: "POST",
|
|
308
|
-
headers: { "content-type": "application/json" },
|
|
309
|
-
body: JSON.stringify(event),
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
if (!response?.ok) {
|
|
313
|
-
return createResult("webhook", "failed", {
|
|
314
|
-
error: `HTTP ${response?.status ?? "unknown"}`,
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return createResult("webhook", "sent", { statusCode: response.status });
|
|
319
|
-
} catch (error) {
|
|
320
|
-
return createResult("webhook", "failed", { error: error.message });
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function createNotifierInstance(channels, deps) {
|
|
325
|
-
async function notify(event) {
|
|
326
|
-
const normalizedEvent = normalizeEvent(event, { host: deps.hostname });
|
|
327
|
-
const results = {
|
|
328
|
-
bell: await sendBell(channels.bell, deps),
|
|
329
|
-
toast: await sendToast(normalizedEvent, channels.toast, deps),
|
|
330
|
-
webhook: await sendWebhook(normalizedEvent, channels.webhook, deps),
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
return freezeRecord({
|
|
334
|
-
event: normalizedEvent,
|
|
335
|
-
results: Object.freeze(results),
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function setChannel(channel, config) {
|
|
340
|
-
return createNotifierInstance(
|
|
341
|
-
updateChannelConfig(channels, channel, config, deps.env, deps.platform),
|
|
342
|
-
deps,
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
return Object.freeze({ notify, setChannel });
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* 팀 세션 알림기 팩토리.
|
|
351
|
-
* - bell: 터미널 BEL 문자
|
|
352
|
-
* - toast: Windows PowerShell 기반 toast (BurntToast 우선, WinRT fallback)
|
|
353
|
-
* - webhook: TRIFLUX_NOTIFY_WEBHOOK JSON POST
|
|
354
|
-
*
|
|
355
|
-
* Immutable pattern: setChannel()은 기존 notifier를 수정하지 않고 새 notifier를 반환한다.
|
|
356
|
-
*
|
|
357
|
-
* @param {object} [opts]
|
|
358
|
-
* @param {object} [opts.channels] — 채널별 초기 설정
|
|
359
|
-
* @param {object} [opts.env=process.env] — 환경 변수 소스
|
|
360
|
-
* @param {NodeJS.WriteStream|{write:function}} [opts.stdout=process.stdout] — bell 출력 대상
|
|
361
|
-
* @param {string} [opts.platform=process.platform] — 플랫폼 override (test 용)
|
|
362
|
-
* @param {string} [opts.hostname=os.hostname()] — 기본 host override
|
|
363
|
-
* @param {object} [opts.deps] — 테스트용 의존성 주입
|
|
364
|
-
* @returns {{ notify(event: object): Promise<object>, setChannel(channel: string, config: object|boolean): object }}
|
|
365
|
-
*/
|
|
366
|
-
export function createNotifier(opts = {}) {
|
|
367
|
-
const env = opts.env || process.env;
|
|
368
|
-
const platform = opts.platform || process.platform;
|
|
369
|
-
const deps = Object.freeze({
|
|
370
|
-
env,
|
|
371
|
-
stdout: opts.stdout || process.stdout,
|
|
372
|
-
execFile: opts.deps?.execFile || execFile,
|
|
373
|
-
fetch: opts.deps?.fetch || globalThis.fetch?.bind(globalThis),
|
|
374
|
-
platform,
|
|
375
|
-
hostname: opts.hostname || os.hostname(),
|
|
376
|
-
powerShellCandidates: Object.freeze(
|
|
377
|
-
opts.deps?.powerShellCandidates || ["pwsh", "powershell.exe"],
|
|
378
|
-
),
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
return createNotifierInstance(
|
|
382
|
-
normalizeChannels(opts.channels, env, platform),
|
|
383
|
-
deps,
|
|
384
|
-
);
|
|
385
|
-
}
|