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.
Files changed (140) hide show
  1. package/README.md +63 -354
  2. package/USER_GUIDE.md +55 -378
  3. package/dist/commands/cds.command.js +69 -60
  4. package/dist/commands/cds.command.js.map +1 -1
  5. package/dist/commands/cf-db.command.d.ts +2 -0
  6. package/dist/commands/cf-db.command.js +606 -0
  7. package/dist/commands/cf-db.command.js.map +1 -0
  8. package/dist/commands/cf.command.js +291 -280
  9. package/dist/commands/cf.command.js.map +1 -1
  10. package/dist/commands/gitlab.command.d.ts +2 -0
  11. package/dist/commands/gitlab.command.js +351 -0
  12. package/dist/commands/gitlab.command.js.map +1 -0
  13. package/dist/commands/npmrc.command.js +50 -44
  14. package/dist/commands/npmrc.command.js.map +1 -1
  15. package/dist/core/cache.d.ts +1 -1
  16. package/dist/core/cache.js +58 -31
  17. package/dist/core/cache.js.map +1 -1
  18. package/dist/core/cds.js +32 -22
  19. package/dist/core/cds.js.map +1 -1
  20. package/dist/core/cf-env-parser.d.ts +1 -1
  21. package/dist/core/cf-env-parser.js +4 -1
  22. package/dist/core/cf-env-parser.js.map +1 -1
  23. package/dist/core/cf.d.ts +1 -1
  24. package/dist/core/cf.js +46 -31
  25. package/dist/core/cf.js.map +1 -1
  26. package/dist/core/db/db-btp.d.ts +48 -0
  27. package/dist/core/db/db-btp.js +162 -0
  28. package/dist/core/db/db-btp.js.map +1 -0
  29. package/dist/core/db/db-cache.d.ts +35 -0
  30. package/dist/core/db/db-cache.js +164 -0
  31. package/dist/core/db/db-cache.js.map +1 -0
  32. package/dist/core/db/db-connection.d.ts +22 -0
  33. package/dist/core/db/db-connection.js +73 -0
  34. package/dist/core/db/db-connection.js.map +1 -0
  35. package/dist/core/db/db-crypto.d.ts +3 -0
  36. package/dist/core/db/db-crypto.js +54 -0
  37. package/dist/core/db/db-crypto.js.map +1 -0
  38. package/dist/core/db/db-hana-adapter.d.ts +32 -0
  39. package/dist/core/db/db-hana-adapter.js +243 -0
  40. package/dist/core/db/db-hana-adapter.js.map +1 -0
  41. package/dist/core/db/db-metadata.d.ts +25 -0
  42. package/dist/core/db/db-metadata.js +150 -0
  43. package/dist/core/db/db-metadata.js.map +1 -0
  44. package/dist/core/db/db-postgres-adapter.d.ts +30 -0
  45. package/dist/core/db/db-postgres-adapter.js +245 -0
  46. package/dist/core/db/db-postgres-adapter.js.map +1 -0
  47. package/dist/core/db/db-query-files.d.ts +20 -0
  48. package/dist/core/db/db-query-files.js +106 -0
  49. package/dist/core/db/db-query-files.js.map +1 -0
  50. package/dist/core/db/db-query-history.d.ts +5 -0
  51. package/dist/core/db/db-query-history.js +49 -0
  52. package/dist/core/db/db-query-history.js.map +1 -0
  53. package/dist/core/db/db-row.d.ts +22 -0
  54. package/dist/core/db/db-row.js +70 -0
  55. package/dist/core/db/db-row.js.map +1 -0
  56. package/dist/core/db/db-studio-html.d.ts +4 -0
  57. package/dist/core/db/db-studio-html.js +437 -0
  58. package/dist/core/db/db-studio-html.js.map +1 -0
  59. package/dist/core/db/db-studio-server.d.ts +11 -0
  60. package/dist/core/db/db-studio-server.js +465 -0
  61. package/dist/core/db/db-studio-server.js.map +1 -0
  62. package/dist/core/db/db-types.d.ts +174 -0
  63. package/dist/core/db/db-types.js +3 -0
  64. package/dist/core/db/db-types.js.map +1 -0
  65. package/dist/core/db/db-vcap-parser.d.ts +7 -0
  66. package/dist/core/db/db-vcap-parser.js +137 -0
  67. package/dist/core/db/db-vcap-parser.js.map +1 -0
  68. package/dist/core/doctor.d.ts +1 -1
  69. package/dist/core/doctor.js +14 -8
  70. package/dist/core/doctor.js.map +1 -1
  71. package/dist/core/guide.js +31 -26
  72. package/dist/core/guide.js.map +1 -1
  73. package/dist/core/install.d.ts +1 -1
  74. package/dist/core/install.js +17 -11
  75. package/dist/core/install.js.map +1 -1
  76. package/dist/core/navigator.d.ts +17 -0
  77. package/dist/core/navigator.js +140 -0
  78. package/dist/core/navigator.js.map +1 -0
  79. package/dist/core/npmrc.js +29 -16
  80. package/dist/core/npmrc.js.map +1 -1
  81. package/dist/core/process.js +11 -6
  82. package/dist/core/process.js.map +1 -1
  83. package/dist/core/prompts.js +16 -8
  84. package/dist/core/prompts.js.map +1 -1
  85. package/dist/core/repository.d.ts +1 -1
  86. package/dist/core/repository.js +16 -9
  87. package/dist/core/repository.js.map +1 -1
  88. package/dist/core/scanner.d.ts +1 -1
  89. package/dist/core/scanner.js +13 -7
  90. package/dist/core/scanner.js.map +1 -1
  91. package/dist/core/tooling.d.ts +28 -0
  92. package/dist/core/tooling.js +168 -0
  93. package/dist/core/tooling.js.map +1 -0
  94. package/dist/core/types.js +2 -1
  95. package/dist/core/version-conflict.d.ts +2 -2
  96. package/dist/core/version-conflict.js +11 -6
  97. package/dist/core/version-conflict.js.map +1 -1
  98. package/dist/index.js +65 -48
  99. package/dist/index.js.map +1 -1
  100. package/dist/types-local.js +2 -1
  101. package/package.json +12 -6
  102. package/src/commands/cds.command.ts +529 -0
  103. package/src/commands/cf-db.command.ts +636 -0
  104. package/src/commands/cf.command.ts +3345 -0
  105. package/src/commands/gitlab.command.ts +373 -0
  106. package/src/commands/npmrc.command.ts +581 -0
  107. package/src/core/cache.ts +332 -0
  108. package/src/core/cds.ts +278 -0
  109. package/src/core/cf-env-parser.ts +131 -0
  110. package/src/core/cf.ts +271 -0
  111. package/src/core/db/db-btp.ts +207 -0
  112. package/src/core/db/db-cache.ts +215 -0
  113. package/src/core/db/db-connection.ts +79 -0
  114. package/src/core/db/db-crypto.ts +53 -0
  115. package/src/core/db/db-hana-adapter.ts +294 -0
  116. package/src/core/db/db-metadata.ts +174 -0
  117. package/src/core/db/db-postgres-adapter.ts +275 -0
  118. package/src/core/db/db-query-files.ts +130 -0
  119. package/src/core/db/db-query-history.ts +53 -0
  120. package/src/core/db/db-row.ts +93 -0
  121. package/src/core/db/db-studio-html.ts +439 -0
  122. package/src/core/db/db-studio-server.ts +559 -0
  123. package/src/core/db/db-types.ts +195 -0
  124. package/src/core/db/db-vcap-parser.ts +182 -0
  125. package/src/core/doctor.ts +70 -0
  126. package/src/core/guide.ts +261 -0
  127. package/src/core/install.ts +91 -0
  128. package/src/core/navigator.ts +164 -0
  129. package/src/core/npmrc.ts +171 -0
  130. package/src/core/process.ts +75 -0
  131. package/src/core/prompts.ts +225 -0
  132. package/src/core/repository.ts +36 -0
  133. package/src/core/scanner.ts +41 -0
  134. package/src/core/tooling.ts +207 -0
  135. package/src/core/types.ts +152 -0
  136. package/src/core/version-conflict.ts +46 -0
  137. package/src/index.ts +460 -0
  138. package/src/types/external.d.ts +3 -0
  139. package/src/types-local.ts +11 -0
  140. package/tsconfig.json +17 -0
@@ -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
+ }
@@ -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
+ }