@patimweb/pi-abap-fs 1.0.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/LICENSE +21 -0
- package/README.md +101 -0
- package/index.ts +82 -0
- package/package.json +51 -0
- package/screenshot.png +0 -0
- package/src/client.ts +177 -0
- package/src/config.ts +174 -0
- package/src/formatting/formatters.ts +254 -0
- package/src/tools/abap-activate.ts +73 -0
- package/src/tools/abap-lock.ts +79 -0
- package/src/tools/abap-node-contents.ts +57 -0
- package/src/tools/abap-object-structure.ts +60 -0
- package/src/tools/abap-object-types.ts +38 -0
- package/src/tools/abap-profile.ts +87 -0
- package/src/tools/abap-query.ts +50 -0
- package/src/tools/abap-read.ts +62 -0
- package/src/tools/abap-search.ts +67 -0
- package/src/tools/abap-setup.ts +81 -0
- package/src/tools/abap-status.ts +32 -0
- package/src/tools/abap-syntax-check.ts +76 -0
- package/src/tools/abap-table.ts +67 -0
- package/src/tools/abap-test-connection.ts +54 -0
- package/src/tools/abap-transports.ts +46 -0
- package/src/tools/abap-unit-test.ts +85 -0
- package/src/tools/abap-unlock.ts +71 -0
- package/src/tools/abap-where-used.ts +65 -0
- package/src/tools/abap-write.ts +120 -0
- package/src/types.ts +55 -0
- package/tsconfig.json +17 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Patrick Weppelmann
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# pi ABAP FS
|
|
2
|
+
|
|
3
|
+
An [ABAP remote filesystem](https://github.com/marcellourbani/vscode_abap_remote_fs)-style
|
|
4
|
+
extension for the [pi coding agent](https://github.com/earendil-works/pi-coding-agent).
|
|
5
|
+
|
|
6
|
+
It gives the pi agent **live access to an SAP system** via the ADT (ABAP Development
|
|
7
|
+
Tools) protocol: search and read real objects, edit and activate source code, run ABAP
|
|
8
|
+
unit tests, and query live tables — all through plain-language prompts.
|
|
9
|
+
|
|
10
|
+
This is a port of the core `vscode_abap_remote_fs` capabilities to pi. It is built on
|
|
11
|
+
[`abap-adt-api`](https://www.npmjs.com/package/abap-adt-api), the same ADT client
|
|
12
|
+
library the VS Code extension uses.
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- pi coding agent (Node 26+)
|
|
17
|
+
- An SAP system with **ADT (ABAP Development Tools) enabled**
|
|
18
|
+
- Reading/browsing works on NetWeaver 7.51+; writing on older systems may need the
|
|
19
|
+
`abapfs_extensions` plugin (see upstream docs).
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pi install npm:@patimweb/pi-abap-fs
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
or run directly for a quick test:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pi -e ./index.ts
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Setup
|
|
34
|
+
|
|
35
|
+
1. Ask pi to configure a connection, or call the tool directly:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
Use abap_setup to configure my dev system:
|
|
39
|
+
name=dev, url=http://vhcalnplci.bti.local:8000,
|
|
40
|
+
username=DEVELOPER, password=..., client=001
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Credentials are stored in `~/.pi/abap-config.json` (mode `0600`, owner-readable only).
|
|
44
|
+
|
|
45
|
+
2. Verify the connection:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
Use abap_test_connection to check the dev profile.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Tools
|
|
52
|
+
|
|
53
|
+
| Tool | Purpose |
|
|
54
|
+
|------|---------|
|
|
55
|
+
| `abap_setup` | Configure an SAP connection profile |
|
|
56
|
+
| `abap_status` | Show configured profiles and the active one |
|
|
57
|
+
| `abap_profile` | List / switch / delete profiles |
|
|
58
|
+
| `abap_test_connection` | Verify a profile can log on |
|
|
59
|
+
| `abap_search` | Search objects by name pattern |
|
|
60
|
+
| `abap_object_structure` | Object metadata and include list |
|
|
61
|
+
| `abap_read` | Read an object's source code |
|
|
62
|
+
| `abap_lock` / `abap_unlock` | Lock management |
|
|
63
|
+
| `abap_write` | Write source code (lock → write → unlock) |
|
|
64
|
+
| `abap_activate` | Activate an object |
|
|
65
|
+
| `abap_syntax_check` | Syntax-check source |
|
|
66
|
+
| `abap_unit_test` | Run ABAP unit tests |
|
|
67
|
+
| `abap_query` | Run an ABAP SQL query |
|
|
68
|
+
| `abap_table` | Preview table/view contents |
|
|
69
|
+
| `abap_where_used` | Where-used list |
|
|
70
|
+
| `abap_object_types` | List searchable object types |
|
|
71
|
+
| `abap_node_contents` | Browse a package's object tree |
|
|
72
|
+
| `abap_transports` | List transport requests |
|
|
73
|
+
|
|
74
|
+
## Example prompts
|
|
75
|
+
|
|
76
|
+
- "How does `ZCL_MY_CLASS` work?" — the agent searches, reads the source and
|
|
77
|
+
related objects.
|
|
78
|
+
- "Find all classes with 'pricing' in the name."
|
|
79
|
+
- "Add method `calculate` to `ZCL_MY_CLASS` and activate it."
|
|
80
|
+
- "Run the unit tests for `ZCL_MY_CLASS` and explain the failures."
|
|
81
|
+
- "Show me the first 20 rows of table `SFLIGHT` where `CARRID = 'LH'`."
|
|
82
|
+
|
|
83
|
+
## Development
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npm install
|
|
87
|
+
npm run typecheck # tsc --noEmit
|
|
88
|
+
npm test # node --test over tests/**/*.test.ts
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The extension is TypeScript loaded directly by pi (via jiti) — no build step.
|
|
92
|
+
|
|
93
|
+
## Release
|
|
94
|
+
|
|
95
|
+
Releases use [semantic-release](https://semantic-release.gitbook.io/semantic-release/)
|
|
96
|
+
on the `main` (and `next`) branches. Commits follow Conventional Commits; CI runs the
|
|
97
|
+
test suite, and the release workflow publishes to npm on merge.
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi ABAP FS Extension
|
|
3
|
+
*
|
|
4
|
+
* Provides ABAP remote filesystem capabilities via the ADT (ABAP Development
|
|
5
|
+
* Tools) protocol, using the `abap-adt-api` package.
|
|
6
|
+
*
|
|
7
|
+
* Tools:
|
|
8
|
+
* - abap_setup: Configure an SAP connection profile
|
|
9
|
+
* - abap_status: Show connection status and profiles
|
|
10
|
+
* - abap_profile: List, switch or delete profiles
|
|
11
|
+
* - abap_test_connection: Verify a profile can log on
|
|
12
|
+
* - abap_search: Search objects by name pattern
|
|
13
|
+
* - abap_object_structure: Object metadata and includes
|
|
14
|
+
* - abap_read: Read object source code
|
|
15
|
+
* - abap_lock / abap_unlock: Lock management
|
|
16
|
+
* - abap_write: Write source code (lock + write + unlock)
|
|
17
|
+
* - abap_activate: Activate an object
|
|
18
|
+
* - abap_syntax_check: Syntax check source
|
|
19
|
+
* - abap_unit_test: Run ABAP unit tests
|
|
20
|
+
* - abap_query: Run ABAP SQL
|
|
21
|
+
* - abap_table: Preview table contents
|
|
22
|
+
* - abap_where_used: Where-used list
|
|
23
|
+
* - abap_object_types: List searchable object types
|
|
24
|
+
* - abap_node_contents: Browse package object tree
|
|
25
|
+
* - abap_transports: List user transports
|
|
26
|
+
*
|
|
27
|
+
* Data-oriented design:
|
|
28
|
+
* - All domain data is represented as plain immutable interfaces (types.ts)
|
|
29
|
+
* - I/O is isolated in the client module (client.ts)
|
|
30
|
+
* - Pure formatting functions convert data to display strings (formatting/)
|
|
31
|
+
* - Each tool is a single-responsibility module (tools/)
|
|
32
|
+
* - Configuration is managed as mutable state with file persistence (config.ts)
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
36
|
+
|
|
37
|
+
import { loadConfig } from "./src/config.ts";
|
|
38
|
+
import { AbapSetupTool } from "./src/tools/abap-setup.ts";
|
|
39
|
+
import { AbapStatusTool } from "./src/tools/abap-status.ts";
|
|
40
|
+
import { AbapProfileTool } from "./src/tools/abap-profile.ts";
|
|
41
|
+
import { AbapTestConnectionTool } from "./src/tools/abap-test-connection.ts";
|
|
42
|
+
import { AbapSearchTool } from "./src/tools/abap-search.ts";
|
|
43
|
+
import { AbapObjectStructureTool } from "./src/tools/abap-object-structure.ts";
|
|
44
|
+
import { AbapReadTool } from "./src/tools/abap-read.ts";
|
|
45
|
+
import { AbapLockTool } from "./src/tools/abap-lock.ts";
|
|
46
|
+
import { AbapUnlockTool } from "./src/tools/abap-unlock.ts";
|
|
47
|
+
import { AbapWriteTool } from "./src/tools/abap-write.ts";
|
|
48
|
+
import { AbapActivateTool } from "./src/tools/abap-activate.ts";
|
|
49
|
+
import { AbapSyntaxCheckTool } from "./src/tools/abap-syntax-check.ts";
|
|
50
|
+
import { AbapUnitTestTool } from "./src/tools/abap-unit-test.ts";
|
|
51
|
+
import { AbapQueryTool } from "./src/tools/abap-query.ts";
|
|
52
|
+
import { AbapTableTool } from "./src/tools/abap-table.ts";
|
|
53
|
+
import { AbapWhereUsedTool } from "./src/tools/abap-where-used.ts";
|
|
54
|
+
import { AbapObjectTypesTool } from "./src/tools/abap-object-types.ts";
|
|
55
|
+
import { AbapNodeContentsTool } from "./src/tools/abap-node-contents.ts";
|
|
56
|
+
import { AbapTransportsTool } from "./src/tools/abap-transports.ts";
|
|
57
|
+
|
|
58
|
+
export default function (pi: ExtensionAPI) {
|
|
59
|
+
// Load saved config on startup.
|
|
60
|
+
loadConfig();
|
|
61
|
+
|
|
62
|
+
// Register all tools.
|
|
63
|
+
pi.registerTool(AbapSetupTool);
|
|
64
|
+
pi.registerTool(AbapStatusTool);
|
|
65
|
+
pi.registerTool(AbapProfileTool);
|
|
66
|
+
pi.registerTool(AbapTestConnectionTool);
|
|
67
|
+
pi.registerTool(AbapSearchTool);
|
|
68
|
+
pi.registerTool(AbapObjectStructureTool);
|
|
69
|
+
pi.registerTool(AbapReadTool);
|
|
70
|
+
pi.registerTool(AbapLockTool);
|
|
71
|
+
pi.registerTool(AbapUnlockTool);
|
|
72
|
+
pi.registerTool(AbapWriteTool);
|
|
73
|
+
pi.registerTool(AbapActivateTool);
|
|
74
|
+
pi.registerTool(AbapSyntaxCheckTool);
|
|
75
|
+
pi.registerTool(AbapUnitTestTool);
|
|
76
|
+
pi.registerTool(AbapQueryTool);
|
|
77
|
+
pi.registerTool(AbapTableTool);
|
|
78
|
+
pi.registerTool(AbapWhereUsedTool);
|
|
79
|
+
pi.registerTool(AbapObjectTypesTool);
|
|
80
|
+
pi.registerTool(AbapNodeContentsTool);
|
|
81
|
+
pi.registerTool(AbapTransportsTool);
|
|
82
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@patimweb/pi-abap-fs",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ABAP remote filesystem extension for pi coding agent. Read, search, write and activate ABAP objects on SAP systems via the ADT protocol.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"abap",
|
|
8
|
+
"sap",
|
|
9
|
+
"adt",
|
|
10
|
+
"pi-extension"
|
|
11
|
+
],
|
|
12
|
+
"author": "Patrick Weppelmann",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=26.0.0"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/Smotherer007/pi-abap-fs.git"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/Smotherer007/pi-abap-fs/issues"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "echo 'No build step defined'",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "node --experimental-strip-types --experimental-test-module-mocks --test 'tests/**/*.test.ts'",
|
|
32
|
+
"test:watch": "node --experimental-strip-types --experimental-test-module-mocks --test --watch 'tests/**/*.test.ts'"
|
|
33
|
+
},
|
|
34
|
+
"pi": {
|
|
35
|
+
"extensions": [
|
|
36
|
+
"./index.ts"
|
|
37
|
+
],
|
|
38
|
+
"image": "https://raw.githubusercontent.com/Smotherer007/pi-abap-fs/main/screenshot.png"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"abap-adt-api": "^8.4.3",
|
|
42
|
+
"typebox": "^1.2.8"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@earendil-works/pi-coding-agent": "^0.84.3",
|
|
46
|
+
"@semantic-release/git": "^10.0.0",
|
|
47
|
+
"@types/node": "^24.0.0",
|
|
48
|
+
"semantic-release": "^25.0.0",
|
|
49
|
+
"typescript": "^5.8.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/screenshot.png
ADDED
|
Binary file
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADT client factory and operation runner.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the `abap-adt-api` ADTClient so tools only deal with plain data and
|
|
5
|
+
* clear, normalized errors. Connection lifecycle is managed per call:
|
|
6
|
+
* a fresh client is created, logged in, used, and its session dropped.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
ADTClient,
|
|
11
|
+
createSSLConfig,
|
|
12
|
+
session_types,
|
|
13
|
+
type AbapObjectStructure,
|
|
14
|
+
type SearchResult,
|
|
15
|
+
type ObjectTypeDescriptor,
|
|
16
|
+
} from "abap-adt-api";
|
|
17
|
+
import type { AbapProfile } from "./types.ts";
|
|
18
|
+
import { AbapObjectNotFoundError } from "./types.ts";
|
|
19
|
+
|
|
20
|
+
// Client creation
|
|
21
|
+
|
|
22
|
+
export function createClient(
|
|
23
|
+
profile: AbapProfile,
|
|
24
|
+
stateful = false,
|
|
25
|
+
): ADTClient {
|
|
26
|
+
const options = profile.allowUnauthorized ? createSSLConfig(true) : {};
|
|
27
|
+
const client = new ADTClient(
|
|
28
|
+
profile.url,
|
|
29
|
+
profile.username,
|
|
30
|
+
profile.password,
|
|
31
|
+
profile.client ?? "",
|
|
32
|
+
profile.language ?? "",
|
|
33
|
+
options,
|
|
34
|
+
);
|
|
35
|
+
client.stateful = stateful
|
|
36
|
+
? session_types.stateful
|
|
37
|
+
: session_types.stateless;
|
|
38
|
+
return client;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Error normalization
|
|
42
|
+
|
|
43
|
+
function rootMessage(err: unknown): string {
|
|
44
|
+
const e = err as { message?: string; localizedMessage?: string };
|
|
45
|
+
if (typeof e?.message === "string" && e.message.length > 0) return e.message;
|
|
46
|
+
return String(err);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Normalize any thrown value into a readable Error for the LLM. */
|
|
50
|
+
export function toError(err: unknown): Error {
|
|
51
|
+
if (err instanceof Error) {
|
|
52
|
+
// abap-adt-api errors already carry a useful message; keep it but strip
|
|
53
|
+
// redundant prefixes so the LLM sees the SAP message directly.
|
|
54
|
+
return new Error(rootMessage(err));
|
|
55
|
+
}
|
|
56
|
+
return new Error(String(err));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Operation runner
|
|
60
|
+
|
|
61
|
+
export interface RunOptions {
|
|
62
|
+
stateful?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Run an operation against a freshly-created, logged-in ADT client.
|
|
67
|
+
* The session is dropped afterwards, and errors are normalized.
|
|
68
|
+
*/
|
|
69
|
+
export async function withClient<T>(
|
|
70
|
+
profile: AbapProfile,
|
|
71
|
+
fn: (client: ADTClient) => Promise<T>,
|
|
72
|
+
options: RunOptions = {},
|
|
73
|
+
): Promise<T> {
|
|
74
|
+
const client = createClient(profile, options.stateful);
|
|
75
|
+
try {
|
|
76
|
+
await client.login();
|
|
77
|
+
return await fn(client);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
throw toError(err);
|
|
80
|
+
} finally {
|
|
81
|
+
try {
|
|
82
|
+
await client.dropSession();
|
|
83
|
+
} catch {
|
|
84
|
+
/* ignore */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Object resolution
|
|
90
|
+
|
|
91
|
+
export interface ResolvedObject {
|
|
92
|
+
/** Object name, e.g. ZCL_MY_CLASS */
|
|
93
|
+
name: string;
|
|
94
|
+
/** ADT type, e.g. CLAS/OC */
|
|
95
|
+
type: string;
|
|
96
|
+
/** Object URL, e.g. /sap/bc/adt/oo/classes/zcl_my_class */
|
|
97
|
+
objectUrl: string;
|
|
98
|
+
/** Parsed object structure */
|
|
99
|
+
structure: AbapObjectStructure;
|
|
100
|
+
/** Main source include URL (target for getObjectSource / setObjectSource) */
|
|
101
|
+
mainInclude: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolve an ABAP object to its structure and main source include.
|
|
106
|
+
*
|
|
107
|
+
* Either `objectUrl` (a full ADT object URL) or `name` (with an optional
|
|
108
|
+
* `type` hint for the search) must be provided.
|
|
109
|
+
*/
|
|
110
|
+
export async function resolveObject(
|
|
111
|
+
client: ADTClient,
|
|
112
|
+
params: {
|
|
113
|
+
name?: string;
|
|
114
|
+
objectUrl?: string;
|
|
115
|
+
type?: string;
|
|
116
|
+
},
|
|
117
|
+
): Promise<ResolvedObject> {
|
|
118
|
+
let objectUrl: string;
|
|
119
|
+
let name: string;
|
|
120
|
+
let type: string;
|
|
121
|
+
|
|
122
|
+
if (params.objectUrl) {
|
|
123
|
+
objectUrl = params.objectUrl;
|
|
124
|
+
const structure = await client.objectStructure(objectUrl);
|
|
125
|
+
name = structure.metaData["adtcore:name"] ?? "";
|
|
126
|
+
type = structure.metaData["adtcore:type"] ?? "";
|
|
127
|
+
return {
|
|
128
|
+
name,
|
|
129
|
+
type,
|
|
130
|
+
objectUrl,
|
|
131
|
+
structure,
|
|
132
|
+
mainInclude: ADTClient.mainInclude(structure),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!params.name) {
|
|
137
|
+
throw new Error("Provide either 'name' or 'objectUrl' to address an object.");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const results = await client.searchObject(params.name, params.type, 20);
|
|
141
|
+
const match = pickSearchResult(results, params.name);
|
|
142
|
+
if (!match) {
|
|
143
|
+
throw new AbapObjectNotFoundError(params.name);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
objectUrl = match["adtcore:uri"];
|
|
147
|
+
const structure = await client.objectStructure(objectUrl);
|
|
148
|
+
name = structure.metaData["adtcore:name"] ?? match["adtcore:name"];
|
|
149
|
+
type = structure.metaData["adtcore:type"] ?? match["adtcore:type"];
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
name,
|
|
153
|
+
type,
|
|
154
|
+
objectUrl,
|
|
155
|
+
structure,
|
|
156
|
+
mainInclude: ADTClient.mainInclude(structure),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Pick the best search result for a query: an exact (case-insensitive) name
|
|
162
|
+
* match wins, otherwise the first result is used.
|
|
163
|
+
*/
|
|
164
|
+
export function pickSearchResult(
|
|
165
|
+
results: ReadonlyArray<SearchResult>,
|
|
166
|
+
query: string,
|
|
167
|
+
): SearchResult | undefined {
|
|
168
|
+
if (results.length === 0) return undefined;
|
|
169
|
+
const q = query.toLowerCase();
|
|
170
|
+
return results.find((r) => r["adtcore:name"].toLowerCase() === q) ?? results[0];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function listObjectTypes(
|
|
174
|
+
client: ADTClient,
|
|
175
|
+
): Promise<ObjectTypeDescriptor[]> {
|
|
176
|
+
return client.objectTypes();
|
|
177
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration persistence and state.
|
|
3
|
+
*
|
|
4
|
+
* Stores named SAP connection profiles in ~/.pi/abap-config.json.
|
|
5
|
+
* Supports multiple profiles with an active profile selector.
|
|
6
|
+
*
|
|
7
|
+
* File format:
|
|
8
|
+
* { "profiles": { "name": AbapProfile, ... }, "activeProfile": "name" }
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { AbapProfile, AbapProfiles } from "./types.ts";
|
|
12
|
+
import { AbapNotConfiguredError, AbapProfileNotFoundError } from "./types.ts";
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
|
|
16
|
+
// Mutable state
|
|
17
|
+
|
|
18
|
+
let profiles: Record<string, AbapProfile> = {};
|
|
19
|
+
let activeProfile: string | null = null;
|
|
20
|
+
|
|
21
|
+
// Path resolution
|
|
22
|
+
|
|
23
|
+
function configPath(): string {
|
|
24
|
+
const home = process.env.HOME || process.env.USERPROFILE || "~";
|
|
25
|
+
return path.join(home, ".pi", "abap-config.json");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Persistence
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Write the profile store.
|
|
32
|
+
*
|
|
33
|
+
* The file holds plaintext passwords, so it must never be group- or
|
|
34
|
+
* world-readable. writeFileSync's `mode` only applies when the file is
|
|
35
|
+
* created, so an existing file keeps its old (possibly 0644) permissions —
|
|
36
|
+
* we therefore write to a private temp file and rename it into place. That
|
|
37
|
+
* is also atomic, so a crash mid-write cannot truncate the config.
|
|
38
|
+
*/
|
|
39
|
+
function persistProfiles(): void {
|
|
40
|
+
const filePath = configPath();
|
|
41
|
+
const dir = path.dirname(filePath);
|
|
42
|
+
if (!fs.existsSync(dir)) {
|
|
43
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const data: AbapProfiles = { profiles, activeProfile };
|
|
47
|
+
const tmpPath = `${filePath}.${process.pid}.tmp`;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2), {
|
|
51
|
+
encoding: "utf-8",
|
|
52
|
+
mode: 0o600,
|
|
53
|
+
});
|
|
54
|
+
fs.chmodSync(tmpPath, 0o600);
|
|
55
|
+
fs.renameSync(tmpPath, filePath);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
try {
|
|
58
|
+
fs.unlinkSync(tmpPath);
|
|
59
|
+
} catch {
|
|
60
|
+
/* ignore */
|
|
61
|
+
}
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function loadConfig(): void {
|
|
67
|
+
try {
|
|
68
|
+
const filePath = configPath();
|
|
69
|
+
if (fs.existsSync(filePath)) {
|
|
70
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
71
|
+
|
|
72
|
+
// Tighten permissions on config files written by older versions.
|
|
73
|
+
try {
|
|
74
|
+
const mode = fs.statSync(filePath).mode & 0o777;
|
|
75
|
+
if (mode !== 0o600) fs.chmodSync(filePath, 0o600);
|
|
76
|
+
} catch {
|
|
77
|
+
/* ignore */
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (raw && typeof raw === "object" && raw.profiles) {
|
|
81
|
+
profiles = raw.profiles;
|
|
82
|
+
if (raw.activeProfile && profiles[raw.activeProfile]) {
|
|
83
|
+
activeProfile = raw.activeProfile;
|
|
84
|
+
} else if (Object.keys(profiles).length > 0) {
|
|
85
|
+
activeProfile = Object.keys(profiles)[0];
|
|
86
|
+
} else {
|
|
87
|
+
activeProfile = null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
profiles = {};
|
|
93
|
+
activeProfile = null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Access
|
|
98
|
+
|
|
99
|
+
export function getConfig(): AbapProfile | null {
|
|
100
|
+
if (!activeProfile) return null;
|
|
101
|
+
return profiles[activeProfile] ?? null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getConfigOrThrow(): AbapProfile {
|
|
105
|
+
const config = getConfig();
|
|
106
|
+
if (!config) {
|
|
107
|
+
throw new AbapNotConfiguredError();
|
|
108
|
+
}
|
|
109
|
+
return config;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Get a specific profile by name, or null if not found. */
|
|
113
|
+
export function getProfile(name: string): AbapProfile | null {
|
|
114
|
+
return profiles[name] ?? null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolve a profile parameter: if a name is given, return that profile
|
|
119
|
+
* (or throw if not found). Otherwise, return the active profile or throw.
|
|
120
|
+
*/
|
|
121
|
+
export function resolveConfig(profileName?: string): AbapProfile {
|
|
122
|
+
if (profileName) {
|
|
123
|
+
const config = getProfile(profileName);
|
|
124
|
+
if (!config) {
|
|
125
|
+
throw new AbapProfileNotFoundError(profileName, Object.keys(profiles));
|
|
126
|
+
}
|
|
127
|
+
return config;
|
|
128
|
+
}
|
|
129
|
+
return getConfigOrThrow();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function getProfiles(): Record<string, AbapProfile> {
|
|
133
|
+
return { ...profiles };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function getActiveProfile(): string | null {
|
|
137
|
+
return activeProfile;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Mutations
|
|
141
|
+
|
|
142
|
+
export function saveProfile(name: string, config: AbapProfile): void {
|
|
143
|
+
profiles[name] = config;
|
|
144
|
+
// Auto-set as active if it's the first profile or no active profile.
|
|
145
|
+
if (!activeProfile || Object.keys(profiles).length === 1) {
|
|
146
|
+
activeProfile = name;
|
|
147
|
+
}
|
|
148
|
+
persistProfiles();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function setActiveProfile(name: string): void {
|
|
152
|
+
if (!profiles[name]) {
|
|
153
|
+
throw new AbapProfileNotFoundError(name, Object.keys(profiles));
|
|
154
|
+
}
|
|
155
|
+
activeProfile = name;
|
|
156
|
+
persistProfiles();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function deleteProfile(name: string): boolean {
|
|
160
|
+
if (!profiles[name]) return false;
|
|
161
|
+
delete profiles[name];
|
|
162
|
+
if (activeProfile === name) {
|
|
163
|
+
const keys = Object.keys(profiles);
|
|
164
|
+
activeProfile = keys.length > 0 ? keys[0] : null;
|
|
165
|
+
}
|
|
166
|
+
persistProfiles();
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** @internal Reset state — for testing only */
|
|
171
|
+
export function _resetForTesting(): void {
|
|
172
|
+
profiles = {};
|
|
173
|
+
activeProfile = null;
|
|
174
|
+
}
|