@socketsecurity/cli 0.7.2 → 0.8.2
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.
- package/README.md +2 -2
- package/lib/commands/info/index.js +3 -3
- package/lib/commands/login/index.js +121 -17
- package/lib/commands/logout/index.js +3 -0
- package/lib/commands/report/create.js +17 -16
- package/lib/commands/report/view.js +20 -5
- package/lib/flags/output.js +2 -2
- package/lib/shadow/link.cjs +12 -12
- package/lib/shadow/npm-injection.cjs +226 -255
- package/lib/shadow/tty-server.cjs +222 -0
- package/lib/utils/api-helpers.js +1 -3
- package/lib/utils/issue-rules.cjs +180 -0
- package/lib/utils/misc.js +1 -1
- package/lib/utils/path-resolve.js +59 -48
- package/lib/utils/sdk.js +55 -11
- package/lib/utils/settings.js +17 -5
- package/lib/utils/{type-helpers.js → type-helpers.cjs} +1 -1
- package/lib/utils/update-notifier.js +3 -0
- package/package.json +11 -15
package/README.md
CHANGED
|
@@ -26,10 +26,10 @@ socket report view QXU8PmK7LfH608RAwfIKdbcHgwEd_ZeWJ9QEGv05FJUQ
|
|
|
26
26
|
|
|
27
27
|
* `socket report create <path(s)-to-folder-or-file>` - creates a report on [socket.dev](https://socket.dev/)
|
|
28
28
|
|
|
29
|
-
Uploads the specified `package.json` and lock files for JavaScript and
|
|
29
|
+
Uploads the specified `package.json` and lock files for JavaScript, Python, and Go dependency manifests.
|
|
30
30
|
If any folder is specified, the ones found in there recursively are uploaded.
|
|
31
31
|
|
|
32
|
-
Supports globbing such as `**/package.json`, `**/requirements.txt`, and `**/
|
|
32
|
+
Supports globbing such as `**/package.json`, `**/requirements.txt`, `**/pyproject.toml`, and `**/go.mod`.
|
|
33
33
|
|
|
34
34
|
Ignores any file specified in your project's `.gitignore`, the `projectIgnorePaths` in your project's [`socket.yml`](https://docs.socket.dev/docs/socket-yml) and on top of that has a sensible set of [default ignores](https://www.npmjs.com/package/ignore-by-default)
|
|
35
35
|
|
|
@@ -11,7 +11,7 @@ import { InputError } from '../../utils/errors.js'
|
|
|
11
11
|
import { getSeverityCount, formatSeverityCount } from '../../utils/format-issues.js'
|
|
12
12
|
import { printFlagList } from '../../utils/formatting.js'
|
|
13
13
|
import { objectSome } from '../../utils/misc.js'
|
|
14
|
-
import { setupSdk } from '../../utils/sdk.js'
|
|
14
|
+
import { FREE_API_KEY, getDefaultKey, setupSdk } from '../../utils/sdk.js'
|
|
15
15
|
|
|
16
16
|
/** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
|
|
17
17
|
export const info = {
|
|
@@ -124,9 +124,9 @@ function setupCommand (name, description, argv, importMeta) {
|
|
|
124
124
|
* @returns {Promise<void|PackageData>}
|
|
125
125
|
*/
|
|
126
126
|
async function fetchPackageData (pkgName, pkgVersion, { includeAllIssues, strict }) {
|
|
127
|
-
const socketSdk = await setupSdk()
|
|
127
|
+
const socketSdk = await setupSdk(getDefaultKey() || FREE_API_KEY)
|
|
128
128
|
const spinner = ora(`Looking up data for version ${pkgVersion} of ${pkgName}`).start()
|
|
129
|
-
const result = await handleApiCall(socketSdk.getIssuesByNPMPackage(pkgName, pkgVersion),
|
|
129
|
+
const result = await handleApiCall(socketSdk.getIssuesByNPMPackage(pkgName, pkgVersion), 'looking up package')
|
|
130
130
|
|
|
131
131
|
if (result.success === false) {
|
|
132
132
|
return handleUnsuccessfulApiResponse('getIssuesByNPMPackage', result, spinner)
|
|
@@ -2,10 +2,12 @@ import isInteractive from 'is-interactive'
|
|
|
2
2
|
import meow from 'meow'
|
|
3
3
|
import ora from 'ora'
|
|
4
4
|
import prompts from 'prompts'
|
|
5
|
+
import terminalLink from 'terminal-link'
|
|
5
6
|
|
|
6
|
-
import { ChalkOrMarkdown } from '../../utils/chalk-markdown.js'
|
|
7
7
|
import { AuthError, InputError } from '../../utils/errors.js'
|
|
8
|
-
import {
|
|
8
|
+
import { prepareFlags } from '../../utils/flags.js'
|
|
9
|
+
import { printFlagList } from '../../utils/formatting.js'
|
|
10
|
+
import { FREE_API_KEY, setupSdk } from '../../utils/sdk.js'
|
|
9
11
|
import { getSetting, updateSetting } from '../../utils/settings.js'
|
|
10
12
|
|
|
11
13
|
const description = 'Socket API login'
|
|
@@ -14,6 +16,16 @@ const description = 'Socket API login'
|
|
|
14
16
|
export const login = {
|
|
15
17
|
description,
|
|
16
18
|
run: async (argv, importMeta, { parentName }) => {
|
|
19
|
+
const flags = prepareFlags({
|
|
20
|
+
apiBaseUrl: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'API server to connect to for login',
|
|
23
|
+
},
|
|
24
|
+
apiProxy: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'Proxy to use when making connection to API server'
|
|
27
|
+
}
|
|
28
|
+
})
|
|
17
29
|
const name = parentName + ' login'
|
|
18
30
|
const cli = meow(`
|
|
19
31
|
Usage
|
|
@@ -21,46 +33,138 @@ export const login = {
|
|
|
21
33
|
|
|
22
34
|
Logs into the Socket API by prompting for an API key
|
|
23
35
|
|
|
36
|
+
Options
|
|
37
|
+
${printFlagList({
|
|
38
|
+
'api-base-url': flags.apiBaseUrl.description,
|
|
39
|
+
'api-proxy': flags.apiProxy.description
|
|
40
|
+
}, 8)}
|
|
41
|
+
|
|
24
42
|
Examples
|
|
25
43
|
$ ${name}
|
|
26
44
|
`, {
|
|
27
45
|
argv,
|
|
28
46
|
description,
|
|
29
47
|
importMeta,
|
|
48
|
+
flags
|
|
30
49
|
})
|
|
31
50
|
|
|
51
|
+
/**
|
|
52
|
+
* @param {{aborted: boolean}} state
|
|
53
|
+
*/
|
|
54
|
+
const promptAbortHandler = (state) => {
|
|
55
|
+
if (state.aborted) {
|
|
56
|
+
process.nextTick(() => process.exit(1))
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
32
60
|
if (cli.input.length) cli.showHelp()
|
|
33
61
|
|
|
34
62
|
if (!isInteractive()) {
|
|
35
63
|
throw new InputError('cannot prompt for credentials in a non-interactive shell')
|
|
36
64
|
}
|
|
37
|
-
|
|
38
|
-
|
|
65
|
+
/**
|
|
66
|
+
* @type {{ apiKey: string }}
|
|
67
|
+
*/
|
|
68
|
+
const result = await prompts({
|
|
39
69
|
type: 'password',
|
|
40
70
|
name: 'apiKey',
|
|
41
|
-
message: `Enter your ${
|
|
71
|
+
message: `Enter your ${terminalLink(
|
|
42
72
|
'Socket.dev API key',
|
|
43
73
|
'https://docs.socket.dev/docs/api-keys'
|
|
44
|
-
)}`,
|
|
74
|
+
)} (leave blank for a public key)`,
|
|
75
|
+
onState: promptAbortHandler
|
|
45
76
|
})
|
|
46
77
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
78
|
+
const apiKey = result.apiKey || FREE_API_KEY
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @type {string | null | undefined}
|
|
82
|
+
*/
|
|
83
|
+
let apiBaseUrl = cli.flags.apiBaseUrl
|
|
84
|
+
apiBaseUrl ??= getSetting('apiBaseUrl') ??
|
|
85
|
+
undefined
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @type {string | null | undefined}
|
|
89
|
+
*/
|
|
90
|
+
let apiProxy = cli.flags.apiProxy
|
|
91
|
+
apiProxy ??= getSetting('apiProxy') ??
|
|
92
|
+
undefined
|
|
51
93
|
|
|
52
94
|
const spinner = ora('Verifying API key...').start()
|
|
53
95
|
|
|
54
|
-
|
|
55
|
-
|
|
96
|
+
/** @type {import('@socketsecurity/sdk').SocketSdkReturnType<'getOrganizations'>['data']} */
|
|
97
|
+
let orgs
|
|
98
|
+
|
|
56
99
|
try {
|
|
57
|
-
const sdk = await setupSdk()
|
|
58
|
-
const
|
|
59
|
-
if (!
|
|
60
|
-
|
|
100
|
+
const sdk = await setupSdk(apiKey, apiBaseUrl, apiProxy)
|
|
101
|
+
const result = await sdk.getOrganizations()
|
|
102
|
+
if (!result.success) throw new AuthError()
|
|
103
|
+
orgs = result.data
|
|
104
|
+
spinner.succeed('API key verified\n')
|
|
61
105
|
} catch (e) {
|
|
62
|
-
updateSetting('apiKey', oldKey)
|
|
63
106
|
spinner.fail('Invalid API key')
|
|
107
|
+
return
|
|
64
108
|
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @template T
|
|
112
|
+
* @param {T | null | undefined} value
|
|
113
|
+
* @returns {value is T}
|
|
114
|
+
*/
|
|
115
|
+
const nonNullish = value => value != null
|
|
116
|
+
|
|
117
|
+
/** @type {prompts.Choice[]} */
|
|
118
|
+
const enforcedChoices = Object.values(orgs.organizations)
|
|
119
|
+
.filter(nonNullish)
|
|
120
|
+
.filter(org => org.plan === 'enterprise')
|
|
121
|
+
.map(org => ({
|
|
122
|
+
title: org.name,
|
|
123
|
+
value: org.id
|
|
124
|
+
}))
|
|
125
|
+
|
|
126
|
+
/** @type {string[]} */
|
|
127
|
+
let enforcedOrgs = []
|
|
128
|
+
|
|
129
|
+
if (enforcedChoices.length > 1) {
|
|
130
|
+
/**
|
|
131
|
+
* @type { {id: string} }
|
|
132
|
+
*/
|
|
133
|
+
const { id } = await prompts({
|
|
134
|
+
type: 'select',
|
|
135
|
+
name: 'id',
|
|
136
|
+
hint: '\n Pick "None" if this is a personal device',
|
|
137
|
+
message: 'Which organization\'s policies should Socket enforce system-wide?',
|
|
138
|
+
choices: enforcedChoices.concat({
|
|
139
|
+
title: 'None',
|
|
140
|
+
value: null
|
|
141
|
+
}),
|
|
142
|
+
onState: promptAbortHandler
|
|
143
|
+
})
|
|
144
|
+
if (id) enforcedOrgs = [id]
|
|
145
|
+
} else if (enforcedChoices.length) {
|
|
146
|
+
/**
|
|
147
|
+
* @type { {confirmOrg: boolean} }
|
|
148
|
+
*/
|
|
149
|
+
const { confirmOrg } = await prompts({
|
|
150
|
+
type: 'confirm',
|
|
151
|
+
name: 'confirmOrg',
|
|
152
|
+
message: `Should Socket enforce ${enforcedChoices[0]?.title}'s security policies system-wide?`,
|
|
153
|
+
initial: true,
|
|
154
|
+
onState: promptAbortHandler
|
|
155
|
+
})
|
|
156
|
+
if (confirmOrg) {
|
|
157
|
+
const existing = /** @type {undefined | {value: string}} */(enforcedChoices[0])
|
|
158
|
+
if (existing) {
|
|
159
|
+
enforcedOrgs = [existing.value]
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// MUST DO all updateSetting ON SAME TICK TO AVOID PARTIAL WRITE
|
|
164
|
+
updateSetting('enforcedOrgs', enforcedOrgs)
|
|
165
|
+
const oldKey = getSetting('apiKey')
|
|
166
|
+
updateSetting('apiKey', apiKey)
|
|
167
|
+
updateSetting('apiBaseUrl', apiBaseUrl)
|
|
168
|
+
spinner.succeed(`API credentials ${oldKey ? 'updated' : 'set'}`)
|
|
65
169
|
}
|
|
66
170
|
}
|
|
@@ -27,6 +27,9 @@ export const logout = {
|
|
|
27
27
|
if (cli.input.length) cli.showHelp()
|
|
28
28
|
|
|
29
29
|
updateSetting('apiKey', null)
|
|
30
|
+
updateSetting('apiBaseUrl', null)
|
|
31
|
+
updateSetting('apiProxy', null)
|
|
32
|
+
updateSetting('enforcedOrgs', null)
|
|
30
33
|
ora('Successfully logged out').succeed()
|
|
31
34
|
}
|
|
32
35
|
}
|
|
@@ -86,7 +86,7 @@ async function setupCommand (name, description, argv, importMeta) {
|
|
|
86
86
|
...validationFlags,
|
|
87
87
|
debug: {
|
|
88
88
|
type: 'boolean',
|
|
89
|
-
|
|
89
|
+
shortFlag: 'd',
|
|
90
90
|
default: false,
|
|
91
91
|
description: 'Output debug information',
|
|
92
92
|
},
|
|
@@ -97,7 +97,7 @@ async function setupCommand (name, description, argv, importMeta) {
|
|
|
97
97
|
},
|
|
98
98
|
view: {
|
|
99
99
|
type: 'boolean',
|
|
100
|
-
|
|
100
|
+
shortFlag: 'v',
|
|
101
101
|
default: false,
|
|
102
102
|
description: 'Will wait for and return the created report'
|
|
103
103
|
},
|
|
@@ -107,10 +107,10 @@ async function setupCommand (name, description, argv, importMeta) {
|
|
|
107
107
|
Usage
|
|
108
108
|
$ ${name} <paths-to-package-folders-and-files>
|
|
109
109
|
|
|
110
|
-
Uploads the specified "package.json" and lock files for JavaScript and
|
|
110
|
+
Uploads the specified "package.json" and lock files for JavaScript, Python, and Go dependency manifests.
|
|
111
111
|
If any folder is specified, the ones found in there recursively are uploaded.
|
|
112
112
|
|
|
113
|
-
Supports globbing such as "**/package.json", "**/requirements.txt",
|
|
113
|
+
Supports globbing such as "**/package.json", "**/requirements.txt", "**/pyproject.toml", and "**/go.mod".
|
|
114
114
|
|
|
115
115
|
Ignores any file specified in your project's ".gitignore", your project's
|
|
116
116
|
"socket.yml" file's "projectIgnorePaths" and also has a sensible set of
|
|
@@ -118,13 +118,13 @@ async function setupCommand (name, description, argv, importMeta) {
|
|
|
118
118
|
|
|
119
119
|
Options
|
|
120
120
|
${printFlagList({
|
|
121
|
-
'
|
|
122
|
-
'
|
|
123
|
-
'
|
|
124
|
-
'
|
|
125
|
-
'
|
|
126
|
-
'
|
|
127
|
-
'
|
|
121
|
+
'all': 'Include all issues',
|
|
122
|
+
'debug': 'Output debug information',
|
|
123
|
+
'dry-run': 'Only output what will be done without actually doing it',
|
|
124
|
+
'json': 'Output result as json',
|
|
125
|
+
'markdown': 'Output result as markdown',
|
|
126
|
+
'strict': 'Exits with an error code if any matching issues are found',
|
|
127
|
+
'view': 'Will wait for and return the created report'
|
|
128
128
|
}, 6)}
|
|
129
129
|
|
|
130
130
|
Examples
|
|
@@ -179,15 +179,16 @@ async function setupCommand (name, description, argv, importMeta) {
|
|
|
179
179
|
}
|
|
180
180
|
})
|
|
181
181
|
|
|
182
|
-
// TODO: setupSdk(getDefaultKey() || FREE_API_KEY)
|
|
183
182
|
const socketSdk = await setupSdk()
|
|
184
183
|
const supportedFiles = await socketSdk.getReportSupportedFiles()
|
|
185
184
|
.then(res => {
|
|
186
185
|
if (!res.success) handleUnsuccessfulApiResponse('getReportSupportedFiles', res, ora())
|
|
187
186
|
return res.data
|
|
188
|
-
}).catch(
|
|
189
|
-
|
|
190
|
-
|
|
187
|
+
}).catch(
|
|
188
|
+
/** @type {(cause: Error) => never} */
|
|
189
|
+
(cause) => {
|
|
190
|
+
throw new ErrorWithCause('Failed getting supported files for report', { cause })
|
|
191
|
+
})
|
|
191
192
|
|
|
192
193
|
const packagePaths = await getPackageFiles(cwd, cli.input, config, supportedFiles, debugLog)
|
|
193
194
|
|
|
@@ -220,7 +221,7 @@ async function createReport (packagePaths, { config, cwd, debugLog, dryRun }) {
|
|
|
220
221
|
const socketSdk = await setupSdk()
|
|
221
222
|
const spinner = ora(`Creating report with ${packagePaths.length} package files`).start()
|
|
222
223
|
const apiCall = socketSdk.createReportFromFilePaths(packagePaths, cwd, config?.issueRules)
|
|
223
|
-
const result = await handleApiCall(apiCall,
|
|
224
|
+
const result = await handleApiCall(apiCall, 'creating report')
|
|
224
225
|
|
|
225
226
|
if (result.success === false) {
|
|
226
227
|
return handleUnsuccessfulApiResponse('createReport', result, spinner)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import chalk from 'chalk'
|
|
4
4
|
import meow from 'meow'
|
|
5
5
|
import ora from 'ora'
|
|
6
|
+
import { ErrorWithCause } from 'pony-cause'
|
|
6
7
|
|
|
7
8
|
import { outputFlags, validationFlags } from '../../flags/index.js'
|
|
8
9
|
import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js'
|
|
@@ -102,6 +103,8 @@ function setupCommand (name, description, argv, importMeta) {
|
|
|
102
103
|
* @typedef {import('@socketsecurity/sdk').SocketSdkReturnType<'getReport'>["data"]} ReportData
|
|
103
104
|
*/
|
|
104
105
|
|
|
106
|
+
const MAX_TIMEOUT_RETRY = 5
|
|
107
|
+
|
|
105
108
|
/**
|
|
106
109
|
* @param {string} reportId
|
|
107
110
|
* @param {Pick<CommandContext, 'includeAllIssues' | 'strict'>} context
|
|
@@ -111,8 +114,22 @@ export async function fetchReportData (reportId, { includeAllIssues, strict }) {
|
|
|
111
114
|
// Do the API call
|
|
112
115
|
|
|
113
116
|
const socketSdk = await setupSdk()
|
|
114
|
-
const spinner = ora(`Fetching report with ID ${reportId}`).start()
|
|
115
|
-
|
|
117
|
+
const spinner = ora(`Fetching report with ID ${reportId} (this could take a while)`).start()
|
|
118
|
+
/** @type {import('@socketsecurity/sdk').SocketSdkResultType<'getReport'> | undefined} */
|
|
119
|
+
let result
|
|
120
|
+
for (let retry = 1; !result; ++retry) {
|
|
121
|
+
try {
|
|
122
|
+
result = await handleApiCall(socketSdk.getReport(reportId), 'fetching report')
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (
|
|
125
|
+
retry >= MAX_TIMEOUT_RETRY ||
|
|
126
|
+
!(err instanceof ErrorWithCause) ||
|
|
127
|
+
err.cause?.cause?.response?.statusCode !== 524
|
|
128
|
+
) {
|
|
129
|
+
throw err
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
116
133
|
|
|
117
134
|
if (result.success === false) {
|
|
118
135
|
return handleUnsuccessfulApiResponse('getReport', result, spinner)
|
|
@@ -147,9 +164,7 @@ export function formatReportDataOutput (data, { name, outputJson, outputMarkdown
|
|
|
147
164
|
console.log(JSON.stringify(data, undefined, 2))
|
|
148
165
|
} else {
|
|
149
166
|
const format = new ChalkOrMarkdown(!!outputMarkdown)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
console.log('\nDetailed info on socket.dev: ' + format.hyperlink(reportId, url, { fallbackToUrl: true }))
|
|
167
|
+
console.log('\nDetailed info on socket.dev: ' + format.hyperlink(reportId, data.url, { fallbackToUrl: true }))
|
|
153
168
|
if (!outputMarkdown) {
|
|
154
169
|
console.log(chalk.dim('\nOr rerun', chalk.italic(name), 'using the', chalk.italic('--json'), 'flag to get full JSON output'))
|
|
155
170
|
}
|
package/lib/flags/output.js
CHANGED
|
@@ -3,13 +3,13 @@ import { prepareFlags } from '../utils/flags.js'
|
|
|
3
3
|
export const outputFlags = prepareFlags({
|
|
4
4
|
json: {
|
|
5
5
|
type: 'boolean',
|
|
6
|
-
|
|
6
|
+
shortFlag: 'j',
|
|
7
7
|
default: false,
|
|
8
8
|
description: 'Output result as json',
|
|
9
9
|
},
|
|
10
10
|
markdown: {
|
|
11
11
|
type: 'boolean',
|
|
12
|
-
|
|
12
|
+
shortFlag: 'm',
|
|
13
13
|
default: false,
|
|
14
14
|
description: 'Output result as markdown',
|
|
15
15
|
},
|
package/lib/shadow/link.cjs
CHANGED
|
@@ -5,34 +5,34 @@ const path = require('path')
|
|
|
5
5
|
const which = require('which')
|
|
6
6
|
|
|
7
7
|
if (process.platform === 'win32') {
|
|
8
|
-
console.error('Socket
|
|
8
|
+
console.error('Socket dependency manager Windows suppport is limited to WSL at this time.')
|
|
9
9
|
process.exit(1)
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* @param {string} realDirname path to shadow/bin
|
|
14
14
|
* @param {'npm' | 'npx'} binname
|
|
15
|
-
* @returns {string} path to
|
|
15
|
+
* @returns {string} path to original bin
|
|
16
16
|
*/
|
|
17
17
|
function installLinks (realDirname, binname) {
|
|
18
|
-
const
|
|
19
|
-
// find
|
|
20
|
-
const
|
|
18
|
+
const realShadowBinDir = realDirname
|
|
19
|
+
// find package manager being shadowed by this process
|
|
20
|
+
const bins = which.sync(binname, {
|
|
21
21
|
all: true
|
|
22
22
|
})
|
|
23
23
|
let shadowIndex = -1
|
|
24
|
-
const
|
|
25
|
-
const isShadow = realpathSync(path.dirname(
|
|
24
|
+
const binpath = bins.find((binPath, i) => {
|
|
25
|
+
const isShadow = realpathSync(path.dirname(binPath)) === realShadowBinDir
|
|
26
26
|
if (isShadow) {
|
|
27
27
|
shadowIndex = i
|
|
28
28
|
}
|
|
29
29
|
return !isShadow
|
|
30
30
|
})
|
|
31
|
-
if (
|
|
32
|
-
return
|
|
31
|
+
if (binpath && process.platform === 'win32') {
|
|
32
|
+
return binpath
|
|
33
33
|
}
|
|
34
|
-
if (!
|
|
35
|
-
console.error(
|
|
34
|
+
if (!binpath) {
|
|
35
|
+
console.error(`Socket unable to locate ${binname}; ensure it is available in the PATH environment variable`)
|
|
36
36
|
process.exit(127)
|
|
37
37
|
}
|
|
38
38
|
if (shadowIndex === -1) {
|
|
@@ -45,6 +45,6 @@ function installLinks (realDirname, binname) {
|
|
|
45
45
|
process.env['PATH']
|
|
46
46
|
}`
|
|
47
47
|
}
|
|
48
|
-
return
|
|
48
|
+
return binpath
|
|
49
49
|
}
|
|
50
50
|
module.exports = installLinks
|