@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/dist/index.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  export * from './app.js';
2
+ export * from './color.js';
2
3
  export * from './config.js';
4
+ export * from './console.js';
3
5
  export * from './encrypt.js';
6
+ export * from './error.js';
4
7
  export * from './passphrase.js';
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  export * from './app.js';
2
+ export * from './color.js';
2
3
  export * from './config.js';
4
+ export * from './console.js';
3
5
  export * from './encrypt.js';
6
+ export * from './error.js';
4
7
  export * from './passphrase.js';
@@ -1,10 +1,18 @@
1
1
  import { Config } from './config.js';
2
+ /**
3
+ * Gets the passphrase directory from the configuration.
4
+ *
5
+ * This function replaces any tilde (`~`) in the directory path with the user's home directory.
6
+ *
7
+ * @param config The configuration to use.
8
+ * @return The resolved passphrase directory path.
9
+ */
10
+ export declare function getPassphraseDirectory(config: Config): string;
2
11
  /**
3
12
  * Retrieves the passphrase from the passphrase file.
4
13
  *
5
- * First, gets the passphrase object from the passphrase file path specified in
6
- * the configuration object. Second, gets and returns the passphrase associated
7
- * with the given target name.
14
+ * The passphrase file is located in the directory specified by `config.passphraseDirectory`
15
+ * and is named after the `targetName`.
8
16
  *
9
17
  * @param config The configuration to use.
10
18
  * @param targetName The name of the target.
@@ -1,26 +1,32 @@
1
1
  import fsExtra from 'fs-extra';
2
- import { PassphraseFileNotFoundError, PassphraseNotFoundError, } from './error.js';
2
+ import { PassphraseFileNotFoundError } from './error.js';
3
3
  import os from 'node:os';
4
+ import path from 'node:path';
5
+ /**
6
+ * Gets the passphrase directory from the configuration.
7
+ *
8
+ * This function replaces any tilde (`~`) in the directory path with the user's home directory.
9
+ *
10
+ * @param config The configuration to use.
11
+ * @return The resolved passphrase directory path.
12
+ */
13
+ export function getPassphraseDirectory(config) {
14
+ return config.passphraseDirectory.replaceAll(/~/g, os.homedir());
15
+ }
4
16
  /**
5
17
  * Retrieves the passphrase from the passphrase file.
6
18
  *
7
- * First, gets the passphrase object from the passphrase file path specified in
8
- * the configuration object. Second, gets and returns the passphrase associated
9
- * with the given target name.
19
+ * The passphrase file is located in the directory specified by `config.passphraseDirectory`
20
+ * and is named after the `targetName`.
10
21
  *
11
22
  * @param config The configuration to use.
12
23
  * @param targetName The name of the target.
13
24
  * @return The passphrase associated with the target name.
14
25
  */
15
26
  export function getPassphrase(config, targetName) {
16
- const passphraseFilePath = config.passphrase_file_path.replaceAll(/~/g, os.homedir());
27
+ const passphraseFilePath = path.join(getPassphraseDirectory(config), targetName);
17
28
  if (!fsExtra.existsSync(passphraseFilePath)) {
18
29
  throw new PassphraseFileNotFoundError(passphraseFilePath);
19
30
  }
20
- const passphraseObject = fsExtra.readJsonSync(passphraseFilePath);
21
- const passphrase = passphraseObject[targetName] || null;
22
- if (passphrase === null) {
23
- throw new PassphraseNotFoundError(passphraseFilePath, targetName);
24
- }
25
- return passphrase;
31
+ return fsExtra.readFileSync(passphraseFilePath, 'utf-8').trim();
26
32
  }
package/env.sh ADDED
@@ -0,0 +1 @@
1
+ PATH="$PWD/bin:$PATH"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@typinghare/trick",
3
3
  "description": "Save credential files to remote safely and easily.",
4
- "version": "2.0.0",
4
+ "version": "2.1.0",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "type": "module",
@@ -15,26 +15,26 @@
15
15
  "homepage": "https://github.com/typinghare/trick",
16
16
  "license": "MIT",
17
17
  "dependencies": {
18
- "chalk": "^5.4.1",
19
- "commander": "^14.0.0",
20
- "execa": "^9.5.3",
21
- "fs-extra": "^11.3.0"
18
+ "chalk": "^5.6.2",
19
+ "commander": "^14.0.2",
20
+ "execa": "^9.6.1",
21
+ "fs-extra": "^11.3.3"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/fs-extra": "^11.0.4",
25
- "eslint": "^9.27.0",
26
- "eslint-plugin-jsonc": "^2.20.1",
27
- "jsonc-eslint-parser": "^2.4.0",
28
- "prettier": "^3.5.3",
29
- "typescript": "^5.8.3"
25
+ "eslint": "^9.39.2",
26
+ "eslint-plugin-jsonc": "^2.21.0",
27
+ "jsonc-eslint-parser": "^2.4.2",
28
+ "prettier": "^3.7.4",
29
+ "typescript": "^5.9.3"
30
30
  },
