simplemdg-dev-cli 2.0.4 → 2.4.5
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 +62 -354
- package/USER_GUIDE.md +55 -376
- 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 +40 -0
- package/dist/core/db/db-cache.js +188 -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 +36 -0
- package/dist/core/db/db-hana-adapter.js +251 -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 +34 -0
- package/dist/core/db/db-postgres-adapter.js +259 -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 +28 -0
- package/dist/core/db/db-row.js +123 -0
- package/dist/core/db/db-row.js.map +1 -0
- package/dist/core/db/db-studio-client.d.ts +1 -0
- package/dist/core/db/db-studio-client.js +401 -0
- package/dist/core/db/db-studio-client.js.map +1 -0
- package/dist/core/db/db-studio-html.d.ts +4 -0
- package/dist/core/db/db-studio-html.js +83 -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 +528 -0
- package/dist/core/db/db-studio-server.js.map +1 -0
- package/dist/core/db/db-studio-styles.d.ts +1 -0
- package/dist/core/db/db-studio-styles.js +225 -0
- package/dist/core/db/db-studio-styles.js.map +1 -0
- package/dist/core/db/db-types.d.ts +214 -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 +242 -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 +306 -0
- package/src/core/db/db-metadata.ts +174 -0
- package/src/core/db/db-postgres-adapter.ts +293 -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 +157 -0
- package/src/core/db/db-studio-client.ts +397 -0
- package/src/core/db/db-studio-html.ts +85 -0
- package/src/core/db/db-studio-server.ts +626 -0
- package/src/core/db/db-studio-styles.ts +221 -0
- package/src/core/db/db-types.ts +243 -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,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
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import fastGlob from "fast-glob";
|
|
3
|
+
import { execa } from "execa";
|
|
4
|
+
import { splitCommand } from "./process";
|
|
5
|
+
import type { TInstallRepositoryOptions, TInstallRepositoryResult } from "./types";
|
|
6
|
+
|
|
7
|
+
function replaceVariables(content: string, variableValues: Record<string, string>): string {
|
|
8
|
+
return content.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (fullMatch: string, variableName: string) => {
|
|
9
|
+
return variableValues[variableName] ?? fullMatch;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function mergePackageOverrides(packageJson: Record<string, unknown>, temporaryOverrides: Record<string, string>): Record<string, unknown> {
|
|
14
|
+
if (Object.keys(temporaryOverrides).length === 0) {
|
|
15
|
+
return packageJson;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const currentOverrides = typeof packageJson.overrides === "object" && packageJson.overrides !== null && !Array.isArray(packageJson.overrides)
|
|
19
|
+
? packageJson.overrides as Record<string, unknown>
|
|
20
|
+
: {};
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
...packageJson,
|
|
24
|
+
overrides: {
|
|
25
|
+
...currentOverrides,
|
|
26
|
+
...temporaryOverrides,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function installRepository(options: TInstallRepositoryOptions): Promise<TInstallRepositoryResult> {
|
|
32
|
+
const filePaths = await fastGlob(options.filePatterns, {
|
|
33
|
+
cwd: options.repositoryPath,
|
|
34
|
+
absolute: true,
|
|
35
|
+
onlyFiles: true,
|
|
36
|
+
ignore: ["node_modules/**", "dist/**", ".git/**"],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const originalContents = new Map<string, string>();
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
for (const filePath of filePaths) {
|
|
43
|
+
const originalContent = await fs.readFile(filePath, "utf8");
|
|
44
|
+
originalContents.set(filePath, originalContent);
|
|
45
|
+
let nextContent = replaceVariables(originalContent, options.variableValues);
|
|
46
|
+
|
|
47
|
+
if (filePath.endsWith("package.json")) {
|
|
48
|
+
const packageJson = JSON.parse(nextContent) as Record<string, unknown>;
|
|
49
|
+
nextContent = `${JSON.stringify(mergePackageOverrides(packageJson, options.temporaryOverrides), null, 2)}\n`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (nextContent !== originalContent) {
|
|
53
|
+
await fs.writeFile(filePath, nextContent, "utf8");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { command, args } = splitCommand(options.installCommand);
|
|
58
|
+
const childProcess = execa(command, args, {
|
|
59
|
+
cwd: options.repositoryPath,
|
|
60
|
+
reject: false,
|
|
61
|
+
all: false,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
let stdout = "";
|
|
65
|
+
let stderr = "";
|
|
66
|
+
|
|
67
|
+
childProcess.stdout?.on("data", (chunk: Buffer) => {
|
|
68
|
+
const value = chunk.toString();
|
|
69
|
+
stdout += value;
|
|
70
|
+
options.onLog?.(value);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
childProcess.stderr?.on("data", (chunk: Buffer) => {
|
|
74
|
+
const value = chunk.toString();
|
|
75
|
+
stderr += value;
|
|
76
|
+
options.onErrorLog?.(value);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const result = await childProcess;
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
stdout,
|
|
83
|
+
stderr,
|
|
84
|
+
exitCode: result.exitCode ?? 0,
|
|
85
|
+
};
|
|
86
|
+
} finally {
|
|
87
|
+
for (const [filePath, content] of originalContents.entries()) {
|
|
88
|
+
await fs.writeFile(filePath, content, "utf8");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { searchableSelectChoice } from "./prompts";
|
|
4
|
+
|
|
5
|
+
const BACK_VALUE = "__SMDG_NAV_BACK__";
|
|
6
|
+
const EXIT_VALUE = "__SMDG_NAV_EXIT__";
|
|
7
|
+
|
|
8
|
+
// Commands that exist only for internal/background use and should never be
|
|
9
|
+
// offered in the interactive menu.
|
|
10
|
+
const INTERNAL_COMMAND_NAMES = new Set(["apps-cache-refresh"]);
|
|
11
|
+
|
|
12
|
+
function isHidden(command: Command): boolean {
|
|
13
|
+
return Boolean((command as unknown as { _hidden?: boolean })._hidden);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getNavigableChildren(command: Command): Command[] {
|
|
17
|
+
return command.commands.filter((child) => {
|
|
18
|
+
if (child.name() === "help") {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (isHidden(child)) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return !INTERNAL_COMMAND_NAMES.has(child.name());
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isGroup(command: Command): boolean {
|
|
31
|
+
return getNavigableChildren(command).length > 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildBreadcrumb(command: Command): string {
|
|
35
|
+
const names: string[] = [];
|
|
36
|
+
let current: Command | null | undefined = command;
|
|
37
|
+
|
|
38
|
+
while (current) {
|
|
39
|
+
names.unshift(current.name());
|
|
40
|
+
current = current.parent;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The bin is published as `smdg`; show that shorter name in breadcrumbs.
|
|
44
|
+
if (names[0] === "simplemdg") {
|
|
45
|
+
names[0] = "smdg";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return names.join(" ");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildChoiceTitle(command: Command): string {
|
|
52
|
+
const aliases = command.aliases();
|
|
53
|
+
const aliasText = aliases.length ? chalk.gray(` (${aliases.join(", ")})`) : "";
|
|
54
|
+
const marker = isGroup(command) ? chalk.cyan(" ›") : "";
|
|
55
|
+
const description = command.description();
|
|
56
|
+
const descriptionText = description ? ` ${chalk.gray(`— ${description}`)}` : "";
|
|
57
|
+
|
|
58
|
+
return `${chalk.bold(command.name())}${aliasText}${marker}${descriptionText}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function dispatchLeaf(leaf: Command): Promise<void> {
|
|
62
|
+
console.log(chalk.gray(`→ ${buildBreadcrumb(leaf)}`));
|
|
63
|
+
console.log("");
|
|
64
|
+
// Parsing the leaf with no user args runs its action with default options.
|
|
65
|
+
// Each leaf already prompts interactively for whatever it still needs.
|
|
66
|
+
await leaf.parseAsync([], { from: "user" });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Drive interactive navigation starting from a group command. Shows the list of
|
|
71
|
+
* subcommands, lets the user descend into nested groups, and finally runs the
|
|
72
|
+
* selected leaf command (which collects its own options interactively).
|
|
73
|
+
*/
|
|
74
|
+
export async function runGroupNavigator(startGroup: Command): Promise<void> {
|
|
75
|
+
const stack: Command[] = [startGroup];
|
|
76
|
+
|
|
77
|
+
while (stack.length > 0) {
|
|
78
|
+
const current = stack[stack.length - 1];
|
|
79
|
+
const children = getNavigableChildren(current);
|
|
80
|
+
|
|
81
|
+
if (children.length === 0) {
|
|
82
|
+
await dispatchLeaf(current);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const choices = children.map((child) => ({
|
|
87
|
+
title: buildChoiceTitle(child),
|
|
88
|
+
value: child.name(),
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
if (stack.length > 1) {
|
|
92
|
+
choices.push({ title: chalk.yellow("← Back"), value: BACK_VALUE });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
choices.push({ title: chalk.gray("✕ Exit"), value: EXIT_VALUE });
|
|
96
|
+
|
|
97
|
+
let picked: string;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
picked = await searchableSelectChoice({
|
|
101
|
+
message: `${chalk.cyan(buildBreadcrumb(current))} — type to filter, then select`,
|
|
102
|
+
choices,
|
|
103
|
+
allowCustomValue: false,
|
|
104
|
+
limit: 20,
|
|
105
|
+
});
|
|
106
|
+
} catch {
|
|
107
|
+
// ESC / cancel: step back one level, or exit when at the top.
|
|
108
|
+
if (stack.length > 1) {
|
|
109
|
+
stack.pop();
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (picked === EXIT_VALUE) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (picked === BACK_VALUE) {
|
|
121
|
+
stack.pop();
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const selected = children.find((child) => child.name() === picked);
|
|
126
|
+
|
|
127
|
+
if (!selected) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (isGroup(selected)) {
|
|
132
|
+
stack.push(selected);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await dispatchLeaf(selected);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Attach the interactive navigator to every group command in the tree. After
|
|
143
|
+
* this, invoking a group without a subcommand (e.g. `smdg cf` or `smdg cf db`)
|
|
144
|
+
* opens a searchable menu of its subcommands instead of printing help.
|
|
145
|
+
*
|
|
146
|
+
* Leaf commands keep their own action handlers untouched. Existing default
|
|
147
|
+
* actions on group commands are replaced by the navigator so navigation stays
|
|
148
|
+
* consistent across the whole CLI.
|
|
149
|
+
*/
|
|
150
|
+
export function enableInteractiveNavigation(program: Command): void {
|
|
151
|
+
const attach = (command: Command): void => {
|
|
152
|
+
for (const child of command.commands) {
|
|
153
|
+
if (isGroup(child)) {
|
|
154
|
+
child.action(async () => {
|
|
155
|
+
await runGroupNavigator(child);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
attach(child);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
attach(program);
|
|
164
|
+
}
|