@typinghare/trick 2.0.1 β†’ 2.1.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.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 James Chen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md CHANGED
@@ -1,8 +1,21 @@
1
1
  # [Trick](https://github.com/typinghare/trick)
2
2
 
3
- # Install
3
+ **Trick** is a CLI tool that helps you **safely encrypt sensitive files** (such as `.env`, API keys, or credentials) so they can be stored in Git repositories and easily restored on other machines or servers.
4
4
 
5
- ```shell
5
+ It uses **OpenSSL (AES-256-CBC + PBKDF2)** under the hood and keeps encryption keys **outside your repository**.
6
+
7
+ ## Features
8
+
9
+ * πŸ” Encrypt and decrypt sensitive files with strong encryption
10
+ * 🎯 Group files into **targets**
11
+ * πŸ“¦ Store encrypted files under a dedicated `.trick/` directory
12
+ * πŸ—οΈ Keep passphrases outside your repo (per-target, permission-protected)
13
+ * πŸ” Works across machines and servers
14
+ * βš™οΈ Fully configurable, project-aware
15
+
16
+ ## Installation
17
+
18
+ ```bash
6
19
  # npm
7
20
  npm install -g @typinghare/trick
8
21
 
@@ -13,81 +26,202 @@ pnpm add -g @typinghare/trick
13
26
  yarn add -g @typinghare/trick
14
27
  ```
15
28
 
29
+ > **Requirements**
30
+ >
31
+ > * Node.js β‰₯ 18
32
+ > * `openssl` available in your system PATH
33
+
16
34
  ## Philosophy
17
35
 
18
- We often add sensitive and credential files, such as `.env` and `api_key.conf`, to `.gitignore`, preventing them from being committed or even pushed to remote depots for safety reasons. Then, we have to manually copy the file to the server. It would be effortless if we only had one file, but imagine we have a lot in a bigger project. Even worse, some careless people (me) have even lost these sensitive files after changing computers!
36
+ Sensitive files are usually added to `.gitignore` to avoid accidental leaks.
37
+ But that means:
38
+
39
+ * You must manually copy them to every new machine
40
+ * They’re easy to lose
41
+ * They don’t version well
42
+
43
+ **Trick encrypts those files**, allowing you to commit the encrypted versions safely, while keeping passphrases out of Git entirely.
44
+
19
45
 
20
- **Trick** helps you to encrypt sensitive files with a passphrase so that you can upload the credential file to Git platforms. Later on the server, just use the same passphrase to decrypt the files with ease.
21
46
 
22
- ## Quick Example
47
+ ## Getting Started
23
48
 
24
- Set up the **target** with the files needed to be encrypted:
49
+ ### 1. Initialize Trick
50
+
51
+ Run this inside your project:
25
52
 
26
53
  ```bash
27
- # This will create a trick.config.json in the current working directory
28
- # trick add <target> [files...]
29
- $ trick add MyTargetName .env api_key.conf
54
+ trick init
55
+ ```
56
+
57
+ This creates a `trick.config.json` in your project root.
58
+
30
59
 
31
- # Display the list of target names and the files bound
32
- $ trick list
60
+
61
+ ### 2. Add Files to a Target
62
+
63
+ A **target** is a named group of files to encrypt together.
64
+
65
+ ```bash
66
+ trick add MyTarget .env api_key.conf
33
67
  ```
34
68
 
35
- Create a `passphrase.json` file under `~/.config` with the following content:
69
+ List all targets:
36
70
 
37
- ```json
38
- {
39
- "MyTargetName": "Reg5eGPXWdmeW0i08uaygBlfbXP+tJlnu7z551Qt568="
40
- }
71
+ ```bash
72
+ trick list
41
73
  ```
42
74
 
43
- Here, the key is the target name, and the value is the `passphrase` that is used to encrypt/decrypt the files associated with this target name.
44
75
 
45
- Encrypt the files:
76
+
77
+ ### 3. Set a Passphrase for the Target
78
+
79
+ Each target has its **own passphrase file** stored locally (not in Git).
46
80
 
47
81
  ```bash
48
- $ trick encrypt MyTargetName
82
+ trick set-passphrase MyTarget
49
83
  ```
50
84
 
51
- You will see the following output:
85
+ This creates a file at:
52
86
 
53
- ```text
54
- [ENCRYPTED] .env -> .trick/encrypted/.env.enc
55
- [ENCRYPTED] api_key.conf -> .trick/encrypted/api_key.conf.enc
56
87
  ```
