@twin.org/cli-core 0.9.3-next.7 → 0.9.3-next.8

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.
@@ -1,8 +1,8 @@
1
1
  // Copyright 2024 IOTA Stiftung.
2
2
  // SPDX-License-Identifier: Apache-2.0.
3
- import { exec, spawn } from "node:child_process";
3
+ import { spawn } from "node:child_process";
4
4
  import { accessSync, readFileSync, statSync } from "node:fs";
5
- import { access, mkdir, readFile, stat, writeFile } from "node:fs/promises";
5
+ import { access, mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
6
6
  import path from "node:path";
7
7
  import { Coerce, I18n, Is, ObjectHelper } from "@twin.org/core";
8
8
  import { CLIDisplay } from "./cliDisplay.js";
@@ -111,21 +111,36 @@ export class CLIUtils {
111
111
  }
112
112
  }
113
113
  /**
114
- * Find the NPM root based on a package.json path.
115
- * @param rootFolder The path to the package.json.
116
- * @returns The root path.
114
+ * Find the root folder of a package by walking up the directory tree looking in the
115
+ * node_modules folders, this supports package managers which do not install a flat structure.
116
+ * @param packageName The name of the package to locate.
117
+ * @param startFolder The folder to start the search from.
118
+ * @returns The resolved root folder of the package, or undefined if it could not be located.
117
119
  */
