@oh-my-pi/pi-coding-agent 6.7.670 → 6.8.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/CHANGELOG.md +28 -0
- package/package.json +6 -7
- package/src/cli/session-picker.ts +27 -28
- package/src/cli/setup-cli.ts +7 -16
- package/src/cli/update-cli.ts +1 -1
- package/src/config.ts +1 -1
- package/src/core/agent-session.ts +202 -37
- package/src/core/agent-storage.ts +1 -1
- package/src/core/auth-storage.ts +15 -25
- package/src/core/bash-executor.ts +63 -105
- package/src/core/custom-commands/loader.ts +1 -1
- package/src/core/custom-tools/loader.ts +1 -1
- package/src/core/custom-tools/types.ts +1 -2
- package/src/core/exec.ts +16 -100
- package/src/core/extensions/index.ts +1 -7
- package/src/core/extensions/loader.ts +1 -1
- package/src/core/extensions/runner.ts +1 -1
- package/src/core/extensions/types.ts +2 -2
- package/src/core/extensions/wrapper.ts +15 -20
- package/src/core/frontmatter.ts +1 -1
- package/src/core/history-storage.ts +3 -6
- package/src/core/hooks/index.ts +2 -2
- package/src/core/hooks/loader.ts +1 -1
- package/src/core/hooks/tool-wrapper.ts +14 -26
- package/src/core/hooks/types.ts +1 -2
- package/src/core/keybindings.ts +1 -1
- package/src/core/mcp/client.ts +13 -13
- package/src/core/mcp/json-rpc.ts +1 -1
- package/src/core/mcp/loader.ts +1 -1
- package/src/core/mcp/manager.ts +2 -2
- package/src/core/mcp/tool-cache.ts +1 -1
- package/src/core/mcp/transports/http.ts +32 -70
- package/src/core/model-registry.ts +1 -1
- package/src/core/plugins/installer.ts +13 -11
- package/src/core/prompt-templates.ts +4 -9
- package/src/core/python-executor.ts +23 -18
- package/src/core/python-gateway-coordinator.ts +29 -28
- package/src/core/python-kernel.ts +230 -211
- package/src/core/sdk.ts +10 -13
- package/src/core/session-manager.ts +1 -1
- package/src/core/settings-manager.ts +22 -9
- package/src/core/skills.ts +1 -1
- package/src/core/ssh/connection-manager.ts +19 -33
- package/src/core/ssh/ssh-executor.ts +39 -35
- package/src/core/ssh/sshfs-mount.ts +14 -33
- package/src/core/storage-migration.ts +1 -1
- package/src/core/streaming-output.ts +183 -127
- package/src/core/system-prompt.ts +119 -79
- package/src/core/title-generator.ts +1 -1
- package/src/core/tools/ask.ts +2 -2
- package/src/core/tools/bash.ts +3 -3
- package/src/core/tools/calculator.ts +1 -1
- package/src/core/tools/exa/mcp-client.ts +1 -1
- package/src/core/tools/exa/render.ts +1 -1
- package/src/core/tools/find.ts +39 -71
- package/src/core/tools/gemini-image.ts +1 -1
- package/src/core/tools/grep.ts +88 -100
- package/src/core/tools/index.ts +1 -1
- package/src/core/tools/ls.ts +1 -1
- package/src/core/tools/lsp/client.ts +50 -50
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +1 -1
- package/src/core/tools/lsp/config.ts +1 -1
- package/src/core/tools/lsp/index.ts +2 -4
- package/src/core/tools/lsp/lspmux.ts +1 -1
- package/src/core/tools/lsp/rust-analyzer.ts +2 -2
- package/src/core/tools/lsp/utils.ts +0 -14
- package/src/core/tools/notebook.ts +1 -1
- package/src/core/tools/patch/shared.ts +3 -4
- package/src/core/tools/python.ts +3 -3
- package/src/core/tools/read.ts +29 -68
- package/src/core/tools/render-utils.ts +0 -5
- package/src/core/tools/ssh.ts +3 -3
- package/src/core/tools/task/model-resolver.ts +7 -9
- package/src/core/tools/task/worker.ts +144 -139
- package/src/core/tools/todo-write.ts +1 -1
- package/src/core/tools/truncate.ts +2 -2
- package/src/core/tools/web-fetch.ts +13 -15
- package/src/core/tools/web-scrapers/types.ts +1 -3
- package/src/core/tools/web-scrapers/utils.ts +14 -13
- package/src/core/tools/web-scrapers/youtube.ts +39 -12
- package/src/core/tools/web-search/auth.ts +1 -1
- package/src/core/tools/write.ts +1 -1
- package/src/core/ttsr.ts +1 -1
- package/src/core/utils.ts +1 -187
- package/src/core/voice-controller.ts +1 -1
- package/src/core/voice-supervisor.ts +11 -38
- package/src/core/voice.ts +1 -8
- package/src/discovery/codex.ts +1 -1
- package/src/index.ts +4 -4
- package/src/main.ts +5 -10
- package/src/migrations.ts +1 -1
- package/src/modes/index.ts +7 -40
- package/src/modes/interactive/components/extensions/state-manager.ts +1 -1
- package/src/modes/interactive/components/hook-editor.ts +12 -9
- package/src/modes/interactive/components/login-dialog.ts +24 -11
- package/src/modes/interactive/components/settings-defs.ts +9 -0
- package/src/modes/interactive/components/status-line.ts +36 -35
- package/src/modes/interactive/components/todo-display.ts +1 -1
- package/src/modes/interactive/components/tool-execution.ts +1 -1
- package/src/modes/interactive/controllers/command-controller.ts +50 -84
- package/src/modes/interactive/controllers/extension-ui-controller.ts +76 -76
- package/src/modes/interactive/controllers/input-controller.ts +12 -11
- package/src/modes/interactive/interactive-mode.ts +10 -11
- package/src/modes/interactive/theme/theme.ts +1 -1
- package/src/modes/interactive/types.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +91 -121
- package/src/modes/rpc/rpc-mode.ts +71 -79
- package/src/prompts/system/ttsr-interrupt.md +7 -0
- package/src/utils/clipboard.ts +57 -141
- package/src/utils/shell-snapshot.ts +12 -60
- package/src/utils/shell.ts +35 -56
- package/src/utils/tools-manager.ts +42 -71
- package/src/core/logger.ts +0 -111
- package/src/modes/cleanup.ts +0 -23
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { rename } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
3
4
|
import { type Settings as SettingsItem, settingsCapability } from "../capability/settings";
|
|
4
5
|
import { getAgentDbPath, getAgentDir } from "../config";
|
|
5
6
|
import { loadCapability } from "../discovery";
|
|
6
7
|
import type { SymbolPreset } from "../modes/interactive/theme/theme";
|
|
7
8
|
import { AgentStorage } from "./agent-storage";
|
|
8
|
-
import { logger } from "./logger";
|
|
9
9
|
|
|
10
10
|
export interface CompactionSettings {
|
|
11
11
|
enabled?: boolean; // default: true
|
|
@@ -125,6 +125,7 @@ export interface EditSettings {
|
|
|
125
125
|
fuzzyMatch?: boolean; // default: true (accept high-confidence fuzzy matches for whitespace/indentation)
|
|
126
126
|
fuzzyThreshold?: number; // default: 0.95 (similarity threshold for fuzzy matching)
|
|
127
127
|
patchMode?: boolean; // default: true (use codex-style apply-patch format instead of oldText/newText)
|
|
128
|
+
streamingAbort?: boolean; // default: false (abort streaming edit tool calls when patch preview fails)
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
export type { SymbolPreset };
|
|
@@ -322,7 +323,7 @@ const DEFAULT_SETTINGS: Settings = {
|
|
|
322
323
|
mcp: { enableProjectConfig: true },
|
|
323
324
|
lsp: { formatOnWrite: false, diagnosticsOnWrite: true, diagnosticsOnEdit: false },
|
|
324
325
|
python: { toolMode: "both", kernelMode: "session", sharedGateway: true },
|
|
325
|
-
edit: { fuzzyMatch: true, fuzzyThreshold: 0.95 },
|
|
326
|
+
edit: { fuzzyMatch: true, fuzzyThreshold: 0.95, streamingAbort: false },
|
|
326
327
|
ttsr: { enabled: true, contextMode: "discard", repeatMode: "once", repeatGap: 10 },
|
|
327
328
|
voice: {
|
|
328
329
|
enabled: false,
|
|
@@ -514,7 +515,7 @@ export class SettingsManager {
|
|
|
514
515
|
*/
|
|
515
516
|
static async create(cwd: string = process.cwd(), agentDir: string = getAgentDir()): Promise<SettingsManager> {
|
|
516
517
|
const storage = AgentStorage.open(getAgentDbPath(agentDir));
|
|
517
|
-
SettingsManager.migrateLegacySettingsFile(storage, agentDir);
|
|
518
|
+
await SettingsManager.migrateLegacySettingsFile(storage, agentDir);
|
|
518
519
|
|
|
519
520
|
// Use capability API to load user-level settings from all providers
|
|
520
521
|
const result = await loadCapability(settingsCapability.id, { cwd });
|
|
@@ -577,21 +578,21 @@ export class SettingsManager {
|
|
|
577
578
|
return SettingsManager.migrateSettings(settings as Record<string, unknown>);
|
|
578
579
|
}
|
|
579
580
|
|
|
580
|
-
private static migrateLegacySettingsFile(storage: AgentStorage, agentDir: string): void {
|
|
581
|
+
private static async migrateLegacySettingsFile(storage: AgentStorage, agentDir: string): Promise<void> {
|
|
581
582
|
const settingsPath = join(agentDir, "settings.json");
|
|
582
|
-
|
|
583
|
+
const settingsFile = Bun.file(settingsPath);
|
|
584
|
+
if (!(await settingsFile.exists())) return;
|
|
583
585
|
if (storage.getSettings() !== null) return;
|
|
584
586
|
|
|
585
587
|
try {
|
|
586
|
-
const
|
|
587
|
-
const parsed = JSON.parse(content);
|
|
588
|
+
const parsed = JSON.parse(await settingsFile.text());
|
|
588
589
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
589
590
|
return;
|
|
590
591
|
}
|
|
591
592
|
const migrated = SettingsManager.migrateSettings(parsed as Record<string, unknown>);
|
|
592
593
|
storage.saveSettings(migrated);
|
|
593
594
|
try {
|
|
594
|
-
|
|
595
|
+
await rename(settingsPath, `${settingsPath}.bak`);
|
|
595
596
|
} catch (error) {
|
|
596
597
|
logger.warn("SettingsManager failed to backup settings.json", { error: String(error) });
|
|
597
598
|
}
|
|
@@ -1301,6 +1302,18 @@ export class SettingsManager {
|
|
|
1301
1302
|
await this.save();
|
|
1302
1303
|
}
|
|
1303
1304
|
|
|
1305
|
+
getEditStreamingAbort(): boolean {
|
|
1306
|
+
return this.settings.edit?.streamingAbort ?? false;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
async setEditStreamingAbort(enabled: boolean): Promise<void> {
|
|
1310
|
+
if (!this.globalSettings.edit) {
|
|
1311
|
+
this.globalSettings.edit = {};
|
|
1312
|
+
}
|
|
1313
|
+
this.globalSettings.edit.streamingAbort = enabled;
|
|
1314
|
+
await this.save();
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1304
1317
|
getNormativeRewrite(): boolean {
|
|
1305
1318
|
return this.settings.normativeRewrite ?? false;
|
|
1306
1319
|
}
|
package/src/core/skills.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { realpath } from "node:fs/promises";
|
|
3
3
|
import { basename, join } from "node:path";
|
|
4
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
4
5
|
import { minimatch } from "minimatch";
|
|
5
6
|
import { skillCapability } from "../capability/skill";
|
|
6
7
|
import type { SourceMeta } from "../capability/types";
|
|
7
8
|
import type { Skill as CapabilitySkill, SkillFrontmatter as ImportedSkillFrontmatter } from "../discovery";
|
|
8
9
|
import { loadCapability } from "../discovery";
|
|
9
10
|
import { parseFrontmatter } from "./frontmatter";
|
|
10
|
-
import { logger } from "./logger";
|
|
11
11
|
import type { SkillsSettings } from "./settings-manager";
|
|
12
12
|
|
|
13
13
|
// Re-export SkillFrontmatter for backward compatibility
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { $ } from "bun";
|
|
4
6
|
import { CONFIG_DIR_NAME } from "../../config";
|
|
5
|
-
import { logger } from "../logger";
|
|
6
7
|
|
|
7
8
|
export interface SSHConnectionTarget {
|
|
8
9
|
name: string;
|
|
@@ -107,32 +108,17 @@ function buildCommonArgs(host: SSHConnectionTarget): string[] {
|
|
|
107
108
|
return args;
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
return
|
|
111
|
+
async function runSshSync(args: string[]): Promise<{ exitCode: number | null; stderr: string }> {
|
|
112
|
+
const result = await $`ssh ${args}`.nothrow();
|
|
113
|
+
return { exitCode: result.exitCode, stderr: result.stderr.toString().trim() };
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
function
|
|
116
|
-
const result =
|
|
117
|
-
stdin: "ignore",
|
|
118
|
-
stdout: "ignore",
|
|
119
|
-
stderr: "pipe",
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
return { exitCode: result.exitCode, stderr: decodeOutput(result.stderr) };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function runSshCaptureSync(args: string[]): { exitCode: number | null; stdout: string; stderr: string } {
|
|
126
|
-
const result = Bun.spawnSync(["ssh", ...args], {
|
|
127
|
-
stdin: "ignore",
|
|
128
|
-
stdout: "pipe",
|
|
129
|
-
stderr: "pipe",
|
|
130
|
-
});
|
|
131
|
-
|
|
116
|
+
async function runSshCaptureSync(args: string[]): Promise<{ exitCode: number | null; stdout: string; stderr: string }> {
|
|
117
|
+
const result = await $`ssh ${args}`.nothrow();
|
|
132
118
|
return {
|
|
133
119
|
exitCode: result.exitCode,
|
|
134
|
-
stdout:
|
|
135
|
-
stderr:
|
|
120
|
+
stdout: result.stdout.toString().trim(),
|
|
121
|
+
stderr: result.stderr.toString().trim(),
|
|
136
122
|
};
|
|
137
123
|
}
|
|
138
124
|
|
|
@@ -266,7 +252,7 @@ async function persistHostInfo(host: SSHConnectionTarget, info: SSHHostInfo): Pr
|
|
|
266
252
|
|
|
267
253
|
async function probeHostInfo(host: SSHConnectionTarget): Promise<SSHHostInfo> {
|
|
268
254
|
const command = 'echo "$OSTYPE|$SHELL|$BASH_VERSION" 2>/dev/null || echo "%OS%|%COMSPEC%|"';
|
|
269
|
-
const result = runSshCaptureSync(buildRemoteCommand(host, command));
|
|
255
|
+
const result = await runSshCaptureSync(buildRemoteCommand(host, command));
|
|
270
256
|
if (result.exitCode !== 0 && !result.stdout) {
|
|
271
257
|
logger.debug("SSH host probe failed", { host: host.name, error: result.stderr });
|
|
272
258
|
const fallback: SSHHostInfo = {
|
|
@@ -329,11 +315,11 @@ async function probeHostInfo(host: SSHConnectionTarget): Promise<SSHHostInfo> {
|
|
|
329
315
|
const hasBash = !unexpandedPosixVars && (Boolean(bashVersion) || shell === "bash");
|
|
330
316
|
let compatShell: SSHHostInfo["compatShell"];
|
|
331
317
|
if (os === "windows" && host.compat !== false) {
|
|
332
|
-
const bashProbe = runSshCaptureSync(buildRemoteCommand(host, 'bash -lc "echo OMP_BASH_OK"'));
|
|
318
|
+
const bashProbe = await runSshCaptureSync(buildRemoteCommand(host, 'bash -lc "echo OMP_BASH_OK"'));
|
|
333
319
|
if (bashProbe.exitCode === 0 && bashProbe.stdout.includes("OMP_BASH_OK")) {
|
|
334
320
|
compatShell = "bash";
|
|
335
321
|
} else {
|
|
336
|
-
const shProbe = runSshCaptureSync(buildRemoteCommand(host, 'sh -lc "echo OMP_SH_OK"'));
|
|
322
|
+
const shProbe = await runSshCaptureSync(buildRemoteCommand(host, 'sh -lc "echo OMP_SH_OK"'));
|
|
337
323
|
if (shProbe.exitCode === 0 && shProbe.stdout.includes("OMP_SH_OK")) {
|
|
338
324
|
compatShell = "sh";
|
|
339
325
|
}
|
|
@@ -406,7 +392,7 @@ export async function ensureConnection(host: SSHConnectionTarget): Promise<void>
|
|
|
406
392
|
validateKeyPermissions(host.keyPath);
|
|
407
393
|
|
|
408
394
|
const target = buildSshTarget(host);
|
|
409
|
-
const check = runSshSync(["-O", "check", ...buildCommonArgs(host), target]);
|
|
395
|
+
const check = await runSshSync(["-O", "check", ...buildCommonArgs(host), target]);
|
|
410
396
|
if (check.exitCode === 0) {
|
|
411
397
|
activeHosts.set(key, host);
|
|
412
398
|
if (!hostInfoCache.has(key) && !loadHostInfoFromDisk(host)) {
|
|
@@ -415,7 +401,7 @@ export async function ensureConnection(host: SSHConnectionTarget): Promise<void>
|
|
|
415
401
|
return;
|
|
416
402
|
}
|
|
417
403
|
|
|
418
|
-
const start = runSshSync(["-M", "-N", "-f", ...buildCommonArgs(host), target]);
|
|
404
|
+
const start = await runSshSync(["-M", "-N", "-f", ...buildCommonArgs(host), target]);
|
|
419
405
|
if (start.exitCode !== 0) {
|
|
420
406
|
const detail = start.stderr ? `: ${start.stderr}` : "";
|
|
421
407
|
throw new Error(`Failed to start SSH master for ${target}${detail}`);
|
|
@@ -435,24 +421,24 @@ export async function ensureConnection(host: SSHConnectionTarget): Promise<void>
|
|
|
435
421
|
}
|
|
436
422
|
}
|
|
437
423
|
|
|
438
|
-
function closeConnectionInternal(host: SSHConnectionTarget): void {
|
|
424
|
+
async function closeConnectionInternal(host: SSHConnectionTarget): Promise<void> {
|
|
439
425
|
const target = buildSshTarget(host);
|
|
440
|
-
runSshSync(["-O", "exit", ...buildCommonArgs(host), target]);
|
|
426
|
+
await runSshSync(["-O", "exit", ...buildCommonArgs(host), target]);
|
|
441
427
|
}
|
|
442
428
|
|
|
443
429
|
export async function closeConnection(hostName: string): Promise<void> {
|
|
444
430
|
const host = activeHosts.get(hostName);
|
|
445
431
|
if (!host) {
|
|
446
|
-
closeConnectionInternal({ name: hostName, host: hostName });
|
|
432
|
+
await closeConnectionInternal({ name: hostName, host: hostName });
|
|
447
433
|
return;
|
|
448
434
|
}
|
|
449
|
-
closeConnectionInternal(host);
|
|
435
|
+
await closeConnectionInternal(host);
|
|
450
436
|
activeHosts.delete(hostName);
|
|
451
437
|
}
|
|
452
438
|
|
|
453
439
|
export async function closeAllConnections(): Promise<void> {
|
|
454
440
|
for (const [name, host] of Array.from(activeHosts.entries())) {
|
|
455
|
-
closeConnectionInternal(host);
|
|
441
|
+
await closeConnectionInternal(host);
|
|
456
442
|
activeHosts.delete(name);
|
|
457
443
|
}
|
|
458
444
|
}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import { logger } from "../logger";
|
|
4
|
-
import { OutputSink, pumpStream } from "../streaming-output";
|
|
5
|
-
import { DEFAULT_MAX_BYTES } from "../tools/truncate";
|
|
6
|
-
import { ScopeSignal } from "../utils";
|
|
1
|
+
import { cspawn, logger, ptree } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { OutputSink } from "../streaming-output";
|
|
7
3
|
import { buildRemoteCommand, ensureConnection, ensureHostInfo, type SSHConnectionTarget } from "./connection-manager";
|
|
8
4
|
import { hasSshfs, mountRemote } from "./sshfs-mount";
|
|
9
5
|
|
|
@@ -59,8 +55,6 @@ export async function executeSSH(
|
|
|
59
55
|
}
|
|
60
56
|
}
|
|
61
57
|
|
|
62
|
-
using signal = new ScopeSignal(options);
|
|
63
|
-
|
|
64
58
|
let resolvedCommand = command;
|
|
65
59
|
if (options?.compatEnabled) {
|
|
66
60
|
const info = await ensureHostInfo(host);
|
|
@@ -70,43 +64,53 @@ export async function executeSSH(
|
|
|
70
64
|
logger.warn("SSH compat enabled without detected compat shell", { host: host.name });
|
|
71
65
|
}
|
|
72
66
|
}
|
|
73
|
-
const child: Subprocess = Bun.spawn(["ssh", ...buildRemoteCommand(host, resolvedCommand)], {
|
|
74
|
-
stdin: "ignore",
|
|
75
|
-
stdout: "pipe",
|
|
76
|
-
stderr: "pipe",
|
|
77
|
-
});
|
|
78
67
|
|
|
79
|
-
|
|
80
|
-
|
|
68
|
+
const child = cspawn(["ssh", ...buildRemoteCommand(host, resolvedCommand)], {
|
|
69
|
+
signal: options?.signal,
|
|
70
|
+
timeout: options?.timeout,
|
|
81
71
|
});
|
|
82
72
|
|
|
83
|
-
const sink = new OutputSink(
|
|
73
|
+
const sink = new OutputSink({ onLine: options?.onChunk });
|
|
84
74
|
|
|
85
|
-
const writer = sink.getWriter();
|
|
86
75
|
try {
|
|
87
|
-
await Promise.
|
|
88
|
-
|
|
89
|
-
|
|
76
|
+
await Promise.allSettled([
|
|
77
|
+
child.stdout.pipeTo(sink.createWritable()),
|
|
78
|
+
child.stderr.pipeTo(sink.createWritable()),
|
|
90
79
|
]);
|
|
91
80
|
} finally {
|
|
92
|
-
await
|
|
81
|
+
await sink.close();
|
|
93
82
|
}
|
|
94
83
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (signal.timedOut()) {
|
|
99
|
-
const secs = Math.round(options!.timeout! / 1000);
|
|
84
|
+
try {
|
|
85
|
+
await child.exited;
|
|
86
|
+
const exitCode = child.exitCode ?? 0;
|
|
100
87
|
return {
|
|
101
|
-
exitCode
|
|
102
|
-
cancelled:
|
|
103
|
-
...sink.dump(
|
|
88
|
+
exitCode,
|
|
89
|
+
cancelled: false,
|
|
90
|
+
...sink.dump(),
|
|
104
91
|
};
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (err instanceof ptree.Exception) {
|
|
94
|
+
if (err instanceof ptree.TimeoutError) {
|
|
95
|
+
return {
|
|
96
|
+
exitCode: undefined,
|
|
97
|
+
cancelled: true,
|
|
98
|
+
...sink.dump(`SSH command timed out after ${Math.round(options!.timeout! / 1000)} seconds`),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (err.aborted) {
|
|
102
|
+
return {
|
|
103
|
+
exitCode: undefined,
|
|
104
|
+
cancelled: true,
|
|
105
|
+
...sink.dump(`SSH command aborted: ${err.message}`),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
exitCode: err.exitCode,
|
|
110
|
+
cancelled: false,
|
|
111
|
+
...sink.dump(`Unexpected error: ${err.message}`),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
throw err;
|
|
105
115
|
}
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
exitCode: cancelled ? undefined : exitCode,
|
|
109
|
-
cancelled,
|
|
110
|
-
...sink.dump(),
|
|
111
|
-
};
|
|
112
116
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { chmodSync, existsSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { $ } from "bun";
|
|
4
6
|
import { CONFIG_DIR_NAME } from "../../config";
|
|
5
|
-
import { logger } from "../logger";
|
|
6
7
|
import { getControlDir, getControlPathTemplate, type SSHConnectionTarget } from "./connection-manager";
|
|
7
8
|
|
|
8
9
|
const REMOTE_DIR = join(homedir(), CONFIG_DIR_NAME, "remote");
|
|
@@ -22,11 +23,6 @@ function ensureDir(path: string, mode = 0o700): void {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
function decodeOutput(buffer?: Uint8Array): string {
|
|
26
|
-
if (!buffer || buffer.length === 0) return "";
|
|
27
|
-
return new TextDecoder().decode(buffer).trim();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
26
|
function getMountName(host: SSHConnectionTarget): string {
|
|
31
27
|
const raw = (host.name ?? host.host).trim();
|
|
32
28
|
const sanitized = raw.replace(/[^a-zA-Z0-9._-]+/g, "_");
|
|
@@ -72,24 +68,16 @@ function buildSshfsArgs(host: SSHConnectionTarget): string[] {
|
|
|
72
68
|
return args;
|
|
73
69
|
}
|
|
74
70
|
|
|
75
|
-
function unmountPath(path: string): boolean {
|
|
71
|
+
async function unmountPath(path: string): Promise<boolean> {
|
|
76
72
|
const fusermount = Bun.which("fusermount") ?? Bun.which("fusermount3");
|
|
77
73
|
if (fusermount) {
|
|
78
|
-
const result =
|
|
79
|
-
stdin: "ignore",
|
|
80
|
-
stdout: "ignore",
|
|
81
|
-
stderr: "pipe",
|
|
82
|
-
});
|
|
74
|
+
const result = await $`${fusermount} -u ${path}`.quiet().nothrow();
|
|
83
75
|
if (result.exitCode === 0) return true;
|
|
84
76
|
}
|
|
85
77
|
|
|
86
78
|
const umount = Bun.which("umount");
|
|
87
79
|
if (!umount) return false;
|
|
88
|
-
const result =
|
|
89
|
-
stdin: "ignore",
|
|
90
|
-
stdout: "ignore",
|
|
91
|
-
stderr: "pipe",
|
|
92
|
-
});
|
|
80
|
+
const result = await $`${umount} ${path}`.quiet().nothrow();
|
|
93
81
|
return result.exitCode === 0;
|
|
94
82
|
}
|
|
95
83
|
|
|
@@ -97,14 +85,10 @@ export function hasSshfs(): boolean {
|
|
|
97
85
|
return Bun.which("sshfs") !== null;
|
|
98
86
|
}
|
|
99
87
|
|
|
100
|
-
export function isMounted(path: string): boolean {
|
|
88
|
+
export async function isMounted(path: string): Promise<boolean> {
|
|
101
89
|
const mountpoint = Bun.which("mountpoint");
|
|
102
90
|
if (!mountpoint) return false;
|
|
103
|
-
const result =
|
|
104
|
-
stdin: "ignore",
|
|
105
|
-
stdout: "ignore",
|
|
106
|
-
stderr: "ignore",
|
|
107
|
-
});
|
|
91
|
+
const result = await $`${mountpoint} -q ${path}`.quiet().nothrow();
|
|
108
92
|
return result.exitCode === 0;
|
|
109
93
|
}
|
|
110
94
|
|
|
@@ -117,20 +101,17 @@ export async function mountRemote(host: SSHConnectionTarget, remotePath = "/"):
|
|
|
117
101
|
const mountPath = getMountPath(host);
|
|
118
102
|
ensureDir(mountPath);
|
|
119
103
|
|
|
120
|
-
if (isMounted(mountPath)) {
|
|
104
|
+
if (await isMounted(mountPath)) {
|
|
121
105
|
mountedPaths.add(mountPath);
|
|
122
106
|
return mountPath;
|
|
123
107
|
}
|
|
124
108
|
|
|
125
109
|
const target = `${buildSshTarget(host)}:${remotePath}`;
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
stdout: "pipe",
|
|
129
|
-
stderr: "pipe",
|
|
130
|
-
});
|
|
110
|
+
const args = buildSshfsArgs(host);
|
|
111
|
+
const result = await $`sshfs ${args} ${target} ${mountPath}`.nothrow();
|
|
131
112
|
|
|
132
113
|
if (result.exitCode !== 0) {
|
|
133
|
-
const detail =
|
|
114
|
+
const detail = result.stderr.toString().trim();
|
|
134
115
|
const suffix = detail ? `: ${detail}` : "";
|
|
135
116
|
throw new Error(`Failed to mount ${target}${suffix}`);
|
|
136
117
|
}
|
|
@@ -141,12 +122,12 @@ export async function mountRemote(host: SSHConnectionTarget, remotePath = "/"):
|
|
|
141
122
|
|
|
142
123
|
export async function unmountRemote(host: SSHConnectionTarget): Promise<boolean> {
|
|
143
124
|
const mountPath = getMountPath(host);
|
|
144
|
-
if (!isMounted(mountPath)) {
|
|
125
|
+
if (!(await isMounted(mountPath))) {
|
|
145
126
|
mountedPaths.delete(mountPath);
|
|
146
127
|
return false;
|
|
147
128
|
}
|
|
148
129
|
|
|
149
|
-
const success = unmountPath(mountPath);
|
|
130
|
+
const success = await unmountPath(mountPath);
|
|
150
131
|
if (success) {
|
|
151
132
|
mountedPaths.delete(mountPath);
|
|
152
133
|
}
|
|
@@ -156,7 +137,7 @@ export async function unmountRemote(host: SSHConnectionTarget): Promise<boolean>
|
|
|
156
137
|
|
|
157
138
|
export async function unmountAll(): Promise<void> {
|
|
158
139
|
for (const mountPath of Array.from(mountedPaths)) {
|
|
159
|
-
unmountPath(mountPath);
|
|
140
|
+
await unmountPath(mountPath);
|
|
160
141
|
}
|
|
161
142
|
mountedPaths.clear();
|
|
162
143
|
}
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* Original JSON files are backed up to .bak and removed after successful migration.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
7
8
|
import { getAgentDbPath } from "../config";
|
|
8
9
|
import { AgentStorage } from "./agent-storage";
|
|
9
10
|
import type { AuthCredential, AuthCredentialEntry, AuthStorageData } from "./auth-storage";
|
|
10
|
-
import { logger } from "./logger";
|
|
11
11
|
import type { Settings } from "./settings-manager";
|
|
12
12
|
|
|
13
13
|
/** Paths configuration for the storage migration process. */
|