88
+ ~/.config/trick/passphrases/MyTarget
89
+ ```
90
+
91
+ * File permissions are set to `600`
92
+ * You must manually edit this file and paste your passphrase
93
+ * The file content is read as plain text (trimmed)
57
94
 
58
- Encrypted files are all saved to `.trick`. On the server, set the the `passphrase.json` in the same way, and execute:
95
+ > ⚠️ **Important**
96
+ > Back up your passphrase files. Losing them means losing access to your encrypted data.
97
+
98
+
99
+
100
+ ### 4. Encrypt Files
59
101
 
60
102
  ```bash
61
- $ trick decrypt MyTargetName
103
+ trick encrypt MyTarget
104
+ ```
105
+
106
+ Encrypted files are written to:
107
+
108
+ ```
109
+ .trick/<original-path>.enc
62
110
  ```
63
111
 
64
- And you will see that the files are restored:
112
+ Example output:
65
113
 
66
- ```text
67
- [DECRYPTED] .trick/encrypted/.env.enc -> .env
68
- [DECRYPTED] .trick/encrypted/api_key.conf.enc -> api_key.conf
114
+ ```
115
+ 🟩 Encrypted: .env -> .trick/.env.enc
116
+ 🟩 Encrypted: api_key.conf -> .trick/api_key.conf.enc
69
117
  ```
70
118
 
71
- > [!IMPORTANT]
72
- > The `passphrase.json` collects all the passphrases you have. Please back it up in multiple devices every time you edit it!
119
+ You can now commit the `.trick/` directory safely.
73
120
 
74
- ## More Features
75
121
 
76
- ### Default Target Name
77
122
 
78
- You can set the default target name so that you don't need to input it every time:
123
+ ### 5. Decrypt Files (on another machine or server)
124
+
125
+ 1. Copy or recreate the passphrase file:
126
+
127
+ ```
128
+ ~/.config/trick/passphrases/MyTarget
129
+ ```
130
+ 2. Run:
131
+
132
+ ```bash
133
+ trick decrypt MyTarget
134
+ ```
135
+
136
+ Files are restored to their original locations.
137
+
138
+
139
+
140
+ ## Default Targets
141
+
142
+ You can mark targets as **default**, so you don’t need to specify them every time.
79
143
 
80
144
  ```bash
81
- # Set the default target name
82
- $ trick set-default MyTargetName
145
+ trick add-default MyTarget
146
+ ```
83
147
 
84
- # Display the default target name
85
- $ trick get-default
148
+ List default targets:
149
+
150
+ ```bash
151
+ trick list-defaults
86
152
  ```
87
153
 
88
- Now you can encrypt and decrypt more easily:
154
+ Now you can simply run:
89
155
 
90
156
  ```bash
91
- $ trick encrypt
92
- $ trick decrypt
157
+ trick encrypt
158
+ trick decrypt
93
159
  ```
160
+
161
+
162
+
163
+ ## Configuration
164
+
165
+ ### `trick.config.json`
166
+
167
+ Example:
168
+
169
+ ```json
170
+ {
171
+ "targets": {
172
+ "MyTarget": {
173
+ "files": [".env", "api_key.conf"]
174
+ }
175
+ },
176
+ "trickRootDirectory": ".trick",
177
+ "passphraseDirectory": "~/.config/trick/passphrases",
178
+ "defaultTargetNames": ["MyTarget"],
179
+ "encryption": {
180
+ "iterationCount": 100000
181
+ }
182
+ }
183
+ ```
184
+
185
+ ### Key Fields
186
+
187
+ | Field | Description |
188
+ | ------------------------ | ------------------------------------- |
189
+ | `targets` | Mapping of target names to file lists |
190
+ | `trickRootDirectory` | Where encrypted files are stored |
191
+ | `passphraseDirectory` | Where passphrase files live |
192
+ | `defaultTargetNames` | Targets used when none specified |
193
+ | `encryption.iterationCount` | PBKDF2 iteration count |
194
+
195
+
196
+
197
+ ## Commands Overview
198
+
199
+ | Command | Description |
200
+ | ---------------------------------- | -------------------------- |
201
+ | `trick init` | Initialize configuration |
202
+ | `trick config` | Print current config |
203
+ | `trick add <target> [files...]` | Add files to a target |
204
+ | `trick remove <target> [files...]` | Remove files from a target |
205
+ | `trick remove <target> --target` | Remove a target |
206
+ | `trick list` | List targets and files |
207
+ | `trick set-passphrase <target>` | Create passphrase file |
208
+ | `trick encrypt [targets...]` | Encrypt files |
209
+ | `trick decrypt [targets...]` | Decrypt files |
210
+ | `trick add-default [targets...]` | Add default targets |
211
+ | `trick list-defaults` | Show default targets |
212
+
213
+ ## Security Notes
214
+
215
+ * Encryption uses:
216
+
217
+ * **AES-256-CBC**
218
+ * **PBKDF2** with configurable iteration count
219
+ * Passphrases:
220
+
221
+ * Never stored in Git
222
+ * Stored as local files with strict permissions
223
+ * Losing passphrases = losing access to encrypted files
224
+
225
+ ## License
226
+
227
+ MIT
package/dist/app.js CHANGED
@@ -1,27 +1,62 @@
1
1
  import { Command } from 'commander';
