Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
trust-cmd.js285 linesDownload Raw Back to lib
1const BaseCommand = require('./base-cmd.js')
2const { otplease } = require('./utils/auth.js')
3const npmFetch = require('npm-registry-fetch')
4const npa = require('npm-package-arg')
5const { read: _read } = require('read')
6const { input, output, log, META } = require('proc-log')
7const gitinfo = require('hosted-git-info')
8const pkgJson = require('@npmcli/package-json')
9
10const NPM_FRONTEND = 'https://www.npmjs.com'
11
12class TrustCommand extends BaseCommand {
13  // Helper to format template strings with color
14  // Blue text with reset color for interpolated values
15  warnString (strings, ...values) {
16    const chalk = this.npm.chalk
17    const message = strings.reduce((result, str, i) => {
18      return result + chalk.blue(str) + (values[i] ? chalk.reset(values[i]) : '')
19    }, '')
20    return message
21  }
22
23  // Log a warning message with blue formatting
24  warn (strings, ...values) {
25    log.warn('trust', this.warnString(strings, ...values))
26  }
27
28  // dialogue is non-log text that is different from our usual npm prefix logging
29  // it should always show to the user unless --json is specified
30  // it's not controled by log levels
31  dialogue (strings, ...values) {
32    const json = this.config.get('json')
33    if (!json) {
34      output.standard(this.warnString(strings, ...values))
35    }
36  }
37
38  createConfig (pkg, body) {
39    const spec = npa(pkg)
40    const uri = `/-/package/${spec.escapedName}/trust`
41    return otplease(this.npm, this.npm.flatOptions, opts => npmFetch(uri, {
42      ...opts,
43      method: 'POST',
44      body: body,
45    }))
46  }
47
48  logOptions (options, pad = true) {
49    const { values, warnings, fromPackageJson, urls } = { warnings: [], ...options }
50    if (warnings && warnings.length > 0) {
51      for (const warningMsg of warnings) {
52        log.warn('trust', warningMsg)
53      }
54    }
55
56    const json = this.config.get('json')
57    if (json) {
58      // Disable redaction: trust config values (e.g. CircleCI UUIDs) are not secrets
59      output.standard(JSON.stringify(options.values, null, 2), { [META]: true, redact: false })
60      return
61    }
62
63    const chalk = this.npm.chalk
64    const { type, id, ...rest } = values || {}
65
66    if (values) {
67      const lines = []
68      if (type) {
69        lines.push(`type: ${chalk.green(type)}`)
70      }
71      if (id) {
72        lines.push(`id: ${chalk.green(id)}`)
73      }
74      for (const [key, value] of Object.entries(rest)) {
75        if (value !== null && value !== undefined) {
76          const parts = [
77            `${chalk.reset(key)}: ${chalk.green(value)}`,
78          ]
79          if (fromPackageJson && fromPackageJson[key]) {
80            parts.push(`(${chalk.yellow(`from package.json`)})`)
81          }
82          lines.push(parts.join(' '))
83        }
84      }
85      if (pad) {
86        output.standard()
87      }
88      output.standard(lines.join('\n'), { [META]: true, redact: false })
89      // Print URLs on their own lines after config, following the same order as rest keys
90      if (urls) {
91        const urlLines = []
92        for (const key of Object.keys(rest)) {
93          if (urls[key]) {
94            urlLines.push(chalk.blue(urls[key]))
95          }
96        }
97        if (urlLines.length > 0) {
98          output.standard()
99          output.standard(urlLines.join('\n'), { [META]: true, redact: false })
100        }
101      }
102      if (pad) {
103        output.standard()
104      }
105    }
106  }
107
108  async confirmOperation (yes) {
109    // Ask for confirmation unless --yes flag is set
110    if (yes === true) {
111      return
112    }
113    if (yes === false) {
114      throw new Error('User cancelled operation')
115    }
116    const confirm = await input.read(
117      () => _read({ prompt: 'Do you want to proceed? (y/N) ', default: 'n' })
118    )
119    const normalized = confirm.toLowerCase()
120    if (['y', 'yes'].includes(normalized)) {
121      return
122    }
123    throw new Error('User cancelled operation')
124  }
125
126  getFrontendUrl ({ pkgName }) {
127    if (this.registryIsDefault) {
128      return new URL(`/package/${pkgName}`, NPM_FRONTEND).toString()
129    }
130    return null
131  }
132
133  getRepositoryFromPackageJson (pkg) {
134    const info = gitinfo.fromUrl(pkg.repository?.url || pkg?.repository)
135    if (!info) {
136      return null
137    }
138    const repository = info.user + '/' + info.project
139    const type = info.type
140    return { repository, type }
141  }
142
143  async optionalPkgJson () {
144    try {
145      const { content } = await pkgJson.normalize(this.npm.prefix)
146      return content
147    } catch (err) {
148      return {}
149    }
150  }
151
152  get registryIsDefault () {
153    return this.npm.config.defaults.registry === this.npm.config.get('registry')
154  }
155
156  // generic
157  static bodyToOptions (body) {
158    return {
159      ...(body.id) && { id: body.id },
160      ...(body.type) && { type: body.type },
161    }
162  }
163
164  async createConfigCommand ({ positionalArgs, flags }) {
165    const { providerName, providerEntity, providerHostname } = this.constructor
166    const dryRun = this.config.get('dry-run')
167    const yes = this.config.get('yes') // deep-lore this allows for --no-yes
168    const options = await this.flagsToOptions({ positionalArgs, flags, providerHostname })
169    this.dialogue`Establishing trust between ${options.values.package} package and ${providerName}`
170    this.dialogue`Anyone with ${providerEntity} write access can publish to ${options.values.package}`
171    this.dialogue`Two-factor authentication is required for this operation`
172    if (!this.registryIsDefault) {
173      this.warn`Registry ${this.npm.config.get('registry')} may not support trusted publishing`
174    }
175    this.logOptions(options)
176    if (dryRun) {
177      return
178    }
179    await this.confirmOperation(yes)
180    const trustConfig = this.constructor.optionsToBody(options.values)
181    const response = await this.createConfig(options.values.package, [trustConfig])
182    const body = await response.json()
183    this.dialogue`Trust configuration created successfully for ${options.values.package} with the following settings:`
184    this.displayResponseBody({ body, packageName: options.values.package })
185  }
186
187  async flagsToOptions ({ positionalArgs, flags, providerHostname }) {
188    const { entityKey, name, providerEntity, providerFile } = this.constructor
189    const content = await this.optionalPkgJson()
190    const pkgPositional = positionalArgs[0]
191    const pkgJsonName = content.name
192    const git = this.getRepositoryFromPackageJson(content)
193    // the provided positional matches package.json name or no positional provided
194    const matchPkg = (!pkgPositional || pkgPositional === pkgJsonName)
195    const pkgName = pkgPositional || pkgJsonName
196    const usedPkgNameFromPkgJson = !pkgPositional && Boolean(pkgJsonName)
197    const invalidPkgJsonProviderType = matchPkg && git && git?.type !== name
198
199    let entity
200    let entitySource
201
202    if (flags[entityKey]) {
203      entity = flags[entityKey]
204      entitySource = 'flag'
205    } else if (!invalidPkgJsonProviderType && git?.repository) {
206      entity = git.repository
207      entitySource = 'package.json'
208    }
209    const mismatchPkgJsonRepository = matchPkg && git && entity !== git.repository
210    const usedRepositoryInPkgJson = entitySource === 'package.json'
211
212    const warnings = []
213    if (!pkgName) {
214      throw new Error('Package name must be specified either as an argument or in package.json file')
215    }
216
217    if (!flags.file) {
218      throw new Error(`${providerFile} must be specified with the file option`)
219    }
220    if (!flags.file.endsWith('.yml') && !flags.file.endsWith('.yaml')) {
221      throw new Error(`${providerFile} must end in .yml or .yaml`)
222    }
223
224    this.validateFile?.(flags.file)
225
226    if (invalidPkgJsonProviderType) {
227      const message = this.warnString`Repository in package.json is not a ${providerEntity}`
228      if (!flags[entityKey]) {
229        throw new Error(message)
230      } else {
231        warnings.push(message)
232      }
233    } else {
234      if (mismatchPkgJsonRepository) {
235        warnings.push(this.warnString`Repository in package.json (${git.repository}) differs from provided ${providerEntity} (${entity})`)
236      }
237    }
238
239    if (!entity && matchPkg) {
240      throw new Error(`${providerEntity} must be specified with ${entityKey} option or inferred from the package.json repository field`)
241    }
242    if (!entity) {
243      throw new Error(`${providerEntity} must be specified with ${entityKey} option`)
244    }
245
246    this.validateEntity(entity)
247
248    return {
249      values: {
250        package: pkgName,
251        file: flags.file,
252        [entityKey]: entity,
253        ...(flags.environment && { environment: flags.environment }),
254      },
255      fromPackageJson: {
256        [entityKey]: usedRepositoryInPkgJson,
257        package: usedPkgNameFromPkgJson,
258      },
259      warnings: warnings,
260      urls: {
261        package: this.getFrontendUrl({ pkgName }),
262        [entityKey]: this.getEntityUrl({ providerHostname, entity }),
263        file: this.getEntityUrl({ providerHostname, entity, file: flags.file }),
264      },
265    }
266  }
267
268  displayResponseBody ({ body, packageName }) {
269    if (!body || body.length === 0) {
270      this.dialogue`No trust configurations found for package (${packageName})`
271      return
272    }
273    const items = Array.isArray(body) ? body : [body]
274    for (const config of items) {
275      const values = this.constructor.bodyToOptions(config)
276      output.standard()
277      this.logOptions({ values }, false)
278    }
279    output.standard()
280  }
281}
282
283module.exports = TrustCommand
284module.exports.NPM_FRONTEND = NPM_FRONTEND
285 
codekingpro/portable-devtools · Team Ai