@abapify/adt-tui 0.4.0 → 0.4.3
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/dist/index.d.mts +257 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +836 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +3 -3
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import React$1, { ReactNode } from "react";
|
|
2
|
+
//#region src/lib/types.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Core types for ADT TUI
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Hypermedia link extracted from ADT responses
|
|
8
|
+
*/
|
|
9
|
+
interface HypermediaLink {
|
|
10
|
+
href: string;
|
|
11
|
+
rel: string;
|
|
12
|
+
type?: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Parsed ADT response
|
|
17
|
+
*/
|
|
18
|
+
interface ParsedResponse {
|
|
19
|
+
raw: string;
|
|
20
|
+
data: Record<string, unknown>;
|
|
21
|
+
links: HypermediaLink[];
|
|
22
|
+
namespace?: string;
|
|
23
|
+
rootElement?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Navigation entry in history
|
|
27
|
+
*/
|
|
28
|
+
interface NavigationEntry {
|
|
29
|
+
url: string;
|
|
30
|
+
response: ParsedResponse;
|
|
31
|
+
timestamp: Date;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Fetch function type - abstracted from client
|
|
35
|
+
*/
|
|
36
|
+
type FetchFn = (url: string, options?: {
|
|
37
|
+
method?: string;
|
|
38
|
+
headers?: Record<string, string>;
|
|
39
|
+
body?: string;
|
|
40
|
+
}) => Promise<string>;
|
|
41
|
+
/**
|
|
42
|
+
* Menu item for navigation
|
|
43
|
+
*/
|
|
44
|
+
interface MenuItem {
|
|
45
|
+
/** Unique key for React */
|
|
46
|
+
key: string;
|
|
47
|
+
/** Display label */
|
|
48
|
+
label: string;
|
|
49
|
+
/** Icon (emoji) */
|
|
50
|
+
icon?: string;
|
|
51
|
+
/** URL to navigate to (if navigable) */
|
|
52
|
+
href?: string;
|
|
53
|
+
/** Whether this is a back action */
|
|
54
|
+
isBack?: boolean;
|
|
55
|
+
/** Additional info shown dimmed */
|
|
56
|
+
info?: string;
|
|
57
|
+
/** Status indicator */
|
|
58
|
+
status?: {
|
|
59
|
+
text: string;
|
|
60
|
+
color: string;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Page result returned by page components
|
|
65
|
+
*/
|
|
66
|
+
interface PageResult {
|
|
67
|
+
/** Page title */
|
|
68
|
+
title: string;
|
|
69
|
+
/** Title icon */
|
|
70
|
+
icon?: string;
|
|
71
|
+
/** Title color */
|
|
72
|
+
color?: string;
|
|
73
|
+
/** Subtitle (shown dimmed) */
|
|
74
|
+
subtitle?: string;
|
|
75
|
+
/** Content to render (React element) */
|
|
76
|
+
content: React.ReactNode;
|
|
77
|
+
/** Menu items for navigation */
|
|
78
|
+
menu: MenuItem[];
|
|
79
|
+
/** Footer text */
|
|
80
|
+
footer?: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Props passed to every page component
|
|
84
|
+
*/
|
|
85
|
+
interface PageProps {
|
|
86
|
+
/** Current URL */
|
|
87
|
+
url: string;
|
|
88
|
+
/** Parsed response data */
|
|
89
|
+
response: ParsedResponse;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Page component type - returns PageResult, not JSX
|
|
93
|
+
*/
|
|
94
|
+
type PageComponent = (props: PageProps) => PageResult | null;
|
|
95
|
+
/**
|
|
96
|
+
* Route definition
|
|
97
|
+
*/
|
|
98
|
+
interface Route {
|
|
99
|
+
/** URL pattern (string for exact, RegExp for pattern) */
|
|
100
|
+
pattern: RegExp | string;
|
|
101
|
+
/** Page function that returns PageResult */
|
|
102
|
+
page: PageComponent;
|
|
103
|
+
/** Optional name for debugging */
|
|
104
|
+
name?: string;
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/lib/router.d.ts
|
|
108
|
+
/**
|
|
109
|
+
* Router class for URL pattern matching
|
|
110
|
+
*/
|
|
111
|
+
declare class Router {
|
|
112
|
+
private readonly routes;
|
|
113
|
+
private fallback;
|
|
114
|
+
/**
|
|
115
|
+
* Register a route
|
|
116
|
+
*/
|
|
117
|
+
register(route: Route): this;
|
|
118
|
+
/**
|
|
119
|
+
* Set fallback page for unmatched routes
|
|
120
|
+
*/
|
|
121
|
+
setFallback(page: PageComponent): this;
|
|
122
|
+
/**
|
|
123
|
+
* Match URL to a route
|
|
124
|
+
*/
|
|
125
|
+
match(url: string): {
|
|
126
|
+
page: PageComponent;
|
|
127
|
+
name?: string;
|
|
128
|
+
} | null;
|
|
129
|
+
/**
|
|
130
|
+
* List all registered routes
|
|
131
|
+
*/
|
|
132
|
+
listRoutes(): Array<{
|
|
133
|
+
pattern: string;
|
|
134
|
+
name?: string;
|
|
135
|
+
}>;
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/run.d.ts
|
|
139
|
+
interface RunOptions {
|
|
140
|
+
/** Optional starting URL - if not provided, shows prompt */
|
|
141
|
+
startUrl?: string;
|
|
142
|
+
/** Fetch function for making HTTP requests */
|
|
143
|
+
fetch: FetchFn;
|
|
144
|
+
/** Optional custom router */
|
|
145
|
+
router?: Router;
|
|
146
|
+
/** SAP system name for ADT links (e.g., 'TRL', 'NPL') */
|
|
147
|
+
systemName?: string;
|
|
148
|
+
/** Called when app exits */
|
|
149
|
+
onExit?: () => void;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Run the TUI application
|
|
153
|
+
*
|
|
154
|
+
* @returns Promise that resolves when the app exits
|
|
155
|
+
*/
|
|
156
|
+
declare function run(options: RunOptions): Promise<void>;
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/App.d.ts
|
|
159
|
+
interface AppProps {
|
|
160
|
+
/** Optional starting URL - if not provided, shows prompt */
|
|
161
|
+
startUrl?: string;
|
|
162
|
+
/** Fetch function for making HTTP requests */
|
|
163
|
+
fetch: FetchFn;
|
|
164
|
+
/** Optional custom router */
|
|
165
|
+
router?: Router;
|
|
166
|
+
/** SAP system name for ADT links (e.g., 'TRL', 'NPL') */
|
|
167
|
+
systemName?: string;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Main TUI Application
|
|
171
|
+
*/
|
|
172
|
+
declare function App({ startUrl, fetch, router: _router, systemName }: AppProps): React.ReactNode;
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/Navigator.d.ts
|
|
175
|
+
interface NavigatorProps {
|
|
176
|
+
/** Optional starting URL - if not provided, shows prompt */
|
|
177
|
+
startUrl?: string;
|
|
178
|
+
/** Fetch function */
|
|
179
|
+
fetch: FetchFn;
|
|
180
|
+
/** SAP system name for ADT links (e.g., 'TRL', 'NPL') */
|
|
181
|
+
systemName?: string;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Main Navigator component
|
|
185
|
+
*/
|
|
186
|
+
declare function Navigator({ startUrl, fetch, systemName }: NavigatorProps): React.ReactNode;
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/lib/PageRenderer.d.ts
|
|
189
|
+
interface PageRendererProps {
|
|
190
|
+
result: PageResult;
|
|
191
|
+
onNavigate: (url: string) => void;
|
|
192
|
+
onBack: () => void;
|
|
193
|
+
onAction?: (key: string) => void;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* PageRenderer component
|
|
197
|
+
*/
|
|
198
|
+
declare function PageRenderer({ result, onNavigate, onBack, onAction }: PageRendererProps): React.ReactNode;
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/lib/context.d.ts
|
|
201
|
+
interface NavigationContextValue {
|
|
202
|
+
/** Current navigation entry */
|
|
203
|
+
current: NavigationEntry | null;
|
|
204
|
+
/** Navigation history */
|
|
205
|
+
history: NavigationEntry[];
|
|
206
|
+
/** Navigate to URL */
|
|
207
|
+
navigate: (url: string) => Promise<void>;
|
|
208
|
+
/** Go back */
|
|
209
|
+
back: () => void;
|
|
210
|
+
/** Is loading */
|
|
211
|
+
loading: boolean;
|
|
212
|
+
/** Error message */
|
|
213
|
+
error: string | null;
|
|
214
|
+
/** Fetch function */
|
|
215
|
+
fetch: FetchFn;
|
|
216
|
+
}
|
|
217
|
+
interface NavigationProviderProps {
|
|
218
|
+
children: ReactNode;
|
|
219
|
+
/** Fetch function for making requests */
|
|
220
|
+
fetch: FetchFn;
|
|
221
|
+
/** Parse response function */
|
|
222
|
+
parseResponse: (xml: string) => ParsedResponse;
|
|
223
|
+
/** Initial URL to load */
|
|
224
|
+
initialUrl?: string;
|
|
225
|
+
}
|
|
226
|
+
declare function NavigationProvider({ children, fetch, parseResponse, initialUrl }: NavigationProviderProps): React$1.ReactNode;
|
|
227
|
+
/**
|
|
228
|
+
* Hook to access navigation context
|
|
229
|
+
*/
|
|
230
|
+
declare function useNavigation(): NavigationContextValue;
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region src/lib/parser.d.ts
|
|
233
|
+
/**
|
|
234
|
+
* Parse ADT XML response
|
|
235
|
+
*/
|
|
236
|
+
declare function parseResponse$1(xml: string): ParsedResponse;
|
|
237
|
+
/**
|
|
238
|
+
* Get action name from relation URL
|
|
239
|
+
*/
|
|
240
|
+
declare function getActionName(rel: string): string;
|
|
241
|
+
/**
|
|
242
|
+
* Categorize links by type
|
|
243
|
+
*/
|
|
244
|
+
declare function categorizeLinks(links: HypermediaLink[]): {
|
|
245
|
+
navigation: HypermediaLink[];
|
|
246
|
+
actions: HypermediaLink[];
|
|
247
|
+
self: HypermediaLink[];
|
|
248
|
+
};
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region src/pages/GenericPage.d.ts
|
|
251
|
+
/**
|
|
252
|
+
* Generic page - returns PageResult for framework rendering
|
|
253
|
+
*/
|
|
254
|
+
declare function genericPage({ url, response }: PageProps): PageResult;
|
|
255
|
+
//#endregion
|
|
256
|
+
export { App, type AppProps, type FetchFn, type HypermediaLink, type MenuItem, type NavigationEntry, NavigationProvider, Navigator, type PageComponent, type PageProps, PageRenderer, type PageResult, type ParsedResponse, type Route, type RunOptions, categorizeLinks, genericPage, getActionName, parseResponse$1 as parseResponse, run, useNavigation };
|
|
257
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/lib/types.ts","../src/lib/router.ts","../src/run.tsx","../src/App.tsx","../src/Navigator.tsx","../src/lib/PageRenderer.tsx","../src/lib/context.tsx","../src/lib/parser.ts","../src/pages/GenericPage.tsx"],"mappings":";;;;;;;;UASiB;EACf;EACA;EACA;EACA;;;;;UAMe;EACf;EACA,MAAM;EACN,OAAO;EACP;EACA;;;;;UAMe;EACf;EACA,UAAU;EACV,WAAW;;;;;KAMD,WACV,aACA;EACE;EACA,UAAU;EACV;MAEC;;;;UAKY;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;IAAW;IAAc;;;;;;UAMV;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA,SAAS,MAAM;;EAEf,MAAM;;EAEN;;;;;UAMe;;EAEf;;EAEA,UAAU;;;;;KAMA,iBAAiB,OAAO,cAAc;;;;UAKjC;;EAEf,SAAS;;EAET,MAAM;;EAEN;;;;;;;cCrGW;mBACM;UACT;;;;EAKR,SAAS,OAAO;;;;EAQhB,YAAY,MAAM;;;;EAQlB,MAAM;IAAgB,MAAM;IAAe;;;;;EAqB3C,cAAc;IAAQ;IAAiB;;;;;UC3CxB;;EAEf;;EAEA,OAAO;;EAEP,SAAS;;EAET;;EAEA;;;;;;;iBAQoB,IAAI,SAAS,aAAa;;;UCnB/B;;EAEf;;EAEA,OAAO;;EAEP,SAAS;;EAET;;;;;iBAMc,MACd,UACA,OACA,QAAQ,SACR,cACC,WAAW,MAAM;;;UCRV;;EAER;;EAEA,OAAO;;EAEP;;;;;iBAwIc,YACd,UACA,OACA,cACC,iBAAiB,MAAM;;;UC1JhB;EACR,QAAQ;EACR,aAAa;EACb;EACA,YAAY;;;;;iBAkCE,eACd,QACA,YACA,QACA,YACC,oBAAoB,MAAM;;;UC1CnB;;EAER,SAAS;;EAET,SAAS;;EAET,WAAW,gBAAgB;;EAE3B;;EAEA;;EAEA;;EAEA,OAAO;;UAKC;EACR,UAAU;;EAEV,OAAO;;EAEP,gBAAgB,gBAAgB;;EAEhC;;iBAGc,qBACd,UACA,OACA,eACA,cACC,0BAA0B,QAAM;;;;iBA0EnB,iBAAiB;;;;;;iBC5BjB,gBAAc,cAAc;;;;iBAiB5B,cAAc;;;;iBAQd,gBAAgB,OAAO;EACrC,YAAY;EACZ,SAAS;EACT,MAAM;;;;;;;iBCvGQ,cAAc,KAAK,YAAY,YAAY"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
import { Box, Text, render, useInput } from "ink";
|
|
2
|
+
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import Spinner from "ink-spinner";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { exec, execFile } from "node:child_process";
|
|
9
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
import { XMLParser } from "fast-xml-parser";
|
|
11
|
+
import { transportmanagmentSingle } from "@abapify/adt-contracts";
|
|
12
|
+
|
|
13
|
+
//#region src/lib/context.tsx
|
|
14
|
+
/**
|
|
15
|
+
* Navigation Context
|
|
16
|
+
*
|
|
17
|
+
* React context for sharing navigation state across components.
|
|
18
|
+
*/
|
|
19
|
+
const NavigationContext = createContext(null);
|
|
20
|
+
function NavigationProvider({ children, fetch, parseResponse, initialUrl }) {
|
|
21
|
+
const [current, setCurrent] = useState(null);
|
|
22
|
+
const [history, setHistory] = useState([]);
|
|
23
|
+
const [loading, setLoading] = useState(false);
|
|
24
|
+
const [error, setError] = useState(null);
|
|
25
|
+
const navigate = useCallback(async (url) => {
|
|
26
|
+
setLoading(true);
|
|
27
|
+
setError(null);
|
|
28
|
+
try {
|
|
29
|
+
const entry = {
|
|
30
|
+
url,
|
|
31
|
+
response: parseResponse(await fetch(url, {
|
|
32
|
+
method: "GET",
|
|
33
|
+
headers: { Accept: "*/*" }
|
|
34
|
+
})),
|
|
35
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
36
|
+
};
|
|
37
|
+
if (current) setHistory((prev) => [...prev, current]);
|
|
38
|
+
setCurrent(entry);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
41
|
+
} finally {
|
|
42
|
+
setLoading(false);
|
|
43
|
+
}
|
|
44
|
+
}, [
|
|
45
|
+
current,
|
|
46
|
+
fetch,
|
|
47
|
+
parseResponse
|
|
48
|
+
]);
|
|
49
|
+
const back = useCallback(() => {
|
|
50
|
+
if (history.length === 0) return;
|
|
51
|
+
const prev = history[history.length - 1];
|
|
52
|
+
setHistory((h) => h.slice(0, -1));
|
|
53
|
+
setCurrent(prev);
|
|
54
|
+
}, [history]);
|
|
55
|
+
React.useEffect(() => {
|
|
56
|
+
if (initialUrl && !current) navigate(initialUrl);
|
|
57
|
+
}, [
|
|
58
|
+
initialUrl,
|
|
59
|
+
current,
|
|
60
|
+
navigate
|
|
61
|
+
]);
|
|
62
|
+
return /* @__PURE__ */ jsx(NavigationContext.Provider, {
|
|
63
|
+
value: {
|
|
64
|
+
current,
|
|
65
|
+
history,
|
|
66
|
+
navigate,
|
|
67
|
+
back,
|
|
68
|
+
loading,
|
|
69
|
+
error,
|
|
70
|
+
fetch
|
|
71
|
+
},
|
|
72
|
+
children
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Hook to access navigation context
|
|
77
|
+
*/
|
|
78
|
+
function useNavigation() {
|
|
79
|
+
const context = useContext(NavigationContext);
|
|
80
|
+
if (!context) throw new Error("useNavigation must be used within NavigationProvider");
|
|
81
|
+
return context;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region src/lib/parser.ts
|
|
86
|
+
/**
|
|
87
|
+
* XML Response Parser
|
|
88
|
+
*
|
|
89
|
+
* Parses ADT XML responses and extracts hypermedia links.
|
|
90
|
+
*/
|
|
91
|
+
const parser = new XMLParser({
|
|
92
|
+
ignoreAttributes: false,
|
|
93
|
+
attributeNamePrefix: "@_",
|
|
94
|
+
removeNSPrefix: false,
|
|
95
|
+
parseAttributeValue: true,
|
|
96
|
+
trimValues: true
|
|
97
|
+
});
|
|
98
|
+
/**
|
|
99
|
+
* Extract all atom:link elements from parsed XML object
|
|
100
|
+
*/
|
|
101
|
+
function extractLinks(obj, links = []) {
|
|
102
|
+
if (!obj || typeof obj !== "object") return links;
|
|
103
|
+
if (Array.isArray(obj)) for (const item of obj) extractLinks(item, links);
|
|
104
|
+
else {
|
|
105
|
+
const record = obj;
|
|
106
|
+
const linkKey = Object.keys(record).find((k) => k === "atom:link" || k === "link" || k.endsWith(":link"));
|
|
107
|
+
if (linkKey) {
|
|
108
|
+
const linkData = record[linkKey];
|
|
109
|
+
const linkArray = Array.isArray(linkData) ? linkData : [linkData];
|
|
110
|
+
for (const link of linkArray) if (link && typeof link === "object") {
|
|
111
|
+
const linkObj = link;
|
|
112
|
+
const href = linkObj["@_href"];
|
|
113
|
+
const rel = linkObj["@_rel"];
|
|
114
|
+
if (href && rel) links.push({
|
|
115
|
+
href,
|
|
116
|
+
rel,
|
|
117
|
+
type: linkObj["@_type"],
|
|
118
|
+
title: linkObj["@_title"]
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const key of Object.keys(record)) if (key !== linkKey) extractLinks(record[key], links);
|
|
123
|
+
}
|
|
124
|
+
return links;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Detect root element and namespace from parsed XML
|
|
128
|
+
*/
|
|
129
|
+
function detectRootInfo(data) {
|
|
130
|
+
const keys = Object.keys(data).filter((k) => k !== "?xml");
|
|
131
|
+
if (keys.length === 0) return {};
|
|
132
|
+
const rootKey = keys[0];
|
|
133
|
+
const colonIndex = rootKey.indexOf(":");
|
|
134
|
+
if (colonIndex > 0) return {
|
|
135
|
+
namespace: rootKey.substring(0, colonIndex),
|
|
136
|
+
rootElement: rootKey.substring(colonIndex + 1)
|
|
137
|
+
};
|
|
138
|
+
return { rootElement: rootKey };
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Parse ADT XML response
|
|
142
|
+
*/
|
|
143
|
+
function parseResponse(xml) {
|
|
144
|
+
const data = parser.parse(xml);
|
|
145
|
+
const links = extractLinks(data);
|
|
146
|
+
const { namespace, rootElement } = detectRootInfo(data);
|
|
147
|
+
return {
|
|
148
|
+
raw: xml,
|
|
149
|
+
data,
|
|
150
|
+
links,
|
|
151
|
+
namespace,
|
|
152
|
+
rootElement
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get action name from relation URL
|
|
157
|
+
*/
|
|
158
|
+
function getActionName(rel) {
|
|
159
|
+
const lastSlash = rel.lastIndexOf("/");
|
|
160
|
+
return lastSlash >= 0 ? rel.substring(lastSlash + 1) : rel;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Categorize links by type
|
|
164
|
+
*/
|
|
165
|
+
function categorizeLinks(links) {
|
|
166
|
+
const navigation = [];
|
|
167
|
+
const actions = [];
|
|
168
|
+
const self = [];
|
|
169
|
+
for (const link of links) if (link.rel === "self") self.push(link);
|
|
170
|
+
else if (link.rel.includes("/relations/") || link.rel.includes("adturi")) actions.push(link);
|
|
171
|
+
else navigation.push(link);
|
|
172
|
+
return {
|
|
173
|
+
navigation,
|
|
174
|
+
actions,
|
|
175
|
+
self
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/lib/PageRenderer.tsx
|
|
181
|
+
/**
|
|
182
|
+
* PageRenderer - Framework component that renders PageResult
|
|
183
|
+
*
|
|
184
|
+
* Handles:
|
|
185
|
+
* - Header with title/icon
|
|
186
|
+
* - Page content
|
|
187
|
+
* - Menu navigation with keyboard
|
|
188
|
+
* - Footer
|
|
189
|
+
*/
|
|
190
|
+
/**
|
|
191
|
+
* Render a single menu item
|
|
192
|
+
*/
|
|
193
|
+
function MenuItemRow({ item, isSelected }) {
|
|
194
|
+
return /* @__PURE__ */ jsxs(Text, {
|
|
195
|
+
color: isSelected ? "cyan" : void 0,
|
|
196
|
+
children: [
|
|
197
|
+
isSelected ? "❯ " : " ",
|
|
198
|
+
item.icon && `${item.icon} `,
|
|
199
|
+
item.label,
|
|
200
|
+
item.status && /* @__PURE__ */ jsxs(Text, {
|
|
201
|
+
color: item.status.color,
|
|
202
|
+
dimColor: !isSelected,
|
|
203
|
+
children: [
|
|
204
|
+
" ",
|
|
205
|
+
"[",
|
|
206
|
+
item.status.text,
|
|
207
|
+
"]"
|
|
208
|
+
]
|
|
209
|
+
}),
|
|
210
|
+
item.info && /* @__PURE__ */ jsxs(Text, {
|
|
211
|
+
dimColor: true,
|
|
212
|
+
children: [" ", item.info]
|
|
213
|
+
})
|
|
214
|
+
]
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* PageRenderer component
|
|
219
|
+
*/
|
|
220
|
+
function PageRenderer({ result, onNavigate, onBack, onAction }) {
|
|
221
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
222
|
+
useInput((input, key) => {
|
|
223
|
+
if (key.upArrow) setSelectedIndex((i) => Math.max(0, i - 1));
|
|
224
|
+
else if (key.downArrow) setSelectedIndex((i) => Math.min(result.menu.length - 1, i + 1));
|
|
225
|
+
else if (key.return) {
|
|
226
|
+
const item = result.menu[selectedIndex];
|
|
227
|
+
if (item.isBack) onBack();
|
|
228
|
+
else if (item.href) onNavigate(item.href);
|
|
229
|
+
else if (onAction) onAction(item.key);
|
|
230
|
+
} else if (input === "o" || input === "O") onAction?.("openXml");
|
|
231
|
+
else if (input === "e" || input === "E") onAction?.("openAdt");
|
|
232
|
+
});
|
|
233
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
234
|
+
flexDirection: "column",
|
|
235
|
+
padding: 1,
|
|
236
|
+
children: [
|
|
237
|
+
/* @__PURE__ */ jsxs(Box, {
|
|
238
|
+
borderStyle: "round",
|
|
239
|
+
borderColor: result.color || "cyan",
|
|
240
|
+
paddingX: 2,
|
|
241
|
+
marginBottom: 1,
|
|
242
|
+
children: [/* @__PURE__ */ jsxs(Text, {
|
|
243
|
+
bold: true,
|
|
244
|
+
color: result.color || "cyan",
|
|
245
|
+
children: [result.icon && `${result.icon} `, result.title]
|
|
246
|
+
}), result.subtitle && /* @__PURE__ */ jsxs(Text, {
|
|
247
|
+
dimColor: true,
|
|
248
|
+
children: [
|
|
249
|
+
" (",
|
|
250
|
+
result.subtitle,
|
|
251
|
+
")"
|
|
252
|
+
]
|
|
253
|
+
})]
|
|
254
|
+
}),
|
|
255
|
+
/* @__PURE__ */ jsx(Box, {
|
|
256
|
+
flexDirection: "column",
|
|
257
|
+
marginBottom: 1,
|
|
258
|
+
children: result.content
|
|
259
|
+
}),
|
|
260
|
+
/* @__PURE__ */ jsx(Box, {
|
|
261
|
+
flexDirection: "column",
|
|
262
|
+
children: result.menu.map((item, i) => /* @__PURE__ */ jsx(MenuItemRow, {
|
|
263
|
+
item,
|
|
264
|
+
isSelected: i === selectedIndex
|
|
265
|
+
}, item.key))
|
|
266
|
+
}),
|
|
267
|
+
/* @__PURE__ */ jsx(Box, {
|
|
268
|
+
marginTop: 1,
|
|
269
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
270
|
+
dimColor: true,
|
|
271
|
+
children: result.footer || "[↑↓] navigate | [Enter] select"
|
|
272
|
+
})
|
|
273
|
+
})
|
|
274
|
+
]
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region src/pages/sap/bc/adt/cts/transportrequests/[slug].loader.ts
|
|
280
|
+
/**
|
|
281
|
+
* Transport Request/Task Loader
|
|
282
|
+
*
|
|
283
|
+
* Uses adt-contracts for type-safe XML parsing.
|
|
284
|
+
*/
|
|
285
|
+
/**
|
|
286
|
+
* Load and parse transport request/task XML
|
|
287
|
+
*/
|
|
288
|
+
function load(xml) {
|
|
289
|
+
return transformToTransportData(transportmanagmentSingle.parse(xml));
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Transform parsed XSD data to TransportData
|
|
293
|
+
*/
|
|
294
|
+
function transformToTransportData(data) {
|
|
295
|
+
const request = data.root?.request;
|
|
296
|
+
if (!request) throw new Error("No request element found in transport data");
|
|
297
|
+
const isTask = !!request.parent;
|
|
298
|
+
const tasks = [];
|
|
299
|
+
if (request.task && !isTask) {
|
|
300
|
+
const taskArray = Array.isArray(request.task) ? request.task : [request.task];
|
|
301
|
+
for (const task of taskArray) tasks.push({
|
|
302
|
+
number: task.number ?? "",
|
|
303
|
+
owner: task.owner ?? "",
|
|
304
|
+
desc: task.desc ?? "",
|
|
305
|
+
status: task.status ?? "",
|
|
306
|
+
statusText: task.status_text ?? "",
|
|
307
|
+
uri: task.uri ?? ""
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
const objects = [];
|
|
311
|
+
if (request.task && isTask) {
|
|
312
|
+
const taskArray = Array.isArray(request.task) ? request.task : [request.task];
|
|
313
|
+
for (const task of taskArray) if (task.abap_object) {
|
|
314
|
+
const objArray = Array.isArray(task.abap_object) ? task.abap_object : [task.abap_object];
|
|
315
|
+
for (const obj of objArray) objects.push({
|
|
316
|
+
pgmid: obj.pgmid ?? "",
|
|
317
|
+
type: obj.type ?? "",
|
|
318
|
+
name: obj.name ?? "",
|
|
319
|
+
info: obj.obj_info ?? obj.obj_desc ?? "",
|
|
320
|
+
lockStatus: obj.lock_status ?? "",
|
|
321
|
+
uri: obj.uri
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
isTask,
|
|
327
|
+
number: request.number ?? "",
|
|
328
|
+
parent: request.parent,
|
|
329
|
+
owner: request.owner ?? "",
|
|
330
|
+
desc: request.desc ?? "",
|
|
331
|
+
type: request.type ?? "",
|
|
332
|
+
status: request.status ?? "",
|
|
333
|
+
statusText: request.status_text ?? "",
|
|
334
|
+
target: request.target ?? "",
|
|
335
|
+
targetDesc: request.target_desc ?? "",
|
|
336
|
+
tasks,
|
|
337
|
+
objects,
|
|
338
|
+
raw: data
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
//#endregion
|
|
343
|
+
//#region src/pages/sap/bc/adt/cts/transportrequests/[slug].tsx
|
|
344
|
+
/**
|
|
345
|
+
* Transport Request/Task Page
|
|
346
|
+
*
|
|
347
|
+
* Returns PageResult for framework-driven rendering.
|
|
348
|
+
* Uses schema-driven parsing via [slug].loader.ts
|
|
349
|
+
*/
|
|
350
|
+
/**
|
|
351
|
+
* Get status color
|
|
352
|
+
*/
|
|
353
|
+
function getStatusColor(status) {
|
|
354
|
+
switch (status) {
|
|
355
|
+
case "D": return "yellow";
|
|
356
|
+
case "R": return "green";
|
|
357
|
+
case "O": return "cyan";
|
|
358
|
+
default: return "white";
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Transport Page - returns PageResult for framework rendering
|
|
363
|
+
*/
|
|
364
|
+
function transportPage({ url: _url, response }) {
|
|
365
|
+
let transport;
|
|
366
|
+
try {
|
|
367
|
+
transport = load(response.raw);
|
|
368
|
+
} catch (_e) {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
const menu = [{
|
|
372
|
+
key: "back",
|
|
373
|
+
label: "Back",
|
|
374
|
+
icon: "←",
|
|
375
|
+
isBack: true
|
|
376
|
+
}];
|
|
377
|
+
if (transport.isTask && transport.parent) menu.push({
|
|
378
|
+
key: "parent",
|
|
379
|
+
label: "Go to Parent Request",
|
|
380
|
+
icon: "⬆️",
|
|
381
|
+
href: `/sap/bc/adt/cts/transportrequests/${transport.parent}`
|
|
382
|
+
});
|
|
383
|
+
for (const task of transport.tasks) menu.push({
|
|
384
|
+
key: task.number,
|
|
385
|
+
label: `${task.number} - ${task.desc || "(no desc)"}`,
|
|
386
|
+
icon: "📝",
|
|
387
|
+
href: task.uri,
|
|
388
|
+
info: `@${task.owner}`,
|
|
389
|
+
status: {
|
|
390
|
+
text: task.statusText,
|
|
391
|
+
color: getStatusColor(task.status)
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
for (const obj of transport.objects) {
|
|
395
|
+
const lockIcon = obj.lockStatus === "X" ? "🔒" : "📦";
|
|
396
|
+
menu.push({
|
|
397
|
+
key: `${obj.pgmid}-${obj.name}`,
|
|
398
|
+
label: `${obj.type} ${obj.name}`,
|
|
399
|
+
icon: lockIcon,
|
|
400
|
+
href: obj.uri,
|
|
401
|
+
info: obj.uri ? void 0 : "(no link)"
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
for (const action of [
|
|
405
|
+
{
|
|
406
|
+
rel: "consistencycheck",
|
|
407
|
+
label: "Consistency Check",
|
|
408
|
+
icon: "🔍"
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
rel: "newreleasejobs",
|
|
412
|
+
label: "Release",
|
|
413
|
+
icon: "🚀"
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
rel: "transportchecks",
|
|
417
|
+
label: "Transport Checks",
|
|
418
|
+
icon: "✅"
|
|
419
|
+
}
|
|
420
|
+
]) {
|
|
421
|
+
const link = response.links.find((l) => l.rel.includes(action.rel));
|
|
422
|
+
if (link) menu.push({
|
|
423
|
+
key: action.rel,
|
|
424
|
+
label: action.label,
|
|
425
|
+
icon: action.icon,
|
|
426
|
+
href: link.href
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
const content = /* @__PURE__ */ jsxs(Box, {
|
|
430
|
+
flexDirection: "column",
|
|
431
|
+
children: [
|
|
432
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
433
|
+
/* @__PURE__ */ jsx(Text, {
|
|
434
|
+
bold: true,
|
|
435
|
+
children: "Description:"
|
|
436
|
+
}),
|
|
437
|
+
" ",
|
|
438
|
+
transport.desc || "(no description)"
|
|
439
|
+
] }),
|
|
440
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
441
|
+
/* @__PURE__ */ jsx(Text, {
|
|
442
|
+
bold: true,
|
|
443
|
+
children: "Owner:"
|
|
444
|
+
}),
|
|
445
|
+
" ",
|
|
446
|
+
transport.owner
|
|
447
|
+
] }),
|
|
448
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
449
|
+
/* @__PURE__ */ jsx(Text, {
|
|
450
|
+
bold: true,
|
|
451
|
+
children: "Type:"
|
|
452
|
+
}),
|
|
453
|
+
" ",
|
|
454
|
+
transport.type
|
|
455
|
+
] }),
|
|
456
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
457
|
+
/* @__PURE__ */ jsx(Text, {
|
|
458
|
+
bold: true,
|
|
459
|
+
children: "Status:"
|
|
460
|
+
}),
|
|
461
|
+
" ",
|
|
462
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
463
|
+
color: getStatusColor(transport.status),
|
|
464
|
+
children: [
|
|
465
|
+
transport.statusText,
|
|
466
|
+
" (",
|
|
467
|
+
transport.status,
|
|
468
|
+
")"
|
|
469
|
+
]
|
|
470
|
+
})
|
|
471
|
+
] }),
|
|
472
|
+
transport.target && /* @__PURE__ */ jsxs(Text, { children: [
|
|
473
|
+
/* @__PURE__ */ jsx(Text, {
|
|
474
|
+
bold: true,
|
|
475
|
+
children: "Target:"
|
|
476
|
+
}),
|
|
477
|
+
" ",
|
|
478
|
+
transport.target,
|
|
479
|
+
" ",
|
|
480
|
+
transport.targetDesc && `(${transport.targetDesc})`
|
|
481
|
+
] }),
|
|
482
|
+
transport.tasks.length > 0 && /* @__PURE__ */ jsx(Box, {
|
|
483
|
+
marginTop: 1,
|
|
484
|
+
children: /* @__PURE__ */ jsxs(Text, {
|
|
485
|
+
bold: true,
|
|
486
|
+
color: "yellow",
|
|
487
|
+
children: [
|
|
488
|
+
"📋 Tasks (",
|
|
489
|
+
transport.tasks.length,
|
|
490
|
+
"):"
|
|
491
|
+
]
|
|
492
|
+
})
|
|
493
|
+
}),
|
|
494
|
+
transport.objects.length > 0 && /* @__PURE__ */ jsx(Box, {
|
|
495
|
+
marginTop: 1,
|
|
496
|
+
children: /* @__PURE__ */ jsxs(Text, {
|
|
497
|
+
bold: true,
|
|
498
|
+
color: "cyan",
|
|
499
|
+
children: [
|
|
500
|
+
"📦 Objects (",
|
|
501
|
+
transport.objects.length,
|
|
502
|
+
"):"
|
|
503
|
+
]
|
|
504
|
+
})
|
|
505
|
+
})
|
|
506
|
+
]
|
|
507
|
+
});
|
|
508
|
+
const isTask = transport.isTask;
|
|
509
|
+
return {
|
|
510
|
+
title: `${isTask ? "Task" : "Transport Request"}: ${transport.number}`,
|
|
511
|
+
icon: isTask ? "�" : "�",
|
|
512
|
+
color: isTask ? "yellow" : "cyan",
|
|
513
|
+
subtitle: isTask && transport.parent ? `Parent: ${transport.parent}` : void 0,
|
|
514
|
+
content,
|
|
515
|
+
menu,
|
|
516
|
+
footer: "[o] XML in VS Code | [e] open in Eclipse ADT"
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
//#endregion
|
|
521
|
+
//#region src/pages/_routes.ts
|
|
522
|
+
/**
|
|
523
|
+
* Auto-generated route manifest
|
|
524
|
+
* DO NOT EDIT - Generated by scripts/generate-routes.ts
|
|
525
|
+
*/
|
|
526
|
+
const routes = [{
|
|
527
|
+
pattern: "/sap/bc/adt/cts/transportrequests/:slug",
|
|
528
|
+
regex: /* @__PURE__ */ new RegExp("^/sap/bc/adt/cts/transportrequests/([^/]+)$"),
|
|
529
|
+
page: transportPage
|
|
530
|
+
}];
|
|
531
|
+
/**
|
|
532
|
+
* Match URL to route
|
|
533
|
+
*/
|
|
534
|
+
function matchRoute(url) {
|
|
535
|
+
for (const route of routes) if (route.regex.test(url)) return route;
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
//#endregion
|
|
540
|
+
//#region src/pages/GenericPage.tsx
|
|
541
|
+
/**
|
|
542
|
+
* Generic Page Component
|
|
543
|
+
*
|
|
544
|
+
* Fallback page for unknown endpoints - shows payload + links.
|
|
545
|
+
* Returns PageResult for framework-driven rendering.
|
|
546
|
+
*/
|
|
547
|
+
/**
|
|
548
|
+
* Generic page - returns PageResult for framework rendering
|
|
549
|
+
*/
|
|
550
|
+
function genericPage({ url, response }) {
|
|
551
|
+
const summary = extractSummary(response.data);
|
|
552
|
+
const menu = [{
|
|
553
|
+
key: "back",
|
|
554
|
+
label: "Back",
|
|
555
|
+
icon: "←",
|
|
556
|
+
isBack: true
|
|
557
|
+
}];
|
|
558
|
+
for (const link of response.links) {
|
|
559
|
+
const title = link.title || getActionName(link.rel);
|
|
560
|
+
let displayPath = link.href;
|
|
561
|
+
const currentBase = url.split("/").slice(0, -1).join("/");
|
|
562
|
+
if (link.href.startsWith(currentBase)) displayPath = "." + link.href.slice(currentBase.length);
|
|
563
|
+
else if (link.href.startsWith("/sap/bc/adt/")) displayPath = link.href.replace("/sap/bc/adt/", "/");
|
|
564
|
+
if (displayPath.length > 40) displayPath = "..." + displayPath.slice(-37);
|
|
565
|
+
menu.push({
|
|
566
|
+
key: link.href,
|
|
567
|
+
label: title,
|
|
568
|
+
icon: "🔗",
|
|
569
|
+
href: link.href,
|
|
570
|
+
info: displayPath
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
const content = /* @__PURE__ */ jsxs(Box, {
|
|
574
|
+
flexDirection: "column",
|
|
575
|
+
children: [/* @__PURE__ */ jsxs(Text, {
|
|
576
|
+
dimColor: true,
|
|
577
|
+
children: ["URL: ", url]
|
|
578
|
+
}), /* @__PURE__ */ jsx(Box, {
|
|
579
|
+
marginTop: 1,
|
|
580
|
+
flexDirection: "column",
|
|
581
|
+
children: summary.length > 0 ? summary.map((item, i) => /* @__PURE__ */ jsxs(Text, { children: [
|
|
582
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
583
|
+
color: "yellow",
|
|
584
|
+
children: [item.key, ":"]
|
|
585
|
+
}),
|
|
586
|
+
" ",
|
|
587
|
+
item.value
|
|
588
|
+
] }, i)) : /* @__PURE__ */ jsx(Text, {
|
|
589
|
+
dimColor: true,
|
|
590
|
+
children: "No summary available"
|
|
591
|
+
})
|
|
592
|
+
})]
|
|
593
|
+
});
|
|
594
|
+
return {
|
|
595
|
+
title: `${response.namespace || "unknown"}:${response.rootElement || "response"}`,
|
|
596
|
+
icon: "📄",
|
|
597
|
+
color: "cyan",
|
|
598
|
+
content,
|
|
599
|
+
menu,
|
|
600
|
+
footer: `[o] XML | [e] Eclipse ADT | ${response.links.length} links`
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Extract key fields for summary display
|
|
605
|
+
*/
|
|
606
|
+
function extractSummary(data) {
|
|
607
|
+
const summary = [];
|
|
608
|
+
const seen = /* @__PURE__ */ new Set();
|
|
609
|
+
function extract(obj, prefix = "") {
|
|
610
|
+
if (!obj || typeof obj !== "object") return;
|
|
611
|
+
if (Array.isArray(obj)) {
|
|
612
|
+
obj.forEach((item) => extract(item, prefix));
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const record = obj;
|
|
616
|
+
for (const [key, value] of Object.entries(record)) {
|
|
617
|
+
if (key.includes("link") || key === "?xml") continue;
|
|
618
|
+
if (key.startsWith("@_") && typeof value === "string" || typeof value === "number") {
|
|
619
|
+
const cleanKey = key.replace("@_", "").replace(/^\w+:/, "");
|
|
620
|
+
if (seen.has(cleanKey) || cleanKey === "version" || cleanKey === "encoding") continue;
|
|
621
|
+
seen.add(cleanKey);
|
|
622
|
+
if ([
|
|
623
|
+
"number",
|
|
624
|
+
"owner",
|
|
625
|
+
"desc",
|
|
626
|
+
"status",
|
|
627
|
+
"status_text",
|
|
628
|
+
"type",
|
|
629
|
+
"target",
|
|
630
|
+
"name",
|
|
631
|
+
"uri"
|
|
632
|
+
].some((f) => cleanKey.includes(f))) summary.push({
|
|
633
|
+
key: cleanKey,
|
|
634
|
+
value: String(value)
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
if (typeof value === "object") extract(value, key);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
extract(data);
|
|
641
|
+
return summary.slice(0, 8);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
//#endregion
|
|
645
|
+
//#region src/Navigator.tsx
|
|
646
|
+
/**
|
|
647
|
+
* Navigator Component
|
|
648
|
+
*
|
|
649
|
+
* Main entry point - renders pages using the framework.
|
|
650
|
+
* Pages return PageResult, Navigator handles rendering.
|
|
651
|
+
*/
|
|
652
|
+
/**
|
|
653
|
+
* Open raw XML in IDE (VS Code)
|
|
654
|
+
*/
|
|
655
|
+
function openXmlInIde(xml, url) {
|
|
656
|
+
const filename = `adt_${url.replaceAll(/[^a-zA-Z0-9]/g, "_").slice(-50)}.xml`;
|
|
657
|
+
const filepath = join(tmpdir(), filename);
|
|
658
|
+
writeFileSync(filepath, xml, "utf-8");
|
|
659
|
+
exec(`code "${filepath}"`, (error) => {
|
|
660
|
+
if (error) exec(`xdg-open "${filepath}"`);
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Resolve absolute path to a launcher binary from an allow-list of known
|
|
665
|
+
* install locations. Prevents PATH-hijack (S4036) by avoiding bare-name
|
|
666
|
+
* lookups when an absolute path is available. Falls back to the bare name
|
|
667
|
+
* only if none of the known paths exist, so packaged distributions with
|
|
668
|
+
* non-standard layouts still function.
|
|
669
|
+
*/
|
|
670
|
+
function resolveLauncher(candidates, fallbackName) {
|
|
671
|
+
for (const p of candidates) if (existsSync(p)) return p;
|
|
672
|
+
return fallbackName;
|
|
673
|
+
}
|
|
674
|
+
const XDG_OPEN = resolveLauncher(["/usr/bin/xdg-open", "/usr/local/bin/xdg-open"], "xdg-open");
|
|
675
|
+
const OPEN_MAC = resolveLauncher(["/usr/bin/open", "/usr/local/bin/open"], "open");
|
|
676
|
+
/**
|
|
677
|
+
* Open in Eclipse ADT
|
|
678
|
+
* Format: adt://[system]/[path]
|
|
679
|
+
* @see https://help.sap.com/docs/abap-cloud/abap-development-tools-user-guide/opening-adt-links
|
|
680
|
+
*/
|
|
681
|
+
function openInAdt(systemName, url) {
|
|
682
|
+
const adtUrl = `adt://${systemName}${url}`;
|
|
683
|
+
execFile(XDG_OPEN, [adtUrl], (error) => {
|
|
684
|
+
if (error) execFile(OPEN_MAC, [adtUrl], (err2) => {
|
|
685
|
+
if (err2) console.error("Failed to open ADT link:", err2.message);
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Inner component that uses navigation context
|
|
691
|
+
*/
|
|
692
|
+
function NavigatorInner({ systemName }) {
|
|
693
|
+
const { current, loading, error, navigate, back } = useNavigation();
|
|
694
|
+
const pageResult = useMemo(() => {
|
|
695
|
+
if (!current) return null;
|
|
696
|
+
return (matchRoute(current.url)?.page ?? genericPage)({
|
|
697
|
+
url: current.url,
|
|
698
|
+
response: current.response
|
|
699
|
+
});
|
|
700
|
+
}, [current]);
|
|
701
|
+
const handleAction = (key) => {
|
|
702
|
+
if (key === "openXml" && current) openXmlInIde(current.response.raw, current.url);
|
|
703
|
+
else if (key === "openAdt" && current && systemName) openInAdt(systemName, current.url);
|
|
704
|
+
};
|
|
705
|
+
if (loading) return /* @__PURE__ */ jsxs(Box, {
|
|
706
|
+
padding: 1,
|
|
707
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
708
|
+
color: "cyan",
|
|
709
|
+
children: /* @__PURE__ */ jsx(Spinner, { type: "dots" })
|
|
710
|
+
}), /* @__PURE__ */ jsx(Text, { children: " Loading..." })]
|
|
711
|
+
});
|
|
712
|
+
if (error) return /* @__PURE__ */ jsxs(Box, {
|
|
713
|
+
padding: 1,
|
|
714
|
+
flexDirection: "column",
|
|
715
|
+
children: [/* @__PURE__ */ jsxs(Text, {
|
|
716
|
+
color: "red",
|
|
717
|
+
children: ["❌ Error: ", error]
|
|
718
|
+
}), /* @__PURE__ */ jsx(Text, {
|
|
719
|
+
dimColor: true,
|
|
720
|
+
children: "Press Ctrl+C to exit"
|
|
721
|
+
})]
|
|
722
|
+
});
|
|
723
|
+
if (!current) return /* @__PURE__ */ jsx(UrlPrompt, { onSubmit: navigate });
|
|
724
|
+
if (!pageResult) return /* @__PURE__ */ jsx(Box, {
|
|
725
|
+
padding: 1,
|
|
726
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
727
|
+
dimColor: true,
|
|
728
|
+
children: "No content loaded"
|
|
729
|
+
})
|
|
730
|
+
});
|
|
731
|
+
return /* @__PURE__ */ jsx(PageRenderer, {
|
|
732
|
+
result: pageResult,
|
|
733
|
+
onNavigate: navigate,
|
|
734
|
+
onBack: back,
|
|
735
|
+
onAction: handleAction
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Main Navigator component
|
|
740
|
+
*/
|
|
741
|
+
function Navigator({ startUrl, fetch, systemName }) {
|
|
742
|
+
return /* @__PURE__ */ jsx(NavigationProvider, {
|
|
743
|
+
fetch,
|
|
744
|
+
parseResponse,
|
|
745
|
+
initialUrl: startUrl,
|
|
746
|
+
children: /* @__PURE__ */ jsx(NavigatorInner, { systemName })
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* URL input prompt component
|
|
751
|
+
*/
|
|
752
|
+
function UrlPrompt({ onSubmit }) {
|
|
753
|
+
const [url, setUrl] = useState("");
|
|
754
|
+
const handleSubmit = () => {
|
|
755
|
+
if (url.trim()) onSubmit(url.trim());
|
|
756
|
+
};
|
|
757
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
758
|
+
flexDirection: "column",
|
|
759
|
+
padding: 1,
|
|
760
|
+
children: [
|
|
761
|
+
/* @__PURE__ */ jsx(Text, {
|
|
762
|
+
color: "cyan",
|
|
763
|
+
bold: true,
|
|
764
|
+
children: "ADT TUI - Hypermedia Navigator"
|
|
765
|
+
}),
|
|
766
|
+
/* @__PURE__ */ jsxs(Box, {
|
|
767
|
+
marginTop: 1,
|
|
768
|
+
children: [/* @__PURE__ */ jsx(Text, { children: "Enter URL: " }), /* @__PURE__ */ jsx(TextInput, {
|
|
769
|
+
value: url,
|
|
770
|
+
onChange: setUrl,
|
|
771
|
+
onSubmit: handleSubmit
|
|
772
|
+
})]
|
|
773
|
+
}),
|
|
774
|
+
/* @__PURE__ */ jsx(Box, {
|
|
775
|
+
marginTop: 1,
|
|
776
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
777
|
+
dimColor: true,
|
|
778
|
+
children: "Example: /sap/bc/adt/cts/transportrequests/TRLK900001"
|
|
779
|
+
})
|
|
780
|
+
})
|
|
781
|
+
]
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
//#endregion
|
|
786
|
+
//#region src/App.tsx
|
|
787
|
+
/**
|
|
788
|
+
* Main App Component
|
|
789
|
+
*
|
|
790
|
+
* Entry point for the TUI application.
|
|
791
|
+
*/
|
|
792
|
+
/**
|
|
793
|
+
* Main TUI Application
|
|
794
|
+
*/
|
|
795
|
+
function App({ startUrl, fetch, router: _router, systemName }) {
|
|
796
|
+
const [, setResizeKey] = useState(0);
|
|
797
|
+
useEffect(() => {
|
|
798
|
+
const handleResize = () => {
|
|
799
|
+
setResizeKey((k) => k + 1);
|
|
800
|
+
};
|
|
801
|
+
process.stdout.on("resize", handleResize);
|
|
802
|
+
return () => {
|
|
803
|
+
process.stdout.off("resize", handleResize);
|
|
804
|
+
};
|
|
805
|
+
}, []);
|
|
806
|
+
return /* @__PURE__ */ jsx(Navigator, {
|
|
807
|
+
startUrl,
|
|
808
|
+
fetch,
|
|
809
|
+
systemName
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
//#endregion
|
|
814
|
+
//#region src/run.tsx
|
|
815
|
+
/**
|
|
816
|
+
* Run TUI Application
|
|
817
|
+
*
|
|
818
|
+
* Helper to render the TUI and wait for exit.
|
|
819
|
+
*/
|
|
820
|
+
/**
|
|
821
|
+
* Run the TUI application
|
|
822
|
+
*
|
|
823
|
+
* @returns Promise that resolves when the app exits
|
|
824
|
+
*/
|
|
825
|
+
async function run(options) {
|
|
826
|
+
const { onExit, ...appProps } = options;
|
|
827
|
+
console.clear();
|
|
828
|
+
const { waitUntilExit, clear } = render(/* @__PURE__ */ jsx(App, { ...appProps }));
|
|
829
|
+
await waitUntilExit();
|
|
830
|
+
clear();
|
|
831
|
+
onExit?.();
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
//#endregion
|
|
835
|
+
export { App, NavigationProvider, Navigator, PageRenderer, categorizeLinks, genericPage, getActionName, parseResponse, run, useNavigation };
|
|
836
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["Page0"],"sources":["../src/lib/context.tsx","../src/lib/parser.ts","../src/lib/PageRenderer.tsx","../src/pages/sap/bc/adt/cts/transportrequests/[slug].loader.ts","../src/pages/sap/bc/adt/cts/transportrequests/[slug].tsx","../src/pages/_routes.ts","../src/pages/GenericPage.tsx","../src/Navigator.tsx","../src/App.tsx","../src/run.tsx"],"sourcesContent":["/**\n * Navigation Context\n *\n * React context for sharing navigation state across components.\n */\n\nimport React, {\n createContext,\n useContext,\n useState,\n useCallback,\n type ReactNode,\n} from 'react';\nimport type { NavigationEntry, ParsedResponse, FetchFn } from './types';\n\ninterface NavigationContextValue {\n /** Current navigation entry */\n current: NavigationEntry | null;\n /** Navigation history */\n history: NavigationEntry[];\n /** Navigate to URL */\n navigate: (url: string) => Promise<void>;\n /** Go back */\n back: () => void;\n /** Is loading */\n loading: boolean;\n /** Error message */\n error: string | null;\n /** Fetch function */\n fetch: FetchFn;\n}\n\nconst NavigationContext = createContext<NavigationContextValue | null>(null);\n\ninterface NavigationProviderProps {\n children: ReactNode;\n /** Fetch function for making requests */\n fetch: FetchFn;\n /** Parse response function */\n parseResponse: (xml: string) => ParsedResponse;\n /** Initial URL to load */\n initialUrl?: string;\n}\n\nexport function NavigationProvider({\n children,\n fetch,\n parseResponse,\n initialUrl,\n}: NavigationProviderProps): React.ReactNode {\n const [current, setCurrent] = useState<NavigationEntry | null>(null);\n const [history, setHistory] = useState<NavigationEntry[]>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const navigate = useCallback(\n async (url: string) => {\n setLoading(true);\n setError(null);\n\n try {\n const response = await fetch(url, {\n method: 'GET',\n headers: { Accept: '*/*' },\n });\n\n const parsed = parseResponse(response);\n const entry: NavigationEntry = {\n url,\n response: parsed,\n timestamp: new Date(),\n };\n\n // Push current to history if exists\n if (current) {\n setHistory((prev: NavigationEntry[]) => [...prev, current]);\n }\n\n setCurrent(entry);\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err));\n } finally {\n setLoading(false);\n }\n },\n [current, fetch, parseResponse],\n );\n\n const back = useCallback(() => {\n if (history.length === 0) return;\n\n const prev = history[history.length - 1];\n setHistory((h: NavigationEntry[]) => h.slice(0, -1));\n setCurrent(prev);\n }, [history]);\n\n // Load initial URL on mount\n React.useEffect(() => {\n if (initialUrl && !current) {\n navigate(initialUrl);\n }\n }, [initialUrl, current, navigate]);\n\n return (\n <NavigationContext.Provider\n value={{\n current,\n history,\n navigate,\n back,\n loading,\n error,\n fetch,\n }}\n >\n {children}\n </NavigationContext.Provider>\n );\n}\n\n/**\n * Hook to access navigation context\n */\nexport function useNavigation(): NavigationContextValue {\n const context = useContext(NavigationContext);\n if (!context) {\n throw new Error('useNavigation must be used within NavigationProvider');\n }\n return context;\n}\n","/**\n * XML Response Parser\n *\n * Parses ADT XML responses and extracts hypermedia links.\n */\n\nimport { XMLParser } from 'fast-xml-parser';\nimport type { ParsedResponse, HypermediaLink } from './types';\n\nconst parser = new XMLParser({\n ignoreAttributes: false,\n attributeNamePrefix: '@_',\n removeNSPrefix: false,\n parseAttributeValue: true,\n trimValues: true,\n});\n\n/**\n * Extract all atom:link elements from parsed XML object\n */\nfunction extractLinks(\n obj: unknown,\n links: HypermediaLink[] = [],\n): HypermediaLink[] {\n if (!obj || typeof obj !== 'object') return links;\n\n if (Array.isArray(obj)) {\n for (const item of obj) {\n extractLinks(item, links);\n }\n } else {\n const record = obj as Record<string, unknown>;\n\n const linkKey = Object.keys(record).find(\n (k) => k === 'atom:link' || k === 'link' || k.endsWith(':link'),\n );\n\n if (linkKey) {\n const linkData = record[linkKey];\n const linkArray = Array.isArray(linkData) ? linkData : [linkData];\n\n for (const link of linkArray) {\n if (link && typeof link === 'object') {\n const linkObj = link as Record<string, unknown>;\n const href = linkObj['@_href'] as string;\n const rel = linkObj['@_rel'] as string;\n\n if (href && rel) {\n links.push({\n href,\n rel,\n type: linkObj['@_type'] as string | undefined,\n title: linkObj['@_title'] as string | undefined,\n });\n }\n }\n }\n }\n\n for (const key of Object.keys(record)) {\n if (key !== linkKey) {\n extractLinks(record[key], links);\n }\n }\n }\n\n return links;\n}\n\n/**\n * Detect root element and namespace from parsed XML\n */\nfunction detectRootInfo(data: Record<string, unknown>): {\n namespace?: string;\n rootElement?: string;\n} {\n const keys = Object.keys(data).filter((k) => k !== '?xml');\n if (keys.length === 0) return {};\n\n const rootKey = keys[0];\n const colonIndex = rootKey.indexOf(':');\n\n if (colonIndex > 0) {\n return {\n namespace: rootKey.substring(0, colonIndex),\n rootElement: rootKey.substring(colonIndex + 1),\n };\n }\n\n return { rootElement: rootKey };\n}\n\n/**\n * Parse ADT XML response\n */\nexport function parseResponse(xml: string): ParsedResponse {\n const data = parser.parse(xml) as Record<string, unknown>;\n const links = extractLinks(data);\n const { namespace, rootElement } = detectRootInfo(data);\n\n return {\n raw: xml,\n data,\n links,\n namespace,\n rootElement,\n };\n}\n\n/**\n * Get action name from relation URL\n */\nexport function getActionName(rel: string): string {\n const lastSlash = rel.lastIndexOf('/');\n return lastSlash >= 0 ? rel.substring(lastSlash + 1) : rel;\n}\n\n/**\n * Categorize links by type\n */\nexport function categorizeLinks(links: HypermediaLink[]): {\n navigation: HypermediaLink[];\n actions: HypermediaLink[];\n self: HypermediaLink[];\n} {\n const navigation: HypermediaLink[] = [];\n const actions: HypermediaLink[] = [];\n const self: HypermediaLink[] = [];\n\n for (const link of links) {\n if (link.rel === 'self') {\n self.push(link);\n } else if (\n link.rel.includes('/relations/') ||\n link.rel.includes('adturi')\n ) {\n actions.push(link);\n } else {\n navigation.push(link);\n }\n }\n\n return { navigation, actions, self };\n}\n","/**\n * PageRenderer - Framework component that renders PageResult\n *\n * Handles:\n * - Header with title/icon\n * - Page content\n * - Menu navigation with keyboard\n * - Footer\n */\n\nimport { useState } from 'react';\nimport { Box, Text, useInput } from 'ink';\nimport type { PageResult, MenuItem } from './types';\n\ninterface PageRendererProps {\n result: PageResult;\n onNavigate: (url: string) => void;\n onBack: () => void;\n onAction?: (key: string) => void;\n}\n\n/**\n * Render a single menu item\n */\nfunction MenuItemRow({\n item,\n isSelected,\n}: {\n item: MenuItem;\n isSelected: boolean;\n}) {\n const prefix = isSelected ? '❯ ' : ' ';\n\n return (\n <Text color={isSelected ? 'cyan' : undefined}>\n {prefix}\n {item.icon && `${item.icon} `}\n {item.label}\n {item.status && (\n <Text color={item.status.color} dimColor={!isSelected}>\n {' '}\n [{item.status.text}]\n </Text>\n )}\n {item.info && <Text dimColor> {item.info}</Text>}\n </Text>\n );\n}\n\n/**\n * PageRenderer component\n */\nexport function PageRenderer({\n result,\n onNavigate,\n onBack,\n onAction,\n}: PageRendererProps): React.ReactNode {\n const [selectedIndex, setSelectedIndex] = useState(0);\n\n // Handle keyboard input\n useInput((input, key) => {\n if (key.upArrow) {\n setSelectedIndex((i) => Math.max(0, i - 1));\n } else if (key.downArrow) {\n setSelectedIndex((i) => Math.min(result.menu.length - 1, i + 1));\n } else if (key.return) {\n const item = result.menu[selectedIndex];\n if (item.isBack) {\n onBack();\n } else if (item.href) {\n onNavigate(item.href);\n } else if (onAction) {\n onAction(item.key);\n }\n } else if (input === 'o' || input === 'O') {\n // Open XML in VS Code\n onAction?.('openXml');\n } else if (input === 'e' || input === 'E') {\n // Open in Eclipse ADT\n onAction?.('openAdt');\n }\n });\n\n return (\n <Box flexDirection=\"column\" padding={1}>\n {/* Header */}\n <Box\n borderStyle=\"round\"\n borderColor={result.color || 'cyan'}\n paddingX={2}\n marginBottom={1}\n >\n <Text bold color={result.color || 'cyan'}>\n {result.icon && `${result.icon} `}\n {result.title}\n </Text>\n {result.subtitle && <Text dimColor> ({result.subtitle})</Text>}\n </Box>\n\n {/* Content */}\n <Box flexDirection=\"column\" marginBottom={1}>\n {result.content}\n </Box>\n\n {/* Menu */}\n <Box flexDirection=\"column\">\n {result.menu.map((item, i) => (\n <MenuItemRow\n key={item.key}\n item={item}\n isSelected={i === selectedIndex}\n />\n ))}\n </Box>\n\n {/* Footer */}\n <Box marginTop={1}>\n <Text dimColor>\n {result.footer || '[↑↓] navigate | [Enter] select'}\n </Text>\n </Box>\n </Box>\n );\n}\n","/**\n * Transport Request/Task Loader\n *\n * Uses adt-contracts for type-safe XML parsing.\n */\n\nimport {\n transportmanagmentSingle,\n type TransportResponse,\n} from '@abapify/adt-contracts';\n\n// Re-export the schema for type inference\nexport { transportmanagmentSingle as schema };\n\n// Use type from contracts package\ntype TransportRoot = TransportResponse;\n\n// Extract nested types - TransportResponse has root.request structure\nexport type Request = NonNullable<\n NonNullable<TransportRoot['root']>['request']\n>;\nexport type Task = NonNullable<Request['task']>[number];\nexport type AbapObject = NonNullable<Task['abap_object']>[number];\n\n/**\n * Parsed transport data with computed properties\n */\nexport interface TransportData {\n /** Whether this is a task (vs request) */\n isTask: boolean;\n /** Transport/Task number */\n number: string;\n /** Parent request number (for tasks) */\n parent?: string;\n /** Owner user ID */\n owner: string;\n /** Description */\n desc: string;\n /** Transport type */\n type: string;\n /** Status code (D=Modifiable, R=Released, O=Protected) */\n status: string;\n /** Status text */\n statusText: string;\n /** Target system */\n target: string;\n /** Target description */\n targetDesc: string;\n /** Child tasks (for requests) */\n tasks: TaskInfo[];\n /** Objects (for tasks) */\n objects: ObjectInfo[];\n /** Raw parsed data */\n raw: TransportRoot;\n}\n\nexport interface TaskInfo {\n number: string;\n owner: string;\n desc: string;\n status: string;\n statusText: string;\n uri: string;\n}\n\nexport interface ObjectInfo {\n pgmid: string;\n type: string;\n name: string;\n info: string;\n lockStatus: string;\n uri?: string;\n}\n\n/**\n * Load and parse transport request/task XML\n */\nexport function load(xml: string): TransportData {\n const parsed = transportmanagmentSingle.parse(xml);\n return transformToTransportData(parsed);\n}\n\n/**\n * Transform parsed XSD data to TransportData\n */\nfunction transformToTransportData(data: TransportRoot): TransportData {\n const request = data.root?.request;\n\n if (!request) {\n throw new Error('No request element found in transport data');\n }\n\n // Check if this is a task view (has parent) or request view\n const isTask = !!request.parent;\n\n // Extract tasks\n const tasks: TaskInfo[] = [];\n if (request.task && !isTask) {\n const taskArray = Array.isArray(request.task)\n ? request.task\n : [request.task];\n for (const task of taskArray) {\n tasks.push({\n number: task.number ?? '',\n owner: task.owner ?? '',\n desc: task.desc ?? '',\n status: task.status ?? '',\n statusText: task.status_text ?? '',\n uri: task.uri ?? '',\n });\n }\n }\n\n // Extract objects (from task's abap_object)\n const objects: ObjectInfo[] = [];\n if (request.task && isTask) {\n // When viewing a task, the task element contains objects\n const taskArray = Array.isArray(request.task)\n ? request.task\n : [request.task];\n for (const task of taskArray) {\n if (task.abap_object) {\n const objArray = Array.isArray(task.abap_object)\n ? task.abap_object\n : [task.abap_object];\n for (const obj of objArray) {\n objects.push({\n pgmid: obj.pgmid ?? '',\n type: obj.type ?? '',\n name: obj.name ?? '',\n info: obj.obj_info ?? obj.obj_desc ?? '',\n lockStatus: obj.lock_status ?? '',\n uri: obj.uri,\n });\n }\n }\n }\n }\n\n return {\n isTask,\n number: request.number ?? '',\n parent: request.parent,\n owner: request.owner ?? '',\n desc: request.desc ?? '',\n type: request.type ?? '',\n status: request.status ?? '',\n statusText: request.status_text ?? '',\n target: request.target ?? '',\n targetDesc: request.target_desc ?? '',\n tasks,\n objects,\n raw: data,\n };\n}\n","/**\n * Transport Request/Task Page\n *\n * Returns PageResult for framework-driven rendering.\n * Uses schema-driven parsing via [slug].loader.ts\n */\n\nimport { Box, Text } from 'ink';\nimport type {\n PageProps,\n PageResult,\n MenuItem,\n} from '../../../../../../lib/types';\nimport { load } from './[slug].loader';\n\n/**\n * Get status color\n */\nfunction getStatusColor(status: string): string {\n switch (status) {\n case 'D':\n return 'yellow'; // Modifiable\n case 'R':\n return 'green'; // Released\n case 'O':\n return 'cyan'; // Protected\n default:\n return 'white';\n }\n}\n\n/**\n * Transport Page - returns PageResult for framework rendering\n */\nexport function transportPage({\n url: _url,\n response,\n}: PageProps): PageResult | null {\n // Parse transport data\n let transport;\n try {\n transport = load(response.raw);\n } catch (_e) {\n return null;\n }\n\n // Build menu items\n const menu: MenuItem[] = [\n { key: 'back', label: 'Back', icon: '←', isBack: true },\n ];\n\n // Add parent link for tasks\n if (transport.isTask && transport.parent) {\n menu.push({\n key: 'parent',\n label: 'Go to Parent Request',\n icon: '⬆️',\n href: `/sap/bc/adt/cts/transportrequests/${transport.parent}`,\n });\n }\n\n // Add tasks (for requests)\n for (const task of transport.tasks) {\n menu.push({\n key: task.number,\n label: `${task.number} - ${task.desc || '(no desc)'}`,\n icon: '📝',\n href: task.uri,\n info: `@${task.owner}`,\n status: { text: task.statusText, color: getStatusColor(task.status) },\n });\n }\n\n // Add objects (for tasks)\n for (const obj of transport.objects) {\n const lockIcon = obj.lockStatus === 'X' ? '🔒' : '📦';\n menu.push({\n key: `${obj.pgmid}-${obj.name}`,\n label: `${obj.type} ${obj.name}`,\n icon: lockIcon,\n href: obj.uri,\n info: obj.uri ? undefined : '(no link)',\n });\n }\n\n // Add actions from links\n const actions = [\n { rel: 'consistencycheck', label: 'Consistency Check', icon: '🔍' },\n { rel: 'newreleasejobs', label: 'Release', icon: '🚀' },\n { rel: 'transportchecks', label: 'Transport Checks', icon: '✅' },\n ];\n\n for (const action of actions) {\n const link = response.links.find((l) => l.rel.includes(action.rel));\n if (link) {\n menu.push({\n key: action.rel,\n label: action.label,\n icon: action.icon,\n href: link.href,\n });\n }\n }\n\n // Build content\n const content = (\n <Box flexDirection=\"column\">\n <Text>\n <Text bold>Description:</Text> {transport.desc || '(no description)'}\n </Text>\n <Text>\n <Text bold>Owner:</Text> {transport.owner}\n </Text>\n <Text>\n <Text bold>Type:</Text> {transport.type}\n </Text>\n <Text>\n <Text bold>Status:</Text>{' '}\n <Text color={getStatusColor(transport.status)}>\n {transport.statusText} ({transport.status})\n </Text>\n </Text>\n {transport.target && (\n <Text>\n <Text bold>Target:</Text> {transport.target}{' '}\n {transport.targetDesc && `(${transport.targetDesc})`}\n </Text>\n )}\n {transport.tasks.length > 0 && (\n <Box marginTop={1}>\n <Text bold color=\"yellow\">\n 📋 Tasks ({transport.tasks.length}):\n </Text>\n </Box>\n )}\n {transport.objects.length > 0 && (\n <Box marginTop={1}>\n <Text bold color=\"cyan\">\n 📦 Objects ({transport.objects.length}):\n </Text>\n </Box>\n )}\n </Box>\n );\n\n const isTask = transport.isTask;\n return {\n title: `${isTask ? 'Task' : 'Transport Request'}: ${transport.number}`,\n icon: isTask ? '�' : '�',\n color: isTask ? 'yellow' : 'cyan',\n subtitle:\n isTask && transport.parent ? `Parent: ${transport.parent}` : undefined,\n content,\n menu,\n footer: '[o] XML in VS Code | [e] open in Eclipse ADT',\n };\n}\n\n// Default export for file-based routing\nexport default transportPage;\n","/**\n * Auto-generated route manifest\n * DO NOT EDIT - Generated by scripts/generate-routes.ts\n */\n\nimport { default as Page0 } from './sap/bc/adt/cts/transportrequests/[slug]';\nimport type { PageComponent } from '../lib/types';\n\nexport interface Route {\n pattern: string;\n regex: RegExp;\n page: PageComponent;\n}\n\nexport const routes: Route[] = [\n {\n pattern: '/sap/bc/adt/cts/transportrequests/:slug',\n regex: new RegExp('^/sap/bc/adt/cts/transportrequests/([^/]+)$'),\n page: Page0,\n },\n];\n\n/**\n * Match URL to route\n */\nexport function matchRoute(url: string): Route | null {\n for (const route of routes) {\n if (route.regex.test(url)) {\n return route;\n }\n }\n return null;\n}\n","/**\n * Generic Page Component\n *\n * Fallback page for unknown endpoints - shows payload + links.\n * Returns PageResult for framework-driven rendering.\n */\n\n// React import not needed with JSX transform\nimport { Box, Text } from 'ink';\nimport type { PageProps, PageResult, MenuItem } from '../lib/types';\nimport { getActionName } from '../lib/parser';\n\ninterface SummaryItem {\n key: string;\n value: string;\n}\n\n/**\n * Generic page - returns PageResult for framework rendering\n */\nexport function genericPage({ url, response }: PageProps): PageResult {\n // Extract summary from data\n const summary = extractSummary(response.data);\n\n // Build menu items from links\n const menu: MenuItem[] = [\n { key: 'back', label: 'Back', icon: '←', isBack: true },\n ];\n\n for (const link of response.links) {\n const title = link.title || getActionName(link.rel);\n\n // Make path relative/shorter for display\n let displayPath = link.href;\n const currentBase = url.split('/').slice(0, -1).join('/');\n if (link.href.startsWith(currentBase)) {\n displayPath = '.' + link.href.slice(currentBase.length);\n } else if (link.href.startsWith('/sap/bc/adt/')) {\n displayPath = link.href.replace('/sap/bc/adt/', '/');\n }\n if (displayPath.length > 40) {\n displayPath = '...' + displayPath.slice(-37);\n }\n\n menu.push({\n key: link.href,\n label: title,\n icon: '🔗',\n href: link.href,\n info: displayPath,\n });\n }\n\n // Build content\n const content = (\n <Box flexDirection=\"column\">\n <Text dimColor>URL: {url}</Text>\n <Box marginTop={1} flexDirection=\"column\">\n {summary.length > 0 ? (\n summary.map((item, i) => (\n <Text key={i}>\n <Text color=\"yellow\">{item.key}:</Text> {item.value}\n </Text>\n ))\n ) : (\n <Text dimColor>No summary available</Text>\n )}\n </Box>\n </Box>\n );\n\n return {\n title: `${response.namespace || 'unknown'}:${response.rootElement || 'response'}`,\n icon: '📄',\n color: 'cyan',\n content,\n menu,\n footer: `[o] XML | [e] Eclipse ADT | ${response.links.length} links`,\n };\n}\n\n/**\n * Extract key fields for summary display\n */\nfunction extractSummary(data: Record<string, unknown>): SummaryItem[] {\n const summary: SummaryItem[] = [];\n const seen = new Set<string>();\n\n function extract(obj: unknown, prefix = ''): void {\n if (!obj || typeof obj !== 'object') return;\n if (Array.isArray(obj)) {\n obj.forEach((item) => extract(item, prefix));\n return;\n }\n\n const record = obj as Record<string, unknown>;\n for (const [key, value] of Object.entries(record)) {\n // Skip links and xml declaration\n if (key.includes('link') || key === '?xml') continue;\n\n // Extract interesting attributes (those starting with @_)\n if (\n (key.startsWith('@_') && typeof value === 'string') ||\n typeof value === 'number'\n ) {\n const cleanKey = key.replace('@_', '').replace(/^\\w+:/, '');\n // Skip duplicates and uninteresting fields\n if (\n seen.has(cleanKey) ||\n cleanKey === 'version' ||\n cleanKey === 'encoding'\n )\n continue;\n seen.add(cleanKey);\n\n // Limit to important fields\n const important = [\n 'number',\n 'owner',\n 'desc',\n 'status',\n 'status_text',\n 'type',\n 'target',\n 'name',\n 'uri',\n ];\n if (important.some((f) => cleanKey.includes(f))) {\n summary.push({ key: cleanKey, value: String(value) });\n }\n }\n\n // Recurse into nested objects\n if (typeof value === 'object') {\n extract(value, key);\n }\n }\n }\n\n extract(data);\n return summary.slice(0, 8); // Limit to 8 items\n}\n\n// removeLinks function removed - was unused\n","/**\n * Navigator Component\n *\n * Main entry point - renders pages using the framework.\n * Pages return PageResult, Navigator handles rendering.\n */\n\nimport { useState, useMemo } from 'react';\nimport { Box, Text } from 'ink';\nimport Spinner from 'ink-spinner';\nimport TextInput from 'ink-text-input';\nimport { writeFileSync, existsSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\nimport { exec, execFile } from 'node:child_process';\nimport { NavigationProvider, useNavigation } from './lib/context';\nimport { parseResponse } from './lib/parser';\nimport { PageRenderer } from './lib/PageRenderer';\nimport { matchRoute } from './pages/_routes';\nimport { genericPage } from './pages/GenericPage';\nimport type { FetchFn, PageComponent, PageResult } from './lib/types';\n\ninterface NavigatorProps {\n /** Optional starting URL - if not provided, shows prompt */\n startUrl?: string;\n /** Fetch function */\n fetch: FetchFn;\n /** SAP system name for ADT links (e.g., 'TRL', 'NPL') */\n systemName?: string;\n}\n\n/**\n * Open raw XML in IDE (VS Code)\n */\nfunction openXmlInIde(xml: string, url: string): void {\n const safeName = url.replaceAll(/[^a-zA-Z0-9]/g, '_').slice(-50);\n const filename = `adt_${safeName}.xml`;\n const filepath = join(tmpdir(), filename);\n writeFileSync(filepath, xml, 'utf-8');\n exec(`code \"${filepath}\"`, (error) => {\n if (error) {\n exec(`xdg-open \"${filepath}\"`);\n }\n });\n}\n\n/**\n * Resolve absolute path to a launcher binary from an allow-list of known\n * install locations. Prevents PATH-hijack (S4036) by avoiding bare-name\n * lookups when an absolute path is available. Falls back to the bare name\n * only if none of the known paths exist, so packaged distributions with\n * non-standard layouts still function.\n */\nfunction resolveLauncher(\n candidates: readonly string[],\n fallbackName: string,\n): string {\n for (const p of candidates) {\n if (existsSync(p)) return p;\n }\n return fallbackName;\n}\n\nconst XDG_OPEN = resolveLauncher(\n ['/usr/bin/xdg-open', '/usr/local/bin/xdg-open'],\n 'xdg-open',\n);\nconst OPEN_MAC = resolveLauncher(\n ['/usr/bin/open', '/usr/local/bin/open'],\n 'open',\n);\n\n/**\n * Open in Eclipse ADT\n * Format: adt://[system]/[path]\n * @see https://help.sap.com/docs/abap-cloud/abap-development-tools-user-guide/opening-adt-links\n */\nfunction openInAdt(systemName: string, url: string): void {\n const adtUrl = `adt://${systemName}${url}`;\n // Use execFile (arg array, no shell) to avoid shell-command injection\n // via systemName/url. Try xdg-open (Linux), fall back to open (macOS).\n // Paths resolved above via allow-list to prevent PATH-hijack (S4036).\n execFile(XDG_OPEN, [adtUrl], (error) => {\n if (error) {\n execFile(OPEN_MAC, [adtUrl], (err2) => {\n if (err2) {\n console.error('Failed to open ADT link:', err2.message);\n }\n });\n }\n });\n}\n\n/**\n * Inner component that uses navigation context\n */\nfunction NavigatorInner({ systemName }: { systemName?: string }) {\n const { current, loading, error, navigate, back } = useNavigation();\n\n // Get page result\n const pageResult = useMemo<PageResult | null>(() => {\n if (!current) return null;\n\n // Try file-based route matching first\n const fileRoute = matchRoute(current.url);\n const pageFn: PageComponent = fileRoute?.page ?? genericPage;\n\n return pageFn({ url: current.url, response: current.response });\n }, [current]);\n\n // Handle actions\n const handleAction = (key: string) => {\n if (key === 'openXml' && current) {\n openXmlInIde(current.response.raw, current.url);\n } else if (key === 'openAdt' && current && systemName) {\n openInAdt(systemName, current.url);\n }\n };\n\n if (loading) {\n return (\n <Box padding={1}>\n <Text color=\"cyan\">\n <Spinner type=\"dots\" />\n </Text>\n <Text> Loading...</Text>\n </Box>\n );\n }\n\n if (error) {\n return (\n <Box padding={1} flexDirection=\"column\">\n <Text color=\"red\">❌ Error: {error}</Text>\n <Text dimColor>Press Ctrl+C to exit</Text>\n </Box>\n );\n }\n\n if (!current) {\n return <UrlPrompt onSubmit={navigate} />;\n }\n\n if (!pageResult) {\n return (\n <Box padding={1}>\n <Text dimColor>No content loaded</Text>\n </Box>\n );\n }\n\n return (\n <PageRenderer\n result={pageResult}\n onNavigate={navigate}\n onBack={back}\n onAction={handleAction}\n />\n );\n}\n\n/**\n * Main Navigator component\n */\nexport function Navigator({\n startUrl,\n fetch,\n systemName,\n}: NavigatorProps): React.ReactNode {\n return (\n <NavigationProvider\n fetch={fetch}\n parseResponse={parseResponse}\n initialUrl={startUrl}\n >\n <NavigatorInner systemName={systemName} />\n </NavigationProvider>\n );\n}\n\n/**\n * URL input prompt component\n */\nfunction UrlPrompt({ onSubmit }: { onSubmit: (url: string) => void }) {\n const [url, setUrl] = useState('');\n\n const handleSubmit = () => {\n if (url.trim()) {\n onSubmit(url.trim());\n }\n };\n\n return (\n <Box flexDirection=\"column\" padding={1}>\n <Text color=\"cyan\" bold>\n ADT TUI - Hypermedia Navigator\n </Text>\n <Box marginTop={1}>\n <Text>Enter URL: </Text>\n <TextInput value={url} onChange={setUrl} onSubmit={handleSubmit} />\n </Box>\n <Box marginTop={1}>\n <Text dimColor>\n Example: /sap/bc/adt/cts/transportrequests/TRLK900001\n </Text>\n </Box>\n </Box>\n );\n}\n","/**\n * Main App Component\n *\n * Entry point for the TUI application.\n */\n\nimport { useState, useEffect } from 'react';\nimport { Navigator } from './Navigator';\nimport type { FetchFn } from './lib/types';\nimport type { Router } from './lib/router';\n\nexport interface AppProps {\n /** Optional starting URL - if not provided, shows prompt */\n startUrl?: string;\n /** Fetch function for making HTTP requests */\n fetch: FetchFn;\n /** Optional custom router */\n router?: Router;\n /** SAP system name for ADT links (e.g., 'TRL', 'NPL') */\n systemName?: string;\n}\n\n/**\n * Main TUI Application\n */\nexport function App({\n startUrl,\n fetch,\n router: _router,\n systemName,\n}: AppProps): React.ReactNode {\n const [, setResizeKey] = useState(0);\n\n // Handle terminal resize - just trigger re-render, don't clear\n useEffect(() => {\n const handleResize = () => {\n setResizeKey((k) => k + 1);\n };\n\n process.stdout.on('resize', handleResize);\n return () => {\n process.stdout.off('resize', handleResize);\n };\n }, []);\n\n return (\n <Navigator startUrl={startUrl} fetch={fetch} systemName={systemName} />\n );\n}\n","/**\n * Run TUI Application\n *\n * Helper to render the TUI and wait for exit.\n */\n\n// React import not needed with JSX transform\nimport { render } from 'ink';\nimport { App } from './App';\nimport type { FetchFn } from './lib/types';\nimport type { Router } from './lib/router';\n\nexport interface RunOptions {\n /** Optional starting URL - if not provided, shows prompt */\n startUrl?: string;\n /** Fetch function for making HTTP requests */\n fetch: FetchFn;\n /** Optional custom router */\n router?: Router;\n /** SAP system name for ADT links (e.g., 'TRL', 'NPL') */\n systemName?: string;\n /** Called when app exits */\n onExit?: () => void;\n}\n\n/**\n * Run the TUI application\n *\n * @returns Promise that resolves when the app exits\n */\nexport async function run(options: RunOptions): Promise<void> {\n const { onExit, ...appProps } = options;\n\n // Clear terminal on start\n console.clear();\n\n const { waitUntilExit, clear } = render(<App {...appProps} />);\n\n await waitUntilExit();\n\n // Cleanup\n clear();\n onExit?.();\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abapify/adt-tui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Terminal UI components for ADT - page-based navigation with Ink",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"README.md"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@abapify/adt-contracts": "0.4.
|
|
20
|
+
"@abapify/adt-contracts": "0.4.3",
|
|
21
21
|
"fast-xml-parser": "^5.3.1",
|
|
22
22
|
"ink": "5.1.0",
|
|
23
23
|
"ink-select-input": "^6.0.0",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@types/react": "18.3.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@abapify/adt-client": "0.4.
|
|
32
|
+
"@abapify/adt-client": "0.4.3"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|