@typinghare/trick 2.0.0 → 2.1.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/src/config.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import fsExtra from 'fs-extra'
2
2
  import {
3
- ReadConfigError,
4
- TargetNotFoundError,
5
- WriteConfigError,
3
+ ReadConfigError,
4
+ TargetNotFoundError,
5
+ WriteConfigError,
6
+ RootDirectoryNotFoundError,
6
7
  } from './error.js'
8
+ import { join, resolve } from 'path'
7
9
 
8
10
  /**
9
11
  * The name of the configuration file to look for in the root directory.
@@ -11,20 +13,26 @@ import {
11
13
  export const CONFIG_FILE_NAME: string = 'trick.config.json'
12
14
 
13
15
  /**
14
- * Config type.
16
+ * A list of root markers.
17
+ */
18
+ export const ROOT_MARKERS: string[] = ['.git', CONFIG_FILE_NAME]
19
+
20
+ /**
21
+ * Represents Trick configuration type.
15
22
  *
16
23
  * @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.
24
+ * @property trickRootDirectory The name of the Trick root directory under the project root.
25
+ * @property passphraseDirectory The path to the passphrase directory.
26
+ * @property defaultTargetNames A list of default target names. If no target name is specified
27
+ * when running Trick commands, these target names will be used.
20
28
  * @property encryption Encryption configuration.
21
29
  */
22
30
  export interface Config {
23
- targets: { [name: string]: Target }
24
- default_target_name: string | null
25
- root_directory: string
26
- passphrase_file_path: string
27
- encryption: Encryption
31
+ targets: { [name: string]: Target }
32
+ trickRootDirectory: string
33
+ passphraseDirectory: string
34
+ defaultTargetNames: string[]
35
+ encryption: Encryption
28
36
  }
29
37
 
30
38
  /**
@@ -33,91 +41,129 @@ export interface Config {
33
41
  * @property files A list of files to encrypt/decrypt.
34
42
  */
35
43
  export interface Target {
36
- files: string[]
44
+ files: string[]
37
45
  }
38
46
 
39
47
  /**
40
48
  * Encryption configuration.
41
49
  *
42
- * @property iteration_count The number of iteration.
50
+ * @property iterationCount The number of iteration.
43
51
  */
44
52
  export interface Encryption {
45
- iteration_count: number
53
+ iterationCount: number
46
54
  }
47
55
 
48
56
  /**
49
57
  * Default configuration.
50
58
  */
51
59
  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
- },
60
+ targets: {},
61
+ trickRootDirectory: '.trick',
62
+ passphraseDirectory: '~/.config/trick/passphrases',
63
+ defaultTargetNames: [],
64
+ encryption: {
65
+ iterationCount: 100_000,
66
+ },
67
+ }
68
+
69
+ /**
70
+ * Recursively searches for the root directory of a project based on specified root markers.
71
+ *
72
+ * @param directory - The current directory to check. Defaults to the current working directory.
73
+ * @returns The path to the root directory.
74
+ * @throws {RootDirectoryNotFoundError} If the root directory cannot be found.
75
+ */
76
+ export function getRootDirectory(directory: string | null = null): string {
77
+ if (!directory) {
78
+ return getRootDirectory(process.cwd())
79
+ }
80
+
81
+ if (directory === '/') {
82
+ throw new RootDirectoryNotFoundError()
83
+ }
84
+
85
+ for (const marker of ROOT_MARKERS) {
86
+ if (fsExtra.existsSync(join(directory, marker))) {
87
+ return directory
88
+ }
89
+ }
90
+
91
+ return getRootDirectory(resolve(join(directory, '..')))
92
+ }
93
+
94
+ /**
95
+ * Gets the full path to the configuration file based on the root directory.
96
+ *
97
+ * @return The full path to the configuration file.
98
+ * @throws {RootDirectoryNotFoundError} If the root directory cannot be found.
99
+ */
100
+ export function getConfigFilePath(): string {
101
+ return resolve(getRootDirectory() + '/' + CONFIG_FILE_NAME)
59
102
  }
60
103
 
61
104
  /**
62
105
  * Writes a configuration object to the configuration file.
63
106
  *
64
107
  * @param config The configuration to write.
65
- * @throws {WriteConfigError} If error occurs when writing to the configuration
66
- * file.
108
+ * @param createInRoot Whether to create the configuration file in the root directory if it
109
+ * doesn't exist. Defaults to false.
110
+ * @throws {WriteConfigError} If error occurs when writing to the configuration file.
67
111
  */
