@typinghare/trick 1.0.6 → 2.0.0
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 +86 -0
- package/dist/app.d.ts +1 -1
- package/dist/app.js +79 -89
- package/dist/config.d.ts +71 -14
- package/dist/config.js +62 -25
- package/dist/encrypt.d.ts +68 -14
- package/dist/encrypt.js +77 -28
- 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 +23 -12
- package/src/app.ts +110 -125
- package/src/config.ts +96 -31
- 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/.prettierrc.yaml +0 -6
- 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,140 +1,168 @@
|
|
|
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.0')
|
|
11
|
+
program.description('Save credential files to remote safely and easily.')
|
|
25
12
|
|
|
26
13
|
program
|
|
27
14
|
.command('add')
|
|
28
|
-
.description('
|
|
29
|
-
.argument('<
|
|
30
|
-
.argument('[files...]', 'Files
|
|
31
|
-
.action(async (
|
|
15
|
+
.description('Add files to a target.')
|
|
16
|
+
.argument('<name>', '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
|
-
getTargetFromConfig(config,
|
|
21
|
+
const target = getTargetFromConfig(config, targetName)
|
|
22
|
+
target.files.push(...files)
|
|
35
23
|
} catch (err) {
|
|
36
|
-
config.
|
|
37
|
-
config.targets
|
|
38
|
-
secret_name: secretName,
|
|
39
|
-
files,
|
|
40
|
-
})
|
|
41
|
-
return true
|
|
24
|
+
config.default_target_name = targetName
|
|
25
|
+
config.targets[targetName] = { files }
|
|
42
26
|
}
|
|
43
27
|
|
|
44
|
-
|
|
45
|
-
`Target with the secret name already exists: ${secretName}`
|
|
46
|
-
)
|
|
47
|
-
console.error('Abort adding target')
|
|
48
|
-
process.exit(1)
|
|
28
|
+
return true
|
|
49
29
|
})
|
|
50
30
|
})
|
|
51
31
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
32
|
+
program
|
|
33
|
+
.command('remove')
|
|
34
|
+
.description('Remove files from a target.')
|
|
35
|
+
.argument('<name>', 'The name of the target')
|
|
36
|
+
.argument('[files...]', 'Files to remove')
|
|
37
|
+
.option('-t, --target', 'Remove the target instead.')
|
|
38
|
+
.action(
|
|
39
|
+
async (
|
|
40
|
+
targetName: string,
|
|
41
|
+
files: string[],
|
|
42
|
+
options: {
|
|
43
|
+
target: boolean
|
|
44
|
+
}
|
|
45
|
+
): Promise<void> => {
|
|
46
|
+
if (options.target) {
|
|
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}`)
|
|
52
|
+
|
|
53
|
+
return true
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Remove files from the target
|
|
58
|
+
await updateConfig((config) => {
|
|
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)
|
|
68
|
+
}
|
|
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
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return true
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
function getTargetName(
|
|
88
|
+
targetNameOrNull: string | null,
|
|
89
|
+
defaultTargetName: Config['default_target_name']
|
|
55
90
|
): string {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
91
|
+
const targetName: string | null =
|
|
92
|
+
targetNameOrNull === null ? defaultTargetName : targetNameOrNull
|
|
59
93
|
|
|
60
|
-
if (
|
|
94
|
+
if (targetName === null) {
|
|
61
95
|
throw new Error(
|
|
62
|
-
'
|
|
96
|
+
'Target is not specified and the default target name is null!'
|
|
63
97
|
)
|
|
64
98
|
}
|
|
65
99
|
|
|
66
|
-
return
|
|
100
|
+
return targetName
|
|
67
101
|
}
|
|
68
102
|
|
|
69
103
|
program
|
|
70
104
|
.command('encrypt')
|
|
71
105
|
.description('Encrypt the credential files.')
|
|
72
|
-
.argument(
|
|
73
|
-
|
|
74
|
-
'The name of secret in the environment',
|
|
75
|
-
undefined
|
|
76
|
-
)
|
|
77
|
-
.action(async (secretName?: string): Promise<void> => {
|
|
106
|
+
.argument('[target]', 'The name of the target', null)
|
|
107
|
+
.action(async (targetNameOrNull: string | null): Promise<void> => {
|
|
78
108
|
await updateConfig((config) => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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)
|
|
82
115
|
const srcFilePaths: string[] = target.files
|
|
83
|
-
fsExtra.ensureDir(
|
|
116
|
+
fsExtra.ensureDir(config.root_directory)
|
|
84
117
|
encryptFiles(
|
|
85
118
|
srcFilePaths,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
config.iteration_count
|
|
119
|
+
config.root_directory,
|
|
120
|
+
passphrase,
|
|
121
|
+
config.encryption.iteration_count
|
|
89
122
|
)
|
|
90
|
-
return false
|
|
91
123
|
})
|
|
92
124
|
})
|
|
93
125
|
|
|
94
126
|
program
|
|
95
127
|
.command('decrypt')
|
|
96
128
|
.description('Decrypt the credential files.')
|
|
97
|
-
.argument(
|
|
98
|
-
|
|
99
|
-
'The name of secret in the environment',
|
|
100
|
-
undefined
|
|
101
|
-
)
|
|
102
|
-
.action(async (secretName?: string): Promise<void> => {
|
|
129
|
+
.argument('[target]', 'The name of the target', null)
|
|
130
|
+
.action(async (targetNameOrNull: string | null): Promise<void> => {
|
|
103
131
|
await updateConfig((config) => {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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)
|
|
107
138
|
const srcFilePaths: string[] = target.files
|
|
108
|
-
fsExtra.ensureDir(
|
|
139
|
+
fsExtra.ensureDir(config.root_directory)
|
|
109
140
|
decryptFiles(
|
|
110
141
|
srcFilePaths,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
config.iteration_count
|
|
142
|
+
config.root_directory,
|
|
143
|
+
passphrase,
|
|
144
|
+
config.encryption.iteration_count
|
|
114
145
|
)
|
|
115
|
-
return false
|
|
116
146
|
})
|
|
117
147
|
})
|
|
118
148
|
|
|
119
149
|
program
|
|
120
150
|
.command('set-default')
|
|
121
|
-
.description('Set the default
|
|
122
|
-
.argument('<
|
|
123
|
-
.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> => {
|
|
124
154
|
await updateConfig((config) => {
|
|
125
|
-
config.
|
|
126
|
-
|
|
155
|
+
config.default_target_name = targetName
|
|
127
156
|
return true
|
|
128
157
|
})
|
|
129
158
|
})
|
|
130
159
|
|
|
131
160
|
program
|
|
132
161
|
.command('get-default')
|
|
133
|
-
.description('Get the default
|
|
162
|
+
.description('Get the default target name.')
|
|
134
163
|
.action(async (): Promise<void> => {
|
|
135
164
|
await updateConfig((config) => {
|
|
136
|
-
console.log(config.
|
|
137
|
-
return false
|
|
165
|
+
console.log(config.default_target_name)
|
|
138
166
|
})
|
|
139
167
|
})
|
|
140
168
|
|
|
@@ -143,14 +171,12 @@ program
|
|
|
143
171
|
.description('Display a list of targets.')
|
|
144
172
|
.action(async (): Promise<void> => {
|
|
145
173
|
await updateConfig((config) => {
|
|
146
|
-
for (const target of config.targets) {
|
|
147
|
-
console.log(chalk.cyan(
|
|
174
|
+
for (const [targetName, target] of Object.entries(config.targets)) {
|
|
175
|
+
console.log(chalk.cyan(targetName))
|
|
148
176
|
for (const file of target.files) {
|
|
149
177
|
console.log(' ' + chalk.yellow(file))
|
|
150
178
|
}
|
|
151
179
|
}
|
|
152
|
-
|
|
153
|
-
return false
|
|
154
180
|
})
|
|
155
181
|
})
|
|
156
182
|
|
|
@@ -160,44 +186,3 @@ process.on('uncaughtException', (err) => {
|
|
|
160
186
|
resolve_error(err)
|
|
161
187
|
process.exit(1)
|
|
162
188
|
})
|
|
163
|
-
|
|
164
|
-
export function resolve_error(err: any): void {
|
|
165
|
-
if (!(err instanceof Error)) {
|
|
166
|
-
console.error(`Unknown error: ${err}`)
|
|
167
|
-
process.exit(2)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (err instanceof WriteConfigError) {
|
|
171
|
-
console.error(chalk.red('Fail to write Trick config file'))
|
|
172
|
-
} else if (err instanceof ReadConfigError) {
|
|
173
|
-
console.error(chalk.red('Fail to read Trick config file'))
|
|
174
|
-
} else if (err instanceof SecretNotFoundError) {
|
|
175
|
-
console.error(chalk.red(err.message))
|
|
176
|
-
} else if (err instanceof TargetNotFoundError) {
|
|
177
|
-
console.error(chalk.red(err.message))
|
|
178
|
-
} else if (err instanceof FailToEncryptFileError) {
|
|
179
|
-
console.error(chalk.red(err.message))
|
|
180
|
-
if (err.opensslErrMessage) {
|
|
181
|
-
console.error(chalk.red(err.opensslErrMessage))
|
|
182
|
-
} else {
|
|
183
|
-
console.error(
|
|
184
|
-
chalk.yellow(
|
|
185
|
-
'Make sure the file exists and you have enough permission to access it'
|
|
186
|
-
)
|
|
187
|
-
)
|
|
188
|
-
}
|
|
189
|
-
} else if (err instanceof FailToDecryptFileError) {
|
|
190
|
-
console.error(chalk.red(err.message))
|
|
191
|
-
if (err.opensslErrMessage) {
|
|
192
|
-
console.error(chalk.red(err.opensslErrMessage))
|
|
193
|
-
} else {
|
|
194
|
-
console.error(
|
|
195
|
-
chalk.yellow(
|
|
196
|
-
'Make sure the file exists and you have enough permission to access it'
|
|
197
|
-
)
|
|
198
|
-
)
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
process.exit(1)
|
|
203
|
-
}
|
package/src/config.ts
CHANGED
|
@@ -1,37 +1,89 @@
|
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
export
|
|
39
|
+
/**
|
|
40
|
+
* Encryption configuration.
|
|
41
|
+
*
|
|
42
|
+
* @property iteration_count The number of iteration.
|
|
43
|
+
*/
|
|
44
|
+
export interface Encryption {
|
|
45
|
+
iteration_count: number
|
|
22
46
|
}
|
|
23
47
|
|
|
24
|
-
|
|
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
|
+
},
|
|
25
59
|
}
|
|
26
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
|
+
*/
|
|
27
68
|
export async function writeConfig(config: Config): Promise<void> {
|
|
28
69
|
try {
|
|
29
|
-
await fsExtra.
|
|
70
|
+
await fsExtra.writeFile(
|
|
71
|
+
CONFIG_FILE_NAME,
|
|
72
|
+
JSON.stringify(config, null, 2)
|
|
73
|
+
)
|
|
30
74
|
} catch (err) {
|
|
31
|
-
throw new WriteConfigError()
|
|
75
|
+
throw new WriteConfigError(err)
|
|
32
76
|
}
|
|
33
77
|
}
|
|
34
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
|
+
*/
|
|
35
87
|
export async function readConfig(): Promise<Config | null> {
|
|
36
88
|
if (!fsExtra.existsSync(CONFIG_FILE_NAME)) {
|
|
37
89
|
return null
|
|
@@ -40,37 +92,50 @@ export async function readConfig(): Promise<Config | null> {
|
|
|
40
92
|
try {
|
|
41
93
|
return (await fsExtra.readJSON(CONFIG_FILE_NAME)) as Config
|
|
42
94
|
} catch (err) {
|
|
43
|
-
throw new ReadConfigError()
|
|
95
|
+
throw new ReadConfigError(err)
|
|
44
96
|
}
|
|
45
97
|
}
|
|
46
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
|
+
*/
|
|
47
114
|
export async function updateConfig(
|
|
48
|
-
callback:
|
|
115
|
+
callback: (Config: Config) => boolean | void
|
|
49
116
|
): Promise<void> {
|
|
50
|
-
const config: Config = (await readConfig()) ||
|
|
117
|
+
const config: Config = (await readConfig()) || DEFAULT_CONFIG
|
|
51
118
|
if (callback(config)) {
|
|
52
119
|
await writeConfig(config)
|
|
53
120
|
}
|
|
54
121
|
}
|
|
55
122
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
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
|
+
*/
|
|
64
131
|
export function getTargetFromConfig(
|
|
65
132
|
config: Config,
|
|
66
|
-
|
|
133
|
+
targetName: string
|
|
67
134
|
): Target {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return target
|
|
72
|
-
}
|
|
135
|
+
const target = config.targets[targetName]
|
|
136
|
+
if (!target) {
|
|
137
|
+
throw new TargetNotFoundError(targetName)
|
|
73
138
|
}
|
|
74
139
|
|
|
75
|
-
|
|
140
|
+
return target
|
|
76
141
|
}
|