31
31
  "prettier": {
32
32
  "trailingComma": "es5",
33
- "tabWidth": 4,
33
+ "tabWidth": 2,
34
34
  "semi": false,
35
35
  "singleQuote": true,
36
36
  "endOfLine": "lf",
37
- "printWidth": 80
37
+ "printWidth": 100
38
38
  },
39
39
  "scripts": {
40
40
  "build": "node_modules/typescript/bin/tsc"
package/src/app.ts CHANGED
@@ -1,188 +1,265 @@
1
1
  import { Command } from 'commander'
2
- import { Config, getTargetFromConfig, Target, updateConfig } from './config.js'
2
+ import {
3
+ CONFIG_FILE_NAME,
4
+ getRootDirectory,
5
+ getTargetFromConfig,
6
+ Target,
7
+ updateConfig,
8
+ } from './config.js'
3
9
  import { decryptFiles, encryptFiles } from './encrypt.js'
4
10
  import fsExtra from 'fs-extra'
5
- import chalk from 'chalk'
6
- import { getPassphrase } from './passphrase.js'
7
- import { resolve_error } from './error.js'
11
+ import { getPassphrase, getPassphraseDirectory } from './passphrase.js'
12
+ import { resolveError } from './error.js'
13
+ import path from 'path'
14
+ import { colorFilePath, colorTargetName } from './color.js'
15
+ import { success, warning } from './console.js'
8
16
 
9
17
  const program = new Command()
10
- program.version('2.0.0')
18
+ program.version('2.1.0')
11
19
  program.description('Save credential files to remote safely and easily.')
12
20
 
13
21
  program
14
- .command('add')
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> => {
19
- await updateConfig((config) => {
20
- try {
21
- const target = getTargetFromConfig(config, targetName)
22
- target.files.push(...files)
23
- } catch (err) {
24
- config.default_target_name = targetName
25
- config.targets[targetName] = { files }
26
- }
27
-
28
- return true
29
- })
22
+ .command('config')
23
+ .description('Display the current configuration.')
24
+ .action(function (): void {
25
+ updateConfig((config) => {
26
+ console.log(JSON.stringify(config, null, 2))
30
27
  })
28
+ })
31
29
 
32
30
  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
- })
31
+ .command('init')
32
+ .description('Initialize the configuration file.')
33
+ .option('-r, --root', 'Create the configuration file in the root directory.', false)
34
+ .action(function (options: { root: boolean }): void {
35
+ const configFilePath = options.root
36
+ ? path.join(getRootDirectory(), CONFIG_FILE_NAME)
37
+ : path.join(process.cwd(), CONFIG_FILE_NAME)
38
+
39
+ if (fsExtra.existsSync(configFilePath)) {
40
+ console.log(warning(`Configuration file already exists: ${configFilePath}`))
41
+ return
42
+ } else {
43
+ updateConfig(() => true, options.root)
44
+ console.log(success(`Initialized configuration file: ${configFilePath}`))
45
+ }
46
+ })
47
+
48
+ program
49
+ .command('add')
50
+ .description('Add files to a target.')
51
+ .argument('<target>', 'The name of the target to add to')
52
+ .argument('[files...]', 'Files that are added to the target')
53
+ .action(function (targetName: string, files: string[]): void {
54
+ updateConfig((config) => {
55
+ const target = config.targets[targetName]
56
+ if (!target) {
57
+ config.targets[targetName] = { files }
58
+ console.log(success(`Added files to target: ${targetName}`))
59
+ } else {
60
+ for (const file of files) {
61
+ if (target.files.includes(file)) {
62
+ console.log(warning(`File already exists in the target: ${file}`))
63
+ } else {
64
+ target.files.push(file)
65
+ console.log(success(`Added file to target: ${file}`))
66
+ }
84
67
  }
85
- )
86
-
87
- function getTargetName(
88
- targetNameOrNull: string | null,
89
- defaultTargetName: Config['default_target_name']
90
- ): string {
91
- const targetName: string | null =
92
- targetNameOrNull === null ? defaultTargetName : targetNameOrNull
93
-
94
- if (targetName === null) {
95
- throw new Error(
96
- 'Target is not specified and the default target name is null!'
97
- )
68
+ }
69
+
70
+ return true
71
+ })
72
+ })
73
+
74
+ program
75
+ .command('remove')
76
+ .description('Remove files from a target.')
77
+ .argument('<target>', 'The name of the target to remove from')
78
+ .argument('[files...]', 'Files to remove from the target')
79
+ .option('-t, --target', 'Remove the target instead.')
80
+ .action(function (
81
+ targetName: string,
82
+ files: string[],
83
+ options: {
84
+ target: boolean
98
85
  }
86
+ ): void {
87
+ if (options.target) {
88
+ // Remove the target
89
+ return updateConfig((config) => {
90
+ getTargetFromConfig(config, targetName)
91
+ delete config.targets[targetName]
92
+ console.log(`[SUCCESS] Removed target: ${targetName}`)
99
93
 
100
- return targetName
101
- }
94
+ return true
95
+ })
96
+ }
97
+
98
+ // Remove files from the target
99
+ updateConfig((config) => {
100
+ const target = getTargetFromConfig(config, targetName)
101
+ const removedFiles: string[] = []
102
+ const remainingFiles: string[] = []
103
+ for (const file of target.files) {
104
+ if (files.includes(file)) {
105
+ removedFiles.push(file)
106
+ console.log(success(`Removed file: ${file}`))
107
+ } else {
108
+ remainingFiles.push(file)
109
+ }
110
+ }
111
+
112
+ target.files = remainingFiles
113
+ const notFoundFiles = files.filter((it) => !removedFiles.includes(it))
114
+
115
+ for (const notFoundFile of notFoundFiles) {
116
+ console.log(warning(`File not found in the target: ${notFoundFile}`))
117
+ }
118
+
119
+ return true
120
+ })
121
+ })
102
122
 
