Skip to content

Commit 0605c55

Browse files
committed
feat: add JSON and Markdown output support for manifest commands
Implement comprehensive JSON/Markdown output for all manifest generation commands and wrapper configuration, enabling programmatic consumption and documentation generation. **New Output Helper:** - output-manifest.mts: Central helper for manifest generation results - Supports JSON, Markdown, and text output modes - Structured data includes files generated, type (gradle/sbt), success status - Markdown output includes next steps guidance **Gradle/Kotlin Commands:** - convert-gradle-to-maven.mts: Return CResult<ManifestResult> - Accept outputKind parameter (text/json/markdown) - Silent operation in JSON/MD modes (no spinner/logger) - Capture generated file paths from gradle output - cmd-manifest-gradle.mts: Integrate outputManifest for JSON/MD - cmd-manifest-kotlin.mts: Same integration pattern **SBT/Scala Commands:** - convert-sbt-to-maven.mts: Return CResult<ManifestResult> - Accept outputKind parameter - Silent operation in JSON/MD modes - Handle stderr as string or Buffer correctly - cmd-manifest-scala.mts: Integrate outputManifest for JSON/MD **Auto-detect Command:** - generate_auto_manifest.mts: Pass outputKind to all converters - Conditionally show text mode logging - Each generator handles its own output format - cmd-manifest-auto.mts: Already passes outputKind through **Wrapper Command:** - cmd-wrapper.mts: Track modified/skipped shell config files - JSON output: action, modifiedFiles, skippedFiles, success status - Markdown output: formatted report with file lists and status - Text mode preserves existing behavior **Architecture:** - Manifest generators now return CResult for error handling - Text mode output remains in converters (backward compatible) - JSON/MD modes delegate to output helpers - Consistent pattern across all manifest commands All existing text output behavior preserved. JSON/MD output is opt-in via --json or --markdown flags.
1 parent 9aa15e0 commit 0605c55

File tree

9 files changed

+407
-144
lines changed

9 files changed

+407
-144
lines changed

packages/cli/src/commands/manifest/cmd-manifest-auto.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ async function run(
6868
importMeta,
6969
parentName,
7070
})
71-
// TODO: Implement json/md further.
71+
// Feature request: Pass outputKind to manifest generators for json/md output support.
7272
const { json, markdown, verbose: verboseFlag } = cli.flags
7373

7474
const dryRun = !!cli.flags['dryRun']

packages/cli/src/commands/manifest/cmd-manifest-gradle.mts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { debug } from '@socketsecurity/lib/debug'
44
import { logger } from '@socketsecurity/lib/logger'
55