2
- import { getTargetFromConfig, updateConfig } from './config.js';
2
+ import { CONFIG_FILE_NAME, getRootDirectory, getTargetFromConfig, updateConfig, } from './config.js';
3
3
  import { decryptFiles, encryptFiles } from './encrypt.js';
4
4
  import fsExtra from 'fs-extra';
5
- import chalk from 'chalk';
6
- import { getPassphrase } from './passphrase.js';
7
- import { resolve_error } from './error.js';
5
+ import { getPassphrase, getPassphraseDirectory } from './passphrase.js';
6
+ import { resolveError } from './error.js';
7
+ import path from 'path';
8
+ import { colorFilePath, colorTargetName } from './color.js';
9
+ import { success, warning } from './console.js';
8
10
  const program = new Command();
9
- program.version('2.0.1');
11
+ program.version('2.1.0');
10
12
  program.description('Save credential files to remote safely and easily.');
13
+ program
14
+ .command('config')
15
+ .description('Display the current configuration.')
16
+ .action(function () {
17
+ updateConfig((config) => {
18
+ console.log(JSON.stringify(config, null, 2));
19
+ });
20
+ });
21
+ program
22
+ .command('init')
23
+ .description('Initialize the configuration file.')
24
+ .option('-r, --root', 'Create the configuration file in the root directory.', false)
25
+ .action(function (options) {
26
+ const configFilePath = options.root
27
+ ? path.join(getRootDirectory(), CONFIG_FILE_NAME)
28
+ : path.join(process.cwd(), CONFIG_FILE_NAME);
29
+ if (fsExtra.existsSync(configFilePath)) {
30
+ console.log(warning(`Configuration file already exists: ${configFilePath}`));
31
+ return;
32
+ }
33
+ else {
34
+ updateConfig(() => true, options.root);
35
+ console.log(success(`Initialized configuration file: ${configFilePath}`));
36
+ }
37
+ });
11
38
  program
12
39
  .command('add')
13
40
  .description('Add files to a target.')
