@typinghare/trick 1.0.7 → 2.0.1
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 +23 -23
- package/dist/app.d.ts +1 -1
- package/dist/app.js +65 -111
- package/dist/config.d.ts +71 -14
- package/dist/config.js +61 -24
- package/dist/encrypt.d.ts +68 -14
- package/dist/encrypt.js +79 -30
- package/dist/error.d.ts +30 -0
- package/dist/error.js +69 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/passphrase.d.ts +13 -0
- package/dist/passphrase.js +26 -0
- package/eslint.config.js +41 -0
- package/package.json +11 -8
- package/src/app.ts +85 -160
- package/src/config.ts +93 -29
- package/src/encrypt.ts +89 -44
- package/src/error.ts +82 -0
- package/src/index.ts +1 -2
- package/src/passphrase.ts +39 -0
- package/test/resources/really.json +2 -2
- package/test/resources/task.yml +3 -4
- package/test/trick.config.json +13 -0
- package/.wander/jameschan312.cn@gmail.com/.idea/codeStyles/Project.xml +0 -52
- package/.wander/jameschan312.cn@gmail.com/.idea/codeStyles/codeStyleConfig.xml +0 -5
- package/.wander/jameschan312.cn@gmail.com/.idea/jsLibraryMappings.xml +0 -6
- package/.wander/jameschan312.cn@gmail.com/.idea/misc.xml +0 -6
- package/.wander/jameschan312.cn@gmail.com/.idea/modules.xml +0 -8
- package/.wander/jameschan312.cn@gmail.com/.idea/prettier.xml +0 -6
- package/.wander/jameschan312.cn@gmail.com/.idea/trick.iml +0 -14
- package/.wander/jameschan312.cn@gmail.com/.idea/vcs.xml +0 -6
- package/.wander/jameschan312.cn@gmail.com/.idea/webResources.xml +0 -14
- package/dist/constant.d.ts +0 -2
- package/dist/constant.js +0 -3
- package/dist/secret.d.ts +0 -5
- package/dist/secret.js +0 -14
- package/src/constant.ts +0 -4
- package/src/secret.ts +0 -14
- package/trick.config.json +0 -20
package/src/app.ts
CHANGED
|
@@ -1,107 +1,82 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
ReadConfigError,
|
|
5
|
-
Target,
|
|
6
|
-
TargetNotFoundError,
|
|
7
|
-
updateConfig,
|
|
8
|
-
WriteConfigError,
|
|
9
|
-
} from './config.js'
|
|
10
|
-
import {
|
|
11
|
-
decryptFiles,
|
|
12
|
-
encryptFiles,
|
|
13
|
-
FailToDecryptFileError,
|
|
14
|
-
FailToEncryptFileError,
|
|
15
|
-
} from './encrypt.js'
|
|
16
|
-
import { getSecret, SecretNotFoundError } from './secret.js'
|
|
17
|
-
import { TRICK_ENCRYPTED_DIR } from './constant.js'
|
|
2
|
+
import { Config, getTargetFromConfig, Target, updateConfig } from './config.js'
|
|
3
|
+
import { decryptFiles, encryptFiles } from './encrypt.js'
|
|
18
4
|
import fsExtra from 'fs-extra'
|
|
19
5
|
import chalk from 'chalk'
|
|
6
|
+
import { getPassphrase } from './passphrase.js'
|
|
7
|
+
import { resolve_error } from './error.js'
|
|
20
8
|
|
|
21
9
|
const program = new Command()
|
|
22
|
-
|
|
23
|
-
program.
|
|
24
|
-
program.description('Save credential files to remote safely.')
|
|
10
|
+
program.version('2.0.1')
|
|
11
|
+
program.description('Save credential files to remote safely and easily.')
|
|
25
12
|
|
|
26
13
|
program
|
|
27
14
|
.command('add')
|
|
28
|
-
.description('Add
|
|
29
|
-
.argument('<
|
|
30
|
-
.argument('[files...]', 'Files
|
|
31
|
-
.action(async (
|
|
15
|
+
.description('Add files to a target.')
|
|
16
|
+
.argument('<target>', 'The name of the target')
|
|
17
|
+
.argument('[files...]', 'Files that are encrypted')
|
|
18
|
+
.action(async (targetName: string, files: string[]): Promise<void> => {
|
|
32
19
|
await updateConfig((config) => {
|
|
33
20
|
try {
|
|
34
|
-
const target = getTargetFromConfig(config,
|
|
21
|
+
const target = getTargetFromConfig(config, targetName)
|
|
35
22
|
target.files.push(...files)
|
|
36
|
-
|
|
37
|
-
return true
|
|
38
23
|
} catch (err) {
|
|
39
|
-
config.
|
|
40
|
-
config.targets
|
|
41
|
-
|
|
42
|
-
return true
|
|
24
|
+
config.default_target_name = targetName
|
|
25
|
+
config.targets[targetName] = { files }
|
|
43
26
|
}
|
|
27
|
+
|
|
28
|
+
return true
|
|
44
29
|
})
|
|
45
30
|
})
|
|
46
31
|
|
|
47
32
|
program
|
|
48
33
|
.command('remove')
|
|
49
|
-
.description('Remove files from a
|
|
50
|
-
.argument('<
|
|
51
|
-
.argument('[files...]', 'Files to remove
|
|
34
|
+
.description('Remove files from a target.')
|
|
35
|
+
.argument('<target>', 'The name of the target')
|
|
36
|
+
.argument('[files...]', 'Files to remove')
|
|
52
37
|
.option('-t, --target', 'Remove the target instead.')
|
|
53
38
|
.action(
|
|
54
39
|
async (
|
|
55
|
-
|
|
40
|
+
targetName: string,
|
|
56
41
|
files: string[],
|
|
57
42
|
options: {
|
|
58
43
|
target: boolean
|
|
59
44
|
}
|
|
60
45
|
): Promise<void> => {
|
|
61
46
|
if (options.target) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (index == -1) {
|
|
68
|
-
console.log(
|
|
69
|
-
chalk.yellow(
|
|
70
|
-
`[WARNING] Target not found: ${secretName}`
|
|
71
|
-
)
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
return false
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
config.targets.splice(index, 1)
|
|
78
|
-
console.log(`[SUCCESS] Removed target: ${secretName}`)
|
|
47
|
+
// Remove the target
|
|
48
|
+
return await updateConfig((config) => {
|
|
49
|
+
getTargetFromConfig(config, targetName)
|
|
50
|
+
delete config.targets[targetName]
|
|
51
|
+
console.log(`[SUCCESS] Removed target: ${targetName}`)
|
|
79
52
|
|
|
80
53
|
return true
|
|
81
54
|
})
|
|
82
|
-
return
|
|
83
55
|
}
|
|
84
56
|
|
|
57
|
+
// Remove files from the target
|
|
85
58
|
await updateConfig((config) => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
console.log(
|
|
98
|
-
chalk.yellow(
|
|
99
|
-
`[WARNING] File does not exist in the target: ${file}`
|
|
100
|
-
)
|
|
101
|
-
)
|
|
59
|
+
const target = getTargetFromConfig(config, targetName)
|
|
60
|
+
const removedFiles: string[] = []
|
|
61
|
+
const remainingFiles: string[] = []
|
|
62
|
+
for (const file of target.files) {
|
|
63
|
+
if (files.includes(file)) {
|
|
64
|
+
removedFiles.push(file)
|
|
65
|
+
console.log(`[SUCCESS] Removed file: ${file}`)
|
|
66
|
+
} else {
|
|
67
|
+
remainingFiles.push(file)
|
|
102
68
|
}
|
|
103
|
-
}
|
|
104
|
-
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
target.files = remainingFiles
|
|
72
|
+
const notFoundFiles = files.filter(
|
|
73
|
+
(it) => !removedFiles.includes(it)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
for (const notFoundFile of notFoundFiles) {
|
|
77
|
+
console.log(
|
|
78
|
+
`[WARNING] File not found in the target: ${notFoundFile}`
|
|
79
|
+
)
|
|
105
80
|
}
|
|
106
81
|
|
|
107
82
|
return true
|
|
@@ -109,92 +84,85 @@ program
|
|
|
109
84
|
}
|
|
110
85
|
)
|
|
111
86
|
|
|
112
|
-
function
|
|
113
|
-
|
|
114
|
-
|
|
87
|
+
function getTargetName(
|
|
88
|
+
targetNameOrNull: string | null,
|
|
89
|
+
defaultTargetName: Config['default_target_name']
|
|
115
90
|
): string {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
91
|
+
const targetName: string | null =
|
|
92
|
+
targetNameOrNull === null ? defaultTargetName : targetNameOrNull
|
|
119
93
|
|
|
120
|
-
if (
|
|
94
|
+
if (targetName === null) {
|
|
121
95
|
throw new Error(
|
|
122
|
-
'
|
|
96
|
+
'Target is not specified and the default target name is null!'
|
|
123
97
|
)
|
|
124
98
|
}
|
|
125
99
|
|
|
126
|
-
return
|
|
100
|
+
return targetName
|
|
127
101
|
}
|
|
128
102
|
|
|
129
103
|
program
|
|
130
104
|
.command('encrypt')
|
|
131
105
|
.description('Encrypt the credential files.')
|
|
132
|
-
.argument(
|
|
133
|
-
|
|
134
|
-
'The name of secret in the environment',
|
|
135
|
-
undefined
|
|
136
|
-
)
|
|
137
|
-
.action(async (secretName?: string): Promise<void> => {
|
|
106
|
+
.argument('[target]', 'The name of the target', null)
|
|
107
|
+
.action(async (targetNameOrNull: string | null): Promise<void> => {
|
|
138
108
|
await updateConfig((config) => {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
109
|
+
const targetName: string = getTargetName(
|
|
110
|
+
targetNameOrNull,
|
|
111
|
+
config.default_target_name
|
|
112
|
+
)
|
|
113
|
+
const target: Target = getTargetFromConfig(config, targetName)
|
|
114
|
+
const passphrase: string = getPassphrase(config, targetName)
|
|
142
115
|
const srcFilePaths: string[] = target.files
|
|
143
|
-
fsExtra.ensureDir(
|
|
116
|
+
fsExtra.ensureDir(config.root_directory)
|
|
144
117
|
encryptFiles(
|
|
145
118
|
srcFilePaths,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
config.iteration_count
|
|
119
|
+
config.root_directory,
|
|
120
|
+
passphrase,
|
|
121
|
+
config.encryption.iteration_count
|
|
149
122
|
)
|
|
150
|
-
return false
|
|
151
123
|
})
|
|
152
124
|
})
|
|
153
125
|
|
|
154
126
|
program
|
|
155
127
|
.command('decrypt')
|
|
156
128
|
.description('Decrypt the credential files.')
|
|
157
|
-
.argument(
|
|
158
|
-
|
|
159
|
-
'The name of secret in the environment',
|
|
160
|
-
undefined
|
|
161
|
-
)
|
|
162
|
-
.action(async (secretName?: string): Promise<void> => {
|
|
129
|
+
.argument('[target]', 'The name of the target', null)
|
|
130
|
+
.action(async (targetNameOrNull: string | null): Promise<void> => {
|
|
163
131
|
await updateConfig((config) => {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
132
|
+
const targetName: string = getTargetName(
|
|
133
|
+
targetNameOrNull,
|
|
134
|
+
config.default_target_name
|
|
135
|
+
)
|
|
136
|
+
const target: Target = getTargetFromConfig(config, targetName)
|
|
137
|
+
const passphrase: string = getPassphrase(config, targetName)
|
|
167
138
|
const srcFilePaths: string[] = target.files
|
|
168
|
-
fsExtra.ensureDir(
|
|
139
|
+
fsExtra.ensureDir(config.root_directory)
|
|
169
140
|
decryptFiles(
|
|
170
141
|
srcFilePaths,
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
config.iteration_count
|
|
142
|
+
config.root_directory,
|
|
143
|
+
passphrase,
|
|
144
|
+
config.encryption.iteration_count
|
|
174
145
|
)
|
|
175
|
-
return false
|
|
176
146
|
})
|
|
177
147
|
})
|
|
178
148
|
|
|
179
149
|
program
|
|
180
150
|
.command('set-default')
|
|
181
|
-
.description('Set the default
|
|
182
|
-
.argument('<
|
|
183
|
-
.action(async (
|
|
151
|
+
.description('Set the default target name.')
|
|
152
|
+
.argument('<target>', 'The name of the target to set')
|
|
153
|
+
.action(async (targetName: string): Promise<void> => {
|
|
184
154
|
await updateConfig((config) => {
|
|
185
|
-
config.
|
|
186
|
-
|
|
155
|
+
config.default_target_name = targetName
|
|
187
156
|
return true
|
|
188
157
|
})
|
|
189
158
|
})
|
|
190
159
|
|
|
191
160
|
program
|
|
192
161
|
.command('get-default')
|
|
193
|
-
.description('Get the default
|
|
162
|
+
.description('Get the default target name.')
|
|
194
163
|
.action(async (): Promise<void> => {
|
|
195
164
|
await updateConfig((config) => {
|
|
196
|
-
console.log(config.
|
|
197
|
-
return false
|
|
165
|
+
console.log(config.default_target_name)
|
|
198
166
|
})
|
|
199
167
|
})
|
|
200
168
|
|
|
@@ -203,14 +171,12 @@ program
|
|
|
203
171
|
.description('Display a list of targets.')
|
|
204
172
|
.action(async (): Promise<void> => {
|
|
205
173
|
await updateConfig((config) => {
|
|
206
|
-
for (const target of config.targets) {
|
|
207
|
-
console.log(chalk.cyan(
|
|
174
|
+
for (const [targetName, target] of Object.entries(config.targets)) {
|
|
175
|
+
console.log(chalk.cyan(targetName))
|
|
208
176
|
for (const file of target.files) {
|
|
209
177
|
console.log(' ' + chalk.yellow(file))
|
|
210
178
|
}
|
|
211
179
|
}
|
|
212
|
-
|
|
213
|
-
return false
|
|
214
180
|
})
|
|
215
181
|
})
|
|
216
182
|
|
|
@@ -220,44 +186,3 @@ process.on('uncaughtException', (err) => {
|
|
|
220
186
|
resolve_error(err)
|
|
221
187
|
process.exit(1)
|
|
222
188
|
})
|
|
223
|
-
|
|
224
|
-
export function resolve_error(err: any): void {
|
|
225
|
-
if (!(err instanceof Error)) {
|
|
226
|
-
console.error(`Unknown error: ${err}`)
|
|
227
|
-
process.exit(2)
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (err instanceof WriteConfigError) {
|
|
231
|
-
console.error(chalk.red('Fail to write Trick config file'))
|
|
232
|
-
} else if (err instanceof ReadConfigError) {
|
|
233
|
-
console.error(chalk.red('Fail to read Trick config file'))
|
|
234
|
-
} else if (err instanceof SecretNotFoundError) {
|
|
235
|
-
console.error(chalk.red(err.message))
|
|
236
|
-
} else if (err instanceof TargetNotFoundError) {
|
|
237
|
-
console.error(chalk.red(err.message))
|
|
238
|
-
} else if (err instanceof FailToEncryptFileError) {
|
|
239
|
-
console.error(chalk.red(err.message))
|
|
240
|
-
if (err.opensslErrMessage) {
|
|
241
|
-
console.error(chalk.red(err.opensslErrMessage))
|
|
242
|
-
} else {
|
|
243
|
-
console.error(
|
|
244
|
-
chalk.yellow(
|
|
245
|
-
'Make sure the file exists and you have enough permission to access it'
|
|
246
|
-
)
|
|
247
|
-
)
|
|
248
|
-
}
|
|
249
|
-
} else if (err instanceof FailToDecryptFileError) {
|
|
250
|
-
console.error(chalk.red(err.message))
|
|
251
|
-
if (err.opensslErrMessage) {
|
|
252
|
-
console.error(chalk.red(err.opensslErrMessage))
|
|
253
|
-
} else {
|
|
254
|
-
console.error(
|
|
255
|
-
chalk.yellow(
|
|
256
|
-
'Make sure the file exists and you have enough permission to access it'
|
|
257
|
-
)
|
|
258
|
-
)
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
process.exit(1)
|
|
263
|
-
}
|
package/src/config.ts
CHANGED
|
@@ -1,27 +1,70 @@
|
|
|
1
1
|
import fsExtra from 'fs-extra'
|
|
2
|
+
import {
|
|
3
|
+
ReadConfigError,
|
|
4
|
+
TargetNotFoundError,
|
|
5
|
+
WriteConfigError,
|
|
6
|
+
} from './error.js'
|
|
2
7
|
|
|
8
|
+
/**
|
|
9
|
+
* The name of the configuration file to look for in the root directory.
|
|
10
|
+
*/
|
|
3
11
|
export const CONFIG_FILE_NAME: string = 'trick.config.json'
|
|
4
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Config type.
|
|
15
|
+
*
|
|
16
|
+
* @property targets Mapping from target names to target objects.
|
|
17
|
+
* @property default_target_name The name of the default target.
|
|
18
|
+
* @property root_directory The root directory.
|
|
19
|
+
* @property passphrase_file_path The path to the passphrase file.
|
|
20
|
+
* @property encryption Encryption configuration.
|
|
21
|
+
*/
|
|
5
22
|
export interface Config {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
23
|
+
targets: { [name: string]: Target }
|
|
24
|
+
default_target_name: string | null
|
|
25
|
+
root_directory: string
|
|
26
|
+
passphrase_file_path: string
|
|
27
|
+
encryption: Encryption
|
|
9
28
|
}
|
|
10
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Target type.
|
|
32
|
+
*
|
|
33
|
+
* @property files A list of files to encrypt/decrypt.
|
|
34
|
+
*/
|
|
11
35
|
export interface Target {
|
|
12
|
-
secret_name: string
|
|
13
36
|
files: string[]
|
|
14
37
|
}
|
|
15
38
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Encryption configuration.
|
|
41
|
+
*
|
|
42
|
+
* @property iteration_count The number of iteration.
|
|
43
|
+
*/
|
|
44
|
+
export interface Encryption {
|
|
45
|
+
iteration_count: number
|
|
19
46
|
}
|
|
20
47
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Default configuration.
|
|
50
|
+
*/
|
|
51
|
+
const DEFAULT_CONFIG: Config = {
|
|
52
|
+
targets: {},
|
|
53
|
+
default_target_name: null,
|
|
54
|
+
root_directory: '.trick',
|
|
55
|
+
passphrase_file_path: '~/.config/trick_passphrase.json',
|
|
56
|
+
encryption: {
|
|
57
|
+
iteration_count: 100_000,
|
|
58
|
+
},
|
|
59
|
+
}
|
|
24
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Writes a configuration object to the configuration file.
|
|
63
|
+
*
|
|
64
|
+
* @param config The configuration to write.
|
|
65
|
+
* @throws {WriteConfigError} If error occurs when writing to the configuration
|
|
66
|
+
* file.
|
|
67
|
+
*/
|
|
25
68
|
export async function writeConfig(config: Config): Promise<void> {
|
|
26
69
|
try {
|
|
27
70
|
await fsExtra.writeFile(
|
|
@@ -29,10 +72,18 @@ export async function writeConfig(config: Config): Promise<void> {
|
|
|
29
72
|
JSON.stringify(config, null, 2)
|
|
30
73
|
)
|
|
31
74
|
} catch (err) {
|
|
32
|
-
throw new WriteConfigError()
|
|
75
|
+
throw new WriteConfigError(err)
|
|
33
76
|
}
|
|
34
77
|
}
|
|
35
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Retrieves the configuration object from the configuration file.
|
|
81
|
+
*
|
|
82
|
+
* @return The configuration object retrieved from the configuration object;
|
|
83
|
+
* null if the configuration file doesn't exist.
|
|
84
|
+
* @throws {ReadConfigError} If error occurs when reading the configuration
|
|
85
|
+
* file.
|
|
86
|
+
*/
|
|
36
87
|
export async function readConfig(): Promise<Config | null> {
|
|
37
88
|
if (!fsExtra.existsSync(CONFIG_FILE_NAME)) {
|
|
38
89
|
return null
|
|
@@ -41,37 +92,50 @@ export async function readConfig(): Promise<Config | null> {
|
|
|
41
92
|
try {
|
|
42
93
|
return (await fsExtra.readJSON(CONFIG_FILE_NAME)) as Config
|
|
43
94
|
} catch (err) {
|
|
44
|
-
throw new ReadConfigError()
|
|
95
|
+
throw new ReadConfigError(err)
|
|
45
96
|
}
|
|
46
97
|
}
|
|
47
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Updates the configuration object.
|
|
101
|
+
*
|
|
102
|
+
* This function first retrieves the configuration object fromthe configuration
|
|
103
|
+
* file. If the configuration file doesn't exist, the default configuration will
|
|
104
|
+
* be used instead.
|
|
105
|
+
*
|
|
106
|
+
* Then it calls the callback function by passing on the configuration object.
|
|
107
|
+
* If the callback function returns `true`, then the object will be written to
|
|
108
|
+
* the configuration file.
|
|
109
|
+
*
|
|
110
|
+
* @param callback The callback function taking the configuraition object
|
|
111
|
+
* retrieved from the configuration file.
|
|
112
|
+
* @see DEFAULT_CONFIG
|
|
113
|
+
*/
|
|
48
114
|
export async function updateConfig(
|
|
49
|
-
callback:
|
|
115
|
+
callback: (Config: Config) => boolean | void
|
|
50
116
|
): Promise<void> {
|
|
51
|
-
const config: Config = (await readConfig()) ||
|
|
117
|
+
const config: Config = (await readConfig()) || DEFAULT_CONFIG
|
|
52
118
|
if (callback(config)) {
|
|
53
119
|
await writeConfig(config)
|
|
54
120
|
}
|
|
55
121
|
}
|
|
56
122
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Gets a target object from a specified configuration object.
|
|
125
|
+
*
|
|
126
|
+
* @param config The configuration object to get the target from.
|
|
127
|
+
* @param targetName The name of the target to get.
|
|
128
|
+
* @return The target object associated with the given name.
|
|
129
|
+
* @throws {TargetNotFoundError} If the target object is not found.
|
|
130
|
+
*/
|
|
65
131
|
export function getTargetFromConfig(
|
|
66
132
|
config: Config,
|
|
67
|
-
|
|
133
|
+
targetName: string
|
|
68
134
|
): Target {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return target
|
|
73
|
-
}
|
|
135
|
+
const target = config.targets[targetName]
|
|
136
|
+
if (!target) {
|
|
137
|
+
throw new TargetNotFoundError(targetName)
|
|
74
138
|
}
|
|
75
139
|
|
|
76
|
-
|
|
140
|
+
return target
|
|
77
141
|
}
|