@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 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
+ }