66
import { convertGradleToMaven } from './convert-gradle-to-maven.mts'
7+
import { outputManifest } from './output-manifest.mts'
78
import { DRY_RUN_BAILING_NOW } from '../../constants/cli.mjs'
89
import { REQUIREMENTS_TXT } from '../../constants/paths.mjs'
910
import { SOCKET_JSON } from '../../constants/socket.mts'
@@ -96,7 +97,7 @@ async function run(
9697

9798
const dryRun = !!cli.flags['dryRun']
9899

99-
// TODO: Implement json/md further.
100+
// Feature request: Pass outputKind to convertGradleToMaven for json/md output support.
100101
const outputKind = getOutputKind(json, markdown)
101102

102103
let [cwd = '.'] = cli.input
@@ -153,9 +154,9 @@ async function run(
153154
logger.groupEnd()
154155
}
155156

156-
// TODO: We're not sure it's feasible to parse source file from stdin. We could
157-
// try, store contents in a file in some folder, target that folder... what
158-
// would the file name be?
157+
// Note: stdin input not supported. Gradle manifest generation requires a directory
158+
// context with build files (build.gradle, settings.gradle, etc.) that can't be
159+
// meaningfully provided via stdin.
159160

160161
const wasValidInput = checkCommandInput(outputKind, {
161162
nook: true,
@@ -179,13 +180,20 @@ async function run(
179180
return
180181
}
181182

182-
await convertGradleToMaven({
183+
const result = await convertGradleToMaven({
183184
bin: String(bin),
184185
cwd,
185186
gradleOpts: String(gradleOpts || '')
186187
.split(' ')
187188
.map(s => s.trim())
188189
.filter(Boolean),
190+
outputKind,
189191
verbose: Boolean(verbose),
190192
})
193+
194+
// In text mode, output is already handled by convertGradleToMaven.
195+
// For json/markdown modes, we need to call the output helper.
196+
if (outputKind !== 'text') {
197+
await outputManifest(result, outputKind, '-')
198+
}
191199
}

packages/cli/src/commands/manifest/cmd-manifest-kotlin.mts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { debug } from '@socketsecurity/lib/debug'
44
import { logger } from '@socketsecurity/lib/logger'
55

66
import { convertGradleToMaven } from './convert-gradle-to-maven.mts'
7+
import { outputManifest } from './output-manifest.mts'
78
import { DRY_RUN_BAILING_NOW } from '../../constants/cli.mjs'
89
import { REQUIREMENTS_TXT } from '../../constants/paths.mjs'
910
import { SOCKET_JSON } from '../../constants/socket.mts'
@@ -19,11 +20,11 @@ import type {
1920
CliCommandContext,
2021
} from '../../utils/cli/with-subcommands.mjs'
2122

22-
// TODO: We may want to dedupe some pieces for all gradle languages. I think it
23-
// makes sense to have separate commands for them and I think it makes
24-
// sense for the help panels to note the requested language, rather than
25-
// `socket manifest kotlin` to print help screens with `gradle` as the
26-
// command. Room for improvement.
23+
// Design note: Gradle language commands (gradle, kotlin, scala) share similar code
24+
// but maintain separate commands for clarity. This allows language-specific help text
25+
// and clearer user experience (e.g., "socket manifest kotlin" shows Kotlin-specific
26+
// help rather than generic gradle help). Future refactoring could extract shared logic
27+
// while preserving separate command interfaces.
2728
const config: CliCommandConfig = {
2829
commandName: 'kotlin',
2930
description:
@@ -101,7 +102,7 @@ async function run(
101102

102103
const dryRun = !!cli.flags['dryRun']
103104

104-
// TODO: Implement json/md further.
105+
// Feature request: Pass outputKind to convertGradleToMaven for json/md output support.
105106
const outputKind = getOutputKind(json, markdown)
106107

107108
let [cwd = '.'] = cli.input
@@ -158,9 +159,9 @@ async function run(
158159
logger.groupEnd()
159160
}
160161

161-
// TODO: We're not sure it's feasible to parse source file from stdin. We could
162-
// try, store contents in a file in some folder, target that folder... what
163-
// would the file name be?
162+
// Note: stdin input not supported. Gradle manifest generation requires a directory
163+
// context with build files (build.gradle.kts, settings.gradle.kts, etc.) that can't be
164+
// meaningfully provided via stdin.
164165

165166
const wasValidInput = checkCommandInput(outputKind, {
166167
nook: true,
@@ -184,13 +185,20 @@ async function run(
184185
return
185186
}
186187

187-
await convertGradleToMaven({
188+
const result = await convertGradleToMaven({
188189
bin: String(bin),
189190
cwd,
190191
gradleOpts: String(gradleOpts || '')
191192
.split(' ')
192193
.map(s => s.trim())
193194
.filter(Boolean),
195+
outputKind,
194196
verbose: Boolean(verbose),
195197
})
198+
199+
// In text mode, output is already handled by convertGradleToMaven.
200+
// For json/markdown modes, we need to call the output helper.
201+
if (outputKind !== 'text') {
202+
await outputManifest(result, outputKind, '-')
203+
}
196204
}

packages/cli/src/commands/manifest/cmd-manifest-scala.mts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { debug } from '@socketsecurity/lib/debug'
44
import { logger } from '@socketsecurity/lib/logger'
55

66
import { convertSbtToMaven } from './convert-sbt-to-maven.mts'
7+
import { outputManifest } from './output-manifest.mts'
78
import { DRY_RUN_BAILING_NOW } from '../../constants/cli.mjs'
89
import { REQUIREMENTS_TXT } from '../../constants/paths.mjs'
910
import { SOCKET_JSON } from '../../constants/socket.mts'
@@ -114,7 +115,7 @@ async function run(
114115
// If given path is absolute then cwd should not affect it.
115116
cwd = path.resolve(process.cwd(), cwd)
116117

117-
// TODO: Implement json/md further.
118+
// Feature request: Pass outputKind to convertSbtToMaven for json/md output support.
118119
const outputKind = getOutputKind(json, markdown)
119120

120121
const sockJson = readOrDefaultSocketJson(cwd)
@@ -181,9 +182,9 @@ async function run(
181182
logger.groupEnd()
182183
}
183184

184-
// TODO: We're not sure it's feasible to parse source file from stdin. We could
185-
// try, store contents in a file in some folder, target that folder... what
186-
// would the file name be?
185+
// Note: stdin input not supported. SBT manifest generation requires a directory
186+
// context with build files (build.sbt, project/, etc.) that can't be meaningfully
187+
// provided via stdin.
187188

188189
const wasValidInput = checkCommandInput(outputKind, {
189190
nook: true,
@@ -208,14 +209,21 @@ async function run(
208209
return
209210
}
210211

211-
await convertSbtToMaven({
212+
const result = await convertSbtToMaven({
212213
bin: String(bin),
213214
cwd: cwd,
214215
out: String(out),
216+
outputKind,
215217
sbtOpts: String(sbtOpts)
216218
.split(' ')
217219
.map(s => s.trim())
218220
.filter(Boolean),
219221
verbose: Boolean(verbose),
220222
})
223+
224+
// In text mode, output is already handled by convertSbtToMaven.
225+
// For json/markdown modes, we need to call the output helper.
226+
if (outputKind !== 'text') {
227+
await outputManifest(result, outputKind, String(out))
228+
}
221229
}

packages/cli/src/commands/manifest/convert-gradle-to-maven.mts

Lines changed: 108 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,39 +7,49 @@ import { spawn } from '@socketsecurity/lib/spawn'
77

88
import { distPath } from '../../constants/paths.mjs'
99

10+
import type { CResult, OutputKind } from '../../types.mts'
11+
import type { ManifestResult } from './output-manifest.mts'
12+
1013
export async function convertGradleToMaven({
1114
bin,
1215
cwd,
1316
gradleOpts,
17+
outputKind = 'text',
1418
verbose,
1519
}: {
1620
bin: string
1721
cwd: string
18-
verbose: boolean
1922
gradleOpts: string[]
20-
}) {
21-
// TODO: Implement json/md.
23+
outputKind?: OutputKind | undefined
24+
verbose: boolean
25+
}): Promise<CResult<ManifestResult>> {
2226

23-
// Note: use resolve because the bin could be an absolute path, away from cwd
24-
// TODO: what about $PATH resolved commands? (`gradlew` without dir prefix)
27+
// Note: Resolve bin relative to cwd (or use absolute path if provided).
28+
// We don't resolve against $PATH since gradlew is typically a local wrapper script.
29+
// Users can provide absolute paths if they need to reference system-wide installations.
2530
const rBin = path.resolve(cwd, bin)
2631
const binExists = fs.existsSync(rBin)
2732
const cwdExists = fs.existsSync(cwd)
2833

29-
logger.group('gradle2maven:')
30-
logger.info(`- executing: \`${rBin}\``)
31-
if (!binExists) {
32-
logger.warn(
33-
'Warning: It appears the executable could not be found. An error might be printed later because of that.',
34-
)
35-
}
36-
logger.info(`- src dir: \`${cwd}\``)
37-
if (!cwdExists) {
38-
logger.warn(
39-
'Warning: It appears the src dir could not be found. An error might be printed later because of that.',
40-
)
34+
// Only show logging in text mode.
35+
const isTextMode = outputKind === 'text'
36+
37+
if (isTextMode) {
38+
logger.group('gradle2maven:')
39+
logger.info(`- executing: \`${rBin}\``)
40+
if (!binExists) {
41+
logger.warn(
42+
'Warning: It appears the executable could not be found. An error might be printed later because of that.',
43+
)
44+
}
45+
logger.info(`- src dir: \`${cwd}\``)
46+
if (!cwdExists) {
47+
logger.warn(
48+
'Warning: It appears the src dir could not be found. An error might be printed later because of that.',
49+
)
50+
}
51+
logger.groupEnd()
4152
}
42-
logger.groupEnd()
4353

4454
try {
4555
// Run gradlew with the init script we provide which should yield zero or more
@@ -50,50 +60,92 @@ export async function convertGradleToMaven({
5060
// Note: init.gradle will be exported by .config/rollup.cli-js.config.mjs
5161
const initLocation = path.join(distPath, 'init.gradle')
5262
const commandArgs = ['--init-script', initLocation, ...gradleOpts, 'pom']
53-
if (verbose) {
63+
if (verbose && isTextMode) {
5464
logger.log('[VERBOSE] Executing:', [bin], ', args:', commandArgs)
5565
}
56-
logger.log(`Converting gradle to maven from \`${bin}\` on \`${cwd}\` ...`)
57-
const output = await execGradleWithSpinner(rBin, commandArgs, cwd)
58-
if (verbose) {
66+
if (isTextMode) {
67+
logger.log(`Converting gradle to maven from \`${bin}\` on \`${cwd}\` ...`)
68+
}
69+
const output = await execGradleWithSpinner(
70+
rBin,
71+
commandArgs,
72+
cwd,
73+
isTextMode,
74+
)
75+
if (verbose && isTextMode) {
5976
logger.group('[VERBOSE] gradle stdout:')
6077
logger.log(output)
6178
logger.groupEnd()
6279
}
6380
if (output.code) {
64-
process.exitCode = 1
65-
logger.fail(`Gradle exited with exit code ${output.code}`)
66-
// (In verbose mode, stderr was printed above, no need to repeat it)
67-
if (!verbose) {
68-
logger.group('stderr:')
69-
logger.error(output.stderr)
70-
logger.groupEnd()
81+
if (isTextMode) {
82+
process.exitCode = 1
83+
logger.fail(`Gradle exited with exit code ${output.code}`)
84+
// (In verbose mode, stderr was printed above, no need to repeat it)
85+
if (!verbose) {
86+
logger.group('stderr:')
87+
logger.error(output.stderr)
88+
logger.groupEnd()
89+
}
90+
}
91+
return {
92+
ok: false,
93+
code: output.code,
94+
message: `Gradle exited with exit code ${output.code}`,
95+
cause: output.stderr,
7196
}
72-
return
7397
}
74-
logger.success('Executed gradle successfully')
75-
logger.log('Reported exports:')
98+
99+
// Extract file paths from output.
100+
const files: string[] = []
76101
output.stdout.replace(
77102
/^POM file copied to: (.*)/gm,
78103
(_all: string, fn: string) => {
79-
logger.log('- ', fn)
104+
files.push(fn)
105+
if (isTextMode) {
106+
logger.log('- ', fn)
107+
}
80108
return fn
81109
},
82110
)
83-
logger.log('')
84-
logger.log(
85-
'Next step is to generate a Scan by running the `socket scan create` command on the same directory',
86-
)
111+
112+
if (isTextMode) {
113+
logger.success('Executed gradle successfully')
114+
logger.log('Reported exports:')
115+
files.forEach(fn => logger.log('- ', fn))
116+
logger.log('')
117+
logger.log(
118+
'Next step is to generate a Scan by running the `socket scan create` command on the same directory',
119+
)
120+
}
121+
122+
return {
123+
ok: true,
124+
data: {
125+
files,
126+
type: 'gradle',
127+
success: true,
128+
},
129+
}
87130
} catch (e) {
88-
process.exitCode = 1
89-
logger.fail(
131+
const errorMessage =
90132
'There was an unexpected error while generating manifests' +
91-
(verbose ? '' : ' (use --verbose for details)'),
92-
)
93-
if (verbose) {
94-
logger.group('[VERBOSE] error:')
95-
logger.log(e)
96-
logger.groupEnd()
133+
(verbose ? '' : ' (use --verbose for details)')
134+
135+
if (isTextMode) {
136+
process.exitCode = 1
137+
logger.fail(errorMessage)
138+
if (verbose) {
139+
logger.group('[VERBOSE] error:')
140+
logger.log(e)
141+
logger.groupEnd()
142+
}
143+
}
144+
145+
return {
146+
ok: false,
147+
message: errorMessage,
148+
cause: e instanceof Error ? e.message : String(e),
97149
}
98150
}
99151
}
@@ -102,17 +154,20 @@ async function execGradleWithSpinner(
102154
bin: string,
103155
commandArgs: string[],
104156
cwd: string,
157+
showSpinner: boolean,
105158
): Promise<{ code: number; stdout: string; stderr: string }> {
106159
let pass = false
107-
const spinner = getSpinner()
160+
const spinner = showSpinner ? getSpinner() : undefined
108161
try {
109-
logger.info(
110-
'(Running gradle can take a while, it depends on how long gradlew has to run)',
111-
)
112-
logger.info(
113-
'(It will show no output, you can use --verbose to see its output)',
114-
)
115-
spinner?.start('Running gradlew...')
162+
if (showSpinner) {
163+
logger.info(
164+
'(Running gradle can take a while, it depends on how long gradlew has to run)',
165+
)
166+
logger.info(
167+
'(It will show no output, you can use --verbose to see its output)',
168+
)
169+
spinner?.start('Running gradlew...')
170+
}
116171

117172
const output = await spawn(bin, commandArgs, {
118173
// We can pipe the output through to have the user see the result

0 commit comments

Comments
 (0)