simplemdg-dev-cli 2.0.4 → 2.4.4
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/README.md +63 -354
- package/USER_GUIDE.md +55 -378
- package/dist/commands/cds.command.js +69 -60
- package/dist/commands/cds.command.js.map +1 -1
- package/dist/commands/cf-db.command.d.ts +2 -0
- package/dist/commands/cf-db.command.js +606 -0
- package/dist/commands/cf-db.command.js.map +1 -0
- package/dist/commands/cf.command.js +291 -280
- package/dist/commands/cf.command.js.map +1 -1
- package/dist/commands/gitlab.command.d.ts +2 -0
- package/dist/commands/gitlab.command.js +351 -0
- package/dist/commands/gitlab.command.js.map +1 -0
- package/dist/commands/npmrc.command.js +50 -44
- package/dist/commands/npmrc.command.js.map +1 -1
- package/dist/core/cache.d.ts +1 -1
- package/dist/core/cache.js +58 -31
- package/dist/core/cache.js.map +1 -1
- package/dist/core/cds.js +32 -22
- package/dist/core/cds.js.map +1 -1
- package/dist/core/cf-env-parser.d.ts +1 -1
- package/dist/core/cf-env-parser.js +4 -1
- package/dist/core/cf-env-parser.js.map +1 -1
- package/dist/core/cf.d.ts +1 -1
- package/dist/core/cf.js +46 -31
- package/dist/core/cf.js.map +1 -1
- package/dist/core/db/db-btp.d.ts +48 -0
- package/dist/core/db/db-btp.js +162 -0
- package/dist/core/db/db-btp.js.map +1 -0
- package/dist/core/db/db-cache.d.ts +35 -0
- package/dist/core/db/db-cache.js +164 -0
- package/dist/core/db/db-cache.js.map +1 -0
- package/dist/core/db/db-connection.d.ts +22 -0
- package/dist/core/db/db-connection.js +73 -0
- package/dist/core/db/db-connection.js.map +1 -0
- package/dist/core/db/db-crypto.d.ts +3 -0
- package/dist/core/db/db-crypto.js +54 -0
- package/dist/core/db/db-crypto.js.map +1 -0
- package/dist/core/db/db-hana-adapter.d.ts +32 -0
- package/dist/core/db/db-hana-adapter.js +243 -0
- package/dist/core/db/db-hana-adapter.js.map +1 -0
- package/dist/core/db/db-metadata.d.ts +25 -0
- package/dist/core/db/db-metadata.js +150 -0
- package/dist/core/db/db-metadata.js.map +1 -0
- package/dist/core/db/db-postgres-adapter.d.ts +30 -0
- package/dist/core/db/db-postgres-adapter.js +245 -0
- package/dist/core/db/db-postgres-adapter.js.map +1 -0
- package/dist/core/db/db-query-files.d.ts +20 -0
- package/dist/core/db/db-query-files.js +106 -0
- package/dist/core/db/db-query-files.js.map +1 -0
- package/dist/core/db/db-query-history.d.ts +5 -0
- package/dist/core/db/db-query-history.js +49 -0
- package/dist/core/db/db-query-history.js.map +1 -0
- package/dist/core/db/db-row.d.ts +22 -0
- package/dist/core/db/db-row.js +70 -0
- package/dist/core/db/db-row.js.map +1 -0
- package/dist/core/db/db-studio-html.d.ts +4 -0
- package/dist/core/db/db-studio-html.js +437 -0
- package/dist/core/db/db-studio-html.js.map +1 -0
- package/dist/core/db/db-studio-server.d.ts +11 -0
- package/dist/core/db/db-studio-server.js +465 -0
- package/dist/core/db/db-studio-server.js.map +1 -0
- package/dist/core/db/db-types.d.ts +174 -0
- package/dist/core/db/db-types.js +3 -0
- package/dist/core/db/db-types.js.map +1 -0
- package/dist/core/db/db-vcap-parser.d.ts +7 -0
- package/dist/core/db/db-vcap-parser.js +137 -0
- package/dist/core/db/db-vcap-parser.js.map +1 -0
- package/dist/core/doctor.d.ts +1 -1
- package/dist/core/doctor.js +14 -8
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/guide.js +31 -26
- package/dist/core/guide.js.map +1 -1
- package/dist/core/install.d.ts +1 -1
- package/dist/core/install.js +17 -11
- package/dist/core/install.js.map +1 -1
- package/dist/core/navigator.d.ts +17 -0
- package/dist/core/navigator.js +140 -0
- package/dist/core/navigator.js.map +1 -0
- package/dist/core/npmrc.js +29 -16
- package/dist/core/npmrc.js.map +1 -1
- package/dist/core/process.js +11 -6
- package/dist/core/process.js.map +1 -1
- package/dist/core/prompts.js +16 -8
- package/dist/core/prompts.js.map +1 -1
- package/dist/core/repository.d.ts +1 -1
- package/dist/core/repository.js +16 -9
- package/dist/core/repository.js.map +1 -1
- package/dist/core/scanner.d.ts +1 -1
- package/dist/core/scanner.js +13 -7
- package/dist/core/scanner.js.map +1 -1
- package/dist/core/tooling.d.ts +28 -0
- package/dist/core/tooling.js +168 -0
- package/dist/core/tooling.js.map +1 -0
- package/dist/core/types.js +2 -1
- package/dist/core/version-conflict.d.ts +2 -2
- package/dist/core/version-conflict.js +11 -6
- package/dist/core/version-conflict.js.map +1 -1
- package/dist/index.js +65 -48
- package/dist/index.js.map +1 -1
- package/dist/types-local.js +2 -1
- package/package.json +12 -6
- package/src/commands/cds.command.ts +529 -0
- package/src/commands/cf-db.command.ts +636 -0
- package/src/commands/cf.command.ts +3345 -0
- package/src/commands/gitlab.command.ts +373 -0
- package/src/commands/npmrc.command.ts +581 -0
- package/src/core/cache.ts +332 -0
- package/src/core/cds.ts +278 -0
- package/src/core/cf-env-parser.ts +131 -0
- package/src/core/cf.ts +271 -0
- package/src/core/db/db-btp.ts +207 -0
- package/src/core/db/db-cache.ts +215 -0
- package/src/core/db/db-connection.ts +79 -0
- package/src/core/db/db-crypto.ts +53 -0
- package/src/core/db/db-hana-adapter.ts +294 -0
- package/src/core/db/db-metadata.ts +174 -0
- package/src/core/db/db-postgres-adapter.ts +275 -0
- package/src/core/db/db-query-files.ts +130 -0
- package/src/core/db/db-query-history.ts +53 -0
- package/src/core/db/db-row.ts +93 -0
- package/src/core/db/db-studio-html.ts +439 -0
- package/src/core/db/db-studio-server.ts +559 -0
- package/src/core/db/db-types.ts +195 -0
- package/src/core/db/db-vcap-parser.ts +182 -0
- package/src/core/doctor.ts +70 -0
- package/src/core/guide.ts +261 -0
- package/src/core/install.ts +91 -0
- package/src/core/navigator.ts +164 -0
- package/src/core/npmrc.ts +171 -0
- package/src/core/process.ts +75 -0
- package/src/core/prompts.ts +225 -0
- package/src/core/repository.ts +36 -0
- package/src/core/scanner.ts +41 -0
- package/src/core/tooling.ts +207 -0
- package/src/core/types.ts +152 -0
- package/src/core/version-conflict.ts +46 -0
- package/src/index.ts +460 -0
- package/src/types/external.d.ts +3 -0
- package/src/types-local.ts +11 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
export type TDatabaseType = "hana" | "postgresql";
|
|
2
|
+
|
|
3
|
+
export type TDatabaseConnectionProfile = {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
type: TDatabaseType;
|
|
7
|
+
region?: string;
|
|
8
|
+
org?: string;
|
|
9
|
+
space?: string;
|
|
10
|
+
app?: string;
|
|
11
|
+
serviceName?: string;
|
|
12
|
+
servicePlan?: string;
|
|
13
|
+
host: string;
|
|
14
|
+
port: number;
|
|
15
|
+
database?: string;
|
|
16
|
+
schema?: string;
|
|
17
|
+
username: string;
|
|
18
|
+
encryptedPassword: string;
|
|
19
|
+
ssl?: boolean;
|
|
20
|
+
sslValidateCertificate?: boolean;
|
|
21
|
+
createdAt: string;
|
|
22
|
+
updatedAt: string;
|
|
23
|
+
lastUsedAt?: string;
|
|
24
|
+
tags?: string[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A connection profile whose password has been decrypted for internal server
|
|
29
|
+
* use. This shape never leaves the local process boundary.
|
|
30
|
+
*/
|
|
31
|
+
export type TResolvedDatabaseConnection = Omit<TDatabaseConnectionProfile, "encryptedPassword"> & {
|
|
32
|
+
password: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A connection profile safe to send to the browser: no password material.
|
|
37
|
+
*/
|
|
38
|
+
export type TPublicDatabaseConnection = Omit<TDatabaseConnectionProfile, "encryptedPassword">;
|
|
39
|
+
|
|
40
|
+
export type TDatabaseColumn = {
|
|
41
|
+
name: string;
|
|
42
|
+
dataType: string;
|
|
43
|
+
length?: number;
|
|
44
|
+
scale?: number;
|
|
45
|
+
nullable: boolean;
|
|
46
|
+
defaultValue?: string;
|
|
47
|
+
isPrimaryKey?: boolean;
|
|
48
|
+
comment?: string;
|
|
49
|
+
position?: number;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type TDatabaseObjectKind =
|
|
53
|
+
| "table"
|
|
54
|
+
| "view"
|
|
55
|
+
| "column-view"
|
|
56
|
+
| "procedure"
|
|
57
|
+
| "function"
|
|
58
|
+
| "synonym"
|
|
59
|
+
| "index";
|
|
60
|
+
|
|
61
|
+
export type TDatabaseObject = {
|
|
62
|
+
schema: string;
|
|
63
|
+
name: string;
|
|
64
|
+
kind: TDatabaseObjectKind;
|
|
65
|
+
type?: string;
|
|
66
|
+
comment?: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type TDatabaseSchema = {
|
|
70
|
+
name: string;
|
|
71
|
+
isSystem: boolean;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type TDatabaseIndex = {
|
|
75
|
+
name: string;
|
|
76
|
+
columns: string[];
|
|
77
|
+
isUnique: boolean;
|
|
78
|
+
isPrimaryKey: boolean;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type TDatabaseQueryResult = {
|
|
82
|
+
fields: string[];
|
|
83
|
+
rows: Array<Record<string, unknown>>;
|
|
84
|
+
rowCount: number;
|
|
85
|
+
affectedRows?: number;
|
|
86
|
+
command?: string;
|
|
87
|
+
durationMs: number;
|
|
88
|
+
truncated?: boolean;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A database service candidate detected inside a parsed VCAP_SERVICES block.
|
|
93
|
+
* The password is kept here only transiently while importing from BTP.
|
|
94
|
+
*/
|
|
95
|
+
export type TDatabaseServiceCandidate = {
|
|
96
|
+
type: TDatabaseType;
|
|
97
|
+
label: string;
|
|
98
|
+
serviceName: string;
|
|
99
|
+
servicePlan?: string;
|
|
100
|
+
host: string;
|
|
101
|
+
port: number;
|
|
102
|
+
database?: string;
|
|
103
|
+
schema?: string;
|
|
104
|
+
username: string;
|
|
105
|
+
password: string;
|
|
106
|
+
ssl: boolean;
|
|
107
|
+
sslValidateCertificate?: boolean;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export type TBtpAppDatabaseImportContext = {
|
|
111
|
+
region?: string;
|
|
112
|
+
org?: string;
|
|
113
|
+
space?: string;
|
|
114
|
+
app?: string;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export type TSavedQuery = {
|
|
118
|
+
id: string;
|
|
119
|
+
name: string;
|
|
120
|
+
connectionType?: TDatabaseType;
|
|
121
|
+
connectionId?: string;
|
|
122
|
+
sql: string;
|
|
123
|
+
tags?: string[];
|
|
124
|
+
createdAt: string;
|
|
125
|
+
updatedAt: string;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export type TQueryHistoryItem = {
|
|
129
|
+
id: string;
|
|
130
|
+
timestamp: string;
|
|
131
|
+
connectionId?: string;
|
|
132
|
+
connectionName?: string;
|
|
133
|
+
connectionType?: TDatabaseType;
|
|
134
|
+
sql: string;
|
|
135
|
+
durationMs: number;
|
|
136
|
+
success: boolean;
|
|
137
|
+
rowCount?: number;
|
|
138
|
+
error?: string;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export type TSqlSafetyAnalysis = {
|
|
142
|
+
isDestructive: boolean;
|
|
143
|
+
isReadOnly: boolean;
|
|
144
|
+
blockedByReadOnly: boolean;
|
|
145
|
+
matchedKeywords: string[];
|
|
146
|
+
reason?: string;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export type TConnectionTestResult = {
|
|
150
|
+
success: boolean;
|
|
151
|
+
message: string;
|
|
152
|
+
serverVersion?: string;
|
|
153
|
+
durationMs: number;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export type TListObjectsOptions = {
|
|
157
|
+
schema?: string;
|
|
158
|
+
kinds?: TDatabaseObjectKind[];
|
|
159
|
+
search?: string;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export type TTableDataOptions = {
|
|
163
|
+
schema: string;
|
|
164
|
+
table: string;
|
|
165
|
+
limit: number;
|
|
166
|
+
offset: number;
|
|
167
|
+
where?: string;
|
|
168
|
+
orderBy?: string;
|
|
169
|
+
orderDirection?: "asc" | "desc";
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Common interface implemented by every database adapter. Adapters own dialect
|
|
174
|
+
* specifics (quoting, system catalog queries) so the rest of the studio is
|
|
175
|
+
* dialect-agnostic.
|
|
176
|
+
*/
|
|
177
|
+
export interface IDatabaseAdapter {
|
|
178
|
+
readonly type: TDatabaseType;
|
|
179
|
+
connect(): Promise<void>;
|
|
180
|
+
disconnect(): Promise<void>;
|
|
181
|
+
testConnection(): Promise<TConnectionTestResult>;
|
|
182
|
+
runQuery(sql: string, options?: { maxRows?: number }): Promise<TDatabaseQueryResult>;
|
|
183
|
+
/** Run a parameterized statement. Placeholders come from `placeholder()`. */
|
|
184
|
+
runParameterized(sql: string, params: unknown[], options?: { maxRows?: number }): Promise<TDatabaseQueryResult>;
|
|
185
|
+
/** Dialect-specific bind placeholder for parameter N (1-based). */
|
|
186
|
+
placeholder(index: number): string;
|
|
187
|
+
listSchemas(): Promise<TDatabaseSchema[]>;
|
|
188
|
+
listObjects(options: TListObjectsOptions): Promise<TDatabaseObject[]>;
|
|
189
|
+
listColumns(schema: string, table: string): Promise<TDatabaseColumn[]>;
|
|
190
|
+
listIndexes(schema: string, table: string): Promise<TDatabaseIndex[]>;
|
|
191
|
+
countRows(schema: string, table: string): Promise<number>;
|
|
192
|
+
getTableData(options: TTableDataOptions): Promise<TDatabaseQueryResult>;
|
|
193
|
+
quoteIdentifier(identifier: string): string;
|
|
194
|
+
buildQualifiedName(schema: string, name: string): string;
|
|
195
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { TDatabaseServiceCandidate, TDatabaseType } from "./db-types";
|
|
2
|
+
|
|
3
|
+
type TVcapServiceEntry = {
|
|
4
|
+
name?: string;
|
|
5
|
+
instance_name?: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
plan?: string;
|
|
8
|
+
tags?: unknown;
|
|
9
|
+
credentials?: Record<string, unknown>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const HANA_HINTS = ["hana", "hana-cloud", "hanatrial", "hdi-shared", "hdi", "saphana"];
|
|
13
|
+
const POSTGRES_HINTS = ["postgres", "postgresql", "timescale", "postgresql-db", "postgres-db"];
|
|
14
|
+
|
|
15
|
+
function toStringValue(value: unknown): string {
|
|
16
|
+
if (value === undefined || value === null) {
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return String(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function toNumberValue(value: unknown, fallback: number): number {
|
|
24
|
+
const parsed = Number(value);
|
|
25
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function toBooleanValue(value: unknown, fallback: boolean): boolean {
|
|
29
|
+
if (typeof value === "boolean") {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (typeof value === "string") {
|
|
34
|
+
const normalized = value.trim().toLowerCase();
|
|
35
|
+
if (["true", "1", "yes", "on"].includes(normalized)) return true;
|
|
36
|
+
if (["false", "0", "no", "off"].includes(normalized)) return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return fallback;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildHaystack(label: string, entry: TVcapServiceEntry, credentials: Record<string, unknown>): string {
|
|
43
|
+
const tags = Array.isArray(entry.tags) ? entry.tags.map(toStringValue).join(" ") : "";
|
|
44
|
+
const credentialUrl = `${toStringValue(credentials.url)} ${toStringValue(credentials.uri)}`;
|
|
45
|
+
return `${label} ${toStringValue(entry.label)} ${toStringValue(entry.name)} ${tags} ${credentialUrl}`.toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function matchesAnyHint(haystack: string, hints: string[]): boolean {
|
|
49
|
+
return hints.some((hint) => haystack.includes(hint));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function detectHanaCandidate(
|
|
53
|
+
label: string,
|
|
54
|
+
entry: TVcapServiceEntry,
|
|
55
|
+
credentials: Record<string, unknown>,
|
|
56
|
+
): TDatabaseServiceCandidate | undefined {
|
|
57
|
+
const host = toStringValue(credentials.host ?? credentials.hostname);
|
|
58
|
+
const user = toStringValue(credentials.user ?? credentials.username ?? credentials.hdi_user);
|
|
59
|
+
const password = toStringValue(credentials.password ?? credentials.hdi_password);
|
|
60
|
+
|
|
61
|
+
if (!host || !user || !password) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const schema = toStringValue(
|
|
66
|
+
credentials.schema ?? credentials.currentSchema ?? credentials.hdi_user ?? credentials.user,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
type: "hana",
|
|
71
|
+
label,
|
|
72
|
+
serviceName: toStringValue(entry.name ?? entry.instance_name ?? label),
|
|
73
|
+
servicePlan: toStringValue(entry.plan) || undefined,
|
|
74
|
+
host,
|
|
75
|
+
port: toNumberValue(credentials.port, 443),
|
|
76
|
+
database: toStringValue(credentials.databaseName ?? credentials.dbname) || undefined,
|
|
77
|
+
schema: schema || undefined,
|
|
78
|
+
username: user,
|
|
79
|
+
password,
|
|
80
|
+
ssl: toBooleanValue(credentials.encrypt, true),
|
|
81
|
+
sslValidateCertificate: toBooleanValue(credentials.sslValidateCertificate ?? credentials.validate_certificate, false),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function detectPostgresCandidate(
|
|
86
|
+
label: string,
|
|
87
|
+
entry: TVcapServiceEntry,
|
|
88
|
+
credentials: Record<string, unknown>,
|
|
89
|
+
): TDatabaseServiceCandidate | undefined {
|
|
90
|
+
const rawUri = toStringValue(credentials.uri ?? credentials.url);
|
|
91
|
+
let parsedUri: URL | undefined;
|
|
92
|
+
|
|
93
|
+
if (rawUri && /^postg(res|resql)?:\/\//i.test(rawUri)) {
|
|
94
|
+
try {
|
|
95
|
+
parsedUri = new URL(rawUri);
|
|
96
|
+
} catch {
|
|
97
|
+
parsedUri = undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const host = toStringValue(credentials.hostname ?? credentials.host ?? parsedUri?.hostname);
|
|
102
|
+
const username = toStringValue(credentials.username ?? credentials.user ?? parsedUri?.username);
|
|
103
|
+
const password = toStringValue(
|
|
104
|
+
credentials.password ?? (parsedUri?.password ? decodeURIComponent(parsedUri.password) : ""),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (!host || !username || !password) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const database = toStringValue(
|
|
112
|
+
credentials.dbname ?? credentials.database ?? parsedUri?.pathname.replace(/^\//, ""),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
type: "postgresql",
|
|
117
|
+
label,
|
|
118
|
+
serviceName: toStringValue(entry.name ?? entry.instance_name ?? label),
|
|
119
|
+
servicePlan: toStringValue(entry.plan) || undefined,
|
|
120
|
+
host,
|
|
121
|
+
port: toNumberValue(credentials.port ?? parsedUri?.port, 5432),
|
|
122
|
+
database: database || undefined,
|
|
123
|
+
schema: toStringValue(credentials.schema) || "public",
|
|
124
|
+
username,
|
|
125
|
+
password,
|
|
126
|
+
ssl: toBooleanValue(credentials.sslrootcert ? true : credentials.ssl, true),
|
|
127
|
+
sslValidateCertificate: toBooleanValue(credentials.sslValidateCertificate, false),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Inspect a parsed VCAP_SERVICES object and return every HANA / PostgreSQL
|
|
133
|
+
* service whose credentials are complete enough to connect.
|
|
134
|
+
*/
|
|
135
|
+
export function detectDatabaseServiceCandidates(vcapServices: unknown): TDatabaseServiceCandidate[] {
|
|
136
|
+
if (!vcapServices || typeof vcapServices !== "object") {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const candidates: TDatabaseServiceCandidate[] = [];
|
|
141
|
+
|
|
142
|
+
for (const [label, entries] of Object.entries(vcapServices as Record<string, unknown>)) {
|
|
143
|
+
if (!Array.isArray(entries)) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (const rawEntry of entries as TVcapServiceEntry[]) {
|
|
148
|
+
const credentials = (rawEntry.credentials ?? {}) as Record<string, unknown>;
|
|
149
|
+
|
|
150
|
+
if (Object.keys(credentials).length === 0) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const haystack = buildHaystack(label, rawEntry, credentials);
|
|
155
|
+
const isHanaShaped = matchesAnyHint(haystack, HANA_HINTS);
|
|
156
|
+
const isPostgresShaped = matchesAnyHint(haystack, POSTGRES_HINTS) || /^postg(res|resql)?:\/\//i.test(toStringValue(credentials.uri ?? credentials.url));
|
|
157
|
+
|
|
158
|
+
if (isPostgresShaped) {
|
|
159
|
+
const postgres = detectPostgresCandidate(label, rawEntry, credentials);
|
|
160
|
+
if (postgres) {
|
|
161
|
+
candidates.push(postgres);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (isHanaShaped) {
|
|
167
|
+
const hana = detectHanaCandidate(label, rawEntry, credentials);
|
|
168
|
+
if (hana) {
|
|
169
|
+
candidates.push(hana);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return candidates;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function describeServiceCandidate(candidate: TDatabaseServiceCandidate): string {
|
|
179
|
+
const databaseTypeLabel: Record<TDatabaseType, string> = { hana: "HANA", postgresql: "PostgreSQL" };
|
|
180
|
+
const target = candidate.database || candidate.schema || candidate.host;
|
|
181
|
+
return `${candidate.serviceName} · ${databaseTypeLabel[candidate.type]} · ${target}`;
|
|
182
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import type { TDoctorPackageResult, TPackageOccurrence } from "./types";
|
|
4
|
+
|
|
5
|
+
async function findPackageJsonFiles(startPath: string, packageName: string): Promise<string[]> {
|
|
6
|
+
const packagePathParts = packageName.startsWith("@") ? packageName.split("/") : [packageName];
|
|
7
|
+
const result: string[] = [];
|
|
8
|
+
|
|
9
|
+
async function walk(directoryPath: string, depth: number): Promise<void> {
|
|
10
|
+
if (depth > 8) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const entries = await fs.readdir(directoryPath, { withFileTypes: true }).catch(() => []);
|
|
15
|
+
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
if (!entry.isDirectory()) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (entry.name === ".git") {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const entryPath = path.join(directoryPath, entry.name);
|
|
26
|
+
|
|
27
|
+
if (entry.name === "node_modules") {
|
|
28
|
+
const packageJsonPath = path.join(entryPath, ...packagePathParts, "package.json");
|
|
29
|
+
|
|
30
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
31
|
+
result.push(packageJsonPath);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await walk(entryPath, depth + 1);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (entry.name.startsWith(".")) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
await walk(entryPath, depth + 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await walk(startPath, 0);
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function doctorPackage(options: { repositoryPath: string; packageName: string }): Promise<TDoctorPackageResult> {
|
|
51
|
+
const packageJsonFiles = await findPackageJsonFiles(options.repositoryPath, options.packageName);
|
|
52
|
+
const occurrences: TPackageOccurrence[] = [];
|
|
53
|
+
|
|
54
|
+
for (const packageJsonFile of packageJsonFiles) {
|
|
55
|
+
const packageJson = await fs.readJson(packageJsonFile).catch(() => undefined) as { version?: string } | undefined;
|
|
56
|
+
occurrences.push({
|
|
57
|
+
version: packageJson?.version,
|
|
58
|
+
path: packageJsonFile,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const versions = [...new Set(occurrences.map((item) => item.version).filter((version): version is string => Boolean(version)))];
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
packageName: options.packageName,
|
|
66
|
+
versions,
|
|
67
|
+
occurrences,
|
|
68
|
+
hasMultipleVersions: versions.length > 1,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import fs from "fs-extra";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import prompts from "prompts";
|
|
7
|
+
|
|
8
|
+
const GUIDE_FILE_NAME = "USER_GUIDE.md";
|
|
9
|
+
|
|
10
|
+
type TGuideMode = "terminal" | "web" | "commander-help";
|
|
11
|
+
|
|
12
|
+
function getPackageRootPath(): string {
|
|
13
|
+
return path.resolve(__dirname, "..", "..");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function readGuideMarkdown(): Promise<string> {
|
|
17
|
+
const packageRootPath = getPackageRootPath();
|
|
18
|
+
const guidePath = path.join(packageRootPath, GUIDE_FILE_NAME);
|
|
19
|
+
|
|
20
|
+
if (await fs.pathExists(guidePath)) {
|
|
21
|
+
return fs.readFile(guidePath, "utf8");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const readmePath = path.join(packageRootPath, "README.md");
|
|
25
|
+
|
|
26
|
+
if (await fs.pathExists(readmePath)) {
|
|
27
|
+
return fs.readFile(readmePath, "utf8");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return `# SimpleMDG Dev CLI\n\nGuide file was not found. Run smdg --help to see command help.`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function escapeHtml(value: string): string {
|
|
34
|
+
return value
|
|
35
|
+
.replaceAll("&", "&")
|
|
36
|
+
.replaceAll("<", "<")
|
|
37
|
+
.replaceAll(">", ">")
|
|
38
|
+
.replaceAll('"', """)
|
|
39
|
+
.replaceAll("'", "'");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function inlineMarkdown(value: string): string {
|
|
43
|
+
return escapeHtml(value)
|
|
44
|
+
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
|
45
|
+
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function markdownToHtml(markdown: string): string {
|
|
49
|
+
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
|
50
|
+
const htmlLines: string[] = [];
|
|
51
|
+
let inCodeBlock = false;
|
|
52
|
+
let codeLines: string[] = [];
|
|
53
|
+
let inList = false;
|
|
54
|
+
|
|
55
|
+
function closeList(): void {
|
|
56
|
+
if (inList) {
|
|
57
|
+
htmlLines.push("</ul>");
|
|
58
|
+
inList = false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function closeCodeBlock(): void {
|
|
63
|
+
if (inCodeBlock) {
|
|
64
|
+
htmlLines.push(`<pre><code>${escapeHtml(codeLines.join("\n"))}</code></pre>`);
|
|
65
|
+
codeLines = [];
|
|
66
|
+
inCodeBlock = false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
if (line.trim().startsWith("```")) {
|
|
72
|
+
if (inCodeBlock) {
|
|
73
|
+
closeCodeBlock();
|
|
74
|
+
} else {
|
|
75
|
+
closeList();
|
|
76
|
+
inCodeBlock = true;
|
|
77
|
+
codeLines = [];
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (inCodeBlock) {
|
|
83
|
+
codeLines.push(line);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const trimmedLine = line.trim();
|
|
88
|
+
|
|
89
|
+
if (!trimmedLine) {
|
|
90
|
+
closeList();
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const headingMatch = /^(#{1,4})\s+(.+)$/.exec(trimmedLine);
|
|
95
|
+
if (headingMatch) {
|
|
96
|
+
closeList();
|
|
97
|
+
const level = headingMatch[1].length;
|
|
98
|
+
htmlLines.push(`<h${level}>${inlineMarkdown(headingMatch[2])}</h${level}>`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const bulletMatch = /^[-*]\s+(.+)$/.exec(trimmedLine);
|
|
103
|
+
if (bulletMatch) {
|
|
104
|
+
if (!inList) {
|
|
105
|
+
htmlLines.push("<ul>");
|
|
106
|
+
inList = true;
|
|
107
|
+
}
|
|
108
|
+
htmlLines.push(`<li>${inlineMarkdown(bulletMatch[1])}</li>`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
closeList();
|
|
113
|
+
htmlLines.push(`<p>${inlineMarkdown(trimmedLine)}</p>`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
closeCodeBlock();
|
|
117
|
+
closeList();
|
|
118
|
+
|
|
119
|
+
return htmlLines.join("\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function buildGuideHtml(markdown: string): string {
|
|
123
|
+
return `<!doctype html>
|
|
124
|
+
<html lang="en">
|
|
125
|
+
<head>
|
|
126
|
+
<meta charset="utf-8" />
|
|
127
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
128
|
+
<title>SimpleMDG Dev CLI Guide</title>
|
|
129
|
+
<style>
|
|
130
|
+
:root { color-scheme: light dark; }
|
|
131
|
+
body {
|
|
132
|
+
margin: 0;
|
|
133
|
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
134
|
+
background: #0f172a;
|
|
135
|
+
color: #e5e7eb;
|
|
136
|
+
line-height: 1.65;
|
|
137
|
+
}
|
|
138
|
+
.layout {
|
|
139
|
+
max-width: 980px;
|
|
140
|
+
margin: 0 auto;
|
|
141
|
+
padding: 40px 24px 80px;
|
|
142
|
+
}
|
|
143
|
+
.card {
|
|
144
|
+
background: #111827;
|
|
145
|
+
border: 1px solid #334155;
|
|
146
|
+
border-radius: 18px;
|
|
147
|
+
padding: 28px;
|
|
148
|
+
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.35);
|
|
149
|
+
}
|
|
150
|
+
h1 { margin-top: 0; font-size: 34px; color: #ffffff; }
|
|
151
|
+
h2 { margin-top: 36px; padding-top: 22px; border-top: 1px solid #334155; color: #93c5fd; }
|
|
152
|
+
h3 { color: #bfdbfe; }
|
|
153
|
+
p, li { color: #d1d5db; }
|
|
154
|
+
code {
|
|
155
|
+
background: #1f2937;
|
|
156
|
+
color: #fbbf24;
|
|
157
|
+
padding: 2px 6px;
|
|
158
|
+
border-radius: 6px;
|
|
159
|
+
font-size: 0.92em;
|
|
160
|
+
}
|
|
161
|
+
pre {
|
|
162
|
+
background: #020617;
|
|
163
|
+
color: #e5e7eb;
|
|
164
|
+
padding: 16px;
|
|
165
|
+
border-radius: 12px;
|
|
166
|
+
overflow-x: auto;
|
|
167
|
+
border: 1px solid #1e293b;
|
|
168
|
+
}
|
|
169
|
+
pre code { background: transparent; color: inherit; padding: 0; }
|
|
170
|
+
ul { padding-left: 24px; }
|
|
171
|
+
.hint {
|
|
172
|
+
margin-bottom: 18px;
|
|
173
|
+
padding: 12px 14px;
|
|
174
|
+
border-radius: 12px;
|
|
175
|
+
background: #172554;
|
|
176
|
+
color: #dbeafe;
|
|
177
|
+
border: 1px solid #1d4ed8;
|
|
178
|
+
}
|
|
179
|
+
</style>
|
|
180
|
+
</head>
|
|
181
|
+
<body>
|
|
182
|
+
<main class="layout">
|
|
183
|
+
<div class="hint">Local visual guide. Keep this terminal open. Press Ctrl + C to stop the guide server.</div>
|
|
184
|
+
<article class="card">
|
|
185
|
+
${markdownToHtml(markdown)}
|
|
186
|
+
</article>
|
|
187
|
+
</main>
|
|
188
|
+
</body>
|
|
189
|
+
</html>`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function openBrowser(url: string): void {
|
|
193
|
+
const platform = process.platform;
|
|
194
|
+
|
|
195
|
+
if (platform === "win32") {
|
|
196
|
+
spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (platform === "darwin") {
|
|
201
|
+
spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function printUserGuide(): Promise<void> {
|
|
209
|
+
const markdown = await readGuideMarkdown();
|
|
210
|
+
console.log(markdown);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function openUserGuideInBrowser(port?: string): Promise<void> {
|
|
214
|
+
const markdown = await readGuideMarkdown();
|
|
215
|
+
const html = buildGuideHtml(markdown);
|
|
216
|
+
const requestedPort = port?.trim() ? Number(port.trim()) : 0;
|
|
217
|
+
|
|
218
|
+
if (port?.trim() && (!Number.isInteger(requestedPort) || requestedPort < 1 || requestedPort > 65535)) {
|
|
219
|
+
throw new Error("Guide port must be a number from 1 to 65535");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const server = http.createServer((request, response) => {
|
|
223
|
+
if (request.url === "/" || request.url === "/index.html") {
|
|
224
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
225
|
+
response.end(html);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
230
|
+
response.end("Not found");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
await new Promise<void>((resolve, reject) => {
|
|
234
|
+
server.once("error", reject);
|
|
235
|
+
server.listen(requestedPort, "127.0.0.1", () => resolve());
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const address = server.address();
|
|
239
|
+
const actualPort = typeof address === "object" && address ? address.port : requestedPort;
|
|
240
|
+
const url = `http://127.0.0.1:${actualPort}`;
|
|
241
|
+
|
|
242
|
+
console.log(chalk.green(`SimpleMDG Dev CLI guide: ${url}`));
|
|
243
|
+
console.log(chalk.gray("Press Ctrl + C to stop the guide server."));
|
|
244
|
+
openBrowser(url);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function askRootHelpMode(): Promise<TGuideMode> {
|
|
248
|
+
const response = await prompts({
|
|
249
|
+
type: "select",
|
|
250
|
+
name: "mode",
|
|
251
|
+
message: "How do you want to view SimpleMDG CLI help?",
|
|
252
|
+
choices: [
|
|
253
|
+
{ title: "View quick guide in terminal", value: "terminal" },
|
|
254
|
+
{ title: "Open visual guide in browser", value: "web" },
|
|
255
|
+
{ title: "Show command help", value: "commander-help" },
|
|
256
|
+
],
|
|
257
|
+
initial: 0,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
return (response.mode ?? "commander-help") as TGuideMode;
|
|
261
|
+
}
|