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,171 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
|
|
4
|
+
export type TNpmrcConfig = {
|
|
5
|
+
host: string;
|
|
6
|
+
scope: string;
|
|
7
|
+
packageId: string;
|
|
8
|
+
token: string;
|
|
9
|
+
outputFileName: string;
|
|
10
|
+
alwaysAuth: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function trimSlashes(value: string): string {
|
|
14
|
+
return value.trim().replace(/^\/+/, "").replace(/\/+$/, "");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function normalizeNpmScope(scope: string): string {
|
|
18
|
+
const trimmedScope = scope.trim();
|
|
19
|
+
if (!trimmedScope) {
|
|
20
|
+
throw new Error("Scope is required");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return trimmedScope.startsWith("@") ? trimmedScope : `@${trimmedScope}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function normalizeGitLabHost(host: string): string {
|
|
27
|
+
return trimSlashes(host.replace(/^https?:\/\//, ""));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildGitLabNpmRegistryUrl(options: { host: string; packageId: string }): string {
|
|
31
|
+
const host = normalizeGitLabHost(options.host);
|
|
32
|
+
const packageId = options.packageId.trim();
|
|
33
|
+
|
|
34
|
+
if (!/^\d+$/.test(packageId)) {
|
|
35
|
+
throw new Error("Package ID must be a number");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return `https://${host}/api/v4/projects/${packageId}/packages/npm/`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildGitLabNpmAuthRegistryPath(options: { host: string; packageId: string }): string {
|
|
42
|
+
const host = normalizeGitLabHost(options.host);
|
|
43
|
+
const packageId = options.packageId.trim();
|
|
44
|
+
|
|
45
|
+
if (!/^\d+$/.test(packageId)) {
|
|
46
|
+
throw new Error("Package ID must be a number");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return `//${host}/api/v4/projects/${packageId}/packages/npm/`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function removeExistingManagedLines(options: {
|
|
53
|
+
currentContent: string;
|
|
54
|
+
scope: string;
|
|
55
|
+
host: string;
|
|
56
|
+
}): string[] {
|
|
57
|
+
const normalizedScope = normalizeNpmScope(options.scope);
|
|
58
|
+
const normalizedHost = normalizeGitLabHost(options.host);
|
|
59
|
+
|
|
60
|
+
return options.currentContent
|
|
61
|
+
.split(/\r?\n/)
|
|
62
|
+
.filter((line) => {
|
|
63
|
+
const trimmedLine = line.trim();
|
|
64
|
+
|
|
65
|
+
if (!trimmedLine) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (trimmedLine.startsWith(`${normalizedScope}:registry=`)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (trimmedLine.includes(`${normalizedHost}/api/v4/projects/`) && trimmedLine.includes("/packages/npm/:_authToken=")) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (trimmedLine === "always-auth=true" || trimmedLine === "always-auth=false") {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return true;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function writeNpmrcFile(options: TNpmrcConfig): Promise<string> {
|
|
86
|
+
const outputPath = path.resolve(process.cwd(), options.outputFileName);
|
|
87
|
+
const existingContent = await fs.pathExists(outputPath) ? await fs.readFile(outputPath, "utf8") : "";
|
|
88
|
+
const preservedLines = removeExistingManagedLines({
|
|
89
|
+
currentContent: existingContent,
|
|
90
|
+
scope: options.scope,
|
|
91
|
+
host: options.host,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const scope = normalizeNpmScope(options.scope);
|
|
95
|
+
const registryUrl = buildGitLabNpmRegistryUrl({ host: options.host, packageId: options.packageId });
|
|
96
|
+
const authRegistryPath = buildGitLabNpmAuthRegistryPath({ host: options.host, packageId: options.packageId });
|
|
97
|
+
|
|
98
|
+
const managedLines = [
|
|
99
|
+
`${scope}:registry=${registryUrl}`,
|
|
100
|
+
`${authRegistryPath}:_authToken=${options.token.trim()}`,
|
|
101
|
+
`always-auth=${options.alwaysAuth ? "true" : "false"}`,
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const nextContent = [...preservedLines, ...managedLines].join("\n") + "\n";
|
|
105
|
+
|
|
106
|
+
await fs.writeFile(outputPath, nextContent, "utf8");
|
|
107
|
+
return outputPath;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function parsePackageIdList(input: string): string[] {
|
|
111
|
+
return [...new Set(input
|
|
112
|
+
.split(/[\s,;]+/)
|
|
113
|
+
.map((value) => value.trim())
|
|
114
|
+
.filter(Boolean)
|
|
115
|
+
.filter((value) => /^\d+$/.test(value)))] ;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function readPackageJsonName(repositoryPath: string): Promise<string | undefined> {
|
|
119
|
+
const packageJsonPath = path.join(repositoryPath, "package.json");
|
|
120
|
+
|
|
121
|
+
if (!(await fs.pathExists(packageJsonPath))) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const packageJson = await fs.readJson(packageJsonPath).catch(() => undefined) as { name?: string } | undefined;
|
|
126
|
+
return packageJson?.name;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type TParsedPackageInput = {
|
|
130
|
+
packageId: string;
|
|
131
|
+
packageName: string;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export function parsePackageInputList(input: string): TParsedPackageInput[] {
|
|
135
|
+
const entries: TParsedPackageInput[] = [];
|
|
136
|
+
const keys = new Set<string>();
|
|
137
|
+
|
|
138
|
+
for (const rawLine of input.split(/\r?\n/)) {
|
|
139
|
+
const trimmedLine = rawLine.trim();
|
|
140
|
+
|
|
141
|
+
if (!trimmedLine) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const tokens = trimmedLine.split(/[|,;\t]+/).map((value) => value.trim()).filter(Boolean);
|
|
146
|
+
const firstNumericTokenIndex = tokens.findIndex((value) => /^\d+$/.test(value));
|
|
147
|
+
|
|
148
|
+
if (firstNumericTokenIndex >= 0) {
|
|
149
|
+
const packageId = tokens[firstNumericTokenIndex];
|
|
150
|
+
const packageNameTokens = tokens.filter((_, index) => index !== firstNumericTokenIndex);
|
|
151
|
+
const packageName = packageNameTokens.join(" - ").trim() || packageId;
|
|
152
|
+
const key = packageId;
|
|
153
|
+
|
|
154
|
+
if (!keys.has(key)) {
|
|
155
|
+
keys.add(key);
|
|
156
|
+
entries.push({ packageId, packageName });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const packageId of parsePackageIdList(trimmedLine)) {
|
|
163
|
+
if (!keys.has(packageId)) {
|
|
164
|
+
keys.add(packageId);
|
|
165
|
+
entries.push({ packageId, packageName: packageId });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return entries;
|
|
171
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
|
|
3
|
+
export type TCommandResult = {
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
exitCode: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export async function runCommand(command: string, args: string[], options?: { cwd?: string; reject?: boolean }): Promise<TCommandResult> {
|
|
10
|
+
const result = await execa(command, args, {
|
|
11
|
+
cwd: options?.cwd,
|
|
12
|
+
reject: options?.reject ?? false,
|
|
13
|
+
all: false,
|
|
14
|
+
shell: false,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
stdout: result.stdout,
|
|
19
|
+
stderr: result.stderr,
|
|
20
|
+
exitCode: result.exitCode ?? 0,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function runCommandInherit(command: string, args: string[], options?: { cwd?: string }): Promise<number> {
|
|
25
|
+
const result = await execa(command, args, {
|
|
26
|
+
cwd: options?.cwd,
|
|
27
|
+
stdio: "inherit",
|
|
28
|
+
reject: false,
|
|
29
|
+
shell: false,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return result.exitCode ?? 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function splitCommand(commandLine: string): { command: string; args: string[] } {
|
|
36
|
+
const tokens: string[] = [];
|
|
37
|
+
let current = "";
|
|
38
|
+
let quote: '"' | "'" | undefined;
|
|
39
|
+
|
|
40
|
+
for (let index = 0; index < commandLine.length; index += 1) {
|
|
41
|
+
const character = commandLine[index];
|
|
42
|
+
|
|
43
|
+
if ((character === '"' || character === "'") && !quote) {
|
|
44
|
+
quote = character;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (quote && character === quote) {
|
|
49
|
+
quote = undefined;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!quote && /\s/.test(character)) {
|
|
54
|
+
if (current) {
|
|
55
|
+
tokens.push(current);
|
|
56
|
+
current = "";
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
current += character;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (current) {
|
|
65
|
+
tokens.push(current);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const [command, ...args] = tokens;
|
|
69
|
+
|
|
70
|
+
if (!command) {
|
|
71
|
+
throw new Error("Command is required");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { command, args };
|
|
75
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import prompts from "prompts";
|
|
2
|
+
|
|
3
|
+
const CUSTOM_VALUE_PREFIX = "__SMDG_CUSTOM_VALUE__:";
|
|
4
|
+
|
|
5
|
+
function normalizeValue(value: string): string {
|
|
6
|
+
return value.trim();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function uniqueValues(values: Array<string | undefined>): string[] {
|
|
10
|
+
return [...new Set(values.map((value) => normalizeValue(value ?? "")).filter(Boolean))];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function scoreMatch(input: string, value: string): number {
|
|
14
|
+
const normalizedInput = input.toLowerCase().trim();
|
|
15
|
+
const normalizedValue = value.toLowerCase();
|
|
16
|
+
|
|
17
|
+
if (!normalizedInput) {
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (normalizedValue === normalizedInput) {
|
|
22
|
+
return 100;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (normalizedValue.startsWith(normalizedInput)) {
|
|
26
|
+
return 80;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (normalizedValue.includes(normalizedInput)) {
|
|
30
|
+
return 60;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const inputParts = normalizedInput.split(/\s+/).filter(Boolean);
|
|
34
|
+
|
|
35
|
+
if (inputParts.every((part) => normalizedValue.includes(part))) {
|
|
36
|
+
return 40;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return -1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildSearchableChoices(options: {
|
|
43
|
+
input: string;
|
|
44
|
+
choices: prompts.Choice[];
|
|
45
|
+
allowCustomValue: boolean;
|
|
46
|
+
customValueTitle?: (value: string) => string;
|
|
47
|
+
}): prompts.Choice[] {
|
|
48
|
+
const input = normalizeValue(options.input);
|
|
49
|
+
const scoredChoices = options.choices
|
|
50
|
+
.map((choice, index) => {
|
|
51
|
+
const title = String(choice.title ?? "");
|
|
52
|
+
const value = String(choice.value ?? choice.title ?? "");
|
|
53
|
+
const score = input ? Math.max(scoreMatch(input, title), scoreMatch(input, value)) : 0;
|
|
54
|
+
|
|
55
|
+
return { choice, index, score };
|
|
56
|
+
})
|
|
57
|
+
.filter((item) => !input || item.score >= 0)
|
|
58
|
+
.sort((left, right) => right.score - left.score || left.index - right.index)
|
|
59
|
+
.map((item) => item.choice);
|
|
60
|
+
|
|
61
|
+
const hasExactMatch = options.choices.some((choice) => {
|
|
62
|
+
const title = String(choice.title ?? "").trim().toLowerCase();
|
|
63
|
+
const value = String(choice.value ?? choice.title ?? "").trim().toLowerCase();
|
|
64
|
+
const normalizedInput = input.toLowerCase();
|
|
65
|
+
|
|
66
|
+
return title === normalizedInput || value === normalizedInput;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (options.allowCustomValue && input && !hasExactMatch) {
|
|
70
|
+
return [
|
|
71
|
+
...scoredChoices,
|
|
72
|
+
{
|
|
73
|
+
title: options.customValueTitle?.(input) ?? `Use typed value: ${input}`,
|
|
74
|
+
value: `${CUSTOM_VALUE_PREFIX}${input}`,
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return scoredChoices;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function searchableSelectChoice<TValue extends string>(options: {
|
|
83
|
+
message: string;
|
|
84
|
+
choices: Array<{ title: string; value: TValue; description?: string }>;
|
|
85
|
+
validateCustomValue?: (value: string) => true | string;
|
|
86
|
+
allowCustomValue?: boolean;
|
|
87
|
+
customValueTitle?: (value: string) => string;
|
|
88
|
+
limit?: number;
|
|
89
|
+
}): Promise<string> {
|
|
90
|
+
const allowCustomValue = options.allowCustomValue ?? true;
|
|
91
|
+
const validate = options.validateCustomValue ?? ((value: string) => value.trim() ? true : "Value is required");
|
|
92
|
+
const choices = options.choices.map((choice) => ({
|
|
93
|
+
title: choice.title,
|
|
94
|
+
value: choice.value,
|
|
95
|
+
description: choice.description,
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
if (choices.length === 0) {
|
|
99
|
+
const response = await prompts({
|
|
100
|
+
type: "text",
|
|
101
|
+
name: "value",
|
|
102
|
+
message: options.message,
|
|
103
|
+
validate,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!response.value) {
|
|
107
|
+
throw new Error("Cancelled");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return String(response.value).trim();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const response = await prompts({
|
|
114
|
+
type: "autocomplete",
|
|
115
|
+
name: "value",
|
|
116
|
+
message: options.message,
|
|
117
|
+
choices,
|
|
118
|
+
initial: 0,
|
|
119
|
+
limit: options.limit ?? 12,
|
|
120
|
+
suggest: async (input: string, currentChoices: prompts.Choice[]) => buildSearchableChoices({
|
|
121
|
+
input,
|
|
122
|
+
choices: currentChoices,
|
|
123
|
+
allowCustomValue,
|
|
124
|
+
customValueTitle: options.customValueTitle,
|
|
125
|
+
}),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (!response.value) {
|
|
129
|
+
throw new Error("Cancelled");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const selectedValue = String(response.value);
|
|
133
|
+
|
|
134
|
+
if (selectedValue.startsWith(CUSTOM_VALUE_PREFIX)) {
|
|
135
|
+
const customValue = selectedValue.slice(CUSTOM_VALUE_PREFIX.length).trim();
|
|
136
|
+
const validationResult = validate(customValue);
|
|
137
|
+
|
|
138
|
+
if (validationResult !== true) {
|
|
139
|
+
throw new Error(validationResult);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return customValue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return selectedValue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function searchableSelectOrInput(options: {
|
|
149
|
+
message: string;
|
|
150
|
+
values: string[];
|
|
151
|
+
initialValue?: string;
|
|
152
|
+
inputMessage?: string;
|
|
153
|
+
validate?: (value: string) => true | string;
|
|
154
|
+
allowCustomValue?: boolean;
|
|
155
|
+
customValueTitle?: (value: string) => string;
|
|
156
|
+
limit?: number;
|
|
157
|
+
}): Promise<string> {
|
|
158
|
+
const values = uniqueValues([options.initialValue, ...options.values]);
|
|
159
|
+
const choices = values.map((value) => ({ title: value, value }));
|
|
160
|
+
const allowCustomValue = options.allowCustomValue ?? true;
|
|
161
|
+
const validate = options.validate ?? ((value: string) => value.trim() ? true : "Value is required");
|
|
162
|
+
|
|
163
|
+
if (choices.length > 0) {
|
|
164
|
+
const response = await prompts({
|
|
165
|
+
type: "autocomplete",
|
|
166
|
+
name: "value",
|
|
167
|
+
message: options.message,
|
|
168
|
+
choices,
|
|
169
|
+
initial: 0,
|
|
170
|
+
limit: options.limit ?? 12,
|
|
171
|
+
suggest: async (input: string, currentChoices: prompts.Choice[]) => buildSearchableChoices({
|
|
172
|
+
input,
|
|
173
|
+
choices: currentChoices,
|
|
174
|
+
allowCustomValue,
|
|
175
|
+
customValueTitle: options.customValueTitle,
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (!response.value) {
|
|
180
|
+
throw new Error("Cancelled");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const selectedValue = String(response.value);
|
|
184
|
+
|
|
185
|
+
if (selectedValue.startsWith(CUSTOM_VALUE_PREFIX)) {
|
|
186
|
+
const customValue = selectedValue.slice(CUSTOM_VALUE_PREFIX.length).trim();
|
|
187
|
+
const validationResult = validate(customValue);
|
|
188
|
+
|
|
189
|
+
if (validationResult !== true) {
|
|
190
|
+
throw new Error(validationResult);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return customValue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return selectedValue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const input = await prompts({
|
|
200
|
+
type: "text",
|
|
201
|
+
name: "value",
|
|
202
|
+
message: options.inputMessage ?? options.message,
|
|
203
|
+
initial: options.initialValue ?? values[0] ?? "",
|
|
204
|
+
validate,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (!input.value) {
|
|
208
|
+
throw new Error("Cancelled");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return String(input.value).trim();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function selectFromHistoryOrInput(options: {
|
|
215
|
+
message: string;
|
|
216
|
+
values: string[];
|
|
217
|
+
initialValue?: string;
|
|
218
|
+
inputMessage?: string;
|
|
219
|
+
validate?: (value: string) => true | string;
|
|
220
|
+
}): Promise<string> {
|
|
221
|
+
return searchableSelectOrInput({
|
|
222
|
+
...options,
|
|
223
|
+
allowCustomValue: true,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import type { TRepositoryInfo } from "./types";
|
|
4
|
+
|
|
5
|
+
export async function findNearestRepository(startPath: string): Promise<TRepositoryInfo | undefined> {
|
|
6
|
+
let currentPath = path.resolve(startPath);
|
|
7
|
+
|
|
8
|
+
while (true) {
|
|
9
|
+
if (await fs.pathExists(path.join(currentPath, "package.json"))) {
|
|
10
|
+
return { repositoryPath: currentPath };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const parentPath = path.dirname(currentPath);
|
|
14
|
+
|
|
15
|
+
if (parentPath === currentPath) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
currentPath = parentPath;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function resolveRepositoryPath(cwd: string): Promise<string> {
|
|
24
|
+
const absoluteCwd = path.resolve(cwd);
|
|
25
|
+
const repository = await findNearestRepository(absoluteCwd);
|
|
26
|
+
|
|
27
|
+
if (repository?.repositoryPath) {
|
|
28
|
+
return repository.repositoryPath;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (await fs.pathExists(path.join(absoluteCwd, "package.json"))) {
|
|
32
|
+
return absoluteCwd;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
throw new Error(`Cannot find repository from ${absoluteCwd}`);
|
|
36
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import fastGlob from "fast-glob";
|
|
4
|
+
import type { TScannedVariable } from "./types";
|
|
5
|
+
|
|
6
|
+
const VARIABLE_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
7
|
+
|
|
8
|
+
export async function scanRepositoryVariables(options: {
|
|
9
|
+
repositoryPath: string;
|
|
10
|
+
filePatterns: string[];
|
|
11
|
+
}): Promise<TScannedVariable[]> {
|
|
12
|
+
const filePaths = await fastGlob(options.filePatterns, {
|
|
13
|
+
cwd: options.repositoryPath,
|
|
14
|
+
absolute: true,
|
|
15
|
+
onlyFiles: true,
|
|
16
|
+
ignore: ["node_modules/**", "dist/**", ".git/**"],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const result: TScannedVariable[] = [];
|
|
20
|
+
|
|
21
|
+
for (const filePath of filePaths) {
|
|
22
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
23
|
+
const occurrencesByVariable = new Map<string, number>();
|
|
24
|
+
let match: RegExpExecArray | null;
|
|
25
|
+
|
|
26
|
+
while ((match = VARIABLE_PATTERN.exec(content)) !== null) {
|
|
27
|
+
const variableName = match[1];
|
|
28
|
+
occurrencesByVariable.set(variableName, (occurrencesByVariable.get(variableName) ?? 0) + 1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const [variableName, occurrences] of occurrencesByVariable.entries()) {
|
|
32
|
+
result.push({
|
|
33
|
+
variableName,
|
|
34
|
+
occurrences,
|
|
35
|
+
filePath: path.relative(options.repositoryPath, filePath),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return result.sort((left, right) => left.variableName.localeCompare(right.variableName));
|
|
41
|
+
}
|