@oh-my-pi/pi-coding-agent 12.12.2 → 12.13.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 +30 -0
- package/package.json +7 -7
- package/src/cli/ssh-cli.ts +179 -0
- package/src/cli.ts +1 -0
- package/src/commands/ssh.ts +60 -0
- package/src/commit/prompts/analysis-system.md +1 -3
- package/src/config/prompt-templates.ts +20 -5
- package/src/config/settings-schema.ts +1 -1
- package/src/discovery/ssh.ts +26 -19
- package/src/modes/controllers/ssh-command-controller.ts +452 -0
- package/src/modes/interactive-mode.ts +6 -0
- package/src/modes/types.ts +1 -0
- package/src/patch/hashline.ts +237 -135
- package/src/patch/index.ts +37 -39
- package/src/patch/shared.ts +37 -23
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/grep.md +12 -8
- package/src/prompts/tools/hashline.md +98 -53
- package/src/prompts/tools/read.md +1 -3
- package/src/session/agent-session.ts +1 -1
- package/src/session/auth-storage.ts +6 -0
- package/src/slash-commands/builtin-registry.ts +20 -0
- package/src/ssh/config-writer.ts +183 -0
- package/src/tools/bash-interactive.ts +47 -7
- package/src/tools/grep.ts +1 -1
- package/src/tools/read.ts +2 -2
- package/src/tools/ssh.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [12.13.0] - 2026-02-19
|
|
6
|
+
### Breaking Changes
|
|
7
|
+
|
|
8
|
+
- Removed automatic line relocation when hash references become stale; edits with mismatched line hashes now fail with an error instead of silently relocating to matching lines elsewhere in the file
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Added `ssh` command for managing SSH host configurations (add, list, remove)
|
|
13
|
+
- Added `/ssh` slash command in interactive mode to manage SSH hosts with subcommands
|
|
14
|
+
- Added support for SSH host configuration at project and user scopes (.omp/ssh.json and ~/.omp/agent/ssh.json)
|
|
15
|
+
- Added `--host`, `--user`, `--port`, `--key`, `--desc`, `--compat`, and `--scope` flags for SSH host configuration
|
|
16
|
+
- Added discovery of SSH hosts from project configuration files alongside manually configured hosts
|
|
17
|
+
- Added NanoGPT as a login provider (`/login nanogpt`) with API key prompt flow linking to `https://nano-gpt.com/api` ([#111](https://github.com/can1357/oh-my-pi/issues/111))
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Updated hashline reference format from `LINE:HASH` to `LINE#ID` throughout the codebase for improved clarity
|
|
22
|
+
- Renamed hashline edit operations: `set_line` → `set`, `replace_lines` → `set_range`, `insert_after` → `insert` with support for `before` and `between` anchors
|
|
23
|
+
- Changed hashline edit `body` field from string to array of strings for clearer multiline handling
|
|
24
|
+
- Updated handlebars helpers: renamed `hashline` to `hlineref` and added `hlinefull` for formatted line output
|
|
25
|
+
- Improved insert operation to support `before`, `after`, and `between` (both anchors) positioning modes
|
|
26
|
+
- Made autocorrect heuristics (boundary echo stripping, indent restoration) conditional on `PI_HL_AUTOCORRECT` environment variable
|
|
27
|
+
- Updated SSH host discovery to load from managed omp config paths (.omp/ssh.json and ~/.omp/agent/ssh.json) in addition to legacy root-level ssh.json and .ssh.json files
|
|
28
|
+
- Improved terminal output handling in interactive bash sessions to ensure all queued writes complete before returning results
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Fixed insert-between operation to properly validate adjacent anchor lines and strip boundary echoes from both sides
|
|
33
|
+
- Fixed terminal output handling to properly queue and serialize writes, preventing dropped or corrupted output in interactive bash sessions
|
|
34
|
+
|
|
5
35
|
## [12.12.1] - 2026-02-19
|
|
6
36
|
|
|
7
37
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.13.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -84,12 +84,12 @@
|
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
86
|
"@mozilla/readability": "0.6.0",
|
|
87
|
-
"@oh-my-pi/omp-stats": "12.
|
|
88
|
-
"@oh-my-pi/pi-agent-core": "12.
|
|
89
|
-
"@oh-my-pi/pi-ai": "12.
|
|
90
|
-
"@oh-my-pi/pi-natives": "12.
|
|
91
|
-
"@oh-my-pi/pi-tui": "12.
|
|
92
|
-
"@oh-my-pi/pi-utils": "12.
|
|
87
|
+
"@oh-my-pi/omp-stats": "12.13.0",
|
|
88
|
+
"@oh-my-pi/pi-agent-core": "12.13.0",
|
|
89
|
+
"@oh-my-pi/pi-ai": "12.13.0",
|
|
90
|
+
"@oh-my-pi/pi-natives": "12.13.0",
|
|
91
|
+
"@oh-my-pi/pi-tui": "12.13.0",
|
|
92
|
+
"@oh-my-pi/pi-utils": "12.13.0",
|
|
93
93
|
"@sinclair/typebox": "^0.34.48",
|
|
94
94
|
"@xterm/headless": "^6.0.0",
|
|
95
95
|
"ajv": "^8.18.0",
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSH CLI command handlers.
|
|
3
|
+
*
|
|
4
|
+
* Handles `omp ssh <command>` subcommands for SSH host configuration management.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getSSHConfigPath } from "@oh-my-pi/pi-utils/dirs";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import { addSSHHost, readSSHConfigFile, removeSSHHost, type SSHHostConfig } from "../ssh/config-writer";
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Types
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
export type SSHAction = "add" | "remove" | "list";
|
|
16
|
+
|
|
17
|
+
export interface SSHCommandArgs {
|
|
18
|
+
action: SSHAction;
|
|
19
|
+
args: string[];
|
|
20
|
+
flags: {
|
|
21
|
+
json?: boolean;
|
|
22
|
+
host?: string;
|
|
23
|
+
user?: string;
|
|
24
|
+
port?: string;
|
|
25
|
+
key?: string;
|
|
26
|
+
desc?: string;
|
|
27
|
+
compat?: boolean;
|
|
28
|
+
scope?: "project" | "user";
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// =============================================================================
|
|
33
|
+
// Main dispatcher
|
|
34
|
+
// =============================================================================
|
|
35
|
+
|
|
36
|
+
export async function runSSHCommand(cmd: SSHCommandArgs): Promise<void> {
|
|
37
|
+
switch (cmd.action) {
|
|
38
|
+
case "add":
|
|
39
|
+
await handleAdd(cmd);
|
|
40
|
+
break;
|
|
41
|
+
case "remove":
|
|
42
|
+
await handleRemove(cmd);
|
|
43
|
+
break;
|
|
44
|
+
case "list":
|
|
45
|
+
await handleList(cmd);
|
|
46
|
+
break;
|
|
47
|
+
default:
|
|
48
|
+
process.stdout.write(chalk.red(`Unknown action: ${cmd.action}\n`));
|
|
49
|
+
process.stdout.write(`Valid actions: add, remove, list\n`);
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// =============================================================================
|
|
55
|
+
// Handlers
|
|
56
|
+
// =============================================================================
|
|
57
|
+
|
|
58
|
+
async function handleAdd(cmd: SSHCommandArgs): Promise<void> {
|
|
59
|
+
const name = cmd.args[0];
|
|
60
|
+
if (!name) {
|
|
61
|
+
process.stdout.write(chalk.red("Error: Host name required\n"));
|
|
62
|
+
process.stdout.write(
|
|
63
|
+
chalk.dim("Usage: omp ssh add <name> --host <address> [--user <user>] [--port <port>] [--key <path>]\n"),
|
|
64
|
+
);
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const host = cmd.flags.host;
|
|
70
|
+
if (!host) {
|
|
71
|
+
process.stdout.write(chalk.red("Error: --host is required\n"));
|
|
72
|
+
process.stdout.write(chalk.dim("Usage: omp ssh add <name> --host <address>\n"));
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Validate port if provided
|
|
78
|
+
if (cmd.flags.port !== undefined) {
|
|
79
|
+
const port = Number.parseInt(cmd.flags.port, 10);
|
|
80
|
+
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
81
|
+
process.stdout.write(chalk.red("Error: Port must be an integer between 1 and 65535\n"));
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const hostConfig: SSHHostConfig = { host };
|
|
88
|
+
if (cmd.flags.user) hostConfig.username = cmd.flags.user;
|
|
89
|
+
if (cmd.flags.port) hostConfig.port = Number.parseInt(cmd.flags.port, 10);
|
|
90
|
+
if (cmd.flags.key) hostConfig.keyPath = cmd.flags.key;
|
|
91
|
+
if (cmd.flags.desc) hostConfig.description = cmd.flags.desc;
|
|
92
|
+
if (cmd.flags.compat) hostConfig.compat = true;
|
|
93
|
+
|
|
94
|
+
const scope = cmd.flags.scope ?? "project";
|
|
95
|
+
const filePath = getSSHConfigPath(scope);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await addSSHHost(filePath, name, hostConfig);
|
|
99
|
+
process.stdout.write(chalk.green(`Added SSH host "${name}" to ${scope} config\n`));
|
|
100
|
+
} catch (err) {
|
|
101
|
+
process.stdout.write(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}\n`));
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function handleRemove(cmd: SSHCommandArgs): Promise<void> {
|
|
107
|
+
const name = cmd.args[0];
|
|
108
|
+
if (!name) {
|
|
109
|
+
process.stdout.write(chalk.red("Error: Host name required\n"));
|
|
110
|
+
process.stdout.write(chalk.dim("Usage: omp ssh remove <name> [--scope project|user]\n"));
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const scope = cmd.flags.scope ?? "project";
|
|
116
|
+
const filePath = getSSHConfigPath(scope);
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await removeSSHHost(filePath, name);
|
|
120
|
+
process.stdout.write(chalk.green(`Removed SSH host "${name}" from ${scope} config\n`));
|
|
121
|
+
} catch (err) {
|
|
122
|
+
process.stdout.write(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}\n`));
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function handleList(cmd: SSHCommandArgs): Promise<void> {
|
|
128
|
+
const projectPath = getSSHConfigPath("project");
|
|
129
|
+
const userPath = getSSHConfigPath("user");
|
|
130
|
+
|
|
131
|
+
const [projectConfig, userConfig] = await Promise.all([readSSHConfigFile(projectPath), readSSHConfigFile(userPath)]);
|
|
132
|
+
|
|
133
|
+
const projectHosts = projectConfig.hosts ?? {};
|
|
134
|
+
const userHosts = userConfig.hosts ?? {};
|
|
135
|
+
|
|
136
|
+
if (cmd.flags.json) {
|
|
137
|
+
process.stdout.write(JSON.stringify({ project: projectHosts, user: userHosts }, null, 2));
|
|
138
|
+
process.stdout.write("\n");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const hasProject = Object.keys(projectHosts).length > 0;
|
|
143
|
+
const hasUser = Object.keys(userHosts).length > 0;
|
|
144
|
+
|
|
145
|
+
if (!hasProject && !hasUser) {
|
|
146
|
+
process.stdout.write(chalk.dim("No SSH hosts configured\n"));
|
|
147
|
+
process.stdout.write(chalk.dim("Add one with: omp ssh add <name> --host <address>\n"));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (hasProject) {
|
|
152
|
+
process.stdout.write(chalk.bold("Project SSH Hosts (.omp/ssh.json):\n"));
|
|
153
|
+
printHosts(projectHosts);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (hasProject && hasUser) {
|
|
157
|
+
process.stdout.write("\n");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (hasUser) {
|
|
161
|
+
process.stdout.write(chalk.bold("User SSH Hosts (~/.omp/agent/ssh.json):\n"));
|
|
162
|
+
printHosts(userHosts);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// =============================================================================
|
|
167
|
+
// Helpers
|
|
168
|
+
// =============================================================================
|
|
169
|
+
|
|
170
|
+
function printHosts(hosts: Record<string, SSHHostConfig>): void {
|
|
171
|
+
for (const [name, config] of Object.entries(hosts)) {
|
|
172
|
+
const parts = [chalk.cyan(name), config.host];
|
|
173
|
+
if (config.username) parts.push(chalk.dim(config.username));
|
|
174
|
+
if (config.port && config.port !== 22) parts.push(chalk.dim(`port:${config.port}`));
|
|
175
|
+
if (config.keyPath) parts.push(chalk.dim(config.keyPath));
|
|
176
|
+
if (config.description) parts.push(chalk.dim(`- ${config.description}`));
|
|
177
|
+
process.stdout.write(` ${parts.join(" ")}\n`);
|
|
178
|
+
}
|
|
179
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -23,6 +23,7 @@ const commands: CommandEntry[] = [
|
|
|
23
23
|
{ name: "plugin", load: () => import("./commands/plugin").then(m => m.default) },
|
|
24
24
|
{ name: "setup", load: () => import("./commands/setup").then(m => m.default) },
|
|
25
25
|
{ name: "shell", load: () => import("./commands/shell").then(m => m.default) },
|
|
26
|
+
{ name: "ssh", load: () => import("./commands/ssh").then(m => m.default) },
|
|
26
27
|
{ name: "stats", load: () => import("./commands/stats").then(m => m.default) },
|
|
27
28
|
{ name: "update", load: () => import("./commands/update").then(m => m.default) },
|
|
28
29
|
{ name: "search", load: () => import("./commands/web-search").then(m => m.default), aliases: ["q"] },
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manage SSH host configurations.
|
|
3
|
+
*/
|
|
4
|
+
import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
|
5
|
+
import { runSSHCommand, type SSHAction, type SSHCommandArgs } from "../cli/ssh-cli";
|
|
6
|
+
import { initTheme } from "../modes/theme/theme";
|
|
7
|
+
|
|
8
|
+
const ACTIONS: SSHAction[] = ["add", "remove", "list"];
|
|
9
|
+
|
|
10
|
+
export default class SSH extends Command {
|
|
11
|
+
static description = "Manage SSH host configurations";
|
|
12
|
+
|
|
13
|
+
static args = {
|
|
14
|
+
action: Args.string({
|
|
15
|
+
description: "SSH action",
|
|
16
|
+
required: false,
|
|
17
|
+
options: ACTIONS,
|
|
18
|
+
}),
|
|
19
|
+
targets: Args.string({
|
|
20
|
+
description: "Host name or arguments",
|
|
21
|
+
required: false,
|
|
22
|
+
multiple: true,
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
static flags = {
|
|
27
|
+
json: Flags.boolean({ description: "Output JSON" }),
|
|
28
|
+
host: Flags.string({ description: "Host address" }),
|
|
29
|
+
user: Flags.string({ description: "Username" }),
|
|
30
|
+
port: Flags.string({ description: "Port number" }),
|
|
31
|
+
key: Flags.string({ description: "Identity key path" }),
|
|
32
|
+
desc: Flags.string({ description: "Host description" }),
|
|
33
|
+
compat: Flags.boolean({ description: "Enable compatibility mode" }),
|
|
34
|
+
scope: Flags.string({ description: "Config scope (project|user)", options: ["project", "user"] }),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
async run(): Promise<void> {
|
|
38
|
+
const { args, flags } = await this.parse(SSH);
|
|
39
|
+
const action = (args.action ?? "list") as SSHAction;
|
|
40
|
+
const targets = Array.isArray(args.targets) ? args.targets : args.targets ? [args.targets] : [];
|
|
41
|
+
|
|
42
|
+
const cmd: SSHCommandArgs = {
|
|
43
|
+
action,
|
|
44
|
+
args: targets,
|
|
45
|
+
flags: {
|
|
46
|
+
json: flags.json,
|
|
47
|
+
host: flags.host,
|
|
48
|
+
user: flags.user,
|
|
49
|
+
port: flags.port,
|
|
50
|
+
key: flags.key,
|
|
51
|
+
desc: flags.desc,
|
|
52
|
+
compat: flags.compat,
|
|
53
|
+
scope: flags.scope as "project" | "user" | undefined,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
await initTheme();
|
|
58
|
+
await runSSHCommand(cmd);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -76,7 +76,6 @@ Call create_conventional_analysis with:
|
|
|
76
76
|
}
|
|
77
77
|
</output_format>
|
|
78
78
|
|
|
79
|
-
<examples>
|
|
80
79
|
<example name="feature-with-api">
|
|
81
80
|
{
|
|
82
81
|
"type": "feat",
|
|
@@ -146,5 +145,4 @@ Call create_conventional_analysis with:
|
|
|
146
145
|
"details": [],
|
|
147
146
|
"issue_refs": []
|
|
148
147
|
}
|
|
149
|
-
</example>
|
|
150
|
-
</examples>
|
|
148
|
+
</example>
|
|
@@ -230,13 +230,28 @@ handlebars.registerHelper("jtdToTypeScript", (schema: unknown): string => jtdToT
|
|
|
230
230
|
handlebars.registerHelper("jsonStringify", (value: unknown): string => JSON.stringify(value));
|
|
231
231
|
|
|
232
232
|
/**
|
|
233
|
-
* {{
|
|
234
|
-
* Returns `"lineNum
|
|
233
|
+
* {{hlineref lineNum "content"}} — compute a real hashline ref for prompt examples.
|
|
234
|
+
* Returns `"lineNum#hash"` using the actual hash algorithm.
|
|
235
235
|
*/
|
|
236
|
-
|
|
236
|
+
function formatHashlineRef(lineNum: unknown, content: unknown): { num: number; text: string; ref: string } {
|
|
237
237
|
const num = typeof lineNum === "number" ? lineNum : Number.parseInt(String(lineNum), 10);
|
|
238
|
-
const
|
|
239
|
-
|
|
238
|
+
const text = typeof content === "string" ? content : String(content ?? "");
|
|
239
|
+
const ref = `${num}#${computeLineHash(num, text)}`;
|
|
240
|
+
return { num, text, ref };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
handlebars.registerHelper("hlineref", (lineNum: unknown, content: unknown): string => {
|
|
244
|
+
const { ref } = formatHashlineRef(lineNum, content);
|
|
245
|
+
return ref;
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* {{hlinefull lineNum "content"}} — format a full read-style line with prefix.
|
|
250
|
+
* Returns `"lineNum#hash|content"`.
|
|
251
|
+
*/
|
|
252
|
+
handlebars.registerHelper("hlinefull", (lineNum: unknown, content: unknown): string => {
|
|
253
|
+
const { ref, text } = formatHashlineRef(lineNum, content);
|
|
254
|
+
return `${ref}|${text}`;
|
|
240
255
|
});
|
|
241
256
|
|
|
242
257
|
export function renderPromptTemplate(template: string, context: TemplateContext = {}): string {
|
|
@@ -287,7 +287,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
287
287
|
ui: {
|
|
288
288
|
tab: "config",
|
|
289
289
|
label: "Read hash lines",
|
|
290
|
-
description: "Include line hashes in read output for hashline edit mode (LINE
|
|
290
|
+
description: "Include line hashes in read output for hashline edit mode (LINE#ID|content)",
|
|
291
291
|
},
|
|
292
292
|
},
|
|
293
293
|
showHardwareCursor: {
|
package/src/discovery/ssh.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SSH JSON Provider
|
|
3
3
|
*
|
|
4
|
-
* Discovers SSH hosts from
|
|
5
|
-
* Priority: 5 (low, project
|
|
4
|
+
* Discovers SSH hosts from managed omp config paths and legacy root ssh.json files.
|
|
5
|
+
* Priority: 5 (low, project/user config discovery)
|
|
6
6
|
*/
|
|
7
7
|
import * as path from "node:path";
|
|
8
|
+
import { getSSHConfigPath } from "@oh-my-pi/pi-utils/dirs";
|
|
8
9
|
import { registerProvider } from "../capability";
|
|
9
10
|
import { readFile } from "../capability/fs";
|
|
10
11
|
import { type SSHHost, sshCapability } from "../capability/ssh";
|
|
@@ -90,38 +91,39 @@ function normalizeHost(
|
|
|
90
91
|
};
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
async function loadSshJsonFile(
|
|
94
|
+
async function loadSshJsonFile(
|
|
95
|
+
ctx: LoadContext,
|
|
96
|
+
filePath: string,
|
|
97
|
+
level: "user" | "project",
|
|
98
|
+
): Promise<LoadResult<SSHHost>> {
|
|
94
99
|
const items: SSHHost[] = [];
|
|
95
100
|
const warnings: string[] = [];
|
|
96
|
-
|
|
97
|
-
const content = await readFile(path);
|
|
101
|
+
const content = await readFile(filePath);
|
|
98
102
|
if (content === null) {
|
|
99
103
|
return { items, warnings };
|
|
100
104
|
}
|
|
101
|
-
|
|
102
105
|
const parsed = parseJSON<SSHConfigFile>(content);
|
|
103
106
|
if (!parsed) {
|
|
104
|
-
warnings.push(`Failed to parse JSON in ${
|
|
107
|
+
warnings.push(`Failed to parse JSON in ${filePath}`);
|
|
105
108
|
return { items, warnings };
|
|
106
109
|
}
|
|
107
|
-
|
|
108
110
|
const config = expandEnvVarsDeep(parsed);
|
|
109
111
|
if (!config.hosts || typeof config.hosts !== "object") {
|
|
110
|
-
warnings.push(`Missing hosts in ${
|
|
112
|
+
warnings.push(`Missing hosts in ${filePath}`);
|
|
111
113
|
return { items, warnings };
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
const source = createSourceMeta(PROVIDER_ID,
|
|
116
|
+
const source = createSourceMeta(PROVIDER_ID, filePath, level);
|
|
115
117
|
for (const [name, rawHost] of Object.entries(config.hosts)) {
|
|
116
118
|
if (!name.trim()) {
|
|
117
|
-
warnings.push(`Invalid SSH host name in ${
|
|
119
|
+
warnings.push(`Invalid SSH host name in ${filePath}`);
|
|
118
120
|
continue;
|
|
119
121
|
}
|
|
120
122
|
if (!rawHost || typeof rawHost !== "object") {
|
|
121
|
-
warnings.push(`Invalid host entry in ${
|
|
123
|
+
warnings.push(`Invalid host entry in ${filePath}: ${name}`);
|
|
122
124
|
continue;
|
|
123
125
|
}
|
|
124
|
-
const host = normalizeHost(name, rawHost, source,
|
|
126
|
+
const host = normalizeHost(name, rawHost, source, ctx.home, warnings);
|
|
125
127
|
if (host) items.push(host);
|
|
126
128
|
}
|
|
127
129
|
|
|
@@ -130,14 +132,19 @@ async function loadSshJsonFile(_ctx: LoadContext, path: string): Promise<LoadRes
|
|
|
130
132
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
131
133
|
};
|
|
132
134
|
}
|
|
133
|
-
|
|
134
135
|
async function load(ctx: LoadContext): Promise<LoadResult<SSHHost>> {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
const candidateSources: Array<{ path: string; level: "user" | "project" }> = [
|
|
137
|
+
{ path: getSSHConfigPath("project", ctx.cwd), level: "project" },
|
|
138
|
+
{ path: getSSHConfigPath("user", ctx.cwd), level: "user" },
|
|
139
|
+
{ path: path.join(ctx.cwd, "ssh.json"), level: "project" },
|
|
140
|
+
{ path: path.join(ctx.cwd, ".ssh.json"), level: "project" },
|
|
141
|
+
];
|
|
142
|
+
const uniqueSources = candidateSources.filter(
|
|
143
|
+
(source, index, arr) => arr.findIndex(candidate => candidate.path === source.path) === index,
|
|
144
|
+
);
|
|
145
|
+
const results = await Promise.all(uniqueSources.map(source => loadSshJsonFile(ctx, source.path, source.level)));
|
|
138
146
|
const allItems = results.flatMap(r => r.items);
|
|
139
147
|
const allWarnings = results.flatMap(r => r.warnings ?? []);
|
|
140
|
-
|
|
141
148
|
return {
|
|
142
149
|
items: allItems,
|
|
143
150
|
warnings: allWarnings.length > 0 ? allWarnings : undefined,
|
|
@@ -147,7 +154,7 @@ async function load(ctx: LoadContext): Promise<LoadResult<SSHHost>> {
|
|
|
147
154
|
registerProvider(sshCapability.id, {
|
|
148
155
|
id: PROVIDER_ID,
|
|
149
156
|
displayName: DISPLAY_NAME,
|
|
150
|
-
description: "Load SSH hosts from ssh.json
|
|
157
|
+
description: "Load SSH hosts from managed omp paths and legacy ssh.json/.ssh.json files",
|
|
151
158
|
priority: 5,
|
|
152
159
|
load,
|
|
153
160
|
});
|