14
- .argument('<target>', 'The name of the target')
15
- .argument('[files...]', 'Files that are encrypted')
16
- .action(async (targetName, files) => {
17
- await updateConfig((config) => {
18
- try {
19
- const target = getTargetFromConfig(config, targetName);
20
- target.files.push(...files);
21
- }
22
- catch (err) {
23
- config.default_target_name = targetName;
41
+ .argument('<target>', 'The name of the target to add to')
42
+ .argument('[files...]', 'Files that are added to the target')
43
+ .action(function (targetName, files) {
44
+ updateConfig((config) => {
45
+ const target = config.targets[targetName];
46
+ if (!target) {
24
47
  config.targets[targetName] = { files };
48
+ console.log(success(`Added files to target: ${targetName}`));
49
+ }
50
+ else {
51
+ for (const file of files) {
52
+ if (target.files.includes(file)) {
53
+ console.log(warning(`File already exists in the target: ${file}`));
54
+ }
55
+ else {
56
+ target.files.push(file);
57
+ console.log(success(`Added file to target: ${file}`));
58
+ }
59
+ }
25
60
  }
26
61
  return true;
27
62
  });
@@ -29,13 +64,13 @@ program
29
64
  program
30
65
  .command('remove')
31
66
  .description('Remove files from a target.')
32
- .argument('<target>', 'The name of the target')
33
- .argument('[files...]', 'Files to remove')
67
+ .argument('<target>', 'The name of the target to remove from')
68
+ .argument('[files...]', 'Files to remove from the target')
34
69
  .option('-t, --target', 'Remove the target instead.')
35
- .action(async (targetName, files, options) => {
70
+ .action(function (targetName, files, options) {
36
71
  if (options.target) {
37
72
  // Remove the target
38
- return await updateConfig((config) => {
73
+ return updateConfig((config) => {
39
74
  getTargetFromConfig(config, targetName);
40
75
  delete config.targets[targetName];
41
76
  console.log(`[SUCCESS] Removed target: ${targetName}`);
@@ -43,14 +78,14 @@ program
43
78
  });
44
79
  }
45
80
  // Remove files from the target
46
- await updateConfig((config) => {
81
+ updateConfig((config) => {
47
82
  const target = getTargetFromConfig(config, targetName);
48
83
  const removedFiles = [];
49
84
  const remainingFiles = [];
50
85
  for (const file of target.files) {
51
86
  if (files.includes(file)) {
52
87
  removedFiles.push(file);
53
- console.log(`[SUCCESS] Removed file: ${file}`);
88
+ console.log(success(`Removed file: ${file}`));
54
89
  }
55
90
  else {
56
91
  remainingFiles.push(file);
@@ -59,79 +94,136 @@ program
59
94
  target.files = remainingFiles;
60
95
  const notFoundFiles = files.filter((it) => !removedFiles.includes(it));
61
96
  for (const notFoundFile of notFoundFiles) {
62
- console.log(`[WARNING] File not found in the target: ${notFoundFile}`);
97
+ console.log(warning(`File not found in the target: ${notFoundFile}`));
63
98
  }
64
99
  return true;
65
100
  });
66
101
  });
67
- function getTargetName(targetNameOrNull, defaultTargetName) {
68
- const targetName = targetNameOrNull === null ? defaultTargetName : targetNameOrNull;
69
- if (targetName === null) {
70
- throw new Error('Target is not specified and the default target name is null!');
71
- }
72
- return targetName;
73
- }
74
102
  program
75
103
  .command('encrypt')
76
104
  .description('Encrypt the credential files.')
77
- .argument('[target]', 'The name of the target', null)
78
- .action(async (targetNameOrNull) => {
79
- await updateConfig((config) => {
80
- const targetName = getTargetName(targetNameOrNull, config.default_target_name);
81
- const target = getTargetFromConfig(config, targetName);
82
- const passphrase = getPassphrase(config, targetName);
83
- const srcFilePaths = target.files;
84
- fsExtra.ensureDir(config.root_directory);
85
- encryptFiles(srcFilePaths, config.root_directory, passphrase, config.encryption.iteration_count);
105
+ .argument('[targetNames...]', 'The names of targets')
106
+ .action(function (targetNames) {
107
+ updateConfig((config) => {
108
+ if (targetNames.length === 0) {
109
+ targetNames.push(...config.defaultTargetNames);
110
+ }
111
+ if (targetNames.length === 0) {
112
+ console.log(warning('No target names specified and no default targets set.'));
113
+ return;
114
+ }
115
+ const rootDirectory = getRootDirectory();
116
+ const trickRootDirectory = path.resolve(rootDirectory, config.trickRootDirectory);
117
+ for (const targetName of targetNames) {
118
+ const target = getTargetFromConfig(config, targetName);
119
+ const passphrase = getPassphrase(config, targetName);
120
+ const srcFilePaths = target.files;
121
+ fsExtra.ensureDir(trickRootDirectory);
122
+ encryptFiles(srcFilePaths, trickRootDirectory, passphrase, config.encryption.iterationCount);
123
+ }
86
124
  });
87
125
  });
88
126
  program
89
127
  .command('decrypt')
90
128
  .description('Decrypt the credential files.')
91
- .argument('[target]', 'The name of the target', null)
92
- .action(async (targetNameOrNull) => {
93
- await updateConfig((config) => {
94
- const targetName = getTargetName(targetNameOrNull, config.default_target_name);
95
- const target = getTargetFromConfig(config, targetName);
96
- const passphrase = getPassphrase(config, targetName);
97
- const srcFilePaths = target.files;
98
- fsExtra.ensureDir(config.root_directory);
99
- decryptFiles(srcFilePaths, config.root_directory, passphrase, config.encryption.iteration_count);
129
+ .argument('[targetNames...]', 'The names of the targets')
130
+ .action(function (targetNames) {
131
+ updateConfig((config) => {
132
+ if (targetNames.length === 0) {
133
+ targetNames.push(...config.defaultTargetNames);
134
+ }
135
+ if (targetNames.length === 0) {
136
+ console.log(warning('No target names specified and no default targets set.'));
137
+ return;
138
+ }
139
+ const rootDirectory = getRootDirectory();
140
+ const trickRootDirectory = path.resolve(rootDirectory, config.trickRootDirectory);
141
+ for (const targetName of targetNames) {
142
+ const target = getTargetFromConfig(config, targetName);
143
+ const passphrase = getPassphrase(config, targetName);
144
+ const srcFilePaths = target.files;
145
+ fsExtra.ensureDir(trickRootDirectory);
146
+ decryptFiles(srcFilePaths, trickRootDirectory, passphrase, config.encryption.iterationCount);
147
+ }
100
148
  });
101
149
  });
102
150
  program
103
- .command('set-default')
104
- .description('Set the default target name.')
105
- .argument('<target>', 'The name of the target to set')
106
- .action(async (targetName) => {
107
- await updateConfig((config) => {
108
- config.default_target_name = targetName;
109
- return true;
151
+ .command('add-default')
152
+ .description('Add default target names.')
153
+ .argument('[targetNames...]', 'The names of targets to add')
154
+ .action(function (targetNames) {
155
+ updateConfig((config) => {
156
+ let addedAny = false;
157
+ for (const targetName of targetNames) {
158
+ if (!config.targets[targetName]) {
159
+ console.log(warning(`Target not found: ${targetName}`));
160
+ continue;
161
+ }
162
+ if (config.defaultTargetNames.includes(targetName)) {
163
+ console.log(warning(`Target name already in default list: ${targetName}`));
164
+ continue;
165
+ }
166
+ config.defaultTargetNames.push(targetName);
167
+ console.log(success(`Added default target name: ${targetName}`));
168
+ addedAny = true;
169
+ }
170
+ return addedAny;
110
171
  });
111
172
  });
112
173
  program
113
- .command('get-default')
114
- .description('Get the default target name.')
115
- .action(async () => {
116
- await updateConfig((config) => {
117
- console.log(config.default_target_name);
174
+ .command('list-defaults')
175
+ .description('Display the default target name.')
176
+ .action(function () {
177
+ updateConfig((config) => {
178
+ for (const targetName of config.defaultTargetNames) {
179
+ console.log(colorTargetName(targetName));
180
+ }
118
181
  });
119
182
  });
120
183
  program
121
184
  .command('list')
122
185
  .description('Display a list of targets.')
123
- .action(async () => {
124
- await updateConfig((config) => {
186
+ .action(function () {
187
+ updateConfig((config) => {
125
188
  for (const [targetName, target] of Object.entries(config.targets)) {
126
- console.log(chalk.cyan(targetName));
189
+ console.log(colorTargetName(targetName));
127
190
  for (const file of target.files) {
128
- console.log(' ' + chalk.yellow(file));
191
+ console.log(' ' + colorFilePath(file));
129
192
  }
130
193
  }
131
194
  });
132
195
  });
133
- program.parse();
196
+ program
197
+ .command('set-passphrase')
198
+ .description('Set passphrase for a target.')
199
+ .argument('<target>', 'The name of the target to set passphrase for')
200
+ .action(function (targetName) {
201
+ updateConfig((config) => {
202
+ const passphraseDirectory = getPassphraseDirectory(config);
203
+ if (!fsExtra.existsSync(passphraseDirectory)) {
204
+ fsExtra.ensureDirSync(passphraseDirectory);
205
+ console.log(success(`Created passphrase directory: ${passphraseDirectory}`));
206
+ }
207
+ const passphraseFile = path.join(passphraseDirectory, targetName);
208
+ if (!fsExtra.existsSync(passphraseFile)) {
209
+ fsExtra.createFileSync(passphraseFile);
210
+ fsExtra.chmodSync(passphraseFile, 0o600);
211
+ console.log(success(`Created passphrase file: ${passphraseFile}`));
212
+ console.log(success(`You have to edit the file to set the passphrase.`));
213
+ }
214
+ else {
215
+ console.log(warning(`Passphrase file already exists: ${passphraseFile}`));
216
+ }
217
+ });
218
+ });
219
+ try {
220
+ program.parse();
221
+ }
222
+ catch (err) {
223
+ resolveError(err);
224
+ process.exit(1);
225
+ }
134
226
  process.on('uncaughtException', (err) => {
135
- resolve_error(err);
227
+ resolveError(err);
136
228
  process.exit(1);
137
229
  });
@@ -0,0 +1,2 @@
1
+ export declare function colorTargetName(targetName: string): string;
2
+ export declare function colorFilePath(filePath: string): string;
package/dist/color.js ADDED
@@ -0,0 +1,7 @@
1
+ import chalk from 'chalk';
2
+ export function colorTargetName(targetName) {
3
+ return chalk.cyan(targetName);
4
+ }
5
+ export function colorFilePath(filePath) {
6
+ return chalk.yellow(filePath);
7
+ }