u-foo 1.4.1 → 1.6.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/README.md +21 -0
- package/README.zh-CN.md +21 -0
- package/bin/ufoo.js +15 -7
- package/modules/AGENTS.template.md +4 -102
- package/package.json +3 -2
- package/scripts/global-chat-switch-benchmark.js +406 -0
- package/src/agent/activityDetector.js +328 -0
- package/src/agent/activityStatePublisher.js +67 -0
- package/src/agent/activityStateWriter.js +40 -0
- package/src/agent/internalRunner.js +13 -0
- package/src/agent/launcher.js +47 -7
- package/src/agent/notifier.js +73 -4
- package/src/agent/ptyRunner.js +81 -34
- package/src/agent/ufooAgent.js +192 -6
- package/src/bus/message.js +1 -9
- package/src/bus/subscriber.js +2 -0
- package/src/bus/utils.js +10 -0
- package/src/chat/agentBar.js +21 -3
- package/src/chat/agentViewController.js +2 -0
- package/src/chat/chatLogController.js +28 -5
- package/src/chat/commandExecutor.js +127 -3
- package/src/chat/commands.js +8 -0
- package/src/chat/daemonConnection.js +77 -4
- package/src/chat/daemonCoordinator.js +36 -0
- package/src/chat/daemonMessageRouter.js +22 -0
- package/src/chat/daemonTransport.js +47 -5
- package/src/chat/daemonTransportDefaults.js +1 -0
- package/src/chat/dashboardKeyController.js +89 -1
- package/src/chat/dashboardView.js +312 -93
- package/src/chat/index.js +683 -41
- package/src/chat/inputHistoryController.js +33 -3
- package/src/chat/inputListenerController.js +22 -12
- package/src/chat/layout.js +12 -7
- package/src/chat/projectCloseController.js +119 -0
- package/src/chat/projectRuntimes.js +55 -0
- package/src/chat/statusLineController.js +52 -6
- package/src/chat/streamTracker.js +6 -0
- package/src/chat/transport.js +41 -5
- package/src/cli.js +167 -4
- package/src/daemon/index.js +54 -5
- package/src/daemon/ipcServer.js +6 -1
- package/src/daemon/ops.js +245 -35
- package/src/daemon/status.js +3 -1
- package/src/init/index.js +32 -3
- package/src/projects/projectId.js +29 -0
- package/src/projects/registry.js +279 -0
- package/src/ufoo/agentsStore.js +44 -0
package/src/daemon/ops.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { spawn } = require("child_process");
|
|
1
|
+
const { spawn, spawnSync } = require("child_process");
|
|
2
2
|
const fs = require("fs");
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const { loadConfig } = require("../config");
|
|
@@ -37,6 +37,44 @@ function toTmuxBinary(agent = "") {
|
|
|
37
37
|
return "";
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function normalizeLaunchScope(value, fallback = "inplace") {
|
|
41
|
+
const raw = String(value || "").trim().toLowerCase();
|
|
42
|
+
if (!raw) return fallback;
|
|
43
|
+
if (raw === "inplace" || raw === "same" || raw === "current" || raw === "tab" || raw === "pane") {
|
|
44
|
+
return "inplace";
|
|
45
|
+
}
|
|
46
|
+
if (
|
|
47
|
+
raw === "window"
|
|
48
|
+
|| raw === "separate"
|
|
49
|
+
|| raw === "new"
|
|
50
|
+
|| raw === "new-window"
|
|
51
|
+
|| raw === "external"
|
|
52
|
+
|| raw === "1"
|
|
53
|
+
|| raw === "true"
|
|
54
|
+
|| raw === "yes"
|
|
55
|
+
|| raw === "y"
|
|
56
|
+
|| raw === "on"
|
|
57
|
+
) {
|
|
58
|
+
return "window";
|
|
59
|
+
}
|
|
60
|
+
if (raw === "0" || raw === "false" || raw === "no" || raw === "n" || raw === "off") {
|
|
61
|
+
return "inplace";
|
|
62
|
+
}
|
|
63
|
+
return fallback;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeTerminalAppPreference(value = "") {
|
|
67
|
+
const raw = String(value || "").trim().toLowerCase();
|
|
68
|
+
if (!raw) return "";
|
|
69
|
+
if (raw === "terminal" || raw === "apple_terminal" || raw === "apple-terminal") {
|
|
70
|
+
return "terminal";
|
|
71
|
+
}
|
|
72
|
+
if (raw === "iterm2" || raw === "iterm" || raw === "iterm.app") {
|
|
73
|
+
return "iterm2";
|
|
74
|
+
}
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
|
|
40
78
|
function resolveAgentId(projectRoot, agentId) {
|
|
41
79
|
if (!agentId) return agentId;
|
|
42
80
|
if (agentId.includes(":")) return agentId;
|
|
@@ -130,23 +168,40 @@ function runAppleScript(lines) {
|
|
|
130
168
|
});
|
|
131
169
|
}
|
|
132
170
|
|
|
133
|
-
async function openTerminalWindow(runCmd) {
|
|
171
|
+
async function openTerminalWindow(runCmd, options = {}) {
|
|
134
172
|
if (process.platform !== "darwin") {
|
|
135
173
|
throw new Error("Terminal mode is only supported on macOS");
|
|
136
174
|
}
|
|
137
175
|
|
|
176
|
+
const launchScope = normalizeLaunchScope(options.launchScope, "inplace");
|
|
177
|
+
const terminalApp = normalizeTerminalAppPreference(options.terminalApp);
|
|
178
|
+
const preferSeparateWindow = launchScope === "window";
|
|
138
179
|
const escaped = escapeAppleScriptString(runCmd);
|
|
180
|
+
const shouldTryITerm2 = terminalApp
|
|
181
|
+
? terminalApp === "iterm2"
|
|
182
|
+
: isITerm2();
|
|
139
183
|
|
|
140
|
-
if (
|
|
184
|
+
if (shouldTryITerm2) {
|
|
141
185
|
try {
|
|
142
|
-
const script =
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
186
|
+
const script = preferSeparateWindow
|
|
187
|
+
? [
|
|
188
|
+
'tell application "iTerm2"',
|
|
189
|
+
` create window with default profile command "${escaped}"`,
|
|
190
|
+
" activate",
|
|
191
|
+
"end tell",
|
|
192
|
+
]
|
|
193
|
+
: [
|
|
194
|
+
'tell application "iTerm2"',
|
|
195
|
+
" if (count of windows) is 0 then",
|
|
196
|
+
` create window with default profile command "${escaped}"`,
|
|
197
|
+
" else",
|
|
198
|
+
" tell current window",
|
|
199
|
+
` create tab with default profile command "${escaped}"`,
|
|
200
|
+
" end tell",
|
|
201
|
+
" end if",
|
|
202
|
+
" activate",
|
|
203
|
+
"end tell",
|
|
204
|
+
];
|
|
150
205
|
await runAppleScript(script);
|
|
151
206
|
return;
|
|
152
207
|
} catch {
|
|
@@ -154,13 +209,55 @@ async function openTerminalWindow(runCmd) {
|
|
|
154
209
|
}
|
|
155
210
|
}
|
|
156
211
|
|
|
157
|
-
|
|
212
|
+
if (preferSeparateWindow) {
|
|
213
|
+
const script = [
|
|
214
|
+
'tell application "Terminal"',
|
|
215
|
+
` do script "${escaped}"`,
|
|
216
|
+
" activate",
|
|
217
|
+
"end tell",
|
|
218
|
+
];
|
|
219
|
+
await runAppleScript(script);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const preferredScript = [
|
|
158
224
|
'tell application "Terminal"',
|
|
159
|
-
|
|
160
|
-
"
|
|
225
|
+
" activate",
|
|
226
|
+
" if (count of windows) is 0 then",
|
|
227
|
+
` do script "${escaped}"`,
|
|
228
|
+
" else",
|
|
229
|
+
' tell application "System Events"',
|
|
230
|
+
' tell process "Terminal"',
|
|
231
|
+
' keystroke "t" using command down',
|
|
232
|
+
" end tell",
|
|
233
|
+
" end tell",
|
|
234
|
+
" delay 0.08",
|
|
235
|
+
` do script "${escaped}" in selected tab of front window`,
|
|
236
|
+
" end if",
|
|
237
|
+
" activate",
|
|
161
238
|
"end tell",
|
|
162
239
|
];
|
|
163
|
-
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
await runAppleScript(preferredScript);
|
|
243
|
+
return;
|
|
244
|
+
} catch {
|
|
245
|
+
// Accessibility can block System Events key events; fall back to pure Terminal AppleScript.
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const fallbackScript = [
|
|
249
|
+
'tell application "Terminal"',
|
|
250
|
+
" activate",
|
|
251
|
+
" if (count of windows) is 0 then",
|
|
252
|
+
` do script "${escaped}"`,
|
|
253
|
+
" else",
|
|
254
|
+
" set newTab to (do script \"\" in front window)",
|
|
255
|
+
` do script "${escaped}" in newTab`,
|
|
256
|
+
" end if",
|
|
257
|
+
" activate",
|
|
258
|
+
"end tell",
|
|
259
|
+
];
|
|
260
|
+
await runAppleScript(fallbackScript);
|
|
164
261
|
}
|
|
165
262
|
|
|
166
263
|
async function closeTerminalWindowByTty(ttyPath, preferApp = "") {
|
|
@@ -262,7 +359,16 @@ async function tryReuseTerminal(projectRoot, subscriberId, meta, agent, sessionI
|
|
|
262
359
|
/**
|
|
263
360
|
* Spawn managed terminal agent - open a real Terminal session to run the agent
|
|
264
361
|
*/
|
|
265
|
-
async function spawnManagedTerminalAgent(
|
|
362
|
+
async function spawnManagedTerminalAgent(
|
|
363
|
+
projectRoot,
|
|
364
|
+
agent,
|
|
365
|
+
nickname = "",
|
|
366
|
+
processManager = null,
|
|
367
|
+
extraArgs = [],
|
|
368
|
+
extraEnv = "",
|
|
369
|
+
launchScope = "window",
|
|
370
|
+
terminalApp = ""
|
|
371
|
+
) {
|
|
266
372
|
const normalizedAgent = normalizeLaunchAgent(agent);
|
|
267
373
|
const binary = toTerminalBinary(normalizedAgent);
|
|
268
374
|
const agentType = toBusAgentType(normalizedAgent);
|
|
@@ -283,7 +389,7 @@ async function spawnManagedTerminalAgent(projectRoot, agent, nickname = "", proc
|
|
|
283
389
|
|
|
284
390
|
const runCmd = `cd ${shellEscape(projectRoot)} && ${prefix}${modeEnv}${nickEnv}${envPrefix}${binary}${argText}`;
|
|
285
391
|
|
|
286
|
-
await openTerminalWindow(runCmd);
|
|
392
|
+
await openTerminalWindow(runCmd, { launchScope, terminalApp });
|
|
287
393
|
|
|
288
394
|
const subscriberId = await waitForNewSubscriber(projectRoot, agentType, existing, 15000);
|
|
289
395
|
return { child: null, subscriberId: subscriberId || null };
|
|
@@ -455,9 +561,57 @@ function spawnTmuxWindow(projectRoot, agent, nickname = "", extraArgs = [], extr
|
|
|
455
561
|
});
|
|
456
562
|
}
|
|
457
563
|
|
|
458
|
-
|
|
564
|
+
function resolveTmuxPaneTarget() {
|
|
565
|
+
const explicit = String(process.env.UFOO_TMUX_TARGET || "").trim();
|
|
566
|
+
if (explicit) return explicit;
|
|
567
|
+
const preferredPane = String(process.env.UFOO_TMUX_PANE || "").trim();
|
|
568
|
+
if (preferredPane) return preferredPane;
|
|
569
|
+
const currentPane = String(process.env.TMUX_PANE || "").trim();
|
|
570
|
+
if (currentPane) return currentPane;
|
|
571
|
+
return "";
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function spawnTmuxPane(projectRoot, agent, nickname = "", extraArgs = [], extraEnv = "", target = "") {
|
|
575
|
+
return new Promise((resolve, reject) => {
|
|
576
|
+
const normalizedAgent = normalizeLaunchAgent(agent);
|
|
577
|
+
const binary = toTmuxBinary(normalizedAgent);
|
|
578
|
+
if (!binary) {
|
|
579
|
+
reject(new Error(`unsupported agent type: ${agent}`));
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const nickEnv = nickname ? `UFOO_NICKNAME=${shellEscape(nickname)} ` : "";
|
|
583
|
+
const modeEnv = "UFOO_LAUNCH_MODE=tmux ";
|
|
584
|
+
const ttyEnv = "UFOO_TTY_OVERRIDE=$(tty) ";
|
|
585
|
+
const args = Array.isArray(extraArgs) ? extraArgs : [];
|
|
586
|
+
const envPrefix = extraEnv ? `${String(extraEnv).trim()} ` : "";
|
|
587
|
+
const argText = args.length > 0 ? ` ${args.map(shellEscape).join(" ")}` : "";
|
|
588
|
+
const setPaneEnv = `export TMUX_PANE=$(tmux display-message -p '#{pane_id}'); `;
|
|
589
|
+
const runCmd = `cd ${shellEscape(projectRoot)} && ${setPaneEnv}${modeEnv}${nickEnv}${ttyEnv}${envPrefix}${binary}${argText}`;
|
|
590
|
+
|
|
591
|
+
const tmuxArgs = ["split-window", "-d"];
|
|
592
|
+
const normalizedTarget = String(target || "").trim();
|
|
593
|
+
if (normalizedTarget) {
|
|
594
|
+
tmuxArgs.push("-t", normalizedTarget);
|
|
595
|
+
}
|
|
596
|
+
tmuxArgs.push(runCmd);
|
|
597
|
+
|
|
598
|
+
const proc = spawn("tmux", tmuxArgs);
|
|
599
|
+
let stderr = "";
|
|
600
|
+
proc.stderr.on("data", (d) => {
|
|
601
|
+
stderr += d.toString("utf8");
|
|
602
|
+
});
|
|
603
|
+
proc.on("close", (code) => {
|
|
604
|
+
if (code === 0) resolve();
|
|
605
|
+
else reject(new Error(stderr || "tmux split-window failed"));
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async function launchAgent(projectRoot, agent, count = 1, nickname = "", processManager = null, options = {}) {
|
|
459
611
|
const config = loadConfig(projectRoot);
|
|
460
612
|
const mode = config.launchMode || "terminal";
|
|
613
|
+
const launchScope = normalizeLaunchScope(options.launchScope, "inplace");
|
|
614
|
+
const terminalApp = normalizeTerminalAppPreference(options.terminalApp);
|
|
461
615
|
const normalizedAgent = normalizeLaunchAgent(agent);
|
|
462
616
|
if (!normalizedAgent) {
|
|
463
617
|
throw new Error(`unsupported agent type: ${agent}`);
|
|
@@ -465,7 +619,7 @@ async function launchAgent(projectRoot, agent, count = 1, nickname = "", process
|
|
|
465
619
|
|
|
466
620
|
if (mode === "internal") {
|
|
467
621
|
const result = await spawnInternalAgent(projectRoot, normalizedAgent, count, nickname, processManager);
|
|
468
|
-
return { mode: "internal", subscriberIds: result.subscriberIds };
|
|
622
|
+
return { mode: "internal", launchScope, subscriberIds: result.subscriberIds };
|
|
469
623
|
}
|
|
470
624
|
if (mode === "tmux") {
|
|
471
625
|
// Check if tmux is available
|
|
@@ -489,14 +643,27 @@ async function launchAgent(projectRoot, agent, count = 1, nickname = "", process
|
|
|
489
643
|
process.env.UFOO_TMUX_SESSION = firstSession;
|
|
490
644
|
}
|
|
491
645
|
}
|
|
646
|
+
const paneTarget = resolveTmuxPaneTarget();
|
|
647
|
+
const useSeparateWindow = launchScope === "window";
|
|
492
648
|
for (let i = 0; i < count; i += 1) {
|
|
493
649
|
// Use "ucode" as default nickname for ufoo/ucode agents
|
|
494
650
|
const defaultNick = normalizedAgent === "ufoo" ? "ucode" : normalizedAgent;
|
|
495
651
|
const nick = count > 1 ? `${nickname || defaultNick}-${i + 1}` : (nickname || "");
|
|
496
|
-
|
|
497
|
-
|
|
652
|
+
if (useSeparateWindow) {
|
|
653
|
+
// eslint-disable-next-line no-await-in-loop
|
|
654
|
+
await spawnTmuxWindow(projectRoot, normalizedAgent, nick);
|
|
655
|
+
} else {
|
|
656
|
+
try {
|
|
657
|
+
// eslint-disable-next-line no-await-in-loop
|
|
658
|
+
await spawnTmuxPane(projectRoot, normalizedAgent, nick, [], "", paneTarget);
|
|
659
|
+
} catch {
|
|
660
|
+
// Fallback to new window when current pane target cannot be resolved.
|
|
661
|
+
// eslint-disable-next-line no-await-in-loop
|
|
662
|
+
await spawnTmuxWindow(projectRoot, normalizedAgent, nick);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
498
665
|
}
|
|
499
|
-
return { mode: "tmux" };
|
|
666
|
+
return { mode: "tmux", launchScope, subscriberIds: [] };
|
|
500
667
|
}
|
|
501
668
|
// terminal mode - daemon 作为父进程,输出到终端窗口
|
|
502
669
|
if (process.platform !== "darwin") {
|
|
@@ -509,11 +676,20 @@ async function launchAgent(projectRoot, agent, count = 1, nickname = "", process
|
|
|
509
676
|
const defaultNick = normalizedAgent === "ufoo" ? "ucode" : normalizedAgent;
|
|
510
677
|
const nick = count > 1 ? `${nickname || defaultNick}-${i + 1}` : (nickname || "");
|
|
511
678
|
// eslint-disable-next-line no-await-in-loop
|
|
512
|
-
const result = await spawnManagedTerminalAgent(
|
|
679
|
+
const result = await spawnManagedTerminalAgent(
|
|
680
|
+
projectRoot,
|
|
681
|
+
normalizedAgent,
|
|
682
|
+
nick,
|
|
683
|
+
processManager,
|
|
684
|
+
[],
|
|
685
|
+
"",
|
|
686
|
+
launchScope,
|
|
687
|
+
terminalApp
|
|
688
|
+
);
|
|
513
689
|
if (result.subscriberId) subscriberIds.push(result.subscriberId);
|
|
514
690
|
}
|
|
515
691
|
|
|
516
|
-
return { mode: "terminal", subscriberIds };
|
|
692
|
+
return { mode: "terminal", launchScope, subscriberIds };
|
|
517
693
|
}
|
|
518
694
|
|
|
519
695
|
function normalizeAgentType(agentType) {
|
|
@@ -651,43 +827,55 @@ async function resumeAgents(projectRoot, target = "", processManager = null) {
|
|
|
651
827
|
}
|
|
652
828
|
|
|
653
829
|
async function closeAgent(projectRoot, agentId) {
|
|
654
|
-
if (process.platform !== "darwin") {
|
|
655
|
-
return false;
|
|
656
|
-
}
|
|
657
830
|
const resolvedId = resolveAgentId(projectRoot, agentId);
|
|
658
831
|
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
659
|
-
let pid =
|
|
832
|
+
let pid = 0;
|
|
660
833
|
let launchMode = "";
|
|
661
834
|
let tty = "";
|
|
662
835
|
let terminalApp = "";
|
|
836
|
+
let tmuxPane = "";
|
|
837
|
+
let found = false;
|
|
663
838
|
try {
|
|
664
839
|
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
665
840
|
const entry = bus.agents?.[resolvedId];
|
|
666
841
|
if (entry) {
|
|
667
|
-
|
|
842
|
+
found = true;
|
|
843
|
+
const parsedPid = Number.parseInt(entry.pid, 10);
|
|
844
|
+
pid = Number.isFinite(parsedPid) && parsedPid > 0 ? parsedPid : 0;
|
|
668
845
|
launchMode = entry.launch_mode || "";
|
|
669
846
|
tty = entry.tty || "";
|
|
670
847
|
terminalApp = entry.terminal_app || "";
|
|
848
|
+
tmuxPane = entry.tmux_pane || "";
|
|
671
849
|
}
|
|
672
850
|
} catch {
|
|
673
|
-
|
|
851
|
+
found = false;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
if (!found) {
|
|
855
|
+
return { ok: true, already_stopped: true, resolved_agent_id: resolvedId };
|
|
674
856
|
}
|
|
857
|
+
|
|
675
858
|
const adapterRouter = createTerminalAdapterRouter();
|
|
676
859
|
const adapter = adapterRouter.getAdapter({ launchMode, agentId: resolvedId });
|
|
677
|
-
const canCloseWindow =
|
|
860
|
+
const canCloseWindow = process.platform === "darwin"
|
|
861
|
+
&& Boolean(adapter.capabilities.supportsWindowClose)
|
|
862
|
+
&& Boolean(tty);
|
|
678
863
|
|
|
679
864
|
// Close process first for faster state transition in chat.
|
|
680
865
|
let sentSignal = false;
|
|
681
|
-
|
|
866
|
+
let killErr = null;
|
|
867
|
+
if (pid > 0) {
|
|
682
868
|
try {
|
|
683
869
|
process.kill(pid, "SIGTERM");
|
|
684
870
|
sentSignal = true;
|
|
685
|
-
} catch {
|
|
871
|
+
} catch (err) {
|
|
872
|
+
killErr = err || null;
|
|
686
873
|
sentSignal = false;
|
|
687
874
|
}
|
|
688
875
|
}
|
|
689
876
|
|
|
690
|
-
|
|
877
|
+
const pidGone = pid > 0 && !sentSignal && !isAgentPidAlive(pid);
|
|
878
|
+
if (sentSignal || pid === 0 || pidGone) {
|
|
691
879
|
markAgentInactive(projectRoot, resolvedId);
|
|
692
880
|
}
|
|
693
881
|
|
|
@@ -696,7 +884,29 @@ async function closeAgent(projectRoot, agentId) {
|
|
|
696
884
|
void closeTerminalWindowByTty(tty, terminalApp).catch(() => false);
|
|
697
885
|
}
|
|
698
886
|
|
|
699
|
-
|
|
887
|
+
// Tmux pane cleanup: kill the pane after sending SIGTERM to the process.
|
|
888
|
+
if (launchMode === "tmux" && tmuxPane) {
|
|
889
|
+
try {
|
|
890
|
+
spawnSync("tmux", ["kill-pane", "-t", tmuxPane], { stdio: "ignore", timeout: 3000 });
|
|
891
|
+
} catch {
|
|
892
|
+
// ignore - pane may already be gone
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (sentSignal) {
|
|
897
|
+
return { ok: true, resolved_agent_id: resolvedId };
|
|
898
|
+
}
|
|
899
|
+
if (pid === 0 || pidGone) {
|
|
900
|
+
return { ok: true, already_stopped: true, resolved_agent_id: resolvedId };
|
|
901
|
+
}
|
|
902
|
+
const reason = killErr && killErr.message
|
|
903
|
+
? killErr.message
|
|
904
|
+
: "failed to stop process";
|
|
905
|
+
return {
|
|
906
|
+
ok: false,
|
|
907
|
+
error: reason,
|
|
908
|
+
resolved_agent_id: resolvedId,
|
|
909
|
+
};
|
|
700
910
|
}
|
|
701
911
|
|
|
702
912
|
module.exports = { launchAgent, closeAgent, getRecoverableAgents, resumeAgents };
|
package/src/daemon/status.js
CHANGED
|
@@ -147,7 +147,9 @@ function buildStatus(projectRoot, options = {}) {
|
|
|
147
147
|
const launch_mode = meta?.launch_mode || "unknown";
|
|
148
148
|
const tmux_pane = meta?.tmux_pane || "";
|
|
149
149
|
const tty = meta?.tty || "";
|
|
150
|
-
|
|
150
|
+
const activity_state = meta?.activity_state || "";
|
|
151
|
+
const activity_since = meta?.activity_since || "";
|
|
152
|
+
return { id, nickname, display, launch_mode, tmux_pane, tty, activity_state, activity_since };
|
|
151
153
|
});
|
|
152
154
|
|
|
153
155
|
return {
|
package/src/init/index.js
CHANGED
|
@@ -180,23 +180,52 @@ class UfooInit {
|
|
|
180
180
|
|
|
181
181
|
let content = fs.readFileSync(filePath, "utf8");
|
|
182
182
|
const marker = "<!-- ufoo-template -->";
|
|
183
|
+
const block = `${marker}\n${template}\n${marker}`;
|
|
184
|
+
|
|
183
185
|
if (content.includes(marker)) {
|
|
186
|
+
// Replace existing marker block in-place
|
|
184
187
|
const startIdx = content.indexOf(marker);
|
|
185
188
|
const endIdx = content.indexOf(marker, startIdx + marker.length);
|
|
186
189
|
if (endIdx !== -1) {
|
|
187
190
|
content =
|
|
188
191
|
content.slice(0, startIdx) +
|
|
189
|
-
|
|
192
|
+
block +
|
|
190
193
|
content.slice(endIdx + marker.length);
|
|
191
194
|
} else {
|
|
192
|
-
content
|
|
195
|
+
content =
|
|
196
|
+
content.slice(0, startIdx) + block + content.slice(startIdx + marker.length);
|
|
193
197
|
}
|
|
194
198
|
} else {
|
|
195
|
-
|
|
199
|
+
// Insert after first heading line for visibility (not buried at end)
|
|
200
|
+
const headingEnd = this.findFirstHeadingEnd(content);
|
|
201
|
+
if (headingEnd !== -1) {
|
|
202
|
+
content =
|
|
203
|
+
content.slice(0, headingEnd) +
|
|
204
|
+
`\n${block}\n\n` +
|
|
205
|
+
content.slice(headingEnd);
|
|
206
|
+
} else {
|
|
207
|
+
content = `${block}\n\n${content}`;
|
|
208
|
+
}
|
|
196
209
|
}
|
|
197
210
|
fs.writeFileSync(filePath, content, "utf8");
|
|
198
211
|
}
|
|
199
212
|
|
|
213
|
+
findFirstHeadingEnd(content) {
|
|
214
|
+
// ATX heading: # ... (allow leading indentation and EOF without trailing newline)
|
|
215
|
+
const atxHeading = content.match(/^(?:[ \t]{0,3})#{1,6}[ \t]*[^\n]*(?:\n|$)/m);
|
|
216
|
+
// Setext heading: text line + underline (=== or ---)
|
|
217
|
+
const setextHeading = content.match(/^[^\n]+\n(?:=+|-+)[ \t]*(?:\n|$)/m);
|
|
218
|
+
|
|
219
|
+
let bestMatch = null;
|
|
220
|
+
if (atxHeading && setextHeading) {
|
|
221
|
+
bestMatch = atxHeading.index <= setextHeading.index ? atxHeading : setextHeading;
|
|
222
|
+
} else {
|
|
223
|
+
bestMatch = atxHeading || setextHeading;
|
|
224
|
+
}
|
|
225
|
+
if (!bestMatch) return -1;
|
|
226
|
+
return bestMatch.index + bestMatch[0].length;
|
|
227
|
+
}
|
|
228
|
+
|
|
200
229
|
/**
|
|
201
230
|
* 初始化 context 模块
|
|
202
231
|
*/
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
function trimTrailingSlashes(value) {
|
|
6
|
+
if (!value) return value;
|
|
7
|
+
return value.replace(/\/+$/, "") || "/";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function canonicalProjectRoot(projectRoot) {
|
|
11
|
+
const input = String(projectRoot || "").trim();
|
|
12
|
+
if (!input) {
|
|
13
|
+
throw new Error("projectRoot is required");
|
|
14
|
+
}
|
|
15
|
+
const resolved = path.resolve(input);
|
|
16
|
+
const canonical = fs.realpathSync.native ? fs.realpathSync.native(resolved) : fs.realpathSync(resolved);
|
|
17
|
+
return trimTrailingSlashes(canonical);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function buildProjectId(projectRoot) {
|
|
21
|
+
const canonical = canonicalProjectRoot(projectRoot);
|
|
22
|
+
return crypto.createHash("sha1").update(canonical).digest("hex").slice(0, 12);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
trimTrailingSlashes,
|
|
27
|
+
canonicalProjectRoot,
|
|
28
|
+
buildProjectId,
|
|
29
|
+
};
|