68
- export async function writeConfig(config: Config): Promise<void> {
69
- try {
70
- await fsExtra.writeFile(
71
- CONFIG_FILE_NAME,
72
- JSON.stringify(config, null, 2)
73
- )
74
- } catch (err) {
75
- throw new WriteConfigError(err)
112
+ export function writeConfig(config: Config, createInRoot: boolean = true): void {
113
+ try {
114
+ if (createInRoot) {
115
+ const configFilePath = join(getRootDirectory(), CONFIG_FILE_NAME)
116
+ fsExtra.writeFileSync(configFilePath, JSON.stringify(config, null, 2))
117
+ } else {
118
+ // Always create in the current working directory
119
+ const configFilePath = join(process.cwd(), CONFIG_FILE_NAME)
120
+ fsExtra.writeFileSync(configFilePath, JSON.stringify(config, null, 2))
76
121
  }
122
+ } catch (err) {
123
+ throw new WriteConfigError(err)
124
+ }
77
125
  }
78
126
 
79
127
  /**
80
128
  * Retrieves the configuration object from the configuration file.
81
129
  *
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.
130
+ * @return The configuration object retrieved from the configuration object; null if the
131
+ * configuration file doesn't exist.
132
+ * @throws {ReadConfigError} If error occurs when reading the configuration file.
86
133
  */
87
- export async function readConfig(): Promise<Config | null> {
88
- if (!fsExtra.existsSync(CONFIG_FILE_NAME)) {
89
- return null
90
- }
134
+ export function readConfig(): Config | null {
135
+ const configFilePath = getConfigFilePath()
136
+ if (!fsExtra.existsSync(configFilePath)) {
137
+ return null
138
+ }
91
139
 
92
- try {
93
- return (await fsExtra.readJSON(CONFIG_FILE_NAME)) as Config
94
- } catch (err) {
95
- throw new ReadConfigError(err)
96
- }
140
+ try {
141
+ return fsExtra.readJSONSync(configFilePath) as Config
142
+ } catch (err) {
143
+ throw new ReadConfigError(err)
144
+ }
97
145
  }
98
146
 
99
147
  /**
100
148
  * Updates the configuration object.
101
149
  *
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.
150
+ * This function first retrieves the configuration object fromthe configuration file. If the
151
+ * configuration file doesn't exist, the default configuration will be used instead.
152
+ *
153
+ * Then it calls the callback function by passing on the configuration object. If the callback
154
+ * function returns `true`, then the object will be written to the configuration file.
105
155
  *
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.
156
+ * @param callback The callback function taking the configuraition object retrieved from the
157
+ * configuration file.
109
158
  *
110
- * @param callback The callback function taking the configuraition object
111
- * retrieved from the configuration file.
112
159
  * @see DEFAULT_CONFIG
113
160
  */
114
- export async function updateConfig(
115
- callback: (Config: Config) => boolean | void
116
- ): Promise<void> {
117
- const config: Config = (await readConfig()) || DEFAULT_CONFIG
118
- if (callback(config)) {
119
- await writeConfig(config)
120
- }
161
+ export function updateConfig(
162
+ callback: (Config: Config) => boolean | void,
163
+ createInRoot: boolean = true
164
+ ): void {
165
+ const config: Config = readConfig() || DEFAULT_CONFIG
166
+ Boolean(callback(config)) && writeConfig(config, createInRoot)
121
167
  }
122
168
 
123
169
  /**
@@ -128,14 +174,11 @@ export async function updateConfig(
128
174
  * @return The target object associated with the given name.
129
175
  * @throws {TargetNotFoundError} If the target object is not found.
130
176
  */
131
- export function getTargetFromConfig(
132
- config: Config,
133
- targetName: string
134
- ): Target {
135
- const target = config.targets[targetName]
136
- if (!target) {
137
- throw new TargetNotFoundError(targetName)
138
- }
177
+ export function getTargetFromConfig(config: Config, targetName: string): Target {
178
+ const target = config.targets[targetName]
179
+ if (!target) {
180
+ throw new TargetNotFoundError(targetName)
181
+ }
139
182
 
140
- return target
183
+ return target
141
184
  }
package/src/console.ts ADDED
@@ -0,0 +1,11 @@
1
+ export function success(message: string): string {
2
+ return `🟩 ${message}`
3
+ }
4
+
5
+ export function warning(message: string): string {
6
+ return `🟨 ${message}`
7
+ }
8
+
9
+ export function error(message: string): string {
10
+ return `🟥 ${message}`
11
+ }
package/src/encrypt.ts CHANGED
@@ -2,190 +2,167 @@ import { execa } from 'execa'
2
2
  import * as path from 'node:path'
3
3
  import fsExtra from 'fs-extra'
4
4
  import { FailToDecryptFileError, FailToEncryptFileError } from './error.js'
5
+ import { success } from './console.js'
5
6
 
6
7
  /**
7
8
  * Encrypts a file using OpenSSL with AES-256-CBC and PBKDF2 key derivation.
8
9
  *
9
- * This function checks whether the source file exists, constructs an OpenSSL
10
- * command, ensures the destination directory exists, and then executes the
11
- * encryption command.
10
+ * This function checks whether the source file exists, constructs an OpenSSL command, ensures the
11
+ * destination directory exists, and then executes the encryption command.
12
12
  *
13
13
  * @param srcFilePath The path to the source file that needs to be encrypted.
14
14
  * @param destFilePath The path where the encrypted file will be saved.
15
15
  * @param passphrase The passphrase used for encryption.
16
- * @param iteration_count The number of iterations to use for PBKDF2.
16
+ * @param iterationCount The number of iterations to use for PBKDF2.
17
17
  * @returns Resolves when the file is successfully encrypted.
18
- * @throws {FailToEncryptFileError} If the source file does not exist or if
19
- * OpenSSL returns an error during encryption.
20
- * @throws {FailToDecryptFileError} If an unknown error occurs during
21
- * encryption.
18
+ * @throws {FailToEncryptFileError} If the source file does not exist or if OpenSSL returns an error
19
+ * during encryption.
20
+ * @throws {FailToDecryptFileError} If an unknown error occurs during encryption.
22
21
  */
23
22
  export async function encryptFile(
24
- srcFilePath: string,
25
- destFilePath: string,
26
- passphrase: string,
27
- iteration_count: number
23
+ srcFilePath: string,
24
+ destFilePath: string,
25
+ passphrase: string,
26
+ iterationCount: number
28
27
  ): Promise<void> {
29
- if (!(await fsExtra.pathExists(srcFilePath))) {
30
- throw new FailToEncryptFileError(srcFilePath)
31
- }
28
+ if (!(await fsExtra.pathExists(srcFilePath))) {
29
+ throw new FailToEncryptFileError(srcFilePath)
30
+ }
32
31
 
33
- const command: string = [
34
- 'openssl',
35
- 'enc',
36
- '-aes-256-cbc',
37
- '-salt',
38
- '-pbkdf2',
39
- '-iter',
40
- Number(iteration_count),
41
- '-in',
42
- srcFilePath,
43
- '-out',
44
- destFilePath,
45
- '-pass',
46
- `'pass:${passphrase}'`,
47
- ].join(' ')
32
+ const command: string = [
33
+ 'openssl',
34
+ 'enc',
35
+ '-aes-256-cbc',
36
+ '-salt',
37
+ '-pbkdf2',
38
+ '-iter',
39
+ Number(iterationCount),
40
+ '-in',
41
+ srcFilePath,
42
+ '-out',
43
+ destFilePath,
44
+ '-pass',
45
+ `'pass:${passphrase}'`,
46
+ ].join(' ')
48
47
 
49
- await fsExtra.ensureDir(path.dirname(destFilePath))
48
+ await fsExtra.ensureDir(path.dirname(destFilePath))
50
49
 
51
- try {
52
- await execa(`${command}`, { shell: true })
53
- } catch (err) {
54
- if (typeof err == 'object' && err && Object.hasOwn(err, 'stderr')) {
55
- const error = err as { stderr: string }
56
- throw new FailToEncryptFileError(srcFilePath, error.stderr)
57
- } else {
58
- throw new FailToDecryptFileError(
59
- srcFilePath,
60
- 'Unknown error when encrypting the file.'
61
- )
62
- }
50
+ try {
51
+ await execa(`${command}`, { shell: true })
52
+ } catch (err) {
53
+ if (typeof err == 'object' && err && Object.hasOwn(err, 'stderr')) {
54
+ const error = err as { stderr: string }
55
+ throw new FailToEncryptFileError(srcFilePath, error.stderr)
56
+ } else {
57
+ throw new FailToDecryptFileError(srcFilePath, 'Unknown error when encrypting the file.')
63
58
  }
59
+ }
64
60
  }
65
61
 
66
62
  /**
67
63
  * Decrypts a file using OpenSSL with AES-256-CBC and PBKDF2 key derivation.
68
64
  *
69
- * This function checks whether the encrypted file exists, constructs an OpenSSL
70
- * decryption command, ensures the destination directory exists, and then
71
- * executes the decryption command.
65
+ * This function checks whether the encrypted file exists, constructs an OpenSSL decryption command,
66
+ * ensures the destination directory exists, and then executes the decryption command.
72
67
  *
73
68
  * @param srcFilePath The path where the decrypted file will be saved.
74
69
  * @param destFilePath The path to the encrypted source file.
75
70
  * @param passphrase The passphrase used for decryption.
76
- * @param iteration_count The number of iterations used for PBKDF2.
71
+ * @param iterationCount The number of iterations used for PBKDF2.
77
72
  * @returns Resolves when the file is successfully decrypted.
78
- * @throws {FailToDecryptFileError} If the encrypted file does not exist or if
79
- * OpenSSL returns an error during decryption.
80
- * @throws {FailToDecryptFileError} If an unknown error occurs during
81
- * decryption.
73
+ * @throws {FailToDecryptFileError} If the encrypted file does not exist or if OpenSSL returns an
74
+ * error during decryption.
75
+ * @throws {FailToDecryptFileError} If an unknown error occurs during decryption.
82
76
  */
83
77
  export async function decryptFile(
84
- srcFilePath: string,
85
- destFilePath: string,
86
- passphrase: string,
87
- iteration_count: number
78
+ srcFilePath: string,
79
+ destFilePath: string,
80
+ passphrase: string,
81
+ iterationCount: number
88
82
  ): Promise<void> {
89
- if (!(await fsExtra.pathExists(destFilePath))) {
90
- throw new FailToDecryptFileError(destFilePath)
91
- }
83
+ if (!(await fsExtra.pathExists(destFilePath))) {
84
+ throw new FailToDecryptFileError(destFilePath)
85
+ }
92
86
 
93
- const command: string = [
94
- 'openssl',
95
- 'enc',
96
- '-d',
97
- '-aes-256-cbc',
98
- '-salt',
99
- '-pbkdf2',
100
- '-iter',
101
- Number(iteration_count),
102
- '-in',
103
- destFilePath,
104
- '-out',
105
- srcFilePath,
106
- '-pass',
107
- `'pass:${passphrase}'`,
108
- ].join(' ')
87
+ const command: string = [
88
+ 'openssl',
89
+ 'enc',
90
+ '-d',
91
+ '-aes-256-cbc',
92
+ '-salt',
93
+ '-pbkdf2',
94
+ '-iter',
95
+ Number(iterationCount),
96
+ '-in',
97
+ destFilePath,
98
+ '-out',
99
+ srcFilePath,
100
+ '-pass',
101
+ `'pass:${passphrase}'`,
102
+ ].join(' ')
109
103
 
110
- await fsExtra.ensureDir(path.dirname(srcFilePath))
104
+ await fsExtra.ensureDir(path.dirname(srcFilePath))
111
105
 
112
- try {
113
- await execa(`${command}`, { shell: true })
114
- } catch (err) {
115
- if (typeof err == 'object' && err && Object.hasOwn(err, 'stderr')) {
116
- const error = err as { stderr: string }
117
- throw new FailToDecryptFileError(srcFilePath, error.stderr)
118
- } else {
119
- throw new FailToDecryptFileError(
120
- srcFilePath,
121
- 'Unknown error when decrypting the file.'
122
- )
123
- }
106
+ try {
107
+ await execa(`${command}`, { shell: true })
108
+ } catch (err) {
109
+ if (typeof err == 'object' && err && Object.hasOwn(err, 'stderr')) {
110
+ const error = err as { stderr: string }
111
+ throw new FailToDecryptFileError(srcFilePath, error.stderr)
112
+ } else {
113
+ throw new FailToDecryptFileError(srcFilePath, 'Unknown error when decrypting the file.')
124
114
  }
115
+ }
125
116
  }
126
117
 
127
118
  /**
128
- * Encrypts multiple files using OpenSSL with AES-256-CBC and PBKDF2 key
129
- * derivation.
119
+ * Encrypts multiple files using OpenSSL with AES-256-CBC and PBKDF2 key derivation.
130
120
  *
131
- * For each source file path provided, this function constructs the destination
132
- * file path by appending `.enc`, then calls `encryptFile` and logs the
133
- * operation.
121
+ * For each source file path provided, this function constructs the destination file path by
122
+ * appending `.enc`, then calls `encryptFile` and logs the operation.
134
123
  *
135
124
  * @param srcFilePaths An array of file paths to be encrypted.
136
125
  * @param destDir The directory where the encrypted files will be saved.
137
126
  * @param passphrase The passphrase used for encryption.
138
- * @param iteration_count The number of iterations to use for PBKDF2.
127
+ * @param iterationCount The number of iterations to use for PBKDF2.
139
128
  * @returns Resolves when all files are successfully encrypted.
140
129
  * @throws {FailToEncryptFileError} If any file fails to encrypt.
141
130
  */
142
131
  export async function encryptFiles(
143
- srcFilePaths: string[],
144
- destDir: string,
145
- passphrase: string,
146
- iteration_count: number
132
+ srcFilePaths: string[],
133
+ destDir: string,
134
+ passphrase: string,
135
+ iterationCount: number
147
136
  ): Promise<void> {
148
- for (const srcFilePath of srcFilePaths) {
149
- const destFilePath: string = path.join(destDir, srcFilePath + '.enc')
150
- await encryptFile(
151
- srcFilePath,
152
- destFilePath,
153
- passphrase,
154
- iteration_count
155
- )
156
- console.log(`[ENCRYPTED] ${srcFilePath} -> ${destFilePath}`)
157
- }
137
+ for (const srcFilePath of srcFilePaths) {
138
+ const destFilePath: string = path.join(destDir, srcFilePath + '.enc')
139
+ await encryptFile(srcFilePath, destFilePath, passphrase, iterationCount)
140
+ console.log(success(`Encrypted: ${srcFilePath} -> ${destFilePath}`))
141
+ }
158
142
  }
159
143
 
160
144
  /**
161
- * Decrypts multiple files using OpenSSL with AES-256-CBC and PBKDF2 key
162
- * derivation.
145
+ * Decrypts multiple files using OpenSSL with AES-256-CBC and PBKDF2 key derivation.
163
146
  *
164
- * For each source file path provided, this function assumes the corresponding
165
- * encrypted file has the `.enc` extension and calls `decryptFile`, logging the
166
- * operation.
147
+ * For each source file path provided, this function assumes the corresponding encrypted file has
148
+ * the `.enc` extension and calls `decryptFile`, logging the operation.
167
149
  *
168
150
  * @param srcFilePaths An array of original file paths that were encrypted.
169
151
  * @param destDir The directory containing the encrypted files.
170
152
  * @param passphrase The passphrase used for decryption.
171
- * @param iteration_count The number of iterations used for PBKDF2.
153
+ * @param iterationCount The number of iterations used for PBKDF2.
172
154
  * @returns Resolves when all files are successfully decrypted.
173
155
  * @throws {FailToDecryptFileError} If any file fails to decrypt.
174
156
  */
175
157
  export async function decryptFiles(
176
- srcFilePaths: string[],
177
- destDir: string,
178
- passphrase: string,
179
- iteration_count: number
158
+ srcFilePaths: string[],
159
+ destDir: string,
160
+ passphrase: string,
161
+ iterationCount: number
180
162
  ): Promise<void> {
181
- for (const srcFilePath of srcFilePaths) {
182
- const destFilePath: string = path.join(destDir, srcFilePath + '.enc')
183
- await decryptFile(
184
- srcFilePath,
185
- destFilePath,
186
- passphrase,
187
- iteration_count
188
- )
189
- console.log(`[DECRYPTED] ${destFilePath} -> ${srcFilePath}`)
190
- }
163
+ for (const srcFilePath of srcFilePaths) {
164
+ const destFilePath: string = path.join(destDir, srcFilePath + '.enc')
165
+ await decryptFile(srcFilePath, destFilePath, passphrase, iterationCount)
166
+ console.log(success(`Decrypted: ${destFilePath} -> ${srcFilePath}`))
167
+ }
191
168
  }