@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.
Files changed (41) hide show
  1. package/README.md +86 -0
  2. package/dist/app.d.ts +1 -1
  3. package/dist/app.js +79 -89
  4. package/dist/config.d.ts +71 -14
  5. package/dist/config.js +62 -25
  6. package/dist/encrypt.d.ts +68 -14
  7. package/dist/encrypt.js +77 -28
  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 +23 -12
  16. package/src/app.ts +110 -125
  17. package/src/config.ts +96 -31
  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/.prettierrc.yaml +0 -6
  26. package/.wander/jameschan312.cn@gmail.com/.idea/codeStyles/Project.xml +0 -52
  27. package/.wander/jameschan312.cn@gmail.com/.idea/codeStyles/codeStyleConfig.xml +0 -5
  28. package/.wander/jameschan312.cn@gmail.com/.idea/jsLibraryMappings.xml +0 -6
  29. package/.wander/jameschan312.cn@gmail.com/.idea/misc.xml +0 -6
  30. package/.wander/jameschan312.cn@gmail.com/.idea/modules.xml +0 -8
  31. package/.wander/jameschan312.cn@gmail.com/.idea/prettier.xml +0 -6
  32. package/.wander/jameschan312.cn@gmail.com/.idea/trick.iml +0 -14
  33. package/.wander/jameschan312.cn@gmail.com/.idea/vcs.xml +0 -6
  34. package/.wander/jameschan312.cn@gmail.com/.idea/webResources.xml +0 -14
  35. package/dist/constant.d.ts +0 -2
  36. package/dist/constant.js +0 -3
  37. package/dist/secret.d.ts +0 -5
  38. package/dist/secret.js +0 -14
  39. package/src/constant.ts +0 -4
  40. package/src/secret.ts +0 -14
  41. package/trick.config.json +0 -20
package/src/app.ts CHANGED
@@ -1,140 +1,168 @@
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('Trick v1.0.6')
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('Adds a 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('<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, secretName)
21
+ const target = getTargetFromConfig(config, targetName)
22
+ target.files.push(...files)
35
23
  } catch (err) {
36
- config.default_secret_name = secretName
37
- config.targets.push({
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
- console.error(
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
- function checkSecretName(
53
- secretName?: string,
54
- defaultSecretName?: string
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
- if (!secretName) {
57
- secretName = defaultSecretName
58
- }
91
+ const targetName: string | null =
92
+ targetNameOrNull === null ? defaultTargetName : targetNameOrNull
59
93
 
60
- if (!secretName) {
94
+ if (targetName === null) {
61
95
  throw new Error(
62
- 'No secret name given, and the default secret name is not set.'
96
+ 'Target is not specified and the default target name is null!'
63
97
  )
64
98
  }
65
99
 
66
- return secretName
100
+ return targetName
67
101
  }
68
102
 
69
103
  program
70
104
  .command('encrypt')
71
105
  .description('Encrypt the credential files.')
72
- .argument(
73
- '[secret-name]',
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
- secretName = checkSecretName(secretName, config.default_secret_name)
80
- const target: Target = getTargetFromConfig(config, secretName)
81
- 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)
82
115
  const srcFilePaths: string[] = target.files
83
- fsExtra.ensureDir(TRICK_ENCRYPTED_DIR)
116
+ fsExtra.ensureDir(config.root_directory)
84
117
  encryptFiles(
85
118
  srcFilePaths,
86
- TRICK_ENCRYPTED_DIR,
87
- secret,
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
- '[secret-name]',
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
- secretName = checkSecretName(secretName, config.default_secret_name)
105
- const target: Target = getTargetFromConfig(config, secretName)
106
- 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)
107
138
  const srcFilePaths: string[] = target.files
108
- fsExtra.ensureDir(TRICK_ENCRYPTED_DIR)
139
+ fsExtra.ensureDir(config.root_directory)
109
140
  decryptFiles(
110
141
  srcFilePaths,
111
- TRICK_ENCRYPTED_DIR,
112
- secret,
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 secret name.')
122
- .argument('<secret-name>', 'The name of secret in the environment')
123
- .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> => {
124
154
  await updateConfig((config) => {
125
- config.default_secret_name = secretName
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 secret name.')
162
+ .description('Get the default target name.')
134
163
  .action(async (): Promise<void> => {
135
164
  await updateConfig((config) => {
136
- console.log(config.default_secret_name)
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(target.secret_name))
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
- 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: [],
19
- }
20
-
21
- export class WriteConfigError extends Error {
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
- 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
+ },
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.writeJson(CONFIG_FILE_NAME, config)
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: UpdateConfigCallback
115
+ callback: (Config: Config) => boolean | void
49
116
  ): Promise<void> {
50
- const config: Config = (await readConfig()) || defaultConfig
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
- export type UpdateConfigCallback = (config: Config) => boolean
57
-
58
- export class TargetNotFoundError extends Error {
59
- public constructor(public readonly secretName: string) {
60
- super(`Target not found: ${secretName}`)
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
- secretName: string
133
+ targetName: string
67
134
  ): Target {
68
- const targets = config.targets
69
- for (const target of targets) {
70
- if (target.secret_name === secretName) {
71
- return target
72
- }
135
+ const target = config.targets[targetName]
136
+ if (!target) {
137
+ throw new TargetNotFoundError(targetName)
73
138
  }
74
139
 
75
- throw new TargetNotFoundError(secretName)
140
+ return target
76
141
  }