118
- static async findNpmRoot(rootFolder) {
119
- return new Promise((resolve, reject) => {
120
- exec("npm root", { cwd: rootFolder }, (error, stdout, stderr) => {
121
- if (error) {
122
- reject(error);
123
- }
124
- else {
125
- resolve(stdout.trim());
120
+ static async findPackageRoot(packageName, startFolder) {
121
+ let currentFolder = path.resolve(startFolder);
122
+ let searching = true;
123
+ while (searching) {
124
+ // A node_modules folder never contains a nested node_modules folder of its own,
125
+ // the packages inside it do, so there is nothing to check at this level.
126
+ if (path.basename(currentFolder) !== "node_modules") {
127
+ const packageFolder = path.join(currentFolder, "node_modules", packageName);
128
+ if (await CLIUtils.fileExists(path.join(packageFolder, "package.json"))) {
129
+ // The entry can be a symlink, so resolve it to the real location, this makes
130
+ // sure that walking up from this folder finds the dependencies of the package
131
+ // and not those of the folder linking to it.
132
+ try {
133
+ return await realpath(packageFolder);
134
+ }
135
+ catch {
136
+ return packageFolder;
137
+ }
126
138
  }
127
- });
128
- });
139
+ }
140
+ const parentFolder = path.dirname(currentFolder);
141
+ searching = parentFolder !== currentFolder;
142
+ currentFolder = parentFolder;
143
+ }
129
144
  }
130
145
  /**
131
146
  * Run a shell command.
@@ -1 +1 @@
1
- {"version":3,"file":"cliUtils.js","sourceRoot":"","sources":["../../src/cliUtils.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { exec, spawn } from \"node:child_process\";\nimport { accessSync, readFileSync, statSync } from \"node:fs\";\nimport { access, mkdir, readFile, stat, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { Coerce, I18n, Is, ObjectHelper } from \"@twin.org/core\";\nimport { CLIDisplay } from \"./cliDisplay.js\";\n\n/**\n * Utilities function for helping in the CLI.\n */\nexport class CLIUtils {\n\t/**\n\t * Does the specified file exist.\n\t * @param filename The filename to check for existence.\n\t * @returns True if the file exists.\n\t */\n\tpublic static async fileExists(filename: string): Promise<boolean> {\n\t\ttry {\n\t\t\tconst stats = await stat(filename);\n\t\t\treturn stats.isFile();\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Does the specified file exist, synchronously.\n\t * @param filename The filename to check for existence.\n\t * @returns True if the file exists.\n\t */\n\tpublic static fileExistsSync(filename: string): boolean {\n\t\ttry {\n\t\t\tconst stats = statSync(filename);\n\t\t\treturn stats.isFile();\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Check if the dir exists.\n\t * @param dir The directory to check.\n\t * @returns True if the dir exists.\n\t */\n\tpublic static async dirExists(dir: string): Promise<boolean> {\n\t\ttry {\n\t\t\tawait access(dir);\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Check if the dir exists, synchronously.\n\t * @param dir The directory to check.\n\t * @returns True if the dir exists.\n\t */\n\tpublic static dirExistsSync(dir: string): boolean {\n\t\ttry {\n\t\t\taccessSync(dir);\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Read a JSON file and parse it.\n\t * @param filename The filename to read.\n\t * @returns The parsed JSON.\n\t */\n\tpublic static async readJsonFile<T = unknown>(filename: string): Promise<T | undefined> {\n\t\tif (await CLIUtils.fileExists(filename)) {\n\t\t\tconst content = await readFile(filename, \"utf8\");\n\t\t\treturn JSON.parse(content);\n\t\t}\n\t}\n\n\t/**\n\t * Read a JSON file and parse it, synchronously.\n\t * @param filename The filename to read.\n\t * @returns The parsed JSON.\n\t */\n\tpublic static readJsonFileSync<T = unknown>(filename: string): T | undefined {\n\t\tif (CLIUtils.fileExistsSync(filename)) {\n\t\t\tconst content = readFileSync(filename, \"utf8\");\n\t\t\treturn JSON.parse(content);\n\t\t}\n\t}\n\n\t/**\n\t * Read a file as lines.\n\t * @param filename The filename to read.\n\t * @returns The lines.\n\t */\n\tpublic static async readLinesFile(filename: string): Promise<string[] | undefined> {\n\t\tif (await CLIUtils.fileExists(filename)) {\n\t\t\tconst content = await readFile(filename, \"utf8\");\n\t\t\treturn content.split(\"\\n\");\n\t\t}\n\t}\n\n\t/**\n\t * Read a file as lines, synchronously.\n\t * @param filename The filename to read.\n\t * @returns The lines.\n\t */\n\tpublic static readLinesFileSync(filename: string): string[] | undefined {\n\t\tif (CLIUtils.fileExistsSync(filename)) {\n\t\t\tconst content = readFileSync(filename, \"utf8\");\n\t\t\treturn content.split(\"\\n\");\n\t\t}\n\t}\n\n\t/**\n\t * Find the NPM root based on a package.json path.\n\t * @param rootFolder The path to the package.json.\n\t * @returns The root path.\n\t */\n\tpublic static async findNpmRoot(rootFolder: string): Promise<string> {\n\t\treturn new Promise<string>((resolve, reject) => {\n\t\t\texec(\"npm root\", { cwd: rootFolder }, (error, stdout, stderr) => {\n\t\t\t\tif (error) {\n\t\t\t\t\treject(error);\n\t\t\t\t} else {\n\t\t\t\t\tresolve(stdout.trim());\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * Run a shell command.\n\t * @param command The app to run in the shell.\n\t * @param args The args for the app.\n\t * @param cwd The working directory to execute the command in.\n\t * @returns Promise to wait for command execution to complete.\n\t */\n\tpublic static async runShellCmd(command: string, args: string[], cwd: string): Promise<void> {\n\t\tconst osCommand = process.platform.startsWith(\"win\") ? `${command}.cmd` : command;\n\n\t\treturn CLIUtils.runShellApp(osCommand, args, cwd);\n\t}\n\n\t/**\n\t * Run a shell app.\n\t * @param app The app to run in the shell.\n\t * @param args The args for the app.\n\t * @param cwd The working directory to execute the command in.\n\t * @returns Promise to wait for command execution to complete.\n\t */\n\tpublic static async runShellApp(app: string, args: string[], cwd: string): Promise<void> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst sp = spawn(app, args, {\n\t\t\t\tshell: true,\n\t\t\t\tcwd\n\t\t\t});\n\n\t\t\tsp.stdout?.on(\"data\", data => {\n\t\t\t\tCLIDisplay.write(data);\n\t\t\t});\n\n\t\t\tsp.stderr?.on(\"data\", data => {\n\t\t\t\tCLIDisplay.writeError(data);\n\t\t\t});\n\n\t\t\tsp.on(\"exit\", (exitCode, signals) => {\n\t\t\t\tif (Coerce.number(exitCode) !== 0 || signals?.length) {\n\t\t\t\t\t// eslint-disable-next-line no-restricted-syntax\n\t\t\t\t\treject(new Error(\"Run failed\"));\n\t\t\t\t} else {\n\t\t\t\t\tresolve();\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * Write a JSON file.\n\t * @param jsonFilename The filename to write.\n\t * @param data The data to write.\n\t * @param append Append to the file.\n\t */\n\tpublic static async writeJsonFile<T = unknown>(\n\t\tjsonFilename: string | undefined,\n\t\tdata: T,\n\t\tappend: boolean\n\t): Promise<void> {\n\t\tif (Is.stringValue(jsonFilename)) {\n\t\t\tconst filename = path.resolve(jsonFilename);\n\t\t\tlet currentJson = {};\n\t\t\tif (append) {\n\t\t\t\tCLIDisplay.task(I18n.formatMessage(\"cli.progress.readingJsonFile\"), filename);\n\t\t\t\tcurrentJson = (await CLIUtils.readJsonFile(filename)) ?? {};\n\t\t\t}\n\t\t\tCLIDisplay.task(I18n.formatMessage(\"cli.progress.writingJsonFile\"), filename);\n\t\t\tCLIDisplay.break();\n\n\t\t\tawait mkdir(path.dirname(filename), { recursive: true });\n\t\t\tawait writeFile(\n\t\t\t\tfilename,\n\t\t\t\t`${JSON.stringify(ObjectHelper.merge(currentJson, data), undefined, \"\\t\")}\\n`\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Write an env file.\n\t * @param envFilename The filename to write.\n\t * @param data The data to write.\n\t * @param append Append to the file.\n\t */\n\tpublic static async writeEnvFile(\n\t\tenvFilename: string | undefined,\n\t\tdata: string[],\n\t\tappend: boolean\n\t): Promise<void> {\n\t\tif (Is.stringValue(envFilename)) {\n\t\t\tconst filename = path.resolve(envFilename);\n\n\t\t\tconst outputKeys: string[] = [];\n\t\t\tconst outputDict: { [key: string]: string } = {};\n\n\t\t\tif (append) {\n\t\t\t\tCLIDisplay.task(I18n.formatMessage(\"cli.progress.readingEnvFile\"), filename);\n\t\t\t\tconst lines = await CLIUtils.readLinesFile(filename);\n\t\t\t\tif (Is.arrayValue(lines)) {\n\t\t\t\t\tfor (const line of lines) {\n\t\t\t\t\t\tconst parts = line.split(\"=\");\n\t\t\t\t\t\toutputKeys.push(parts[0]);\n\t\t\t\t\t\toutputDict[parts[0]] = parts.slice(1).join(\"=\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tCLIDisplay.task(I18n.formatMessage(\"cli.progress.writingEnvFile\"), filename);\n\t\t\tCLIDisplay.break();\n\n\t\t\tif (Is.arrayValue(data)) {\n\t\t\t\tfor (const line of data) {\n\t\t\t\t\tconst parts = line.split(\"=\");\n\t\t\t\t\tconst currentIndex = outputKeys.indexOf(parts[0]);\n\t\t\t\t\tif (currentIndex !== -1) {\n\t\t\t\t\t\toutputKeys.splice(currentIndex, 1);\n\t\t\t\t\t}\n\t\t\t\t\toutputKeys.push(parts[0]);\n\t\t\t\t\toutputDict[parts[0]] = parts.slice(1).join(\"=\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst output: string[] = [];\n\t\t\tfor (const outputKey of outputKeys) {\n\t\t\t\toutput.push(`${outputKey}=${outputDict[outputKey]}`);\n\t\t\t}\n\n\t\t\tawait mkdir(path.dirname(filename), { recursive: true });\n\t\t\tawait writeFile(filename, output.join(\"\\n\"));\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"cliUtils.js","sourceRoot":"","sources":["../../src/cliUtils.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { spawn } from \"node:child_process\";\nimport { accessSync, readFileSync, statSync } from \"node:fs\";\nimport { access, mkdir, readFile, realpath, stat, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { Coerce, I18n, Is, ObjectHelper } from \"@twin.org/core\";\nimport { CLIDisplay } from \"./cliDisplay.js\";\n\n/**\n * Utilities function for helping in the CLI.\n */\nexport class CLIUtils {\n\t/**\n\t * Does the specified file exist.\n\t * @param filename The filename to check for existence.\n\t * @returns True if the file exists.\n\t */\n\tpublic static async fileExists(filename: string): Promise<boolean> {\n\t\ttry {\n\t\t\tconst stats = await stat(filename);\n\t\t\treturn stats.isFile();\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Does the specified file exist, synchronously.\n\t * @param filename The filename to check for existence.\n\t * @returns True if the file exists.\n\t */\n\tpublic static fileExistsSync(filename: string): boolean {\n\t\ttry {\n\t\t\tconst stats = statSync(filename);\n\t\t\treturn stats.isFile();\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Check if the dir exists.\n\t * @param dir The directory to check.\n\t * @returns True if the dir exists.\n\t */\n\tpublic static async dirExists(dir: string): Promise<boolean> {\n\t\ttry {\n\t\t\tawait access(dir);\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Check if the dir exists, synchronously.\n\t * @param dir The directory to check.\n\t * @returns True if the dir exists.\n\t */\n\tpublic static dirExistsSync(dir: string): boolean {\n\t\ttry {\n\t\t\taccessSync(dir);\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Read a JSON file and parse it.\n\t * @param filename The filename to read.\n\t * @returns The parsed JSON.\n\t */\n\tpublic static async readJsonFile<T = unknown>(filename: string): Promise<T | undefined> {\n\t\tif (await CLIUtils.fileExists(filename)) {\n\t\t\tconst content = await readFile(filename, \"utf8\");\n\t\t\treturn JSON.parse(content);\n\t\t}\n\t}\n\n\t/**\n\t * Read a JSON file and parse it, synchronously.\n\t * @param filename The filename to read.\n\t * @returns The parsed JSON.\n\t */\n\tpublic static readJsonFileSync<T = unknown>(filename: string): T | undefined {\n\t\tif (CLIUtils.fileExistsSync(filename)) {\n\t\t\tconst content = readFileSync(filename, \"utf8\");\n\t\t\treturn JSON.parse(content);\n\t\t}\n\t}\n\n\t/**\n\t * Read a file as lines.\n\t * @param filename The filename to read.\n\t * @returns The lines.\n\t */\n\tpublic static async readLinesFile(filename: string): Promise<string[] | undefined> {\n\t\tif (await CLIUtils.fileExists(filename)) {\n\t\t\tconst content = await readFile(filename, \"utf8\");\n\t\t\treturn content.split(\"\\n\");\n\t\t}\n\t}\n\n\t/**\n\t * Read a file as lines, synchronously.\n\t * @param filename The filename to read.\n\t * @returns The lines.\n\t */\n\tpublic static readLinesFileSync(filename: string): string[] | undefined {\n\t\tif (CLIUtils.fileExistsSync(filename)) {\n\t\t\tconst content = readFileSync(filename, \"utf8\");\n\t\t\treturn content.split(\"\\n\");\n\t\t}\n\t}\n\n\t/**\n\t * Find the root folder of a package by walking up the directory tree looking in the\n\t * node_modules folders, this supports package managers which do not install a flat structure.\n\t * @param packageName The name of the package to locate.\n\t * @param startFolder The folder to start the search from.\n\t * @returns The resolved root folder of the package, or undefined if it could not be located.\n\t */\n\tpublic static async findPackageRoot(\n\t\tpackageName: string,\n\t\tstartFolder: string\n\t): Promise<string | undefined> {\n\t\tlet currentFolder = path.resolve(startFolder);\n\t\tlet searching = true;\n\n\t\twhile (searching) {\n\t\t\t// A node_modules folder never contains a nested node_modules folder of its own,\n\t\t\t// the packages inside it do, so there is nothing to check at this level.\n\t\t\tif (path.basename(currentFolder) !== \"node_modules\") {\n\t\t\t\tconst packageFolder = path.join(currentFolder, \"node_modules\", packageName);\n\n\t\t\t\tif (await CLIUtils.fileExists(path.join(packageFolder, \"package.json\"))) {\n\t\t\t\t\t// The entry can be a symlink, so resolve it to the real location, this makes\n\t\t\t\t\t// sure that walking up from this folder finds the dependencies of the package\n\t\t\t\t\t// and not those of the folder linking to it.\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn await realpath(packageFolder);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\treturn packageFolder;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst parentFolder = path.dirname(currentFolder);\n\t\t\tsearching = parentFolder !== currentFolder;\n\t\t\tcurrentFolder = parentFolder;\n\t\t}\n\t}\n\n\t/**\n\t * Run a shell command.\n\t * @param command The app to run in the shell.\n\t * @param args The args for the app.\n\t * @param cwd The working directory to execute the command in.\n\t * @returns Promise to wait for command execution to complete.\n\t */\n\tpublic static async runShellCmd(command: string, args: string[], cwd: string): Promise<void> {\n\t\tconst osCommand = process.platform.startsWith(\"win\") ? `${command}.cmd` : command;\n\n\t\treturn CLIUtils.runShellApp(osCommand, args, cwd);\n\t}\n\n\t/**\n\t * Run a shell app.\n\t * @param app The app to run in the shell.\n\t * @param args The args for the app.\n\t * @param cwd The working directory to execute the command in.\n\t * @returns Promise to wait for command execution to complete.\n\t */\n\tpublic static async runShellApp(app: string, args: string[], cwd: string): Promise<void> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst sp = spawn(app, args, {\n\t\t\t\tshell: true,\n\t\t\t\tcwd\n\t\t\t});\n\n\t\t\tsp.stdout?.on(\"data\", data => {\n\t\t\t\tCLIDisplay.write(data);\n\t\t\t});\n\n\t\t\tsp.stderr?.on(\"data\", data => {\n\t\t\t\tCLIDisplay.writeError(data);\n\t\t\t});\n\n\t\t\tsp.on(\"exit\", (exitCode, signals) => {\n\t\t\t\tif (Coerce.number(exitCode) !== 0 || signals?.length) {\n\t\t\t\t\t// eslint-disable-next-line no-restricted-syntax\n\t\t\t\t\treject(new Error(\"Run failed\"));\n\t\t\t\t} else {\n\t\t\t\t\tresolve();\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * Write a JSON file.\n\t * @param jsonFilename The filename to write.\n\t * @param data The data to write.\n\t * @param append Append to the file.\n\t */\n\tpublic static async writeJsonFile<T = unknown>(\n\t\tjsonFilename: string | undefined,\n\t\tdata: T,\n\t\tappend: boolean\n\t): Promise<void> {\n\t\tif (Is.stringValue(jsonFilename)) {\n\t\t\tconst filename = path.resolve(jsonFilename);\n\t\t\tlet currentJson = {};\n\t\t\tif (append) {\n\t\t\t\tCLIDisplay.task(I18n.formatMessage(\"cli.progress.readingJsonFile\"), filename);\n\t\t\t\tcurrentJson = (await CLIUtils.readJsonFile(filename)) ?? {};\n\t\t\t}\n\t\t\tCLIDisplay.task(I18n.formatMessage(\"cli.progress.writingJsonFile\"), filename);\n\t\t\tCLIDisplay.break();\n\n\t\t\tawait mkdir(path.dirname(filename), { recursive: true });\n\t\t\tawait writeFile(\n\t\t\t\tfilename,\n\t\t\t\t`${JSON.stringify(ObjectHelper.merge(currentJson, data), undefined, \"\\t\")}\\n`\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Write an env file.\n\t * @param envFilename The filename to write.\n\t * @param data The data to write.\n\t * @param append Append to the file.\n\t */\n\tpublic static async writeEnvFile(\n\t\tenvFilename: string | undefined,\n\t\tdata: string[],\n\t\tappend: boolean\n\t): Promise<void> {\n\t\tif (Is.stringValue(envFilename)) {\n\t\t\tconst filename = path.resolve(envFilename);\n\n\t\t\tconst outputKeys: string[] = [];\n\t\t\tconst outputDict: { [key: string]: string } = {};\n\n\t\t\tif (append) {\n\t\t\t\tCLIDisplay.task(I18n.formatMessage(\"cli.progress.readingEnvFile\"), filename);\n\t\t\t\tconst lines = await CLIUtils.readLinesFile(filename);\n\t\t\t\tif (Is.arrayValue(lines)) {\n\t\t\t\t\tfor (const line of lines) {\n\t\t\t\t\t\tconst parts = line.split(\"=\");\n\t\t\t\t\t\toutputKeys.push(parts[0]);\n\t\t\t\t\t\toutputDict[parts[0]] = parts.slice(1).join(\"=\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tCLIDisplay.task(I18n.formatMessage(\"cli.progress.writingEnvFile\"), filename);\n\t\t\tCLIDisplay.break();\n\n\t\t\tif (Is.arrayValue(data)) {\n\t\t\t\tfor (const line of data) {\n\t\t\t\t\tconst parts = line.split(\"=\");\n\t\t\t\t\tconst currentIndex = outputKeys.indexOf(parts[0]);\n\t\t\t\t\tif (currentIndex !== -1) {\n\t\t\t\t\t\toutputKeys.splice(currentIndex, 1);\n\t\t\t\t\t}\n\t\t\t\t\toutputKeys.push(parts[0]);\n\t\t\t\t\toutputDict[parts[0]] = parts.slice(1).join(\"=\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst output: string[] = [];\n\t\t\tfor (const outputKey of outputKeys) {\n\t\t\t\toutput.push(`${outputKey}=${outputDict[outputKey]}`);\n\t\t\t}\n\n\t\t\tawait mkdir(path.dirname(filename), { recursive: true });\n\t\t\tawait writeFile(filename, output.join(\"\\n\"));\n\t\t}\n\t}\n}\n"]}
@@ -51,11 +51,13 @@ export declare class CLIUtils {
51
51
  */
52
52
  static readLinesFileSync(filename: string): string[] | undefined;
53
53
  /**
54
- * Find the NPM root based on a package.json path.
55
- * @param rootFolder The path to the package.json.
56
- * @returns The root path.
54
+ * Find the root folder of a package by walking up the directory tree looking in the
55
+ * node_modules folders, this supports package managers which do not install a flat structure.
56
+ * @param packageName The name of the package to locate.
57
+ * @param startFolder The folder to start the search from.
58
+ * @returns The resolved root folder of the package, or undefined if it could not be located.
57
59
  */
58
- static findNpmRoot(rootFolder: string): Promise<string>;
60
+ static findPackageRoot(packageName: string, startFolder: string): Promise<string | undefined>;
59
61
  /**
60
62
  * Run a shell command.
61
63
  * @param command The app to run in the shell.
package/docs/changelog.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.3-next.8](https://github.com/iotaledger/twin-framework/compare/cli-core-v0.9.3-next.7...cli-core-v0.9.3-next.8) (2026-09-07)
4
+
5
+
6
+ ### Features
7
+
8
+ * improve node_modules location detection ([#486](https://github.com/iotaledger/twin-framework/issues/486)) ([c786853](https://github.com/iotaledger/twin-framework/commit/c7868538e9074f789577c0ab55552f3bcec0ef0e))
9
+
10
+
11
+ ### Dependencies
12
+
13
+ * The following workspace dependencies were updated
14
+ * dependencies
15
+ * @twin.org/core bumped from 0.9.3-next.7 to 0.9.3-next.8
16
+ * @twin.org/nameof bumped from 0.9.3-next.7 to 0.9.3-next.8
17
+ * devDependencies
18
+ * @twin.org/nameof-transformer bumped from 0.9.3-next.7 to 0.9.3-next.8
19
+ * @twin.org/nameof-vitest-plugin bumped from 0.9.3-next.7 to 0.9.3-next.8
20
+
3
21
  ## [0.9.3-next.7](https://github.com/iotaledger/twin-framework/compare/cli-core-v0.9.3-next.6...cli-core-v0.9.3-next.7) (2026-09-07)
4
22
 
5
23
 
@@ -202,25 +202,32 @@ The lines.
202
202
 
203
203
  ***
204
204
 
205
- ### findNpmRoot() {#findnpmroot}
205
+ ### findPackageRoot() {#findpackageroot}
206
206
 
207
- > `static` **findNpmRoot**(`rootFolder`): `Promise`\<`string`\>
207
+ > `static` **findPackageRoot**(`packageName`, `startFolder`): `Promise`\<`string` \| `undefined`\>
208
208
 
209
- Find the NPM root based on a package.json path.
209
+ Find the root folder of a package by walking up the directory tree looking in the
210
+ node_modules folders, this supports package managers which do not install a flat structure.
210
211
 
211
212
  #### Parameters
212
213
 
213
- ##### rootFolder
214
+ ##### packageName
214
215
 
215
216
  `string`
216
217
 
217
- The path to the package.json.
218
+ The name of the package to locate.
219
+
220
+ ##### startFolder
221
+
222
+ `string`
223
+
224
+ The folder to start the search from.
218
225
 
219
226
  #### Returns
220
227
 
221
- `Promise`\<`string`\>
228
+ `Promise`\<`string` \| `undefined`\>
222
229
 
223
- The root path.
230
+ The resolved root folder of the package, or undefined if it could not be located.
224
231
 
225
232
  ***
226
233
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twin.org/cli-core",
3
- "version": "0.9.3-next.7",
3
+ "version": "0.9.3-next.8",
4
4
  "description": "Core classes for building a CLI",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,8 +14,8 @@
14
14
  "node": ">=24.0.0"
15
15
  },
16
16
  "dependencies": {
17
- "@twin.org/core": "0.9.3-next.7",
18
- "@twin.org/nameof": "0.9.3-next.7",
17
+ "@twin.org/core": "0.9.3-next.8",
18
+ "@twin.org/nameof": "0.9.3-next.8",
19
19
  "chalk": "5.6.2",
20
20
  "commander": "15.0.0",
21
21
  "dotenv": "17.4.2"