@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.
Files changed (40) hide show
  1. package/README.md +23 -23
  2. package/dist/app.d.ts +1 -1
  3. package/dist/app.js +65 -111
  4. package/dist/config.d.ts +71 -14
  5. package/dist/config.js +61 -24
  6. package/dist/encrypt.d.ts +68 -14
  7. package/dist/encrypt.js +79 -30
  8. package/dist/error.d.ts +30 -0
  9. package/dist/error.js +69 -0
  10. package/dist/index.d.ts +1 -2
  11. package/dist/index.js +1 -2
  12. package/dist/passphrase.d.ts +13 -0
  13. package/dist/passphrase.js +26 -0
  14. package/eslint.config.js +41 -0
  15. package/package.json +11 -8
  16. package/src/app.ts +85 -160
  17. package/src/config.ts +93 -29
  18. package/src/encrypt.ts +89 -44
  19. package/src/error.ts +82 -0
  20. package/src/index.ts +1 -2
  21. package/src/passphrase.ts +39 -0
  22. package/test/resources/really.json +2 -2
  23. package/test/resources/task.yml +3 -4
  24. package/test/trick.config.json +13 -0
  25. package/.wander/jameschan312.cn@gmail.com/.idea/codeStyles/Project.xml +0 -52
  26. package/.wander/jameschan312.cn@gmail.com/.idea/codeStyles/codeStyleConfig.xml +0 -5
  27. package/.wander/jameschan312.cn@gmail.com/.idea/jsLibraryMappings.xml +0 -6
  28. package/.wander/jameschan312.cn@gmail.com/.idea/misc.xml +0 -6
  29. package/.wander/jameschan312.cn@gmail.com/.idea/modules.xml +0 -8
  30. package/.wander/jameschan312.cn@gmail.com/.idea/prettier.xml +0 -6
  31. package/.wander/jameschan312.cn@gmail.com/.idea/trick.iml +0 -14
  32. package/.wander/jameschan312.cn@gmail.com/.idea/vcs.xml +0 -6
  33. package/.wander/jameschan312.cn@gmail.com/.idea/webResources.xml +0 -14
  34. package/dist/constant.d.ts +0 -2
  35. package/dist/constant.js +0 -3
  36. package/dist/secret.d.ts +0 -5
  37. package/dist/secret.js +0 -14
  38. package/src/constant.ts +0 -4
  39. package/src/secret.ts +0 -14
  40. package/trick.config.json +0 -20
package/src/app.ts CHANGED
@@ -1,107 +1,82 @@
1
1
  import { Command } from 'commander'
2
- import {
3
- getTargetFromConfig,
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.version('1.0.7')
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 a target or add files to an existing target.')
29
- .argument('<secret-name>', 'The name of secret in the environment')
30
- .argument('[files...]', 'Files this target will encrypt')
31
- .action(async (secretName: string, files: string[]): Promise<void> => {
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, secretName)
21
+ const target = getTargetFromConfig(config, targetName)
35
22
  target.files.push(...files)
36
-
37
- return true
38
23
  } catch (err) {
39
- config.default_secret_name = secretName
40
- config.targets.push({ secret_name: secretName, files })
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 specific target.')
50
- .argument('<secret-name>', 'The name of secret in the environment.')
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
- secretName: string,
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
- await updateConfig((config) => {
63
- const index: number = config.targets.findIndex(
64
- (target) => target.secret_name === secretName
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
- try {
87
- const target = getTargetFromConfig(config, secretName)
88
- const target_files = target.files
89
- for (const file of files) {
90
- const index = target_files.indexOf(file)
91
- if (index != -1) {
92
- target_files.splice(index, 1)
93
- console.log(`[SUCCESS] Removed file: ${file}`)
94
- continue
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
- } catch (err: unknown) {
104
- config.default_secret_name = secretName
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 checkSecretName(
113
- secretName?: string,
114
- defaultSecretName?: string
87
+ function getTargetName(
88
+ targetNameOrNull: string | null,
89
+ defaultTargetName: Config['default_target_name']
115
90
  ): string {
116
- if (!secretName) {
117
- secretName = defaultSecretName
118
- }
91
+ const targetName: string | null =
92
+ targetNameOrNull === null ? defaultTargetName : targetNameOrNull
119
93
 
120
- if (!secretName) {
94
+ if (targetName === null) {
121
95
  throw new Error(
122
- 'No secret name given, and the default secret name is not set.'
96
+ 'Target is not specified and the default target name is null!'
123
97
  )
124
98
  }
125
99
 
126
- return secretName
100
+ return targetName
127
101
  }
128
102
 
129
103
  program
130
104
  .command('encrypt')
131
105
  .description('Encrypt the credential files.')
132
- .argument(
133
- '[secret-name]',
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
- secretName = checkSecretName(secretName, config.default_secret_name)
140
- const target: Target = getTargetFromConfig(config, secretName)
141
- const secret: string = getSecret(target.secret_name)
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(TRICK_ENCRYPTED_DIR)
116
+ fsExtra.ensureDir(config.root_directory)
144
117
  encryptFiles(
145
118
  srcFilePaths,
146
- TRICK_ENCRYPTED_DIR,
147
- secret,
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
- '[secret-name]',
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
- secretName = checkSecretName(secretName, config.default_secret_name)
165
- const target: Target = getTargetFromConfig(config, secretName)
166
- const secret: string = getSecret(target.secret_name)
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(TRICK_ENCRYPTED_DIR)
139
+ fsExtra.ensureDir(config.root_directory)
169
140
  decryptFiles(
170
141
  srcFilePaths,
171
- TRICK_ENCRYPTED_DIR,
172
- secret,
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 secret name.')
182
- .argument('<secret-name>', 'The name of secret in the environment')
183
- .action(async (secretName: string): Promise<void> => {
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.default_secret_name = secretName
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 secret name.')
162
+ .description('Get the default target name.')
194
163
  .action(async (): Promise<void> => {
195
164
  await updateConfig((config) => {
196
- console.log(config.default_secret_name)
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(target.secret_name))
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
- iteration_count: number
7
- default_secret_name?: string
8
- targets: Target[]
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
- const defaultConfig: Config = {
17
- iteration_count: 114514,
18
- targets: [],
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
- export class WriteConfigError extends Error {}
22
-
23
- export class ReadConfigError extends Error {}
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: UpdateConfigCallback
115
+ callback: (Config: Config) => boolean | void
50
116
  ): Promise<void> {
51
- const config: Config = (await readConfig()) || defaultConfig
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
- export type UpdateConfigCallback = (config: Config) => boolean
58
-
59
- export class TargetNotFoundError extends Error {
60
- public constructor(public readonly secretName: string) {
61
- super(`Target not found: ${secretName}`)
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
- secretName: string
133
+ targetName: string
68
134
  ): Target {
69
- const targets = config.targets
70
- for (const target of targets) {
71
- if (target.secret_name === secretName) {
72
- return target
73
- }
135
+ const target = config.targets[targetName]
136
+ if (!target) {
137
+ throw new TargetNotFoundError(targetName)
74
138
  }
75
139
 
76
- throw new TargetNotFoundError(secretName)
140
+ return target
77
141
  }