diff --git a/.config/rollup.dist.config.mjs b/.config/rollup.dist.config.mjs index 4777a90f0..dcad1c6bd 100644 --- a/.config/rollup.dist.config.mjs +++ b/.config/rollup.dist.config.mjs @@ -67,6 +67,12 @@ function moveDtsFilesSync(namePattern, srcPath, destPath) { } } +function copyInitGradle() { + const filepath = path.join(rootSrcPath, 'commands', 'manifest', 'init.gradle') + const destPath = path.join(rootDistPath, 'init.gradle') + copyFileSync(filepath, destPath) +} + function removeDtsFilesSync(namePattern, srcPath) { for (const filepath of tinyGlobSync([`**/${namePattern}.d.ts{.map,}`], { absolute: true, @@ -81,16 +87,15 @@ function updateDepStatsSync(depStats) { const oldDepStats = existsSync(depStatsPath) ? readJsonSync(depStatsPath) : undefined - Object.assign(depStats.dependencies, { + Object.assign(depStats.dependencies, // Add existing package.json dependencies without old transitives. This // preserves dependencies like '@cyclonedx/cdxgen' and 'synp' that are // indirectly referenced through spawned processes and not directly imported. - ...Object.fromEntries( + Object.fromEntries( Object.entries(pkgJson.dependencies).filter( ({ 0: key }) => !oldDepStats?.transitives?.[key] ) - ) - }) + )) // Remove transitives from dependencies. for (const key of Object.keys(oldDepStats?.transitives ?? {})) { if (pkgJson.dependencies[key]) { @@ -212,6 +217,7 @@ export default () => { }, writeBundle() { moveDtsFilesSync(CONSTANTS, distRequirePath, rootDistPath) + copyInitGradle() removeDtsFilesSync('*', distRequirePath) updateDepStatsSync(requireConfig.meta.depStats) } diff --git a/src/cli.ts b/src/cli.ts index 665e21b24..8de1aa417 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -17,7 +17,7 @@ import { cmdFix } from './commands/fix/cmd-fix.ts' import { cmdInfo } from './commands/info/cmd-info.ts' import { loginCommand } from './commands/login' import { logoutCommand } from './commands/logout' -import { manifestCommand } from './commands/manifest' +import { cmdManifest } from './commands/manifest/cmd-manifest.ts' import { npmCommand } from './commands/npm' import { npxCommand } from './commands/npx' import { optimizeCommand } from './commands/optimize' @@ -68,7 +68,7 @@ void (async () => { analytics: cmdAnalytics, 'diff-scan': cmdDiffScan, 'threat-feed': threatFeedCommand, - manifest: manifestCommand + manifest: cmdManifest }, { aliases: { diff --git a/src/commands/manifest/README.md b/src/commands/manifest/README.md new file mode 100644 index 000000000..c6c883920 --- /dev/null +++ b/src/commands/manifest/README.md @@ -0,0 +1,41 @@ +# Manifest + +(At the time of writing...) + +## Dev + +First build the bundle: + +``` +npm run build:dist +``` + +Then run it like these examples: + +``` +# Scala: +npm exec socket manifest scala -- --bin ~/apps/sbt/bin/sbt ~/socket/repos/scala/akka +# Gradle/Kotlin +npm exec socket manifest yolo -- --cwd ~/socket/repos/kotlin/kotlinx.coroutines +``` + +And upload with this: + +``` +npm exec socket scan create -- --repo=depscantmp --branch=mastertmp --tmp --cwd ~/socket/repos/scala/akka socketdev . +npm exec socket scan create -- --repo=depscantmp --branch=mastertmp --tmp --cwd ~/socket/repos/kotlin/kotlinx.coroutines . +``` + +(The `cwd` option for `create` is necessary because we can't go to the dir and run `npm exec`). + +## Prod + +User flow look something like this: + +``` +socket manifest scala . +socket manifest kotlin . +socket manifest yolo + +socket scan create --repo=depscantmp --branch=mastertmp --tmp socketdev . +``` diff --git a/src/commands/manifest/auto.ts b/src/commands/manifest/auto.ts deleted file mode 100644 index 7793adf51..000000000 --- a/src/commands/manifest/auto.ts +++ /dev/null @@ -1,78 +0,0 @@ -import fs from 'node:fs' - -import meow from 'meow' - -import { scala } from './scala.ts' - -import type { CliSubcommand } from '../../utils/meow-with-subcommands' - -const description = 'Auto-detect build and attempt to generate manifest file' - -const help = (name: string) => ` - Usage - $ ${name} - - Tries to figure out what language your current repo uses. If it finds a - supported case then it will try to generate the manifest file for that - language with the default or detected settings. - - This command takes no arguments except --verbose. -` - -export const auto: CliSubcommand = { - description, - async run(argv, importMeta, { parentName }) { - // Allow `--verbose` to pass through - let verbose = false - const args = argv.filter(arg => { - if (arg === '--verbose') { - verbose = true - return false - } - return true - }) - - const name = `${parentName} auto` - if (args.length) { - // note: meow will exit if it prints the --help screen - meow(help(name), { - argv: ['--help'], - description, - importMeta - }) - } - - const subArgs = [] - if (verbose) subArgs.push('--verbose', '1') - const scalaDir = '.' - if (fs.existsSync(scalaDir)) { - console.log( - 'Detected a Scala sbt build, running default Scala generator...' - ) - subArgs.push(scalaDir) - await scala.run(subArgs, importMeta, { parentName }) - return - } - - // Show new help screen and exit - meow( - ` - $ ${name} - - Unfortunately this script did not discover a supported language in the - current folder. - - - Make sure this script would work with your target build - - Make sure to run it from the correct folder - - Make sure the necessary build tools are available (\`PATH\`) - - If that doesn't work, see \`${name} --help\` for config details - `, - { - argv: ['--help'], - description, - importMeta - } - ) - } -} diff --git a/src/commands/manifest/cmd-auto.ts b/src/commands/manifest/cmd-auto.ts new file mode 100644 index 000000000..e8662732e --- /dev/null +++ b/src/commands/manifest/cmd-auto.ts @@ -0,0 +1,116 @@ +import fs from 'node:fs' +import path from 'node:path' + +import meow from 'meow' + +import { cmdManifestGradle } from './cmd-gradle.ts' +import { cmdManifestScala } from './cmd-scala.ts' +import { commonFlags } from '../../flags.ts' +import { getFlagListOutput } from '../../utils/output-formatting.ts' + +import type { CliCommandConfig } from '../../utils/meow-with-subcommands' + +const config: CliCommandConfig = { + commandName: 'auto', + description: 'Auto-detect build and attempt to generate manifest file', + hidden: false, + flags: { + ...commonFlags, + cwd: { + type: 'string', + description: 'Set the cwd, defaults to process.cwd()' + }, + verbose: { + type: 'boolean', + default: false, + description: 'Enable debug output, may help when running into errors' + } + // TODO: support output flags + }, + help: (parentName, config) => ` + Usage + $ ${parentName} ${config.commandName} + + Options + ${getFlagListOutput(config.flags, 6)} + + Tries to figure out what language your current repo uses. If it finds a + supported case then it will try to generate the manifest file for that + language with the default or detected settings. + ` +} + +export const cmdManifestAuto = { + description: config.description, + hidden: config.hidden, + run +} + +async function run( + argv: readonly string[], + importMeta: ImportMeta, + { parentName }: { parentName: string } +): Promise { + const cli = meow(config.help(parentName, config), { + argv, + description: config.description, + importMeta, + flags: config.flags, + allowUnknownFlags: false + }) + + const verbose = cli.flags['verbose'] ?? false + const cwd = String(cli.flags['cwd']) || false + if (verbose) { + console.group('- ', parentName, config.commandName, ':') + console.group('- flags:', cli.flags) + console.groupEnd() + console.log('- input:', cli.input) + console.log('- cwd:', cwd || process.cwd()) + console.groupEnd() + } + + const subArgs = [] + if (verbose) subArgs.push('--verbose') + + const dir = cwd || '.' + + if (fs.existsSync(path.join(dir, 'build.sbt'))) { + console.log( + 'Detected a Scala sbt build, running default Scala generator...' + ) + if (cwd) subArgs.push('--cwd', cwd) + subArgs.push(dir) + await cmdManifestScala.run(subArgs, importMeta, { parentName }) + return + } + + if (fs.existsSync(path.join(dir, 'gradlew'))) { + console.log('Detected a gradle build, running default gradle generator...') + if (cwd) subArgs.push(cwd) // This command takes the cwd as first arg + await cmdManifestGradle.run(subArgs, importMeta, { parentName }) + return + } + + // Show new help screen and exit + meow( + ` + $ ${parentName} ${config.commandName} + + Unfortunately this script did not discover a supported language in the + current folder. + + - Make sure this script would work with your target build + - Make sure to run it from the correct folder + - Make sure the necessary build tools are available (\`PATH\`) + + If that doesn't work, see \`${parentName} --help\` for config details for + your target language. + `, + { + argv: [], + description: config.description, + importMeta + } + ).showHelp() +} diff --git a/src/commands/manifest/cmd-gradle.ts b/src/commands/manifest/cmd-gradle.ts new file mode 100644 index 000000000..cbc9acfe5 --- /dev/null +++ b/src/commands/manifest/cmd-gradle.ts @@ -0,0 +1,181 @@ +import path from 'node:path' + +import meow from 'meow' + +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import { convertGradleToMaven } from './convert_gradle_to_maven.ts' +import { commonFlags } from '../../flags.ts' +import { getFlagListOutput } from '../../utils/output-formatting.ts' + +import type { CliCommandConfig } from '../../utils/meow-with-subcommands' + +const config: CliCommandConfig = { + commandName: 'gradle', + description: + '[beta] Use Gradle to generate a manifest file (`pom.xml`) for a Gradle/Java/Kotlin/etc project', + hidden: false, + flags: { + ...commonFlags, + bin: { + type: 'string', + description: 'Location of gradlew binary to use, default: CWD/gradlew' + }, + cwd: { + type: 'string', + description: 'Set the cwd, defaults to process.cwd()' + }, + gradleOpts: { + type: 'string', + default: '', + description: + 'Additional options to pass on to ./gradlew, see `./gradlew --help`' + }, + out: { + type: 'string', + default: './socket.pom.xml', + description: + 'Path of output file; where to store the resulting manifest, see also --stdout' + }, + stdout: { + type: 'boolean', + description: 'Print resulting pom.xml to stdout (supersedes --out)' + }, + task: { + type: 'string', + default: 'all', + description: 'Task to target. By default targets all.' + }, + verbose: { + type: 'boolean', + description: 'Print debug messages' + } + }, + help: (parentName, config) => ` + Usage + $ ${parentName} ${config.commandName} [--gradle=path/to/gradle/binary] [--out=path/to/result] DIR + + Options + ${getFlagListOutput(config.flags, 6)} + + Uses gradle, preferably through your local project \`gradlew\`, to generate a + \`pom.xml\` file for each task. If you have no \`gradlew\` you can try the + global \`gradle\` binary but that may not work (hard to predict). + + The \`pom.xml\` is a manifest file similar to \`package.json\` for npm or + or requirements.txt for PyPi), but specifically for Maven, which is Java's + dependency repository. Languages like Kotlin and Scala piggy back on it too. + + There are some caveats with the gradle to \`pom.xml\` conversion: + + - each task will generate its own xml file and by default it generates one xml + for every task. + + - it's possible certain features don't translate well into the xml. If you + think something is missing that could be supported please reach out. + + - it works with your \`gradlew\` from your repo and local settings and config + + Support is beta. Please report issues or give us feedback on what's missing. + + Examples + + $ ${parentName} ${config.commandName} . + $ ${parentName} ${config.commandName} --gradlew=../gradlew . + ` +} + +export const cmdManifestGradle = { + description: config.description, + hidden: config.hidden, + run +} + +async function run( + argv: readonly string[], + importMeta: ImportMeta, + { parentName }: { parentName: string } +): Promise { + // note: meow will exit if it prints the --help screen + const cli = meow(config.help(parentName, config), { + flags: config.flags, + argv: argv.length === 0 ? ['--help'] : argv, + description: config.description, + allowUnknownFlags: false, + importMeta + }) + + const verbose = Boolean(cli.flags['verbose']) + + if (verbose) { + console.group('- ', parentName, config.commandName, ':') + console.group('- flags:', cli.flags) + console.groupEnd() + console.log('- input:', cli.input) + console.groupEnd() + } + + const target = cli.input[0] + if (!target) { + // will exit. + new Spinner() + .start('Parsing...') + .error( + `Failure: Missing DIR argument. See \`${parentName} ${config.commandName} --help\` for details.` + ) + process.exit(1) + } + + if (cli.input.length > 1) { + // will exit. + new Spinner() + .start('Parsing...') + .error( + `Failure: Can only accept one FILE or DIR, received ${cli.input.length} (make sure to escape spaces!). See \`${parentName} ${config.commandName} --help\` for details.` + ) + process.exit(1) + } + + let bin: string + if (cli.flags['bin']) { + bin = cli.flags['bin'] as string + } else { + bin = path.join(target, './gradlew') + } + + let out: string = './socket.pom.xml' + if (cli.flags['out']) { + out = cli.flags['out'] as string + } + if (cli.flags['stdout']) { + out = '-' + } + + // TODO: I'm not sure it's feasible to parse source file from stdin. We could try, store contents in a file in some folder, target that folder... what would the file name be? + if (target === '-') { + new Spinner() + .start('Parsing...') + .error( + `Failure: Currently source code from stdin is not supported. See \`${parentName} ${config.commandName} --help\` for details.` + ) + process.exit(1) + } + + if (verbose) { + console.group() + console.log('- target:', target) + console.log('- gradle bin:', bin) + console.log('- out:', out) + console.groupEnd() + } + + let gradleOpts: Array = [] + if (cli.flags['gradleOpts']) { + gradleOpts = (cli.flags['gradleOpts'] as string) + .split(' ') + .map(s => s.trim()) + .filter(Boolean) + } + + await convertGradleToMaven(target, bin, out, verbose, gradleOpts) +} diff --git a/src/commands/manifest/cmd-kotlin.ts b/src/commands/manifest/cmd-kotlin.ts new file mode 100644 index 000000000..32cad6cd4 --- /dev/null +++ b/src/commands/manifest/cmd-kotlin.ts @@ -0,0 +1,186 @@ +import path from 'node:path' + +import meow from 'meow' + +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import { convertGradleToMaven } from './convert_gradle_to_maven.ts' +import { commonFlags } from '../../flags.ts' +import { getFlagListOutput } from '../../utils/output-formatting.ts' + +import type { CliCommandConfig } from '../../utils/meow-with-subcommands' + +// TODO: we may want to dedupe some pieces for all gradle languages. I think it +// makes sense to have separate commands for them and I think it makes +// sense for the help panels to note the requested language, rather than +// `socket manifest kotlin` to print help screens with `gradle` as the +// command. Room for improvement. +const config: CliCommandConfig = { + commandName: 'kotlin', + description: + '[beta] Use Gradle to generate a manifest file (`pom.xml`) for a Kotlin project', + hidden: false, + flags: { + ...commonFlags, + bin: { + type: 'string', + description: 'Location of gradlew binary to use, default: CWD/gradlew' + }, + cwd: { + type: 'string', + description: 'Set the cwd, defaults to process.cwd()' + }, + gradleOpts: { + type: 'string', + default: '', + description: + 'Additional options to pass on to ./gradlew, see `./gradlew --help`' + }, + out: { + type: 'string', + default: './socket.pom.xml', + description: + 'Path of output file; where to store the resulting manifest, see also --stdout' + }, + stdout: { + type: 'boolean', + description: 'Print resulting pom.xml to stdout (supersedes --out)' + }, + task: { + type: 'string', + default: 'all', + description: 'Task to target. By default targets all.' + }, + verbose: { + type: 'boolean', + description: 'Print debug messages' + } + }, + help: (parentName, config) => ` + Usage + $ ${parentName} ${config.commandName} [--gradle=path/to/gradle/binary] [--out=path/to/result] DIR + + Options + ${getFlagListOutput(config.flags, 6)} + + Uses gradle, preferably through your local project \`gradlew\`, to generate a + \`pom.xml\` file for each task. If you have no \`gradlew\` you can try the + global \`gradle\` binary but that may not work (hard to predict). + + The \`pom.xml\` is a manifest file similar to \`package.json\` for npm or + or requirements.txt for PyPi), but specifically for Maven, which is Java's + dependency repository. Languages like Kotlin and Scala piggy back on it too. + + There are some caveats with the gradle to \`pom.xml\` conversion: + + - each task will generate its own xml file and by default it generates one xml + for every task. (This may be a good thing!) + + - it's possible certain features don't translate well into the xml. If you + think something is missing that could be supported please reach out. + + - it works with your \`gradlew\` from your repo and local settings and config + + Support is beta. Please report issues or give us feedback on what's missing. + + Examples + + $ ${parentName} ${config.commandName} . + $ ${parentName} ${config.commandName} --gradlew=../gradlew . + ` +} + +export const cmdManifestKotlin = { + description: config.description, + hidden: config.hidden, + run +} + +async function run( + argv: readonly string[], + importMeta: ImportMeta, + { parentName }: { parentName: string } +): Promise { + // note: meow will exit if it prints the --help screen + const cli = meow(config.help(parentName, config), { + flags: config.flags, + argv: argv.length === 0 ? ['--help'] : argv, + description: config.description, + allowUnknownFlags: false, + importMeta + }) + + const verbose = Boolean(cli.flags['verbose']) + + if (verbose) { + console.group('- ', parentName, config.commandName, ':') + console.group('- flags:', cli.flags) + console.groupEnd() + console.log('- input:', cli.input) + console.groupEnd() + } + + const target = cli.input[0] + if (!target) { + // will exit. + new Spinner() + .start('Parsing...') + .error( + `Failure: Missing DIR argument. See \`${parentName} ${config.commandName} --help\` for details.` + ) + process.exit(1) + } + + if (cli.input.length > 1) { + // will exit. + new Spinner() + .start('Parsing...') + .error( + `Failure: Can only accept one FILE or DIR, received ${cli.input.length} (make sure to escape spaces!). See \`${parentName} ${config.commandName} --help\` for details.` + ) + process.exit(1) + } + + let bin: string + if (cli.flags['bin']) { + bin = cli.flags['bin'] as string + } else { + bin = path.join(target, './gradlew') + } + + let out: string = './socket.pom.xml' + if (cli.flags['out']) { + out = cli.flags['out'] as string + } + if (cli.flags['stdout']) { + out = '-' + } + + // TODO: I'm not sure it's feasible to parse source file from stdin. We could try, store contents in a file in some folder, target that folder... what would the file name be? + if (target === '-') { + new Spinner() + .start('Parsing...') + .error( + `Failure: Currently source code from stdin is not supported. See \`${parentName} ${config.commandName} --help\` for details.` + ) + process.exit(1) + } + + if (verbose) { + console.group() + console.log('- target:', target) + console.log('- gradle bin:', bin) + console.log('- out:', out) + console.groupEnd() + } + + let gradleOpts: Array = [] + if (cli.flags['gradleOpts']) { + gradleOpts = (cli.flags['gradleOpts'] as string) + .split(' ') + .map(s => s.trim()) + .filter(Boolean) + } + + await convertGradleToMaven(target, bin, out, verbose, gradleOpts) +} diff --git a/src/commands/manifest/cmd-manifest.ts b/src/commands/manifest/cmd-manifest.ts new file mode 100644 index 000000000..a2eeb18e0 --- /dev/null +++ b/src/commands/manifest/cmd-manifest.ts @@ -0,0 +1,77 @@ +import { cmdManifestAuto } from './cmd-auto.ts' +import { cmdManifestGradle } from './cmd-gradle.ts' +import { cmdManifestKotlin } from './cmd-kotlin.ts' +import { cmdManifestScala } from './cmd-scala.ts' +import { commonFlags } from '../../flags.ts' +import { + type CliCommandConfig, + meowWithSubcommands +} from '../../utils/meow-with-subcommands' + +const config: CliCommandConfig = { + commandName: 'manifest', + description: 'Generate a dependency manifest for given file or dir', + hidden: false, + flags: { + ...commonFlags + }, + help: (parentName, config) => ` + Usage + + $ ${parentName} ${config.commandName} + + Generates a declarative dependency manifest (like a package.json for Node.JS + or requirements.txt for PyPi), but for certain supported ecosystems + where it's common to use a dynamic manifest, like Scala's sbt. + + Only certain languages are supported and there may be language specific + configurations available. See \`manifest --help\` for usage details + per language. + + Currently supported language: scala [beta], gradle [beta], kotlin (through + gradle) [beta]. + + Examples + + $ ${parentName} ${config.commandName} scala . + + To have it auto-detect and attempt to run: + + $ ${parentName} ${config.commandName} yolo + ` +} + +export const cmdManifest = { + description: config.description, + hidden: config.hidden, + run +} + +async function run( + argv: readonly string[], + importMeta: ImportMeta, + { parentName }: { parentName: string } +): Promise { + await meowWithSubcommands( + { + auto: cmdManifestAuto, + scala: cmdManifestScala, + gradle: cmdManifestGradle, + kotlin: cmdManifestKotlin + }, + { + argv, + aliases: { + yolo: { + description: config.description, + hidden: true, + argv: ['auto'] + } + }, + description: config.description, + importMeta, + flags: config.flags, + name: `${parentName} ${config.commandName}` + } + ) +} diff --git a/src/commands/manifest/cmd-scala.ts b/src/commands/manifest/cmd-scala.ts new file mode 100644 index 000000000..7d2379f3f --- /dev/null +++ b/src/commands/manifest/cmd-scala.ts @@ -0,0 +1,175 @@ +import meow from 'meow' + +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import { convertSbtToMaven } from './convert_sbt_to_maven.ts' +import { commonFlags } from '../../flags.ts' +import { getFlagListOutput } from '../../utils/output-formatting.ts' + +import type { CliCommandConfig } from '../../utils/meow-with-subcommands' + +const config: CliCommandConfig = { + commandName: 'kotlin', + description: + "[beta] Generate a manifest file (`pom.xml`) from Scala's `build.sbt` file", + hidden: false, + flags: { + ...commonFlags, + bin: { + type: 'string', + default: 'sbt', + description: 'Location of sbt binary to use' + }, + cwd: { + type: 'string', + description: 'Set the cwd, defaults to process.cwd()' + }, + out: { + type: 'string', + default: './socket.pom.xml', + description: + 'Path of output file; where to store the resulting manifest, see also --stdout' + }, + stdout: { + type: 'boolean', + description: 'Print resulting pom.xml to stdout (supersedes --out)' + }, + sbtOpts: { + type: 'string', + default: '', + description: 'Additional options to pass on to sbt, as per `sbt --help`' + }, + verbose: { + type: 'boolean', + description: 'Print debug messages' + } + }, + help: (parentName, config) => ` + Usage + $ ${parentName} ${config.commandName} [--sbt=path/to/sbt/binary] [--out=path/to/result] FILE|DIR + + Options + ${getFlagListOutput(config.flags, 6)} + + Uses \`sbt makePom\` to generate a \`pom.xml\` from your \`build.sbt\` file. + This xml file is the dependency manifest (like a package.json + for Node.js or requirements.txt for PyPi), but specifically for Scala. + + There are some caveats with \`build.sbt\` to \`pom.xml\` conversion: + + - the xml is exported as socket.pom.xml as to not confuse existing build tools + but it will first hit your /target/sbt folder (as a different name) + + - the pom.xml format (standard by Scala) does not support certain sbt features + - \`excludeAll()\`, \`dependencyOverrides\`, \`force()\`, \`relativePath\` + - For details: https://www.scala-sbt.org/1.x/docs/Library-Management.html + + - it uses your sbt settings and local configuration verbatim + + - it can only export one target per run, so if you have multiple targets like + development and production, you must run them separately. + + You can optionally configure the path to the \`sbt\` bin to invoke. + + Support is beta. Please report issues or give us feedback on what's missing. + + Examples + + $ ${parentName} ${config.commandName} ./build.sbt + $ ${parentName} ${config.commandName} --bin=/usr/bin/sbt ./build.sbt + ` +} + +export const cmdManifestScala = { + description: config.description, + hidden: config.hidden, + run +} + +async function run( + argv: readonly string[], + importMeta: ImportMeta, + { parentName }: { parentName: string } +): Promise { + // console.log('scala', argv, parentName) + // note: meow will exit if it prints the --help screen + const cli = meow(config.help(parentName, config), { + flags: config.flags, + argv: argv.length === 0 ? ['--help'] : argv, + description: config.description, + allowUnknownFlags: false, + importMeta + }) + + const verbose = Boolean(cli.flags['verbose']) + + if (verbose) { + console.group('- ', parentName, config.commandName, ':') + console.group('- flags:', cli.flags) + console.groupEnd() + console.log('- input:', cli.input) + console.groupEnd() + } + + const target = cli.input[0] + if (!target) { + // will exit. + new Spinner() + .start('Parsing...') + .error( + `Failure: Missing FILE|DIR argument. See \`${parentName} ${config.commandName} --help\` for details.` + ) + process.exit(1) + } + + if (cli.input.length > 1) { + // will exit. + new Spinner() + .start('Parsing...') + .error( + `Failure: Can only accept one FILE or DIR, received ${cli.input.length} (make sure to escape spaces!). See \`${parentName} ${config.commandName} --help\` for details.` + ) + process.exit(1) + } + + let bin: string = 'sbt' + if (cli.flags['bin']) { + bin = cli.flags['bin'] as string + } + + let out: string = './socket.pom.xml' + if (cli.flags['out']) { + out = cli.flags['out'] as string + } + if (cli.flags['stdout']) { + out = '-' + } + + if (verbose) { + console.group() + console.log('- target:', target) + console.log('- gradle bin:', bin) + console.log('- out:', out) + console.groupEnd() + } + + // TODO: we can make `-` (accept from stdin) work by storing it into /tmp + if (target === '-') { + new Spinner() + .start('Parsing...') + .error( + `Failure: Currently source code from stdin is not supported. See \`${parentName} ${config.commandName} --help\` for details.` + ) + process.exit(1) + } + + let sbtOpts: Array = [] + if (cli.flags['sbtOpts']) { + sbtOpts = (cli.flags['sbtOpts'] as string) + .split(' ') + .map(s => s.trim()) + .filter(Boolean) + } + + await convertSbtToMaven(target, bin, out, verbose, sbtOpts) +} diff --git a/src/commands/manifest/convert_gradle_to_maven.ts b/src/commands/manifest/convert_gradle_to_maven.ts new file mode 100644 index 000000000..3a200e30b --- /dev/null +++ b/src/commands/manifest/convert_gradle_to_maven.ts @@ -0,0 +1,122 @@ +import path from 'node:path' + +import spawn from '@npmcli/promise-spawn' + +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import constants from '../../constants.ts' + +export async function convertGradleToMaven( + target: string, + bin: string, + _out: string, + verbose: boolean, + gradleOpts: Array +) { + const rbin = path.resolve(bin) + const rtarget = path.resolve(target) + // const rout = out === '-' ? '-' : path.resolve(out) + + if (verbose) { + console.group('gradle2maven:') + console.log(`[VERBOSE] - Absolute bin path: \`${rbin}\``) + console.log(`[VERBOSE] - Absolute target path: \`${rtarget}\``) + // console.log(`[VERBOSE] - Absolute out path: \`${rout}\``) + console.groupEnd() + } else { + console.group('gradle2maven:') + console.log(`- executing: \`${bin}\``) + console.log(`- src dir: \`${target}\``) + // console.log(`- dst dir: \`${out}\``) + console.groupEnd() + } + + const spinner = new Spinner() + + spinner.start( + `Converting gradle to maven from \`${bin}\` on \`${target}\`...` + ) + + try { + // Run sbt with the init script we provide which should yield zero or more pom files. + // We have to figure out where to store those pom files such that we can upload them and predict them through the GitHub API. + // We could do a .socket folder. We could do a socket.pom.gz with all the poms, although I'd prefer something plain-text if it is to be committed. + + // Note: init.gradle will be exported by .config/rollup.dist.config.mjs + const initLocation = path.join(constants.rootDistPath, 'init.gradle') + const commandArgs = ['--init-script', initLocation, ...gradleOpts, 'pom'] + + if (verbose) { + console.log('\n[VERBOSE] Executing:', bin, commandArgs) + } + + const output = await spawn(bin, commandArgs, { + cwd: target || '.' + }) + spinner.success() + if (verbose) { + console.group('[VERBOSE] gradle stdout:') + console.log(output) + console.groupEnd() + } + + if (output.stderr) { + spinner.error('There were errors while running gradle') + // (In verbose mode, stderr was printed above, no need to repeat it) + if (!verbose) { + console.group('[VERBOSE] stderr:') + console.error(output.stderr) + console.groupEnd() + } + process.exit(1) + } + + console.log('Reported exports:') + output.stdout.replace( + /^POM file copied to: (.*)/gm, + (_all: string, fn: string) => { + console.log('- ', fn) + return fn + } + ) + + // const loc = output.stdout?.match(/Wrote (.*?.pom)\n/)?.[1]?.trim() + // if (!loc) { + // spinner.error( + // 'There were no errors from sbt but could not find the location of resulting .pom file either' + // ) + // process.exit(1) + // } + // + // // Move the pom file to ...? initial cwd? loc will be an absolute path, or dump to stdout + // if (out === '-') { + // spinner.start('Result:\n```').success() + // console.log(await safeReadFile(loc, 'utf8')) + // console.log('```') + // spinner.start().success(`OK`) + // } else { + // if (verbose) { + // spinner.start( + // `Moving manifest file from \`${loc.replace(/^\/home\/[^/]*?\//, '~/')}\` to \`${out}\`` + // ) + // } else { + // spinner.start('Moving output pom file') + // } + // // TODO: do we prefer fs-extra? renaming can be gnarly on windows and fs-extra's version is better + // await renamep(loc, out) + // spinner.success() + // spinner.start().success(`OK. File should be available in \`${out}\``) + // } + } catch (e) { + spinner.error( + 'There was an unexpected error while running this' + + (verbose ? '' : ' (use --verbose for details)') + ) + if (verbose) { + console.group('[VERBOSE] error:') + console.log(e) + console.groupEnd() + } + process.exit(1) + } +} diff --git a/src/commands/manifest/convert_sbt_to_maven.ts b/src/commands/manifest/convert_sbt_to_maven.ts new file mode 100644 index 000000000..b7b213488 --- /dev/null +++ b/src/commands/manifest/convert_sbt_to_maven.ts @@ -0,0 +1,120 @@ +import path from 'node:path' + +import spawn from '@npmcli/promise-spawn' + +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import { safeReadFile } from '../../utils/fs.ts' + +export async function convertSbtToMaven( + target: string, + bin: string, + out: string, + verbose: boolean, + sbtOpts: Array +) { + const rbin = path.resolve(bin) + const rtarget = path.resolve(target) + // const rout = out === '-' ? '-' : path.resolve(out) + + if (verbose) { + console.group('sbt2maven:') + console.log(`[VERBOSE] - Absolute bin path: \`${rbin}\``) + console.log(`[VERBOSE] - Absolute target path: \`${rtarget}\``) + // console.log(`[VERBOSE] - Absolute out path: \`${rout}\``) + console.groupEnd() + } else { + console.group('sbt2maven:') + console.log(`- executing: \`${bin}\``) + console.log(`- src dir: \`${target}\``) + // console.log(`- dst dir: \`${out}\``) + console.groupEnd() + } + + const spinner = new Spinner() + + spinner.start(`Converting sbt to maven from \`${bin}\` on \`${target}\`...`) + + try { + // Run sbt with the init script we provide which should yield zero or more pom files. + // We have to figure out where to store those pom files such that we can upload them and predict them through the GitHub API. + // We could do a .socket folder. We could do a socket.pom.gz with all the poms, although I'd prefer something plain-text if it is to be committed. + + const output = await spawn(bin, ['makePom'].concat(sbtOpts), { + cwd: target || '.' + }) + spinner.success() + if (verbose) { + console.group('[VERBOSE] sbt stdout:') + console.log(output) + console.groupEnd() + } + + if (output.stderr) { + spinner.error('There were errors while running sbt') + // (In verbose mode, stderr was printed above, no need to repeat it) + if (!verbose) { + console.group('[VERBOSE] stderr:') + console.error(output.stderr) + console.groupEnd() + } + process.exit(1) + } + + const poms: Array = [] + output.stdout.replace(/Wrote (.*?.pom)\n/g, (_all: string, fn: string) => { + poms.push(fn) + return fn + }) + + if (!poms.length) { + spinner.error( + 'There were no errors from sbt but it seems to not have generated any poms either' + ) + process.exit(1) + } + + // Move the pom file to ...? initial cwd? loc will be an absolute path, or dump to stdout + // TODO: what to do with multiple output files? Do we want to dump them to stdout? Raw or with separators or ? + // TODO: maybe we can add an option to target a specific file to dump to stdout + if (out === '-' && poms.length === 1) { + spinner.start('Result:\n```').success() + console.log(await safeReadFile(poms[0] as string, 'utf8')) + console.log('```') + spinner.start().success(`OK`) + } else if (out === '-') { + spinner + .start() + .error( + 'Requested out target was stdout but there are multiple generated files' + ) + poms.forEach(fn => console.error('-', fn)) + console.error('Exiting now...') + process.exit(1) + } else { + // if (verbose) { + // spinner.start( + // `Moving manifest file from \`${loc.replace(/^\/home\/[^/]*?\//, '~/')}\` to \`${out}\`` + // ) + // } else { + // spinner.start('Moving output pom file') + // } + // TODO: do we prefer fs-extra? renaming can be gnarly on windows and fs-extra's version is better + // await renamep(loc, out) + spinner.start().success(`Generated ${poms.length} pom files`) + poms.forEach(fn => console.log('-', fn)) + spinner.start().success(`OK`) + } + } catch (e) { + spinner.error( + 'There was an unexpected error while running this' + + (verbose ? '' : ' (use --verbose for details)') + ) + if (verbose) { + console.group('[VERBOSE] error:') + console.log(e) + console.groupEnd() + } + process.exit(1) + } +} diff --git a/src/commands/manifest/index.ts b/src/commands/manifest/index.ts deleted file mode 100644 index cbc178069..000000000 --- a/src/commands/manifest/index.ts +++ /dev/null @@ -1,70 +0,0 @@ -import meow from 'meow' - -import { auto } from './auto.ts' -import { scala } from './scala' -import { meowWithSubcommands } from '../../utils/meow-with-subcommands' - -import type { CliSubcommand } from '../../utils/meow-with-subcommands' - -const description = 'Generate a dependency manifest for given file or dir' -const help = (name: string) => ` - Usage - - $ ${name} - - Generates a declarative dependency manifest (like a package.json for Node.JS - or requirements.txt for PyPi), but for certain supported ecosystems - where it's common to use a dynamic manifest, like Scala's sbt. - - Only certain languages are supported and there may be language specific - configurations available. See \`manifest --help\` for usage details - per language. - - Currently supported language: scala - - Examples - - $ ${name} scala . - - To have it auto-detect and attempt to run: - - $ ${name} yolo -` - -export const manifestCommand: CliSubcommand = { - description, - hidden: true, - async run(argv, importMeta, { parentName }) { - const name = `${parentName} manifest` - - // Note: this won't catch `socket manifest -xyz --help` sort of cases which - // would fallback to the default meow help behavior. That's fine. - if (argv.length === 0 || argv[0] === '--help') { - meow(help(name), { - argv: ['--help'] as const, // meow will exit() when --help is passed - description, - importMeta - }) - } - - await meowWithSubcommands( - { - scala, - auto - }, - { - argv, - aliases: { - yolo: { - description: auto.description, - hidden: true, - argv: ['auto'] - } - }, - description, - importMeta, - name - } - ) - } -} diff --git a/src/commands/manifest/init.gradle b/src/commands/manifest/init.gradle new file mode 100644 index 000000000..a64e5d93c --- /dev/null +++ b/src/commands/manifest/init.gradle @@ -0,0 +1,250 @@ +// This is a Gradle initialization script that generates Maven POM files for projects +// A POM file describes a project's dependencies and other metadata in XML format + +// This script: +// - Generates Maven POM files for Java/Kotlin/Android projects +// - Handles different types of dependencies (direct, project, version catalog) +// - Supports different project types (Java, Android, root project) +// - Can be invoked with `./gradlew --init-script /path/to/this/script pom` to generate POM files +// - Copies the generated POM to a target location (default: pom.xml) + +initscript { + repositories { + // We need these repositories for Gradle's plugin resolution system + // TODO: it's not clear if we actually need them. + gradlePluginPortal() + mavenCentral() + google() + } + + dependencies { + // No external dependencies needed as we only use Gradle's built-in maven-publish plugin + } +} + +// Apply these configurations to all projects in the build +gradle.allprojects { project -> + // Create a unique name for the Maven publication + // Example: project ':foo:bar' becomes 'maven-foo-bar' + def publicationName = "maven-${project.path.replace(':', '-')}" + if (publicationName.startsWith('maven--')) { + publicationName = 'maven-root' // Special case for root project + } + + // Apply the Maven Publish plugin if not already applied + if (!project.plugins.hasPlugin('maven-publish')) { + project.plugins.apply('maven-publish') + } + + // Register a new task called 'pom' that will generate the POM file. + // This is what allows us to do `gradlew pom`. We could rename it to + // something like socket-generate-pom instead. It should be invisible + // to the user because this script is not part of their repo. + project.tasks.register('pom') { + group = 'publishing' // Group tasks are shown together in ./gradlew tasks (irrelevant) + description = 'Generates a POM file' + // Force task to run every time. Otherwise caching would cause + // subsequent runs without changes to do anything. + // There may be room for improvement; I think this may cause + // everything to run which is theorietically not necessary. + outputs.upToDateWhen { false } + + // Define where POM files will be generated and copied + def defaultPomFile = project.file("build/publications/${publicationName}/pom-default.xml") + def targetPomFile = project.hasProperty('pomPath') ? + project.file(project.property('pomPath')) : // Custom location if specified. You can use `./gradlew pom -PpomPath=path/to/pom.xml` to specify a custom location. + project.file('pom.xml') // Default location + + // Declare task inputs and outputs for Gradle's incremental build system + inputs.file(defaultPomFile) + outputs.file(targetPomFile) + + // The actual work of copying the POM file happens here + doLast { + if (defaultPomFile.exists()) { + // Print the generated POM for inspection + println "\nGenerated POM file for ${publicationName}:" +// println "==================================" +// println defaultPomFile.text +// println "==================================" + + // Copy the POM file to its target location + targetPomFile.parentFile.mkdirs() + targetPomFile.text = defaultPomFile.text + println "\nPOM file copied to: ${targetPomFile.absolutePath}" + } else { + println "No POM file generated at ${defaultPomFile.absolutePath}" + } + } + } + + // Wait for project evaluation to complete before configuring publication + project.afterEvaluate { p -> + p.plugins.withId('maven-publish') { + // Gather project information + def projectPath = p.path + def projectName = p.name + def projectDesc = p.description ?: p.name + def isRootProject = p.path == ':' && !p.subprojects.isEmpty() + def isAndroidProject = p.plugins?.hasPlugin('com.android.library') || + p.plugins?.hasPlugin('com.android.application') + def hasJavaComponent = p.extensions?.findByName('components')?.findByName('java') != null + + // Store all dependencies we find here + def projectDependencies = [] + + // Find all relevant dependency configurations + // We care about implementation, api, compile, and runtime configurations + // TODO: anything we're missing here? tests maybe? + def relevantConfigs = p.configurations.findAll { config -> + !config.name.toLowerCase().contains('test') && + (config.name.endsWith('Implementation') || + config.name.endsWith('Api') || + config.name == 'implementation' || + config.name == 'api' || + config.name == 'compile' || + config.name == 'runtime') + } + + // Process each configuration to find dependencies + relevantConfigs.each { config -> + config.dependencies.each { dep -> + if (dep instanceof ProjectDependency) { + // Handle project dependencies (e.g., implementation(project(":other-module"))) + def depProjectPath = dep.dependencyProject.path + def depProjectName = depProjectPath.substring(depProjectPath.lastIndexOf(':') + 1) + projectDependencies << [ + group: p.group ?: p.rootProject.name, + name: depProjectName, + version: p.version ?: 'unspecified', + scope: config.name.contains('api') ? 'compile' : 'runtime' + ] + } else { + // Handle all other types of dependencies + try { + def group = dep.group + def name = dep.name + def version = dep.version + + // Handle version catalog dependencies (e.g., implementation(libs.some.library)) + if (!group && p.findProperty('libs')) { + def depString = dep.toString() + + // Skip bundles and file dependencies as they need special handling + if (!depString.contains('Bundle') && !dep.toString().contains('DefaultFileCollectionDependency')) { + try { + // Extract library name from version catalog reference + def libName = depString.contains('libs.') ? + depString.substring(depString.indexOf('libs.') + 5) : + depString + def libProvider = p.libs.findLibrary(libName) + if (libProvider.present) { + def dependency = libProvider.get() + projectDependencies << [ + group: dependency.get().module.group, + name: dependency.get().module.name, + version: dependency.versionConstraint.requiredVersion, + scope: config.name.contains('api') ? 'compile' : 'runtime' + ] + } + } catch (Exception e) { + println " - Skipping non-catalog dependency: ${dep}" + } + } + } else if (group && name) { + // Handle regular dependencies (e.g., implementation("group:name:version")) + projectDependencies << [ + group: group, + name: name, + version: version ?: 'unspecified', + scope: config.name.contains('api') ? 'compile' : 'runtime' + ] + } + } catch (Exception e) { + println " - Failed to process dependency: ${e.message}" + } + } + } + } + + // Configure the Maven publication + p.publishing { + publications { + if (!publications.findByName(publicationName)) { + create(publicationName, MavenPublication) { + // Handle different project types + if (isAndroidProject) { + // For Android libraries, we need to wait for the Android plugin to set up + afterEvaluate { + def android = p.extensions.findByName('android') + if (android) { + // Try to get the release variant component + def components = p.components + def componentNames = components.names + + // Look for specific variant components + // Prefer release over debug + if (components.findByName("release")) { + from components.release + } else if (components.findByName("debug")) { + from components.debug + } else { + println "Warning: No release or debug component found for Android project ${p.name}" + // Skip the component for now, will still generate POM + } + } else { + println "Warning: Android extension not found for project ${p.name}" + } + } + } else if (!isRootProject && hasJavaComponent) { + // For Java libraries, use the java component + from components.java + } + // Root project doesn't need a 'from' clause as it's just a POM + + // Configure the POM file content + pom { + // Set packaging type based on project type (why is this necessary?) + packaging = isRootProject ? 'pom' : (isAndroidProject ? 'aar' : 'jar') + name = projectName + description = projectDesc + + // Customize the POM XML + withXml { xml -> + def root = xml.asNode() + def dependencies = root.appendNode('dependencies') + + // Add all collected dependencies to the POM + projectDependencies.each { dep -> + def dependency = dependencies.appendNode('dependency') + // Ensure all values are strings + dependency.appendNode('groupId', String.valueOf(dep.group)) + dependency.appendNode('artifactId', String.valueOf(dep.name)) + dependency.appendNode('version', String.valueOf(dep.version ?: 'unspecified')) + dependency.appendNode('scope', String.valueOf(dep.scope)) + } + + // Add standard properties for root project + if (isRootProject) { + def properties = root.appendNode('properties') + properties.appendNode('kotlin.version', String.valueOf('1.9.0')) + properties.appendNode('java.version', String.valueOf('11')) + properties.appendNode('project.build.sourceEncoding', String.valueOf('UTF-8')) + } + } + } + } + } + } + } + + // Make our pom task depend on the actual POM generation task + project.tasks.named('pom') { + def pomTask = "generatePomFileFor${publicationName.capitalize()}Publication" + if (project.tasks?.findByName(pomTask)) { + dependsOn(pomTask) + } + } + } + } +} diff --git a/src/commands/manifest/scala.ts b/src/commands/manifest/scala.ts deleted file mode 100644 index 52b283017..000000000 --- a/src/commands/manifest/scala.ts +++ /dev/null @@ -1,249 +0,0 @@ -import fs from 'node:fs' -import path from 'node:path' -import util from 'node:util' - -import spawn from '@npmcli/promise-spawn' -import meow from 'meow' - -import { Spinner } from '@socketsecurity/registry/lib/spinner' - -import { safeReadFile } from '../../utils/fs' -import { getFlagListOutput } from '../../utils/output-formatting.ts' - -import type { CliSubcommand } from '../../utils/meow-with-subcommands' - -type ListDescription = - | string - | { description: string; type?: string; default?: string } - -const renamep = util.promisify(fs.rename) - -const description = - "Generate a manifest file (`pom.xml`) from Scala's `build.sbt` file" - -const scalaCmdFlags: Record = { - bin: { - type: 'string', - default: 'sbt', - description: 'Location of sbt binary to use' - }, - out: { - type: 'string', - default: './socket.pom.xml', - description: - 'Path of output file; where to store the resulting manifest, see also --stdout' - }, - stdout: { - type: 'boolean', - description: 'Print resulting pom.xml to stdout (supersedes --out)' - }, - sbtOpts: { - type: 'string', - default: '', - description: 'Additional options to pass on to sbt, as per `sbt --help`' - }, - verbose: { - type: 'boolean', - description: 'Print debug messages' - } -} - -const help = (name: string, flags: Record) => ` - Usage - $ ${name} [--sbt=path/to/sbt/binary] [--out=path/to/result] FILE|DIR - - Options - ${getFlagListOutput(flags, 6)} - - Uses \`sbt makePom\` to generate a \`pom.xml\` from your \`build.sbt\` file. - This xml file is the dependency manifest (like a package.json - for Node.js or requirements.txt for PyPi), but specifically for Scala. - - There are some caveats with \`build.sbt\` to \`pom.xml\` conversion: - - - the xml is exported as socket.pom.xml as to not confuse existing build tools - but it will first hit your /target/sbt folder (as a different name) - - - the pom.xml format (standard by Scala) does not support certain sbt features - - \`excludeAll()\`, \`dependencyOverrides\`, \`force()\`, \`relativePath\` - - For details: https://www.scala-sbt.org/1.x/docs/Library-Management.html - - - it uses your sbt settings and local configuration verbatim - - - it can only export one target per run, so if you have multiple targets like - development and production, you must run them separately. - - You can optionally configure the path to the \`sbt\` bin to invoke. - - Examples - - $ ${name} ./build.sbt - $ ${name} --bin=/usr/bin/sbt ./build.sbt -` - -export const scala: CliSubcommand = { - description, - async run(argv, importMeta, { parentName }) { - // console.log('scala', argv, parentName) - const name = `${parentName} scala` - // note: meow will exit if it prints the --help screen - const cli = meow(help(name, scalaCmdFlags), { - flags: <{ [key: string]: any }>scalaCmdFlags, - argv: argv.length === 0 ? ['--help'] : argv, - description, - allowUnknownFlags: false, - importMeta - }) - - if (cli.flags['verbose']) { - console.log('[VERBOSE] cli.flags:', cli.flags, ', cli.input:', cli.input) - } - - const target = cli.input[0] - if (!target) { - // will exit. - new Spinner() - .start('Parsing...') - .error( - `Failure: Missing FILE|DIR argument. See \`${name} --help\` for details.` - ) - process.exit(1) - } - - if (cli.input.length > 1) { - // will exit. - new Spinner() - .start('Parsing...') - .error( - `Failure: Can only accept one FILE or DIR, received ${cli.input.length} (make sure to escape spaces!). See \`${name} --help\` for details.` - ) - process.exit(1) - } - - let bin: string = 'sbt' - if (cli.flags['bin']) { - bin = cli.flags['bin'] as string - } - - let out: string = './socket.pom.xml' - if (cli.flags['out']) { - out = cli.flags['out'] as string - } - if (cli.flags['stdout']) { - out = '-' - } - - // TODO: we can make `-` (accept from stdin) work by storing it into /tmp - if (target === '-') { - new Spinner() - .start('Parsing...') - .error( - `Failure: Currently source code from stdin is not supported. See \`${name} --help\` for details.` - ) - process.exit(1) - } - - const verbose = (cli.flags['verbose'] as boolean) ?? false - - let sbtOpts: Array = [] - if (cli.flags['sbtOpts']) { - sbtOpts = (cli.flags['sbtOpts'] as string) - .split(' ') - .map(s => s.trim()) - .filter(Boolean) - } - - await startConversion(target, bin, out, verbose, sbtOpts) - } -} - -async function startConversion( - target: string, - bin: string, - out: string, - verbose: boolean, - sbtOpts: Array -) { - const rbin = path.resolve(bin) - const rtarget = path.resolve(target) - const rout = out === '-' ? '-' : path.resolve(out) - - if (verbose) { - console.log(`[VERBOSE] - Absolute bin path: \`${rbin}\``) - console.log(`[VERBOSE] - Absolute target path: \`${rtarget}\``) - console.log(`[VERBOSE] - Absolute out path: \`${rout}\``) - } else { - console.log(`- executing: \`${bin}\``) - console.log(`- src dir: \`${target}\``) - console.log(`- dst dir: \`${out}\``) - } - - const spinner = new Spinner() - - spinner.start(`Running sbt from \`${bin}\` on \`${target}\`...`) - - try { - // We must now run sbt, pick the generated xml from the /target folder (the stdout should tell you the location upon success) and store it somewhere else. - // TODO: Not sure what this somewhere else might be tbh. - - const output = await spawn(bin, ['makePom'].concat(sbtOpts), { - cwd: target || '.' - }) - spinner.success() - if (verbose) { - console.group('[VERBOSE] sbt stdout:') - console.log(output) - console.groupEnd() - } - - if (output.stderr) { - spinner.error('There were errors while running sbt') - // (In verbose mode, stderr was printed above, no need to repeat it) - if (!verbose) { - console.group('[VERBOSE] stderr:') - console.error(output.stderr) - console.groupEnd() - } - process.exit(1) - } - - const loc = output.stdout?.match(/Wrote (.*?.pom)\n/)?.[1]?.trim() - if (!loc) { - spinner.error( - 'There were no errors from sbt but could not find the location of resulting .pom file either' - ) - process.exit(1) - } - - // Move the pom file to ...? initial cwd? loc will be an absolute path, or dump to stdout - if (out === '-') { - spinner.start('Result:\n```').success() - console.log(await safeReadFile(loc, 'utf8')) - console.log('```') - spinner.start().success(`OK`) - } else { - if (verbose) { - spinner.start( - `Moving manifest file from \`${loc.replace(/^\/home\/[^/]*?\//, '~/')}\` to \`${out}\`` - ) - } else { - spinner.start('Moving output pom file') - } - // TODO: do we prefer fs-extra? renaming can be gnarly on windows and fs-extra's version is better - await renamep(loc, out) - spinner.success() - spinner.start().success(`OK. File should be available in \`${out}\``) - } - } catch (e) { - spinner.error( - 'There was an unexpected error while running this' + - (verbose ? '' : ' (use --verbose for details)') - ) - if (verbose) { - console.group('[VERBOSE] error:') - console.log(e) - console.groupEnd() - } - process.exit(1) - } -}