103
123
  program
104
- .command('encrypt')
105
- .description('Encrypt the credential files.')
106
- .argument('[target]', 'The name of the target', null)
107
- .action(async (targetNameOrNull: string | null): Promise<void> => {
108
- await updateConfig((config) => {
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)
115
- const srcFilePaths: string[] = target.files
116
- fsExtra.ensureDir(config.root_directory)
117
- encryptFiles(
118
- srcFilePaths,
119
- config.root_directory,
120
- passphrase,
121
- config.encryption.iteration_count
122
- )
123
- })
124
+ .command('encrypt')
125
+ .description('Encrypt the credential files.')
126
+ .argument('[targetNames...]', 'The names of targets')
127
+ .action(function (targetNames: string[]): void {
128
+ updateConfig((config) => {
129
+ if (targetNames.length === 0) {
130
+ targetNames.push(...config.defaultTargetNames)
131
+ }
132
+
133
+ if (targetNames.length === 0) {
134
+ console.log(warning('No target names specified and no default targets set.'))
135
+ return
136
+ }
137
+
138
+ const rootDirectory = getRootDirectory()
139
+ const trickRootDirectory = path.resolve(rootDirectory, config.trickRootDirectory)
140
+ for (const targetName of targetNames) {
141
+ const target: Target = getTargetFromConfig(config, targetName)
142
+ const passphrase: string = getPassphrase(config, targetName)
143
+ const srcFilePaths: string[] = target.files
144
+
145
+ fsExtra.ensureDir(trickRootDirectory)
146
+ encryptFiles(srcFilePaths, trickRootDirectory, passphrase, config.encryption.iterationCount)
147
+ }
148
+ })
149
+ })
150
+
151
+ program
152
+ .command('decrypt')
153
+ .description('Decrypt the credential files.')
154
+ .argument('[targetNames...]', 'The names of the targets')
155
+ .action(function (targetNames: string[]): void {
156
+ updateConfig((config) => {
157
+ if (targetNames.length === 0) {
158
+ targetNames.push(...config.defaultTargetNames)
159
+ }
160
+
161
+ if (targetNames.length === 0) {
162
+ console.log(warning('No target names specified and no default targets set.'))
163
+ return
164
+ }
165
+
166
+ const rootDirectory = getRootDirectory()
167
+ const trickRootDirectory = path.resolve(rootDirectory, config.trickRootDirectory)
168
+ for (const targetName of targetNames) {
169
+ const target: Target = getTargetFromConfig(config, targetName)
170
+ const passphrase: string = getPassphrase(config, targetName)
171
+ const srcFilePaths: string[] = target.files
172
+
173
+ fsExtra.ensureDir(trickRootDirectory)
174
+ decryptFiles(srcFilePaths, trickRootDirectory, passphrase, config.encryption.iterationCount)
175
+ }
124
176
  })
177
+ })
125
178
 
126
179
  program
127
- .command('decrypt')
128
- .description('Decrypt the credential files.')
129
- .argument('[target]', 'The name of the target', null)
130
- .action(async (targetNameOrNull: string | null): Promise<void> => {
131
- await updateConfig((config) => {
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)
138
- const srcFilePaths: string[] = target.files
139
- fsExtra.ensureDir(config.root_directory)
140
- decryptFiles(
141
- srcFilePaths,
142
- config.root_directory,
143
- passphrase,
144
- config.encryption.iteration_count
145
- )
146
- })
180
+ .command('add-default')
181
+ .description('Add default target names.')
182
+ .argument('[targetNames...]', 'The names of targets to add')
183
+ .action(function (targetNames: string[]): void {
184
+ updateConfig((config) => {
185
+ let addedAny = false
186
+ for (const targetName of targetNames) {
187
+ if (!config.targets[targetName]) {
188
+ console.log(warning(`Target not found: ${targetName}`))
189
+ continue
190
+ }
191
+
192
+ if (config.defaultTargetNames.includes(targetName)) {
193
+ console.log(warning(`Target name already in default list: ${targetName}`))
194
+ continue
195
+ }
196
+
197
+ config.defaultTargetNames.push(targetName)
198
+ console.log(success(`Added default target name: ${targetName}`))
199
+ addedAny = true
200
+ }
201
+
202
+ return addedAny
147
203
  })
204
+ })
148
205
 
149
206
  program
150
- .command('set-default')
151
- .description('Set the default target name.')
152
- .argument('<target>', 'The name of the target to set')
153
- .action(async (targetName: string): Promise<void> => {
154
- await updateConfig((config) => {
155
- config.default_target_name = targetName
156
- return true
157
- })
207
+ .command('list-defaults')
208
+ .description('Display the default target name.')
209
+ .action(function (): void {
210
+ updateConfig((config) => {
211
+ for (const targetName of config.defaultTargetNames) {
212
+ console.log(colorTargetName(targetName))
213
+ }
158
214
  })
215
+ })
159
216
 
160
217
  program
161
- .command('get-default')
162
- .description('Get the default target name.')
163
- .action(async (): Promise<void> => {
164
- await updateConfig((config) => {
165
- console.log(config.default_target_name)
166
- })
218
+ .command('list')
219
+ .description('Display a list of targets.')
220
+ .action(function (): void {
221
+ updateConfig((config) => {
222
+ for (const [targetName, target] of Object.entries(config.targets)) {
223
+ console.log(colorTargetName(targetName))
224
+ for (const file of target.files) {
225
+ console.log(' ' + colorFilePath(file))
226
+ }
227
+ }
167
228
  })
229
+ })
168
230
 
169
231
  program
170
- .command('list')
171
- .description('Display a list of targets.')
172
- .action(async (): Promise<void> => {
173
- await updateConfig((config) => {
174
- for (const [targetName, target] of Object.entries(config.targets)) {
175
- console.log(chalk.cyan(targetName))
176
- for (const file of target.files) {
177
- console.log(' ' + chalk.yellow(file))
178
- }
179
- }
180
- })
232
+ .command('set-passphrase')
233
+ .description('Set passphrase for a target.')
234
+ .argument('<target>', 'The name of the target to set passphrase for')
235
+ .action(function (targetName: string): void {
236
+ updateConfig((config) => {
237
+ const passphraseDirectory = getPassphraseDirectory(config)
238
+ if (!fsExtra.existsSync(passphraseDirectory)) {
239
+ fsExtra.ensureDirSync(passphraseDirectory)
240
+ console.log(success(`Created passphrase directory: ${passphraseDirectory}`))
241
+ }
242
+
243
+ const passphraseFile = path.join(passphraseDirectory, targetName)
244
+ if (!fsExtra.existsSync(passphraseFile)) {
245
+ fsExtra.createFileSync(passphraseFile)
246
+ fsExtra.chmodSync(passphraseFile, 0o600)
247
+ console.log(success(`Created passphrase file: ${passphraseFile}`))
248
+ console.log(success(`You have to edit the file to set the passphrase.`))
249
+ } else {
250
+ console.log(warning(`Passphrase file already exists: ${passphraseFile}`))
251
+ }
181
252
  })
253
+ })
182
254
 
183
- program.parse()
255
+ try {
256
+ program.parse()
257
+ } catch (err) {
258
+ resolveError(err)
259
+ process.exit(1)
260
+ }
184
261
 
185
262
  process.on('uncaughtException', (err) => {
186
- resolve_error(err)
187
- process.exit(1)
263
+ resolveError(err)
264
+ process.exit(1)
188
265
  })
package/src/color.ts ADDED
@@ -0,0 +1,9 @@
1
+ import chalk from 'chalk'
2
+
3
+ export function colorTargetName(targetName: string): string {
4
+ return chalk.cyan(targetName)
5
+ }
6
+
7
+ export function colorFilePath(filePath: string): string {
8
+ return chalk.yellow(filePath)
9
+ }